From 59129c434326bdce1d2f11ac2f85d781cd558a3c Mon Sep 17 00:00:00 2001 From: moabu <47318409+moabu@users.noreply.github.com> Date: Mon, 14 Oct 2024 11:47:13 +0300 Subject: [PATCH] refactor: move oxAuth Co-authored-by: Arnab Dutta Co-authored-by: Arunmozhi Co-authored-by: Arvind Tomar Co-authored-by: Chris B Co-authored-by: Chris Eland Co-authored-by: Christian <59786962+christian-hawk@users.noreply.github.com> Co-authored-by: Christian Eland Co-authored-by: Christian H Co-authored-by: Dhaval D <343411+ossdhaval@users.noreply.github.com> Co-authored-by: Djeumen Rolain Bonaventure Co-authored-by: Dmitry Ognyannikov Co-authored-by: Dzouato Djeumen Rolain Bonaventure Co-authored-by: Ganesh Co-authored-by: Gasmyr Co-authored-by: Guillaume Smaha Co-authored-by: Harjinder Dhanjal Co-authored-by: HemantKMehta <70174684+HemantKMehta@users.noreply.github.com> Co-authored-by: Javier Rojas Co-authored-by: Javier Rojas Blum Co-authored-by: Javier Rojas Blum Co-authored-by: Jose Co-authored-by: Jose G Co-authored-by: Kalle Mustonen <35445553+KalleMus@users.noreply.github.com> Co-authored-by: Kunal Vaidya Co-authored-by: Madhumita Co-authored-by: Madhumita Subramaniam Co-authored-by: Mike Schwartz Co-authored-by: Milton BO Co-authored-by: Milton Ch <32613743+miltonbo@users.noreply.github.com> Co-authored-by: Milton Ch <86965029+Milton-Ch@users.noreply.github.com> Co-authored-by: Milton Ch Co-authored-by: Mobarak Hosen Shakil <20867846+imShakil@users.noreply.github.com> Co-authored-by: Mohammad Abudayyeh <47318409+moabu@users.noreply.github.com> Co-authored-by: Nils Behlen Co-authored-by: Oleksiy Tataryn Co-authored-by: Patrick Ethier Co-authored-by: Rajni Kant Sharma Co-authored-by: Rostislav Kondratenko Co-authored-by: SMan Co-authored-by: Safin Wasi <6601566+SafinWasi@users.noreply.github.com> Co-authored-by: Sam Morris <36208047+shmorri@users.noreply.github.com> Co-authored-by: Stefan Andersson Co-authored-by: Sync bot <54212639+mo-auto@users.noreply.github.com> Co-authored-by: Torstein Krause Johansen Co-authored-by: Whispeak-io-VoiceBiometrics <112541650+Whispeak-io-VoiceBiometrics@users.noreply.github.com> Co-authored-by: William Lowe Co-authored-by: Yuriy Movchan Co-authored-by: Yuriy Zabrovarnyy Co-authored-by: YuriyZ Co-authored-by: al-com <112606283+al-com@users.noreply.github.com> Co-authored-by: aliaksander-samuseu Co-authored-by: arvindsinghtomar Co-authored-by: arvindsinghtomar Co-authored-by: ayman abdelghany Co-authored-by: christian-hawk Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: gasmyr Co-authored-by: jschristie Co-authored-by: livetocode Co-authored-by: madumlao Co-authored-by: maduvena Co-authored-by: maduvena Co-authored-by: michal kepkowski Co-authored-by: miltonbo <32613743+miltonbo@users.noreply.github.com> Co-authored-by: mo-auto <54212639+mo-auto@users.noreply.github.com> Co-authored-by: musman2012 Co-authored-by: mzico Co-authored-by: naveenkumargopi <34319898+naveenkumargopi@users.noreply.github.com> Co-authored-by: nynymike Co-authored-by: ossdhaval <343411+ossdhaval@users.noreply.github.com> Co-authored-by: premeau <61592119+premeau@users.noreply.github.com> Co-authored-by: pujavs Co-authored-by: qbert2k Co-authored-by: rajnikant Co-authored-by: rajnikant Co-authored-by: rajnikantsh Co-authored-by: sahiliamsso Co-authored-by: shekhar16 Co-authored-by: shekhar16 Co-authored-by: shekhar16 Co-authored-by: smogali Co-authored-by: worm333 Co-authored-by: yurem Co-authored-by: yuriyz Co-authored-by: yuriyz Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/CODEOWNERS | 36 + .github/ISSUE_TEMPLATE/issue-report.md | 48 + .../workflows/central_code_quality_check.yml | 164 + oxAuth/.gitignore | 17 + oxAuth/CODE_OF_CONDUCT.md | 46 + oxAuth/Client/.gitignore | 2 + oxAuth/Client/pom.xml | 349 + oxAuth/Client/profiles/.gitignore | 6 + .../profiles/default/client_keystore.pkcs12 | Bin 0 -> 23345 bytes .../config-oxauth-test-data.properties | 26 + .../jar-without-provider-dependencies.xml | 32 + .../oxauth/client/AuthorizationRequest.java | 738 + .../oxauth/client/AuthorizationResponse.java | 413 + .../gluu/oxauth/client/AuthorizeClient.java | 277 + .../BackchannelAuthenticationClient.java | 154 + .../BackchannelAuthenticationRequest.java | 163 + .../BackchannelAuthenticationResponse.java | 100 + .../org/gluu/oxauth/client/BaseClient.java | 277 + .../org/gluu/oxauth/client/BaseRequest.java | 145 + .../org/gluu/oxauth/client/BaseResponse.java | 104 + .../oxauth/client/BaseResponseWithErrors.java | 89 + .../oxauth/client/ClientAuthnEnabler.java | 53 + .../oxauth/client/ClientAuthnRequest.java | 130 + .../gluu/oxauth/client/ClientInfoClient.java | 183 + .../gluu/oxauth/client/ClientInfoRequest.java | 89 + .../oxauth/client/ClientInfoResponse.java | 116 + .../org/gluu/oxauth/client/ClientUtils.java | 57 + .../gluu/oxauth/client/DeviceAuthzClient.java | 116 + .../oxauth/client/DeviceAuthzRequest.java | 116 + .../oxauth/client/DeviceAuthzResponse.java | 122 + .../gluu/oxauth/client/EndSessionClient.java | 139 + .../gluu/oxauth/client/EndSessionRequest.java | 149 + .../oxauth/client/EndSessionResponse.java | 147 + .../client/GluuConfigurationClient.java | 128 + .../client/GluuConfigurationRequest.java | 12 + .../client/GluuConfigurationResponse.java | 61 + .../org/gluu/oxauth/client/JwkClient.java | 140 + .../org/gluu/oxauth/client/JwkRequest.java | 20 + .../org/gluu/oxauth/client/JwkResponse.java | 121 + .../client/OpenIdConfigurationClient.java | 303 + .../client/OpenIdConfigurationRequest.java | 26 + .../client/OpenIdConfigurationResponse.java | 1152 + .../client/OpenIdConnectDiscoveryClient.java | 130 + .../client/OpenIdConnectDiscoveryRequest.java | 133 + .../OpenIdConnectDiscoveryResponse.java | 47 + .../oxauth/client/QueryStringDecoder.java | 51 + .../gluu/oxauth/client/RegisterClient.java | 386 + .../gluu/oxauth/client/RegisterRequest.java | 1822 ++ .../gluu/oxauth/client/RegisterResponse.java | 249 + .../oxauth/client/RevokeSessionClient.java | 71 + .../oxauth/client/RevokeSessionRequest.java | 54 + .../oxauth/client/RevokeSessionResponse.java | 28 + .../org/gluu/oxauth/client/TokenClient.java | 283 + .../org/gluu/oxauth/client/TokenRequest.java | 352 + .../org/gluu/oxauth/client/TokenResponse.java | 194 + .../oxauth/client/TokenRevocationClient.java | 114 + .../oxauth/client/TokenRevocationRequest.java | 68 + .../client/TokenRevocationResponse.java | 113 + .../gluu/oxauth/client/UserInfoClient.java | 216 + .../gluu/oxauth/client/UserInfoRequest.java | 90 + .../gluu/oxauth/client/UserInfoResponse.java | 129 + .../fcm/FirebaseCloudMessagingClient.java | 73 + .../fcm/FirebaseCloudMessagingRequest.java | 100 + .../fcm/FirebaseCloudMessagingResponse.java | 85 + .../client/ciba/ping/PingCallbackClient.java | 84 + .../client/ciba/ping/PingCallbackRequest.java | 69 + .../ciba/ping/PingCallbackResponse.java | 25 + .../client/ciba/push/PushErrorClient.java | 66 + .../client/ciba/push/PushErrorRequest.java | 112 + .../client/ciba/push/PushErrorResponse.java | 25 + .../ciba/push/PushTokenDeliveryClient.java | 66 + .../ciba/push/PushTokenDeliveryRequest.java | 140 + .../ciba/push/PushTokenDeliveryResponse.java | 25 + .../u2f/AuthenticationRequestService.java | 34 + .../client/fido/u2f/FidoU2fClientFactory.java | 60 + .../fido/u2f/RegistrationRequestService.java | 38 + .../fido/u2f/U2fConfigurationService.java | 27 + .../gluu/oxauth/client/model/JwtState.java | 518 + .../client/model/SoftwareStatement.java | 124 + .../oxauth/client/model/authorize/Claim.java | 37 + .../client/model/authorize/ClaimValue.java | 103 + .../client/model/authorize/IdTokenMember.java | 63 + .../authorize/JwtAuthorizationRequest.java | 633 + .../model/authorize/UserInfoMember.java | 69 + .../oxauth/client/service/ClientFactory.java | 82 + .../client/service/IntrospectionService.java | 51 + .../oxauth/client/service/StatService.java | 24 + .../oxauth/client/uma/UmaClientFactory.java | 114 + .../oxauth/client/uma/UmaMetadataService.java | 26 + .../client/uma/UmaPermissionService.java | 38 + .../oxauth/client/uma/UmaResourceService.java | 66 + .../uma/UmaRptIntrospectionService.java | 30 + .../oxauth/client/uma/UmaScopeService.java | 27 + .../oxauth/client/uma/UmaTokenService.java | 43 + .../client/uma/exception/UmaException.java | 30 + .../oxauth/client/uma/wrapper/UmaClient.java | 237 + .../java/org/gluu/oxauth/util/ClientUtil.java | 103 + .../org/gluu/oxauth/util/KeyExporter.java | 137 + .../org/gluu/oxauth/util/KeyGenerator.java | 254 + .../test/java/org/gluu/oxauth/BaseTest.java | 1132 + ...nelAuthenticationExpiredRequestsTests.java | 301 + .../BackchannelAuthenticationPingMode.java | 3614 +++ .../BackchannelAuthenticationPollMode.java | 3534 +++ .../BackchannelAuthenticationPushMode.java | 3544 +++ .../ciba/CibaPingModeJwtAuthRequestTests.java | 434 + .../ciba/CibaPollModeJwtAuthRequestTests.java | 406 + .../gluu/oxauth/ciba/ConfigurationTest.java | 71 + .../gluu/oxauth/ciba/RegistrationTest.java | 1260 + .../java/org/gluu/oxauth/client/Asserter.java | 50 + .../oxauth/client/JSONObjectAsserter.java | 41 + .../oxauth/client/RegisterRequestTest.java | 41 + .../gluu/oxauth/client/ResponseAsserter.java | 77 + .../test/java/org/gluu/oxauth/dev/.gitignore | 1 + .../gluu/oxauth/dev/HostnameVerifierType.java | 36 + .../gluu/oxauth/dev/TestSessionWorkflow.java | 165 + .../dev/manual/AccessTokenManualTest.java | 65 + .../gluu/oxauth/dev/manual/BCFIPSTest.java | 43 + .../manual/MTSLClientAuthenticationTest.java | 85 + ...stWithoutRedirectUriWhenOneRegistered.java | 77 + ...AcceptValidAsymmetricIdTokenSignature.java | 167 + .../AcceptValidSymmetricIdTokenSignature.java | 92 + ...anDiscoverIdentifiersUsingEMailSyntax.java | 92 + .../CanDiscoverIdentifiersUsingUrlSyntax.java | 152 + ...stWithClientSecretBasicAuthentication.java | 107 + ...uestWithClientSecretJwtAuthentication.java | 111 + ...estWithClientSecretPostAuthentication.java | 107 + ...equestWithPrivateKeyJwtAuthentication.java | 115 + .../CanProvideEncryptedIdTokenResponse.java | 408 + .../CanProvideEncryptedUserInfoResponse.java | 459 + .../CanProvideSignedUserInfoResponse.java | 718 + .../CanRequestAndUseClaimsInIdToken.java | 119 + ...RequestAndUseEncryptedIdTokenResponse.java | 107 + ...equestAndUseEncryptedUserInfoResponse.java | 115 + ...anRequestAndUseSignedUserInfoResponse.java | 116 + .../interop/DisplaysLogoInLoginPage.java | 94 + .../interop/DisplaysPolicyUriInLoginPage.java | 90 + .../interop/EnablesDynamicRegistration.java | 63 + .../IgnoresExtraQueryComponentInRequest.java | 85 + ...esAtHashInIdTokenWhenImplicitFlowUsed.java | 115 + ...ncludesCHashInIdTokenWhenCodeFlowUsed.java | 143 + .../oxauth/interop/OPRegistrationJwks.java | 223 + .../oxauth/interop/ProvidingAcrValues.java | 83 + ...dingIdTokenWithEssentialAuthTimeClaim.java | 129 + ...ProvidingIdTokenWithMaxAgeRestriction.java | 345 + ...yRequestedEssentialAndVoluntaryClaims.java | 154 + ...gIndividuallyRequestedEssentialClaims.java | 144 + ...gIndividuallyRequestedVoluntaryClaims.java | 148 + ...enIdConfigurationDiscoveryInformation.java | 35 + ...jectInvalidAsymmetricIdTokenSignature.java | 97 + ...ejectInvalidSymmetricIdTokenSignature.java | 92 + ...tUriNotMatchingARegisteredRedirectUri.java | 80 + ...RegistrationOfRedirectUriWithFragment.java | 53 + ...houtRedirectUriWhenMultipleRegistered.java | 83 + .../RejectRequestWithoutResponseType.java | 44 + ...RequestsWithoutNonceUsingImplicitFlow.java | 86 + ...tsIncorrectAtHashWhenImplicitFlowUsed.java | 118 + ...RejectsIncorrectCHashWhenCodeFlowUsed.java | 117 + ...rectUriWhenQueryParameterDoesNotMatch.java | 83 + .../interop/RejectsSecondUseOfAccessCode.java | 140 + ...ContainingRegisteredRedirectUriValues.java | 53 + ...UserInfoClaimsWithOpenIdRequestObject.java | 143 + ...questingUserInfoClaimsWithScopeValues.java | 102 + ...odeRevokesPreviouslyIssuedAccessToken.java | 163 + ...ormEncodedClientCredentialsInPostBody.java | 106 + ...ToTokenEndpointUsingHttpBasicWithPost.java | 106 + ...nEndpointWithAsymmetricallySignedJWTs.java | 480 + ...enEndpointWithSymmetricallySignedJWTs.java | 254 + ...upportClaimsRequestSpecifyingSubValue.java | 229 + .../interop/SupportCodeResponseType.java | 81 + ...nationOfCodeIdTokenTokenResponseTypes.java | 81 + ...rtCombinationOfCodeTokenResponseTypes.java | 81 + ...CombinationOfIdTokenCodeResponseTypes.java | 81 + ...ombinationOfIdTokenTokenResponseTypes.java | 81 + .../interop/SupportDisplayValuePage.java | 81 + .../interop/SupportDisplayValuePopup.java | 81 + .../interop/SupportIdTokenResponseType.java | 79 + .../interop/SupportPromptValueLogin.java | 153 + .../interop/SupportPromptValueNone.java | 159 + .../interop/SupportRegistrationRead.java | 113 + .../oxauth/interop/SupportRequestFile.java | 139 + .../SupportRequestsContainingNonce.java | 117 + .../interop/SupportRequestsWithoutNonce.java | 111 + .../SupportScopeRequestingAddressClaims.java | 121 + .../SupportScopeRequestingAllBasicClaims.java | 121 + .../SupportScopeRequestingEmailClaims.java | 121 + ...upportScopeRequestingNoSpecificClaims.java | 140 + .../SupportScopeRequestingPhoneClaims.java | 121 + .../SupportScopeRequestingProfileClaims.java | 130 + .../interop/SupportTokenResponseType.java | 81 + .../interop/SupportWebFingerDiscovery.java | 29 + .../interop/Supports3rdPartyInitLogin.java | 59 + .../Supports3rdPartyInitLoginNoHttps.java | 54 + ...imsRequestedWithScopeAndRequestObject.java | 146 + .../SupportsReturningClaimsInIdToken.java | 145 + ...entClaimsInIdTokenAndUserInfoEndpoint.java | 151 + .../gluu/oxauth/interop/UserInfoEndpoint.java | 28 + ...dpointAccessWithFormEncodedBodyMethod.java | 107 + ...serInfoEndpointAccessWithHeaderMethod.java | 100 + .../UsesAsymmetricIdTokenSignatures.java | 374 + .../gluu/oxauth/interop/UsesDiscovery.java | 34 + .../interop/UsesDynamicRegistration.java | 63 + .../UsesSymmetricIdTokenSignatures.java | 198 + ...fiesCorrectAtHashWhenImplicitFlowUsed.java | 115 + .../VerifiesCorrectCHashWhenCodeFlowUsed.java | 114 + .../org/gluu/oxauth/json/JsonApplierTest.java | 55 + .../org/gluu/oxauth/load/LoadConstants.java | 21 + .../load/ObtainAccessTokenLoadTest.java | 119 + .../oxauth/load/RegistrationLoadTest.java | 59 + .../gluu/oxauth/load/UserInfoLoadTest.java | 91 + .../BenchmarkRequestAccessToken.java | 123 + .../BenchmarkRequestAuthorization.java | 135 + .../suite/BenchmarkTestListener.java | 117 + .../suite/BenchmarkTestSuiteListener.java | 33 + .../org/gluu/oxauth/page/AbstractPage.java | 86 + .../org/gluu/oxauth/page/DeviceAuthzPage.java | 29 + .../java/org/gluu/oxauth/page/LoginPage.java | 42 + .../test/java/org/gluu/oxauth/page/Page.java | 7 + .../java/org/gluu/oxauth/page/PageConfig.java | 41 + .../java/org/gluu/oxauth/page/SelectPage.java | 68 + .../ws/rs/AccessTokenAsJwtHttpTest.java | 108 + .../gluu/oxauth/ws/rs/AddressClaimsTest.java | 2193 ++ .../ApplicationTypeRestrictionHttpTest.java | 514 + .../ws/rs/AuthorizationCodeFlowHttpTest.java | 896 + ...AuthorizationResponseCustomHeaderTest.java | 115 + .../rs/AuthorizationResponseModeHttpTest.java | 1149 + .../rs/AuthorizationSupportCustomParams.java | 90 + .../rs/AuthorizeRestWebServiceHttpTest.java | 3017 ++ ...horizeSessionIdRestWebServiceHttpTest.java | 142 + .../ClientAuthenticationFilterHttpTest.java | 189 + .../ws/rs/ClientCredentialsGrantHttpTest.java | 1956 ++ .../rs/ClientInfoRestWebServiceHttpTest.java | 197 + .../oxauth/ws/rs/ClientSecretBasicTest.java | 234 + .../ClientSpecificAccessTokenExpiration.java | 163 + .../org/gluu/oxauth/ws/rs/ClientTestUtil.java | 33 + .../ClientWhiteListBlackListRedirectUris.java | 127 + .../ConfigurationRestWebServiceHttpTest.java | 124 + ...nableClientToRestrictJavascriptOrigin.java | 198 + .../ws/rs/EncodeClaimsInStateParameter.java | 1772 ++ .../rs/EndSessionRestWebServiceHttpTest.java | 261 + .../GluuConfigurationWebServiceHttpTest.java | 25 + .../ws/rs/GrantTypesRestrictionHttpTest.java | 750 + .../ws/rs/IndividualClaimsRequestsTest.java | 2414 ++ .../oxauth/ws/rs/IntrospectionWsHttpTest.java | 77 + .../ws/rs/JwkRestWebServiceHttpTest.java | 72 + ...ultiStepAuthorizationCodeFlowHttpTest.java | 135 + .../gluu/oxauth/ws/rs/MultivaluedClaims.java | 3529 +++ .../ws/rs/OpenIDConnectDiscoveryHttpTest.java | 244 + .../ws/rs/OpenIDRequestObjectHttpTest.java | 2890 ++ .../PersistClientAuthorizationsHttpTest.java | 565 + .../org/gluu/oxauth/ws/rs/PkceHttpTest.java | 216 + .../RegistrationRestWebServiceHttpTest.java | 699 + .../rs/RegistrationWithSoftwareStatement.java | 490 + .../rs/ResponseTypesRestrictionHttpTest.java | 663 + .../oxauth/ws/rs/RevokeSessionHttpTest.java | 91 + ...SOWithMultipleBackendServicesHttpTest.java | 372 + ...ctorIdentifierUrlVerificationHttpTest.java | 450 + .../oxauth/ws/rs/SelectAccountHttpTest.java | 210 + .../ws/rs/SpontaneousScopeHttpTest.java | 109 + .../oxauth/ws/rs/TokenBindingHttpTest.java | 130 + .../oxauth/ws/rs/TokenEncryptionHttpTest.java | 425 + ...EndpointAuthMethodRestrictionHttpTest.java | 10674 +++++++ .../ws/rs/TokenRestWebServiceHttpTest.java | 1460 + .../oxauth/ws/rs/TokenRevocationTest.java | 981 + .../oxauth/ws/rs/TokenSignaturesHttpTest.java | 1258 + .../java/org/gluu/oxauth/ws/rs/UILocales.java | 111 + .../rs/UserAuthenticationFilterHttpTest.java | 320 + .../ws/rs/UserInfoRestWebServiceHttpTest.java | 1868 ++ .../ws/rs/ValidateIdTokenHashesTest.java | 213 + .../org/gluu/oxauth/ws/rs/WebKeysTest.java | 81 + .../deviceauthz/DeviceAuthzFlowHttpTest.java | 609 + .../DeviceAuthzRequestRegistrationTest.java | 273 + .../oxauth/ws/rs/internal/StatWSTest.java | 38 + .../AccessProtectedResourceFlowHttpTest.java | 207 + ...ntAuthenticationByAccessTokenHttpTest.java | 238 + .../ws/rs/uma/MetaDataFlowHttpTest.java | 48 + .../ws/rs/uma/ObtainPatTokenFlowHttpTest.java | 62 + .../rs/uma/RegisterResourceFlowHttpTest.java | 263 + .../gluu/oxauth/ws/rs/uma/ScopeHttpTest.java | 32 + .../UmaRegisterPermissionFlowHttpTest.java | 130 + .../rs/uma/UmaSpontaneousScopeHttpTest.java | 140 + .../src/test/resources/clientkeystore.jks | Bin 0 -> 19673 bytes .../test/resources/interop_Gluu_OX.properties | 44 + .../test/resources/interop_NRI_PHP.properties | 39 + .../interop_Nov_Matake_Test.properties | 39 + .../test/resources/interop_Oreo.properties | 0 .../interop_Roland_Hedberg_Test.properties | 39 + .../resources/interop_Ryo_Ito_Test.properties | 0 .../test/resources/interop_Wenou.properties | 0 .../src/test/resources/interop_rohe_config.py | 70 + .../resources/interop_rohe_python_config.json | 35 + .../resources/oxauth_test_client_keys.zip | Bin 0 -> 9015 bytes .../src/test/resources/testng-benchmark.xml | 19 + .../src/test/resources/testng-multi-authz.xml | 12 + .../src/test/resources/testng.properties | 61 + oxAuth/Client/src/test/resources/testng.xml | 854 + oxAuth/LICENSE | 21 + oxAuth/Model/.gitignore | 1 + oxAuth/Model/pom.xml | 170 + .../authorize/AuthorizeErrorResponseType.java | 191 + .../authorize/AuthorizeRequestParam.java | 49 + .../authorize/AuthorizeResponseParam.java | 31 + .../oxauth/model/authorize/CodeVerifier.java | 141 + .../DeviceAuthorizationRequestParam.java | 24 + .../DeviceAuthorizationResponseParam.java | 50 + .../DeviceAuthzErrorResponseType.java | 63 + ...hannelAuthenticationErrorResponseType.java | 128 + ...BackchannelAuthenticationRequestParam.java | 80 + ...ackchannelAuthenticationResponseParam.java | 30 + ...elDeviceRegistrationErrorResponseType.java | 84 + .../FirebaseCloudMessagingRequestParam.java | 41 + .../FirebaseCloudMessagingResponseParam.java | 41 + .../model/ciba/PushErrorRequestParam.java | 19 + .../model/ciba/PushErrorResponseType.java | 76 + .../ciba/PushTokenDeliveryRequestParam.java | 21 + .../model/common/AuthenticationMethod.java | 102 + .../model/common/AuthorizationMethod.java | 68 + .../common/BackchannelTokenDeliveryMode.java | 94 + .../gluu/oxauth/model/common/CallerType.java | 11 + .../org/gluu/oxauth/model/common/Display.java | 87 + .../gluu/oxauth/model/common/GrantType.java | 178 + .../oxauth/model/common/HasParamName.java | 16 + .../org/gluu/oxauth/model/common/Holder.java | 32 + .../java/org/gluu/oxauth/model/common/Id.java | 52 + .../org/gluu/oxauth/model/common/IdType.java | 66 + .../model/common/IntrospectionResponse.java | 177 + .../gluu/oxauth/model/common/JSONable.java | 18 + .../oxauth/model/common/PairwiseIdType.java | 41 + .../model/common/ProgrammingLanguage.java | 47 + .../org/gluu/oxauth/model/common/Prompt.java | 116 + .../oxauth/model/common/ResponseMode.java | 90 + .../oxauth/model/common/ResponseType.java | 182 + .../oxauth/model/common/ScopeConstants.java | 13 + .../gluu/oxauth/model/common/ScopeType.java | 128 + .../SoftwareStatementValidationType.java | 40 + .../gluu/oxauth/model/common/SubjectType.java | 54 + .../gluu/oxauth/model/common/TokenType.java | 64 + .../oxauth/model/common/TokenTypeHint.java | 94 + .../oxauth/model/common/WebKeyStorage.java | 50 + .../model/common/converter/ListConverter.java | 41 + .../model/configuration/AppConfiguration.java | 2237 ++ .../configuration/AuthenticationFilter.java | 23 + ...AuthenticationProtectionConfiguration.java | 52 + .../model/configuration/BaseFilter.java | 71 + .../CIBAEndUserNotificationConfig.java | 105 + .../ClientAuthenticationFilter.java | 21 + .../model/configuration/Configuration.java | 10 + .../ConfigurationResponseClaim.java | 77 + .../CorsConfigurationFilter.java | 145 + .../model/crypto/AbstractCryptoProvider.java | 235 + .../gluu/oxauth/model/crypto/Certificate.java | 105 + .../model/crypto/CryptoProviderFactory.java | 66 + .../org/gluu/oxauth/model/crypto/Key.java | 131 + .../gluu/oxauth/model/crypto/KeyFactory.java | 31 + .../model/crypto/OxAuthCryptoProvider.java | 585 + .../model/crypto/OxElevenCryptoProvider.java | 168 + .../gluu/oxauth/model/crypto/PrivateKey.java | 40 + .../gluu/oxauth/model/crypto/PublicKey.java | 49 + .../model/crypto/binding/TokenBinding.java | 60 + .../crypto/binding/TokenBindingExtension.java | 41 + .../binding/TokenBindingExtensionType.java | 12 + .../model/crypto/binding/TokenBindingID.java | 54 + .../binding/TokenBindingKeyParameters.java | 49 + .../crypto/binding/TokenBindingMessage.java | 102 + .../binding/TokenBindingMessageParser.java | 80 + .../binding/TokenBindingParseException.java | 26 + .../crypto/binding/TokenBindingStream.java | 20 + .../crypto/binding/TokenBindingType.java | 48 + .../encryption/BlockEncryptionAlgorithm.java | 103 + .../encryption/KeyEncryptionAlgorithm.java | 79 + .../crypto/signature/AbstractSigner.java | 23 + .../crypto/signature/AlgorithmFamily.java | 50 + .../AsymmetricSignatureAlgorithm.java | 138 + .../crypto/signature/ECDSAKeyFactory.java | 131 + .../crypto/signature/ECDSAPrivateKey.java | 65 + .../crypto/signature/ECDSAPublicKey.java | 92 + .../crypto/signature/ECEllipticCurve.java | 66 + .../model/crypto/signature/RSAKeyFactory.java | 141 + .../model/crypto/signature/RSAPrivateKey.java | 77 + .../model/crypto/signature/RSAPublicKey.java | 81 + .../crypto/signature/SignatureAlgorithm.java | 133 + .../oxauth/model/crypto/signature/Signer.java | 18 + .../model/discovery/OAuth2Discovery.java | 324 + .../oxauth/model/discovery/WebFingerLink.java | 32 + .../model/discovery/WebFingerParam.java | 34 + .../model/error/DefaultErrorResponse.java | 51 + .../model/error/ErrorHandlingMethod.java | 116 + .../oxauth/model/error/ErrorResponse.java | 189 + .../gluu/oxauth/model/error/IErrorType.java | 22 + .../exception/InvalidClaimException.java | 17 + .../model/exception/InvalidJweException.java | 25 + .../model/exception/InvalidJwtException.java | 25 + .../exception/InvalidParameterException.java | 25 + .../model/exception/SignatureException.java | 25 + .../fido/u2f/DeviceRegistrationStatus.java | 72 + .../model/fido/u2f/U2fConfiguration.java | 75 + .../oxauth/model/fido/u2f/U2fConstants.java | 20 + .../model/fido/u2f/U2fErrorResponseType.java | 97 + .../fido/u2f/exception/BadInputException.java | 23 + .../u2f/exception/RegistrationNotAllowed.java | 23 + .../u2f/message/RawAuthenticateResponse.java | 61 + .../fido/u2f/message/RawRegisterResponse.java | 59 + .../u2f/protocol/AuthenticateRequest.java | 79 + .../protocol/AuthenticateRequestMessage.java | 60 + .../u2f/protocol/AuthenticateResponse.java | 96 + .../fido/u2f/protocol/AuthenticateStatus.java | 55 + .../model/fido/u2f/protocol/ClientData.java | 73 + .../model/fido/u2f/protocol/DeviceData.java | 101 + .../u2f/protocol/DeviceNotificationConf.java | 73 + .../fido/u2f/protocol/RegisterRequest.java | 71 + .../u2f/protocol/RegisterRequestMessage.java | 64 + .../fido/u2f/protocol/RegisterResponse.java | 83 + .../fido/u2f/protocol/RegisterStatus.java | 57 + .../oxauth/model/gluu/GluuConfiguration.java | 82 + .../model/gluu/GluuErrorResponseType.java | 66 + .../gluu/oxauth/model/json/JsonApplier.java | 195 + .../oxauth/model/json/PropertyDefinition.java | 76 + .../model/jwe/AbstractJweDecrypter.java | 36 + .../model/jwe/AbstractJweEncrypter.java | 33 + .../java/org/gluu/oxauth/model/jwe/Jwe.java | 116 + .../gluu/oxauth/model/jwe/JweDecrypter.java | 27 + .../oxauth/model/jwe/JweDecrypterImpl.java | 126 + .../gluu/oxauth/model/jwe/JweEncrypter.java | 17 + .../oxauth/model/jwe/JweEncrypterImpl.java | 121 + .../model/jwe/KeyDerivationFunction.java | 102 + .../org/gluu/oxauth/model/jwk/Algorithm.java | 115 + .../org/gluu/oxauth/model/jwk/JSONWebKey.java | 281 + .../gluu/oxauth/model/jwk/JSONWebKeySet.java | 128 + .../gluu/oxauth/model/jwk/JWKParameter.java | 31 + .../model/jwk/KeySelectionStrategy.java | 47 + .../org/gluu/oxauth/model/jwk/KeyType.java | 64 + .../java/org/gluu/oxauth/model/jwk/Use.java | 65 + .../oxauth/model/jws/AbstractJwsSigner.java | 87 + .../gluu/oxauth/model/jws/ECDSASigner.java | 168 + .../org/gluu/oxauth/model/jws/HMACSigner.java | 85 + .../org/gluu/oxauth/model/jws/JwsSigner.java | 26 + .../oxauth/model/jws/PlainTextSignature.java | 33 + .../org/gluu/oxauth/model/jws/RSASigner.java | 107 + .../java/org/gluu/oxauth/model/jwt/Jwt.java | 113 + .../gluu/oxauth/model/jwt/JwtClaimName.java | 237 + .../gluu/oxauth/model/jwt/JwtClaimSet.java | 373 + .../org/gluu/oxauth/model/jwt/JwtClaims.java | 205 + .../org/gluu/oxauth/model/jwt/JwtHeader.java | 195 + .../gluu/oxauth/model/jwt/JwtHeaderName.java | 35 + .../oxauth/model/jwt/JwtStateClaimName.java | 106 + .../oxauth/model/jwt/JwtSubClaimObject.java | 36 + .../org/gluu/oxauth/model/jwt/JwtType.java | 48 + .../org/gluu/oxauth/model/jwt/PureJwt.java | 110 + .../model/register/ApplicationType.java | 68 + .../register/RegisterErrorResponseType.java | 97 + .../model/register/RegisterRequestParam.java | 391 + .../model/register/RegisterResponseParam.java | 67 + .../session/EndSessionErrorResponseType.java | 91 + .../model/session/EndSessionRequestParam.java | 40 + .../session/EndSessionResponseParam.java | 15 + .../model/token/ClientAssertionType.java | 50 + .../oxauth/model/token/JsonWebResponse.java | 70 + .../model/token/TokenErrorResponseType.java | 125 + .../TokenRevocationErrorResponseType.java | 80 + .../token/TokenRevocationRequestParam.java | 17 + .../model/uma/ClaimTokenFormatType.java | 34 + .../org/gluu/oxauth/model/uma/JsonLogic.java | 91 + .../gluu/oxauth/model/uma/JsonLogicNode.java | 74 + .../oxauth/model/uma/JsonLogicNodeParser.java | 25 + .../oxauth/model/uma/PermissionTicket.java | 63 + .../gluu/oxauth/model/uma/RPTResponse.java | 54 + .../model/uma/RptIntrospectionResponse.java | 181 + .../gluu/oxauth/model/uma/RptProfiles.java | 21 + .../gluu/oxauth/model/uma/UmaConstants.java | 29 + .../model/uma/UmaErrorResponseType.java | 218 + .../gluu/oxauth/model/uma/UmaMetadata.java | 97 + .../oxauth/model/uma/UmaNeedInfoResponse.java | 74 + .../gluu/oxauth/model/uma/UmaPermission.java | 102 + .../oxauth/model/uma/UmaPermissionList.java | 26 + .../gluu/oxauth/model/uma/UmaResource.java | 158 + .../oxauth/model/uma/UmaResourceResponse.java | 58 + .../oxauth/model/uma/UmaResourceWithId.java | 46 + .../oxauth/model/uma/UmaScopeDescription.java | 76 + .../gluu/oxauth/model/uma/UmaScopeType.java | 42 + .../oxauth/model/uma/UmaTokenResponse.java | 77 + .../model/uma/persistence/UmaPermission.java | 205 + .../model/uma/persistence/UmaResource.java | 252 + .../gluu/oxauth/model/uma/wrapper/Token.java | 89 + .../gluu/oxauth/model/userinfo/Schema.java | 46 + .../userinfo/UserInfoErrorResponseType.java | 63 + .../gluu/oxauth/model/util/Base64Util.java | 90 + .../org/gluu/oxauth/model/util/ByteUtils.java | 22 + .../org/gluu/oxauth/model/util/CertUtils.java | 177 + .../org/gluu/oxauth/model/util/HashUtil.java | 50 + .../org/gluu/oxauth/model/util/JwtUtil.java | 289 + .../java/org/gluu/oxauth/model/util/Pair.java | 59 + .../gluu/oxauth/model/util/QueryBuilder.java | 66 + .../gluu/oxauth/model/util/StringUtils.java | 204 + .../util/SubjectIdentifierGenerator.java | 21 + .../oxauth/model/util/URLPatternList.java | 167 + .../java/org/gluu/oxauth/model/util/Util.java | 289 + oxAuth/Model/src/main/resources/json_logic.js | 464 + .../model/authorize/CodeVerifierTest.java | 56 + .../binding/TokenBindingParserTest.java | 28 + .../gluu/oxauth/model/jwt/JwtClaimsTest.java | 50 + .../gluu/oxauth/model/jwt/JwtTypeTest.java | 16 + .../model/uma/JsonLogicNodeParserTest.java | 26 + .../gluu/oxauth/model/uma/JsonLogicTest.java | 71 + .../org/gluu/oxauth/model/uma/TestUtil.java | 45 + .../gluu/oxauth/model/uma/UmaTestUtil.java | 107 + .../gluu/oxauth/model/util/CertUtilsTest.java | 49 + .../gluu/oxauth/model/util/HashUtilTest.java | 43 + .../gluu/oxauth/model/util/JwtUtilTest.java | 28 + .../org/gluu/oxauth/model/util/Tester.java | 17 + .../oxauth/model/util/URLPatternListTest.java | 54 + .../src/test/resources/json-logic-node.json | 18 + .../src/test/resources/testng-benchmark.xml | 4 + .../src/test/resources/testng-multi-authz.xml | 4 + oxAuth/Model/src/test/resources/testng.xml | 26 + oxAuth/README | 221 + oxAuth/README.md | 9 + oxAuth/Server/.gitignore | 4 + .../src/main/webapp/WEB-INF/.gitignore | 1 + oxAuth/Server/conf/gluu-couchbase.properties | 27 + oxAuth/Server/conf/gluu-ldap.properties | 5 + oxAuth/Server/conf/gluu-spanner.properties | 25 + oxAuth/Server/conf/gluu-sql.properties | 25 + oxAuth/Server/conf/gluu.properties | 8 + oxAuth/Server/conf/keystore.jks | Bin 0 -> 22029 bytes oxAuth/Server/conf/oxauth-config.json | 341 + oxAuth/Server/conf/oxauth-errors.json | 454 + oxAuth/Server/conf/oxauth-static-conf.json | 22 + oxAuth/Server/conf/oxauth-web-keys.json | 139 + oxAuth/Server/conf/salt | 1 + .../inwebo/InWeboExternalAuthenticator.py | 234 + .../oneid/OneIdExternalAuthenticator.py | 236 + .../oneid/docs/workflow.png | Bin 0 -> 37612 bytes .../oneid/docs/workflow.txt | 13 + .../oneid/lib/oneid.py | 145 + .../oneid/lib/stringprep.py | 272 + .../oxpush/oxPushExternalAuthenticator.py | 307 + .../PhoneFactorExternalAuthenticator.py | 200 + .../2.13/PhoneFactorSDK-2.13.jar | Bin 0 -> 36294 bytes .../2.13/PhoneFactorSDK-2.13.pom | 9 + .../PhoneFactorSDK/maven-metadata-local.xml | 12 + .../toopher/ToopherExternalAuthenticator.py | 276 + .../ToopherSDK/1.0.0/ToopherSDK-1.0.0.jar | Bin 0 -> 8589 bytes .../ToopherSDK/1.0.0/ToopherSDK-1.0.0.pom | 9 + .../ToopherSDK/maven-metadata-local.xml | 12 + .../toopher/sdk/pom.xml | 105 + .../com/toopher/AuthenticationStatus.java | 72 + .../main/java/com/toopher/PairingStatus.java | 49 + .../main/java/com/toopher/RequestError.java | 31 + .../src/main/java/com/toopher/ToopherAPI.java | 220 + .../integrations.deprecatred/wikid/README.txt | 37 + .../wikid/WikidExternalAuthenticator.py | 218 + .../Migration_stepts_to_3.1.x.txt | 72 + .../integrations/Migration_stepts_to_4.0.txt | 6 + .../Server/integrations/ThumbSignIn/README.md | 7 + .../ThumbSignInExternalAuthenticator.py | 319 + .../Server/integrations/acr_router/Readme.txt | 3 + .../acr_router/acr_router_authenticator.py | 68 + .../integrations/acr_saml_router/Readme.txt | 1 + .../acr_smal_router_authenticator.py | 82 + .../allowed_countries/allowed_countries.py | 155 + .../integrations/allowed_countries/readme.txt | 3 + .../authz/ConsentGatheringSample.py | 87 + .../integrations/authz/docs/Authz design.dia | Bin 0 -> 4879 bytes .../integrations/authz/docs/Authz design.png | Bin 0 -> 26731 bytes .../azuread/AzureADAuthenticationForGluu.py | 271 + oxAuth/Server/integrations/azuread/README.md | 13 + ...asicPassowrdUpdateExternalAuthenticator.py | 124 + .../auth/pwd/new-password.xhtml | 114 + .../BasicClientGroupExternalAuthenticator.py | 177 + .../basic.client_group/README.txt | 25 + .../basic.client_group/client_group.json | 16 + ...ExternalAuthenticatorWithExternalLogout.py | 91 + .../basic.external_logout/README.txt | 3 + .../LdapAuthConfExternalAuthenticator.py | 125 + .../basic.ldap_auth_confs/README.txt | 2 + .../BasicLockAccountExternalAuthenticator.py | 271 + .../basic.lock.account/README.txt | 15 + ...BasicMultiAuthConfExternalAuthenticator.py | 293 + .../basic.multi_auth_conf/INSTALLATION.txt | 35 + .../basic.multi_auth_conf/README.txt | 38 + .../BasicMultiLoginExternalAuthenticator.py | 122 + .../basic.multi_login/INSTALLATION.txt | 33 + .../integrations/basic.multi_login/README.txt | 13 + ...TestEmailAddressesExternalAuthenticator.py | 97 + .../README.txt | 5 + .../BasicOneSessionExternalAuthenticator.py | 118 + .../integrations/basic.one_session/README.txt | 1 + .../PasswordExpiration.py | 149 + .../basic.password_expiration/README.md | 2 + .../INSTALLATION.txt | 53 + .../PasswordChangeWithValidations.py | 166 + .../README.md | 4 + .../pwd/newpassword.xhtml | 60 + .../BasicRecaptchaExternalAuthenticator.py | 219 + .../integrations/basic.recaptcha/README.md | 15 + .../basic.recaptcha/cert_creds.json | 7 + .../basic.recaptcha/login-page.png | Bin 0 -> 227809 bytes .../BasicResetToStepExternalAuthenticator.py | 100 + .../basic.reset_to_step/README.txt | 5 + .../basic/BasicExternalAuthenticator.py | 88 + .../integrations/basic/INSTALLATION.txt | 27 + oxAuth/Server/integrations/basic/README.txt | 3 + .../integrations/basicMultiAuth_Duo/Readme.md | 38 + .../basicMultiAuth_Duo/basicmultiauthduo.py | 185 + .../bcrypt_ssha_migration/pwd_migration.py | 148 + .../bioid/BioIDExternalAuthenticator.py | 312 + oxAuth/Server/integrations/bioid/README.md | 72 + oxAuth/Server/integrations/bioid/README.txt | 72 + .../cas2/Cas2ExternalAuthenticator.py | 338 + .../cas2_duo/Cas2DuoExternalAuthenticator.py | 182 + oxAuth/Server/integrations/cas2_duo/Readme.md | 34 + .../cas2_duo/docs/joinded_flows.dia | Bin 0 -> 2240 bytes .../cas2_duo/docs/joinded_flows.jpg | Bin 0 -> 31807 bytes oxAuth/Server/integrations/casa/Casa.py | 673 + oxAuth/Server/integrations/casa/README.md | 52 + .../casa_external_user/casa_external.py | 896 + .../integrations/casa_external_user/readme.md | 25 + .../integrations/cert/Generate certs guide.md | 407 + .../cert/Generate certs without configs.md | 27 + .../cert/Quick certs guide for testing.md | 35 + oxAuth/Server/integrations/cert/README.txt | 22 + .../cert/UserCertExternalAuthenticator.py | 482 + .../integrations/cert/docs/Cert design.dia | Bin 0 -> 11048 bytes .../integrations/cert/docs/Cert design.jpg | Bin 0 -> 110176 bytes .../integrations/cert/sample/cert_creds.json | 7 + .../cert/sample/generated_certs.zip | Bin 0 -> 53946 bytes .../ciba/FirebaseEndUserNotification.py | 68 + .../compromised_password.py | 223 + .../compromised_password/readme.txt | 7 + .../ConsentGatheringSample_redirect.py | 87 + .../consent-gathering-with-redirection/README | 45 + .../postauthorize.xhtml | 14 + .../redirect.xhtml | 14 + .../third-party-mock-app/README.txt.txt | 6 + .../third-party-mock-app/consent_page.html | 45 + .../custom_registration/Attributes.json | 10 + .../custom_registration/README.md | 11 + .../custom_registration/reg.xhtml | 264 + .../custom_registration/register.py | 267 + ...essAuthenticationWithDeduceImpossTravel.py | 563 + .../README_PASSWORDLESS_AUTHN_WITH_DEDUCE.md | 111 + ...DuoUniversalPromptExternalAuthenticator.py | 177 + .../duo-universal-prompt/README.txt | 98 + .../Passport_and_Duo-Universal.py | 802 + ...Passport_and_Duo-Universal_ignore_empty.py | 641 + .../duo.passport.combine/readme.md | 76 + .../duo/DuoExternalAuthenticator.py | 239 + .../Server/integrations/duo/INSTALLATION.txt | 44 + oxAuth/Server/integrations/duo/README.txt | 21 + oxAuth/Server/integrations/duo/lib/duo_web.py | 175 + .../duo2_Gluu3/DUO_Universal_3.1.7.py | 177 + .../Server/integrations/duo2_Gluu3/readme.md | 83 + oxAuth/Server/integrations/email2FA/README.md | 55 + .../email2FA/email2FAExternalAuthenticator.py | 366 + .../integrations/email2FA/entertoken.xhtml | 92 + .../fido2/Fido2ExternalAuthenticator.py | 277 + .../integrations/forgot_password/README.md | 26 + .../forgot_password/forgot_password.py | 451 + .../fortinet/FortinetExternalAuthenticator.py | 127 + .../Server/integrations/fortinet/README.txt | 89 + .../gplus/GooglePlusExternalAuthenticator.py | 557 + oxAuth/Server/integrations/gplus/README.txt | 54 + .../gplus/sample/custom_script_entry.ldif | 22 + .../gplus/sample/gplus_client_secrets.json | 1 + oxAuth/Server/integrations/gplus/workflow.txt | 39 + .../integrations/idfirst/README_idfirst.md | 37 + oxAuth/Server/integrations/idfirst/idfirst.py | 98 + oxAuth/Server/integrations/inwebo/inwebo.py | 338 + .../Server/integrations/inwebo/iw_creds.json | 1 + oxAuth/Server/integrations/inwebo/iw_va.xhtml | 93 + .../integrations/inwebo/iwauthenticate.xhtml | 99 + .../Server/integrations/inwebo/iwlogin.xhtml | 94 + .../inwebo/iwlogin_without_password.xhtml | 91 + .../inwebo/iwpushnotification.xhtml | 90 + .../integrations/inwebo/oxauth.properties | 19 + .../integrations/new_acr_link/README.md | 34 + .../integrations/new_acr_link/new_acr_link.py | 148 + .../Server/integrations/otp/Installation.md | 27 + .../otp/OtpExternalAuthenticator.py | 617 + .../otp/Properties description.md | 23 + oxAuth/Server/integrations/otp/Readme.md | 5 + ...tp_integration_authentication_workflow.png | Bin 0 -> 64598 bytes .../0.0.1-SNAPSHOT/_remote.repositories | 6 + .../0.0.1-SNAPSHOT/maven-metadata-local.xml | 36 + ...keyprovisioning-0.0.1-SNAPSHOT-javadoc.jar | Bin 0 -> 71430 bytes ...keyprovisioning-0.0.1-SNAPSHOT-sources.jar | Bin 0 -> 12997 bytes ...ath-otp-keyprovisioning-0.0.1-SNAPSHOT.jar | Bin 0 -> 14268 bytes ...ath-otp-keyprovisioning-0.0.1-SNAPSHOT.pom | 40 + .../maven-metadata-local.xml | 11 + .../0.0.1-SNAPSHOT/_remote.repositories | 6 + .../0.0.1-SNAPSHOT/maven-metadata-local.xml | 36 + .../oath-otp-0.0.1-SNAPSHOT-javadoc.jar | Bin 0 -> 68069 bytes .../oath-otp-0.0.1-SNAPSHOT-sources.jar | Bin 0 -> 13536 bytes .../oath-otp-0.0.1-SNAPSHOT.jar | Bin 0 -> 12279 bytes .../oath-otp-0.0.1-SNAPSHOT.pom | 19 + .../oath/oath-otp/maven-metadata-local.xml | 11 + .../0.0.1-SNAPSHOT/_remote.repositories | 3 + .../0.0.1-SNAPSHOT/maven-metadata-local.xml | 19 + .../oath-parent-0.0.1-SNAPSHOT.pom | 182 + .../oath/oath-parent/maven-metadata-local.xml | 11 + .../otp/sample/otp_configuration.json | 13 + .../integrations/otp/sequence_diagram.txt | 31 + .../passport/PassportExternalAuthenticator.py | 631 + oxAuth/Server/integrations/passport/README.md | 10 + .../sample/passport_script_entry.ldif | 16 + .../PasswordlessAuthentication.py | 445 + .../integrations/passwordless/README.md | 3 + .../integrations/passwordless/bundle.zip | Bin 0 -> 428691 bytes .../passwurd/PasswurdAuthentication.py | 894 + oxAuth/Server/integrations/passwurd/README.md | 88 + .../integrations/passwurd/bundle-bak.zip | Bin 0 -> 474337 bytes .../Server/integrations/passwurd/bundle.zip | Bin 0 -> 447342 bytes .../oxauth/custom/i18n/oxauth.properties | 9 + .../custom/pages/passwurd/enterPwd.xhtml | 69 + .../pages/passwurd/login-template.xhtml | 82 + .../oxauth/custom/pages/passwurd/login.xhtml | 133 + .../custom/pages/passwurd/savePwd.xhtml | 98 + .../passwurd/font-awesome-5.12.1.all.min.js | 5 + .../custom/static/passwurd/logger_pwd.js | 20 + .../custom/static/passwurd/logger_username.js | 21 + .../oxauth/custom/static/passwurd/style.css | 210 + .../custom/static/passwurd/tachyons.min.css | 3 + .../python/libs/passwurd-external_bioid.py | 338 + .../python/libs/passwurd-external_fido2.py | 250 + .../gluu/python/libs/passwurd-external_otp.py | 590 + .../libs/passwurd-external_super_gluu.py | 1084 + oxAuth/Server/integrations/pingid/README.md | 3 + oxAuth/Server/integrations/pingid/bundle.zip | Bin 0 -> 6591 bytes oxAuth/Server/integrations/pingid/flow.png | Bin 0 -> 73332 bytes .../integrations/pingid/oxauth-pingid-1.0.jar | Bin 0 -> 14556 bytes .../pingid/pingIDAuthenticator.py | 329 + oxAuth/Server/integrations/pingid/pom.xml | 73 + .../org/gluu/oxauth/ping/HttpException.java | 31 + .../gluu/oxauth/ping/PPMRequestBroker.java | 66 + .../org/gluu/oxauth/ping/RequestToken.java | 84 + .../gluu/oxauth/ping/ResponseTokenParser.java | 49 + .../oxauth/ping/TokenProcessingException.java | 9 + .../gluu/oxauth/ping/UserManagerBroker.java | 166 + .../main/java/org/gluu/oxauth/ping/Utils.java | 96 + .../integrations/postauthn/postauthn.py | 42 + .../Server/integrations/privacyidea/README.md | 36 + .../pages/auth/privacyidea/privacyidea.xhtml | 98 + .../integrations/privacyidea/privacyidea.py | 338 + .../Server/integrations/registration/read.txt | 6 + .../integrations/registration/register.py | 182 + .../SamlPassportAuthenticator.py | 827 + .../saml.alias.attribute/README.md | 50 + .../saml.alias.attribute/idp_script.py | 148 + .../Gluu Inbound SAML Design (no Session).png | Bin 0 -> 52535 bytes .../Server/integrations/saml/INSTALLATION.txt | 33 + oxAuth/Server/integrations/saml/README.txt | 69 + .../saml/SamlExternalAuthenticator.py | 793 + .../saml2oidc_acr_router/README.md | 14 + .../saml2oidc_acr_router.py | 122 + oxAuth/Server/integrations/smpp/smpp2FA.py | 434 + oxAuth/Server/integrations/stytch/README.md | 104 + .../stytch/stytchExternalAuthenticator.py | 378 + .../SuperGluuExternalAuthenticator.py | 1164 + .../1.0.1.gluu/gcm-server-1.0.1.gluu.jar | Bin 0 -> 28229 bytes .../1.0.1.gluu/gcm-server-1.0.1.gluu.jar.md5 | 1 + .../1.0.1.gluu/gcm-server-1.0.1.gluu.jar.sha1 | 1 + .../1.0.1.gluu/gcm-server-1.0.1.gluu.pom | 8 + .../1.0.1.gluu/gcm-server-1.0.1.gluu.pom.md5 | 1 + .../1.0.1.gluu/gcm-server-1.0.1.gluu.pom.sha1 | 1 + .../google/gcm/gcm-server/maven-metadata.xml | 12 + .../gcm/gcm-server/maven-metadata.xml.md5 | 1 + .../gcm/gcm-server/maven-metadata.xml.sha1 | 1 + .../super_gluu/sample/super_gluu_creds.json | 45 + .../Server/integrations/twilio_sms/README.txt | 7 + .../integrations/twilio_sms/twilio2FA.py | 254 + .../u2f/U2fExternalAuthenticator.py | 214 + .../Server/integrations/uaf/Installation.md | 48 + .../uaf/Properties description.md | 20 + oxAuth/Server/integrations/uaf/Readme.md | 51 + .../uaf/UafExternalAuthenticator.py | 398 + ...af_integration_authentication_workflow.png | Bin 0 -> 59443 bytes .../integrations/uaf/img/oob_qr_code.png | Bin 0 -> 77419 bytes .../uaf/img/typical_uaf_architecture.png | Bin 0 -> 11696 bytes .../uaf/img/uaf_device_integration_models.png | Bin 0 -> 31811 bytes .../integrations/uaf/sequence_diagram.txt | 25 + oxAuth/Server/integrations/whispeak/README.md | 73 + .../integrations/whispeak/whispeak_open_v1.py | 1326 + .../integrations/wwpass/INSTALLATION.md | 244 + oxAuth/Server/integrations/wwpass/README.md | 19 + .../wwpass/pages/auth/wwpass/checkemail.xhtml | 163 + .../wwpass/pages/auth/wwpass/wwpass.xhtml | 285 + .../wwpass/pages/auth/wwpass/wwpassbind.xhtml | 329 + .../wwpass/static/js/wwpass-frontend.js | 6097 ++++ oxAuth/Server/integrations/wwpass/ticket.json | 9 + .../Server/integrations/wwpass/wwpass.ca.crt | 35 + oxAuth/Server/integrations/wwpass/wwpass.py | 235 + .../Server/integrations/wwpass/wwpassauth.py | 289 + .../Server/integrations/yubicloud/README.txt | 35 + .../YubicloudExternalAuthenticator.py | 118 + oxAuth/Server/pom.xml | 1263 + oxAuth/Server/profiles/.gitignore | 5 + .../profiles/default/client_keystore.pkcs12 | Bin 0 -> 23345 bytes .../profiles/default/config-build.properties | 7 + .../config-oxauth-test-data.properties | 34 + .../default/config-oxauth-test.properties | 19 + .../profiles/default/config-oxauth.properties | 17 + oxAuth/Server/src-remove/pages.xml | 55 + .../src/main/docs/mustache/markdown.mustache | 112 + .../oxauth/audit/ApplicationAuditLogger.java | 205 + .../audit/debug/ServletLoggingFilter.java | 143 + .../audit/debug/entity/HttpRequest.java | 75 + .../audit/debug/entity/HttpResponse.java | 27 + .../audit/debug/wrapper/RequestWrapper.java | 148 + .../audit/debug/wrapper/ResponseWrapper.java | 31 + .../oxauth/auth/AuthenticationFilter.java | 534 + .../org/gluu/oxauth/auth/Authenticator.java | 826 + .../org/gluu/oxauth/auth/MTLSService.java | 138 + .../gluu/oxauth/auth/SelectAccountAction.java | 399 + .../authorize/ws/rs/AuthorizeAction.java | 951 + .../ws/rs/AuthorizeRestWebService.java | 194 + .../ws/rs/AuthorizeRestWebServiceImpl.java | 1056 + .../rs/AuthorizeRestWebServiceValidator.java | 315 + .../ws/rs/ConsentGathererService.java | 320 + .../ws/rs/ConsentGatheringSessionService.java | 218 + .../ws/rs/DeviceAuthorizationAction.java | 367 + .../rs/DeviceAuthorizationRestWebService.java | 48 + ...DeviceAuthorizationRestWebServiceImpl.java | 159 + .../oxauth/authorize/ws/rs/LoginAction.java | 31 + .../oxauth/authorize/ws/rs/LogoutAction.java | 302 + .../BackchannelAuthorizeRestWebService.java | 46 + ...ackchannelAuthorizeRestWebServiceImpl.java | 349 + ...annelDeviceRegistrationRestWebService.java | 36 + ...lDeviceRegistrationRestWebServiceImpl.java | 118 + .../ws/rs/CIBAAuthorizeAction.java | 89 + .../CIBAAuthorizeParamsValidatorService.java | 143 + .../oxauth/ciba/CIBAConfigurationService.java | 58 + ...IBADeviceRegistrationValidatorService.java | 47 + .../ciba/CIBAEndUserNotificationService.java | 99 + .../oxauth/ciba/CIBAPingCallbackService.java | 44 + .../oxauth/ciba/CIBAPushErrorService.java | 42 + .../ciba/CIBAPushTokenDeliveryService.java | 45 + .../CIBARegisterClientMetadataService.java | 47 + .../CIBARegisterClientResponseService.java | 39 + .../CIBARegisterParamsValidatorService.java | 126 + .../ws/rs/ClientInfoRestWebService.java | 37 + .../ws/rs/ClientInfoRestWebServiceImpl.java | 135 + .../oxauth/crypto/cert/CertificateParser.java | 56 + .../crypto/random/ChallengeGenerator.java | 12 + .../random/RandomChallengeGenerator.java | 25 + .../SHA256withECDSASignatureVerification.java | 85 + .../signature/SignatureVerification.java | 26 + .../exception/GlobalExceptionHandler.java | 72 + .../GlobalExceptionHandlerFactory.java | 20 + .../InvalidSchemaUpdateException.java | 28 + .../oxauth/exception/UncaughtException.java | 66 + .../fido/u2f/BadConfigurationException.java | 20 + .../fido/u2f/DeviceCompromisedException.java | 30 + .../u2f/InvalidDeviceCounterException.java | 18 + .../u2f/InvalidKeyHandleDeviceException.java | 21 + .../fido/u2f/NoEligableDevicesException.java | 37 + .../org/gluu/oxauth/filter/CorsFilter.java | 168 + .../gluu/oxauth/filter/CorsFilterConfig.java | 119 + .../gluu/ws/rs/GluuConfigurationWS.java | 124 + ...ationFacesLocalizationConfigPopulator.java | 13 + .../oxauth/i18n/CustomResourceBundle.java | 20 + .../org/gluu/oxauth/i18n/LanguageBean.java | 175 + .../gluu/oxauth/idgen/ws/rs/IdGenService.java | 52 + .../oxauth/idgen/ws/rs/InumGenerator.java | 154 + .../ws/rs/IntrospectionWebService.java | 307 + .../oxauth/jwk/ws/rs/JwkRestWebService.java | 49 + .../jwk/ws/rs/JwkRestWebServiceImpl.java | 73 + .../gluu/oxauth/model/GluuOrganization.java | 232 + .../org/gluu/oxauth/model/audit/Action.java | 39 + .../oxauth/model/audit/OAuth2AuditLog.java | 86 + .../oxauth/model/auth/AuthenticationMode.java | 32 + .../authorize/AuthorizeParamsValidator.java | 85 + .../gluu/oxauth/model/authorize/Claim.java | 37 + .../oxauth/model/authorize/ClaimValue.java | 124 + .../model/authorize/ClaimValueType.java | 18 + .../oxauth/model/authorize/IdTokenMember.java | 83 + .../authorize/JwtAuthorizationRequest.java | 526 + .../oxauth/model/authorize/ScopeChecker.java | 94 + .../model/authorize/UserInfoMember.java | 68 + .../ClientInfoErrorResponseType.java | 46 + .../clientinfo/ClientInfoParamsValidator.java | 25 + .../common/AbstractAuthorizationGrant.java | 502 + .../oxauth/model/common/AbstractToken.java | 291 + .../gluu/oxauth/model/common/AccessToken.java | 73 + .../model/common/AuthorizationCode.java | 94 + .../model/common/AuthorizationCodeGrant.java | 89 + .../model/common/AuthorizationGrant.java | 480 + .../model/common/AuthorizationGrantList.java | 369 + .../model/common/AuthorizationGrantType.java | 113 + .../gluu/oxauth/model/common/CIBAGrant.java | 65 + .../gluu/oxauth/model/common/CacheGrant.java | 328 + .../model/common/CibaRequestCacheControl.java | 173 + .../model/common/CibaRequestStatus.java | 42 + .../model/common/ClientCredentialsGrant.java | 56 + .../oxauth/model/common/ClientTokens.java | 52 + .../oxauth/model/common/DefaultScope.java | 49 + .../DeviceAuthorizationCacheControl.java | 133 + .../common/DeviceAuthorizationStatus.java | 27 + .../oxauth/model/common/DeviceCodeGrant.java | 54 + .../oxauth/model/common/ExecutionContext.java | 107 + .../model/common/IAuthorizationGrant.java | 124 + .../model/common/IAuthorizationGrantList.java | 51 + .../org/gluu/oxauth/model/common/IdToken.java | 23 + .../oxauth/model/common/ImplicitGrant.java | 76 + .../oxauth/model/common/RefreshToken.java | 70 + ...ResourceOwnerPasswordCredentialsGrant.java | 56 + .../oxauth/model/common/SessionTokens.java | 53 + .../common/SimpleAuthorizationGrant.java | 13 + .../UnmodifiableAuthorizationGrant.java | 263 + .../org/gluu/oxauth/model/config/Conf.java | 109 + .../model/config/ConfigurationFactory.java | 578 + .../gluu/oxauth/model/config/Constants.java | 36 + .../model/config/WebKeysConfiguration.java | 24 + ...OpenIdConnectDiscoveryParamsValidator.java | 17 + .../oxauth/model/error/ErrorMessageList.java | 36 + .../oxauth/model/error/ErrorMessages.java | 139 + .../model/error/ErrorResponseFactory.java | 176 + .../oxauth/model/error/JsonErrorResponse.java | 83 + .../model/exception/AcrChangedException.java | 40 + .../InvalidSessionStateException.java | 21 + .../exception/InvalidStateException.java | 21 + .../u2f/AuthenticateRequestMessageLdap.java | 59 + .../model/fido/u2f/DeviceRegistration.java | 327 + .../u2f/DeviceRegistrationConfiguration.java | 53 + .../fido/u2f/DeviceRegistrationResult.java | 50 + .../fido/u2f/RegisterRequestMessageLdap.java | 59 + .../model/fido/u2f/RequestMessageLdap.java | 134 + .../gluu/oxauth/model/ldap/CIBARequest.java | 118 + .../model/ldap/ClientAuthorization.java | 123 + .../gluu/oxauth/model/ldap/SchemaEntry.java | 99 + .../oxauth/model/ldap/TokenAttributes.java | 47 + .../org/gluu/oxauth/model/ldap/TokenLdap.java | 314 + .../org/gluu/oxauth/model/ldap/TokenType.java | 42 + .../org/gluu/oxauth/model/ldap/UserGroup.java | 100 + .../registration/RegisterParamsValidator.java | 451 + .../oxauth/model/session/SessionClient.java | 47 + .../oxauth/model/token/ClientAssertion.java | 146 + .../model/token/HandleTokenFactory.java | 44 + .../oxauth/model/token/HttpAuthTokenType.java | 19 + .../oxauth/model/token/IdTokenFactory.java | 364 + .../gluu/oxauth/model/token/JwrService.java | 163 + .../gluu/oxauth/model/token/JwtSigner.java | 112 + .../oxauth/model/token/PersistentJwt.java | 316 + .../model/token/TokenParamsValidator.java | 84 + .../token/ValidateTokenParamsValidator.java | 26 + .../userinfo/UserInfoParamsValidator.java | 25 + .../ws/rs/RegisterRestWebService.java | 106 + .../ws/rs/RegisterRestWebServiceImpl.java | 1092 + .../oxauth/revoke/RevokeRestWebService.java | 46 + .../revoke/RevokeRestWebServiceImpl.java | 152 + .../revoke/RevokeSessionRestWebService.java | 113 + .../org/gluu/oxauth/security/Identity.java | 59 + .../gluu/oxauth/service/AppInitializer.java | 715 + .../gluu/oxauth/service/AttributeService.java | 114 + .../service/AuthenticationFilterService.java | 76 + .../AuthenticationProtectionService.java | 64 + .../oxauth/service/AuthenticationService.java | 868 + .../gluu/oxauth/service/AuthorizeService.java | 327 + .../oxauth/service/BaseAuthFilterService.java | 283 + .../org/gluu/oxauth/service/CleanerTimer.java | 230 + .../service/ClientAuthorizationsService.java | 173 + .../oxauth/service/ClientFilterService.java | 53 + .../gluu/oxauth/service/ClientService.java | 355 + .../gluu/oxauth/service/CookieService.java | 333 + .../CryptoProviderProviderFactory.java | 48 + .../service/DeviceAuthorizationService.java | 183 + .../oxauth/service/ErrorHandlerService.java | 95 + .../org/gluu/oxauth/service/GrantService.java | 374 + .../oxauth/service/KeyGeneratorTimer.java | 180 + ...tomAuthenticationConfigurationService.java | 112 + .../oxauth/service/LocalResponseCache.java | 99 + .../gluu/oxauth/service/MetricService.java | 93 + .../oxauth/service/OrganizationService.java | 65 + .../service/OxAuthConfigurationService.java | 65 + .../service/PairwiseIdentifierService.java | 130 + .../oxauth/service/RedirectUriResponse.java | 71 + .../oxauth/service/RedirectionUriService.java | 262 + .../service/RequestParameterService.java | 281 + .../oxauth/service/ResteasyInitializer.java | 14 + .../org/gluu/oxauth/service/ScopeService.java | 327 + .../service/SectorIdentifierService.java | 150 + .../oxauth/service/ServerCryptoProvider.java | 97 + .../gluu/oxauth/service/SessionIdService.java | 1028 + .../service/SpontaneousScopeService.java | 121 + .../gluu/oxauth/service/UserGroupService.java | 91 + .../org/gluu/oxauth/service/UserService.java | 90 + .../cdi/event/AuthConfigurationEvent.java | 7 + .../service/cdi/event/ExpirationEvent.java | 7 + .../service/cdi/event/KeyGenerationEvent.java | 7 + .../service/cdi/event/ReloadAuthScript.java | 33 + .../oxauth/service/cdi/event/StatEvent.java | 7 + .../service/ciba/CibaEncryptionService.java | 82 + .../service/ciba/CibaRequestService.java | 217 + .../ciba/CibaRequestsProcessorJob.java | 179 + .../service/custom/CustomScriptService.java | 45 + .../service/date/DateFormatterService.java | 59 + .../gluu/oxauth/service/expiration/ExpId.java | 39 + .../oxauth/service/expiration/ExpType.java | 8 + .../ExpirationNotificatorTimer.java | 174 + .../ExternalApplicationSessionService.java | 112 + .../ExternalAuthenticationService.java | 561 + ...xternalCibaEndUserNotificationService.java | 60 + .../ExternalConsentGatheringService.java | 136 + ...ernalDynamicClientRegistrationService.java | 127 + .../external/ExternalDynamicScopeService.java | 119 + .../external/ExternalEndSessionService.java | 54 + .../external/ExternalIdGeneratorService.java | 51 + .../ExternalIntrospectionService.java | 101 + .../external/ExternalPostAuthnService.java | 96 + ...sourceOwnerPasswordCredentialsService.java | 69 + .../external/ExternalRevokeTokenService.java | 52 + .../ExternalSpontaneousScopeService.java | 60 + .../ExternalUmaClaimsGatheringService.java | 168 + .../external/ExternalUmaRptClaimsService.java | 61 + .../external/ExternalUmaRptPolicyService.java | 148 + .../external/ExternalUpdateTokenService.java | 278 + .../context/ConsentGatheringContext.java | 128 + .../DynamicClientRegistrationContext.java | 56 + .../context/DynamicScopeExternalContext.java | 68 + .../external/context/EndSessionContext.java | 56 + ...xternalCibaEndUserNotificationContext.java | 63 + .../context/ExternalIntrospectionContext.java | 82 + .../context/ExternalPostAuthnContext.java | 49 + ...sourceOwnerPasswordCredentialsContext.java | 67 + .../context/ExternalScriptContext.java | 105 + .../context/ExternalUmaRptClaimsContext.java | 56 + .../context/ExternalUpdateTokenContext.java | 131 + .../external/context/RevokeTokenContext.java | 54 + .../SpontaneousScopeExternalContext.java | 57 + ...ternalDefaultPersonAuthenticationType.java | 59 + .../external/session/SessionEvent.java | 66 + .../external/session/SessionEventType.java | 11 + .../service/fido/u2f/ApplicationService.java | 77 + .../fido/u2f/AuthenticationService.java | 338 + .../fido/u2f/ClientDataValidationService.java | 80 + .../fido/u2f/DeviceRegistrationService.java | 241 + .../fido/u2f/RawAuthenticationService.java | 86 + .../fido/u2f/RawRegistrationService.java | 111 + .../service/fido/u2f/RegistrationService.java | 225 + .../service/fido/u2f/RequestService.java | 55 + .../fido/u2f/UserSessionIdService.java | 88 + .../service/fido/u2f/ValidationService.java | 88 + .../service/fido/u2f/util/KeyGenerator.java | 65 + .../oxauth/service/logger/LoggerService.java | 42 + .../gluu/oxauth/service/net/HttpService.java | 285 + .../gluu/oxauth/service/net/HttpService2.java | 44 + .../oxauth/service/push/sns/PushPlatform.java | 25 + .../service/push/sns/PushSnsService.java | 114 + .../gluu/oxauth/service/stat/StatService.java | 283 + .../gluu/oxauth/service/stat/StatTimer.java | 96 + .../service/status/ldap/LdapStatusTimer.java | 117 + .../oxauth/service/token/TokenService.java | 120 + .../servlet/BcFirebaseMessagingSwServlet.java | 62 + .../oxauth/servlet/OpenIdConfiguration.java | 480 + .../oxauth/servlet/OxAuthFaviconServlet.java | 79 + .../oxauth/servlet/OxAuthLogoServlet.java | 78 + .../gluu/oxauth/servlet/SectorIdentifier.java | 82 + .../org/gluu/oxauth/servlet/WebFinger.java | 135 + .../CheckSessionStatusRestWebServiceImpl.java | 116 + .../ws/rs/EndSessionRestWebService.java | 41 + .../ws/rs/EndSessionRestWebServiceImpl.java | 614 + .../oxauth/session/ws/rs/EndSessionUtils.java | 107 + .../session/ws/rs/LogoutTokenFactory.java | 88 + .../token/ws/rs/TokenRestWebService.java | 71 + .../token/ws/rs/TokenRestWebServiceImpl.java | 752 + .../gluu/oxauth/uma/authorization/Claims.java | 74 + .../IPolicyExternalAuthorization.java | 17 + .../PolicyExternalAuthorizationEnum.java | 27 + .../UmaAuthorizationContext.java | 220 + .../UmaAuthorizationContextBuilder.java | 72 + .../uma/authorization/UmaGatherContext.java | 205 + .../gluu/oxauth/uma/authorization/UmaPCT.java | 86 + .../gluu/oxauth/uma/authorization/UmaRPT.java | 99 + .../uma/authorization/UmaScriptByScope.java | 46 + .../uma/authorization/UmaWebException.java | 63 + .../uma/service/RedirectParameters.java | 65 + .../uma/service/UmaExpressionService.java | 173 + .../gluu/oxauth/uma/service/UmaGatherer.java | 263 + .../uma/service/UmaNeedsInfoService.java | 148 + .../oxauth/uma/service/UmaPctService.java | 195 + .../uma/service/UmaPermissionService.java | 188 + .../uma/service/UmaResourceService.java | 208 + .../oxauth/uma/service/UmaRptService.java | 317 + .../oxauth/uma/service/UmaScopeService.java | 228 + .../oxauth/uma/service/UmaSessionService.java | 225 + .../oxauth/uma/service/UmaTokenService.java | 150 + .../uma/service/UmaValidationService.java | 493 + .../gluu/oxauth/uma/ws/rs/UmaGatheringWS.java | 182 + .../gluu/oxauth/uma/ws/rs/UmaMetadataWS.java | 89 + .../ws/rs/UmaPermissionRegistrationWS.java | 117 + .../uma/ws/rs/UmaResourceRegistrationWS.java | 345 + .../uma/ws/rs/UmaRptIntrospectionWS.java | 196 + .../gluu/oxauth/uma/ws/rs/UmaScopeIconWS.java | 60 + .../org/gluu/oxauth/uma/ws/rs/UmaScopeWS.java | 64 + .../ws/rs/UserInfoRestWebService.java | 41 + .../ws/rs/UserInfoRestWebServiceImpl.java | 435 + .../java/org/gluu/oxauth/util/CertUtil.java | 72 + .../gluu/oxauth/util/PasswordValidator.java | 71 + .../gluu/oxauth/util/QueryStringDecoder.java | 66 + .../org/gluu/oxauth/util/RedirectUtil.java | 72 + .../java/org/gluu/oxauth/util/ServerUtil.java | 299 + .../org/gluu/oxauth/util/TokenHashUtil.java | 32 + .../rs/controller/HealthCheckController.java | 52 + .../ws/rs/fido/u2f/U2fAuthenticationWS.java | 210 + .../ws/rs/fido/u2f/U2fConfigurationWS.java | 72 + .../ws/rs/fido/u2f/U2fRegistrationWS.java | 247 + .../gluu/oxauth/ws/rs/stat/StatResponse.java | 35 + .../oxauth/ws/rs/stat/StatResponseItem.java | 43 + .../org/gluu/oxauth/ws/rs/stat/StatWS.java | 340 + .../src/main/resources/META-INF/beans.xml | 7 + .../META-INF/navigation/cas2.navigation.xml | 41 + .../META-INF/navigation/cert.navigation.xml | 53 + .../META-INF/navigation/duo.navigation.xml | 24 + .../META-INF/navigation/gplus.navigation.xml | 41 + .../META-INF/navigation/otp.navigation.xml | 42 + .../META-INF/navigation/oxpush.navigation.xml | 58 + .../navigation/passport.navigation.xml | 47 + .../META-INF/navigation/saml.navigation.xml | 41 + .../navigation/super-gluu.navigation.xml | 24 + .../META-INF/navigation/u2f.navigation.xml | 24 + .../META-INF/navigation/uaf.navigation.xml | 24 + .../navigation/uma2.sample.navigation.xml | 40 + ...lication.ApplicationConfigurationPopulator | 1 + oxAuth/Server/src/main/resources/ehcache.xml | 141 + .../src/main/resources/faces-config.xml | 7 + oxAuth/Server/src/main/resources/log4j2.xml | 174 + .../src/main/resources/oxauth.properties | 410 + .../src/main/resources/oxauth_bg.properties | 204 + .../src/main/resources/oxauth_de.properties | 210 + .../src/main/resources/oxauth_en.properties | 341 + .../src/main/resources/oxauth_es.properties | 153 + .../src/main/resources/oxauth_fr.properties | 270 + .../src/main/resources/oxauth_it.properties | 204 + .../src/main/resources/oxauth_ru.properties | 227 + .../src/main/resources/oxauth_tr.properties | 206 + .../src/main/resources/quartz.properties | 4 + .../resources/validation_messages.properties | 131 + .../main/webapp-jetty/WEB-INF/jetty-env.xml | 21 + .../src/main/webapp-jetty/WEB-INF/web.xml | 101 + .../main/webapp-tomcat/META-INF/context.xml | 18 + .../src/main/webapp/META-INF/MANIFEST.MF | 3 + .../src/main/webapp/WEB-INF/faces-config.xml | 23 + .../webapp/WEB-INF/firebase-messaging-sw.js | 51 + .../layout/authorize-extended-template.xhtml | 132 + .../incl/layout/authorize-template.xhtml | 54 + .../WEB-INF/incl/layout/bioid-template.xhtml | 25 + .../ciba-authorize-extended-template.xhtml | 102 + .../incl/layout/ciba-authorize-template.xhtml | 55 + .../incl/layout/login-extended-template.xhtml | 133 + .../WEB-INF/incl/layout/login-template.xhtml | 301 + .../webapp/WEB-INF/incl/layout/template.xhtml | 48 + .../incl/layout/whispeak-open-template.xhtml | 37 + .../main/webapp/WEB-INF/static/favicon.ico | Bin 0 -> 19608 bytes .../src/main/webapp/WEB-INF/static/logo.png | Bin 0 -> 3544 bytes .../src/main/webapp/auth/bioid/bioid.xhtml | 1045 + .../src/main/webapp/auth/bioid/css/uui.css | 764 + .../main/webapp/auth/bioid/images/back.svg | 3 + .../main/webapp/auth/bioid/images/logo.svg | 1 + .../main/webapp/auth/bioid/images/perfect.png | Bin 0 -> 48739 bytes .../webapp/auth/bioid/images/tooclose.png | Bin 0 -> 49740 bytes .../webapp/auth/bioid/images/toofaraway.png | Bin 0 -> 47230 bytes .../main/webapp/auth/bioid/js/bws.capture.js | 596 + .../main/webapp/auth/bioid/js/getUserMedia.js | 29 + .../webapp/auth/bioid/js/getUserMedia.min.js | 1 + .../webapp/auth/bioid/js/jquery-3.5.1.min.js | 2 + .../main/webapp/auth/bioid/js/objLoader.js | 791 + .../webapp/auth/bioid/js/objLoader.min.js | 1 + .../main/webapp/auth/bioid/js/three.min.js | 927 + .../src/main/webapp/auth/bioid/js/uui.js | 772 + .../main/webapp/auth/bioid/language/de.json | 53 + .../main/webapp/auth/bioid/language/en.json | 53 + .../src/main/webapp/auth/bioid/model/head.obj | 9857 +++++++ .../webapp/auth/bioid/video/nodyourhead.mp4 | Bin 0 -> 2025294 bytes .../src/main/webapp/auth/cas2/cas2login.xhtml | 10 + .../main/webapp/auth/cas2/cas2postlogin.xhtml | 90 + .../main/webapp/auth/cert/cert-invalid.xhtml | 108 + .../main/webapp/auth/cert/cert-login.xhtml | 13 + .../webapp/auth/cert/cert-not-selected.xhtml | 110 + .../src/main/webapp/auth/cert/login.xhtml | 150 + .../webapp/auth/compromised/complogin.xhtml | 87 + .../webapp/auth/compromised/newpassword.xhtml | 117 + .../src/main/webapp/auth/deduce/loginD.xhtml | 144 + .../src/main/webapp/auth/duo/duologin.xhtml | 61 + .../main/webapp/auth/duo/js/Duo-Web-v2.min.js | 1 + .../webapp/auth/email_auth/entertoken.xhtml | 92 + .../src/main/webapp/auth/fido2/js/base64js.js | 1 + .../main/webapp/auth/fido2/js/base64url.js | 64 + .../src/main/webapp/auth/fido2/js/webauthn.js | 169 + .../src/main/webapp/auth/fido2/login.xhtml | 195 + .../src/main/webapp/auth/fido2/platform.xhtml | 214 + .../src/main/webapp/auth/fido2/secKeys.xhtml | 209 + .../src/main/webapp/auth/fido2/step1.xhtml | 198 + .../auth/forgot_password/entertoken.xhtml | 71 + .../webapp/auth/forgot_password/forgot.xhtml | 95 + .../auth/forgot_password/newpassword.xhtml | 72 + .../main/webapp/auth/gplus/gpluslogin.xhtml | 202 + .../webapp/auth/gplus/gpluspostlogin.xhtml | 90 + .../webapp/auth/idfirst/alter_login.xhtml | 156 + .../webapp/auth/idfirst/idfirst_login.xhtml | 89 + .../webapp/auth/inwebo/iwauthenticate.xhtml | 78 + .../src/main/webapp/auth/inwebo/iwlogin.xhtml | 132 + .../main/webapp/auth/oneid/oneidlogin.xhtml | 79 + .../webapp/auth/oneid/oneidpostlogin.xhtml | 91 + .../src/main/webapp/auth/otp/enroll.xhtml | 131 + .../src/main/webapp/auth/otp/otplogin.xhtml | 75 + .../main/webapp/auth/otp_sms/otp_sms.xhtml | 152 + .../webapp/auth/oxpush/oxauthenticate.xhtml | 83 + .../src/main/webapp/auth/oxpush/oxlogin.xhtml | 122 + .../src/main/webapp/auth/oxpush/oxpair.xhtml | 95 + .../main/webapp/auth/passport/img/apple.png | Bin 0 -> 8171 bytes .../main/webapp/auth/passport/img/dropbox.png | Bin 0 -> 1678 bytes .../webapp/auth/passport/img/facebook.png | Bin 0 -> 4822 bytes .../main/webapp/auth/passport/img/github.png | Bin 0 -> 29968 bytes .../main/webapp/auth/passport/img/google.png | Bin 0 -> 105019 bytes .../webapp/auth/passport/img/linkedin.png | Bin 0 -> 33375 bytes .../auth/passport/img/openidconnect.png | Bin 0 -> 1670 bytes .../main/webapp/auth/passport/img/tumblr.png | Bin 0 -> 5048 bytes .../main/webapp/auth/passport/img/twitter.png | Bin 0 -> 41673 bytes .../webapp/auth/passport/img/windowslive.png | Bin 0 -> 1096 bytes .../webapp/auth/passport/passportlogin.xhtml | 344 + .../auth/passport/passportpostlogin.xhtml | 66 + .../auth/passport/sample-redirector.xhtml | 22 + .../webapp/auth/phonefactor/pflogin.xhtml | 81 + .../main/webapp/auth/pwd/newpassword.xhtml | 58 + .../main/webapp/auth/recaptcha/login.xhtml | 187 + .../main/webapp/auth/register/register.xhtml | 99 + .../src/main/webapp/auth/saml/samllogin.xhtml | 10 + .../main/webapp/auth/saml/samlpostlogin.xhtml | 90 + .../main/webapp/auth/super-gluu/login.xhtml | 273 + .../main/webapp/auth/thumbsignin/README.md | 3 + .../webapp/auth/thumbsignin/expired.xhtml | 96 + .../thumbsignin/js/authenticate-bundle.js | 23973 ++++++++++++++++ .../auth/thumbsignin/js/thumbsign_widget.js | 1076 + .../thumbsignin/js/thumbsignin_widget.css | 410 + .../webapp/auth/thumbsignin/tsLogin.xhtml | 374 + .../webapp/auth/thumbsignin/tsRegister.xhtml | 241 + .../thumbsignin/tsRegistrationSuccess.xhtml | 332 + .../webapp/auth/toopher/tpauthenticate.xhtml | 83 + .../src/main/webapp/auth/toopher/tppair.xhtml | 98 + .../src/main/webapp/auth/u2f/login.xhtml | 168 + .../main/webapp/auth/u2f/scripts/u2f-api.js | 830 + .../src/main/webapp/auth/uaf/js/uaf-api.js | 146 + .../src/main/webapp/auth/uaf/login.xhtml | 154 + .../whispeak/whispeak_open_ask_enroll.xhtml | 91 + .../whispeak_open_authentication_submit.xhtml | 183 + .../whispeak_open_enrollment_submit.xhtml | 170 + .../whispeak_open_identification.xhtml | 113 + .../whispeak/whispeak_open_passport.xhtml | 102 + .../whispeak_open_passport_fallback.xhtml | 101 + .../whispeak_open_passport_loading.xhtml | 13 + .../whispeak_open_revocation_data_show.xhtml | 74 + .../main/webapp/auth/wikid/wikidlogin.xhtml | 116 + .../webapp/auth/wikid/wikidregister.xhtml | 116 + oxAuth/Server/src/main/webapp/authorize.xhtml | 59 + .../src/main/webapp/authz/authorize.xhtml | 25 + .../src/main/webapp/authz/transaction.xhtml | 25 + .../src/main/webapp/casa/bioid-template.xhtml | 75 + .../Server/src/main/webapp/casa/bioid.xhtml | 1045 + oxAuth/Server/src/main/webapp/casa/casa.xhtml | 128 + oxAuth/Server/src/main/webapp/casa/cert.xhtml | 13 + .../src/main/webapp/casa/duologin.xhtml | 70 + .../Server/src/main/webapp/casa/fido2.xhtml | 122 + .../main/webapp/casa/fullwidth-template.xhtml | 75 + .../src/main/webapp/casa/login-template.xhtml | 90 + .../Server/src/main/webapp/casa/login.xhtml | 95 + oxAuth/Server/src/main/webapp/casa/otp.xhtml | 45 + .../src/main/webapp/casa/otp_email.xhtml | 44 + .../main/webapp/casa/otp_email_prompt.xhtml | 67 + .../Server/src/main/webapp/casa/otp_sms.xhtml | 44 + .../src/main/webapp/casa/otp_sms_prompt.xhtml | 66 + oxAuth/Server/src/main/webapp/casa/sg.xhtml | 107 + oxAuth/Server/src/main/webapp/casa/u2f.xhtml | 57 + .../main/webapp/ciba/authorizeResponse.xhtml | 56 + oxAuth/Server/src/main/webapp/ciba/home.xhtml | 342 + oxAuth/Server/src/main/webapp/ciba/index.jsp | 1 + .../Server/src/main/webapp/ciba/manifest.json | 7 + .../main/webapp/device_authorization.xhtml | 137 + oxAuth/Server/src/main/webapp/error.xhtml | 65 + .../src/main/webapp/error_service.xhtml | 29 + .../src/main/webapp/error_session.xhtml | 38 + .../src/main/webapp/fonts/FontAwesome.otf | Bin 0 -> 134808 bytes .../main/webapp/fonts/fontawesome-webfont.eot | Bin 0 -> 165742 bytes .../main/webapp/fonts/fontawesome-webfont.svg | 2671 ++ .../main/webapp/fonts/fontawesome-webfont.ttf | Bin 0 -> 165548 bytes .../webapp/fonts/fontawesome-webfont.woff | Bin 0 -> 98024 bytes .../webapp/fonts/fontawesome-webfont.woff2 | Bin 0 -> 77160 bytes .../Server/src/main/webapp/img/IcoArrow.svg | 4 + .../src/main/webapp/img/IcoArrowRed.svg | 4 + oxAuth/Server/src/main/webapp/img/IcoCopy.png | Bin 0 -> 381 bytes oxAuth/Server/src/main/webapp/img/IcoMic.svg | 4 + oxAuth/Server/src/main/webapp/img/IcoPlay.svg | 4 + .../Server/src/main/webapp/img/IcoRetry.svg | 4 + oxAuth/Server/src/main/webapp/img/IcoStop.svg | 4 + .../main/webapp/img/Logo_Whispeak_color.svg | 1 + oxAuth/Server/src/main/webapp/img/android.png | Bin 0 -> 3478 bytes .../Server/src/main/webapp/img/android1.png | Bin 0 -> 3396 bytes .../src/main/webapp/img/background_wave.svg | 1 + .../Server/src/main/webapp/img/buttons_bg.jpg | Bin 0 -> 14804 bytes oxAuth/Server/src/main/webapp/img/close.png | Bin 0 -> 322 bytes .../Server/src/main/webapp/img/email-ver.png | Bin 0 -> 41595 bytes oxAuth/Server/src/main/webapp/img/eye.png | Bin 0 -> 632 bytes .../main/webapp/img/favicon_icosahedron.ico | Bin 0 -> 19608 bytes .../Server/src/main/webapp/img/footer_bg.jpg | Bin 0 -> 13094 bytes .../Server/src/main/webapp/img/glu_icon.png | Bin 0 -> 7578 bytes oxAuth/Server/src/main/webapp/img/ios.png | Bin 0 -> 3137 bytes oxAuth/Server/src/main/webapp/img/iphone.png | Bin 0 -> 3021 bytes oxAuth/Server/src/main/webapp/img/logo.png | Bin 0 -> 3544 bytes .../Server/src/main/webapp/img/logo_white.png | Bin 0 -> 2570 bytes .../src/main/webapp/img/panel_header_bg.png | Bin 0 -> 197 bytes .../Server/src/main/webapp/img/phone-ver.png | Bin 0 -> 7697 bytes oxAuth/Server/src/main/webapp/img/qr_code.png | Bin 0 -> 122696 bytes .../src/main/webapp/img/securitykey.jpg | Bin 0 -> 51755 bytes oxAuth/Server/src/main/webapp/img/sg.png | Bin 0 -> 33658 bytes .../Server/src/main/webapp/img/step_ver.png | Bin 0 -> 19123 bytes .../src/main/webapp/img/step_ver_touchid.png | Bin 0 -> 22055 bytes oxAuth/Server/src/main/webapp/img/touchid.png | Bin 0 -> 36023 bytes oxAuth/Server/src/main/webapp/img/tube_1.jpg | Bin 0 -> 8307 bytes .../Server/src/main/webapp/img/ver_code.png | Bin 0 -> 15871 bytes .../Server/src/main/webapp/img/ver_code_1.png | Bin 0 -> 10131 bytes oxAuth/Server/src/main/webapp/index.jsp | 1 + .../main/webapp/js/bootstrap.bundle.min.js | 7 + .../src/main/webapp/js/bootstrap.min.js | 7 + .../webapp/js/crypto-js-4.1.1/CONTRIBUTING.md | 28 + .../main/webapp/js/crypto-js-4.1.1/LICENSE | 24 + .../main/webapp/js/crypto-js-4.1.1/README.md | 261 + .../src/main/webapp/js/crypto-js-4.1.1/aes.js | 234 + .../main/webapp/js/crypto-js-4.1.1/bower.json | 39 + .../webapp/js/crypto-js-4.1.1/cipher-core.js | 890 + .../main/webapp/js/crypto-js-4.1.1/core.js | 807 + .../webapp/js/crypto-js-4.1.1/crypto-js.js | 6191 ++++ .../crypto-js-4.1.1/docs/QuickStartGuide.wiki | 470 + .../webapp/js/crypto-js-4.1.1/enc-base64.js | 136 + .../js/crypto-js-4.1.1/enc-base64url.js | 140 + .../main/webapp/js/crypto-js-4.1.1/enc-hex.js | 18 + .../webapp/js/crypto-js-4.1.1/enc-latin1.js | 18 + .../webapp/js/crypto-js-4.1.1/enc-utf16.js | 149 + .../webapp/js/crypto-js-4.1.1/enc-utf8.js | 18 + .../main/webapp/js/crypto-js-4.1.1/evpkdf.js | 134 + .../webapp/js/crypto-js-4.1.1/format-hex.js | 66 + .../js/crypto-js-4.1.1/format-openssl.js | 18 + .../webapp/js/crypto-js-4.1.1/hmac-md5.js | 18 + .../js/crypto-js-4.1.1/hmac-ripemd160.js | 18 + .../webapp/js/crypto-js-4.1.1/hmac-sha1.js | 18 + .../webapp/js/crypto-js-4.1.1/hmac-sha224.js | 18 + .../webapp/js/crypto-js-4.1.1/hmac-sha256.js | 18 + .../webapp/js/crypto-js-4.1.1/hmac-sha3.js | 18 + .../webapp/js/crypto-js-4.1.1/hmac-sha384.js | 18 + .../webapp/js/crypto-js-4.1.1/hmac-sha512.js | 18 + .../main/webapp/js/crypto-js-4.1.1/hmac.js | 143 + .../main/webapp/js/crypto-js-4.1.1/index.js | 18 + .../js/crypto-js-4.1.1/lib-typedarrays.js | 76 + .../src/main/webapp/js/crypto-js-4.1.1/md5.js | 268 + .../webapp/js/crypto-js-4.1.1/mode-cfb.js | 80 + .../js/crypto-js-4.1.1/mode-ctr-gladman.js | 116 + .../webapp/js/crypto-js-4.1.1/mode-ctr.js | 58 + .../webapp/js/crypto-js-4.1.1/mode-ecb.js | 40 + .../webapp/js/crypto-js-4.1.1/mode-ofb.js | 54 + .../webapp/js/crypto-js-4.1.1/package.json | 42 + .../webapp/js/crypto-js-4.1.1/pad-ansix923.js | 49 + .../webapp/js/crypto-js-4.1.1/pad-iso10126.js | 44 + .../webapp/js/crypto-js-4.1.1/pad-iso97971.js | 40 + .../js/crypto-js-4.1.1/pad-nopadding.js | 30 + .../webapp/js/crypto-js-4.1.1/pad-pkcs7.js | 18 + .../js/crypto-js-4.1.1/pad-zeropadding.js | 47 + .../main/webapp/js/crypto-js-4.1.1/pbkdf2.js | 145 + .../js/crypto-js-4.1.1/rabbit-legacy.js | 190 + .../main/webapp/js/crypto-js-4.1.1/rabbit.js | 192 + .../src/main/webapp/js/crypto-js-4.1.1/rc4.js | 139 + .../webapp/js/crypto-js-4.1.1/ripemd160.js | 267 + .../main/webapp/js/crypto-js-4.1.1/sha1.js | 150 + .../main/webapp/js/crypto-js-4.1.1/sha224.js | 80 + .../main/webapp/js/crypto-js-4.1.1/sha256.js | 199 + .../main/webapp/js/crypto-js-4.1.1/sha3.js | 326 + .../main/webapp/js/crypto-js-4.1.1/sha384.js | 83 + .../main/webapp/js/crypto-js-4.1.1/sha512.js | 326 + .../webapp/js/crypto-js-4.1.1/tripledes.js | 779 + .../webapp/js/crypto-js-4.1.1/x64-core.js | 304 + .../src/main/webapp/js/fontawesome.min.js | 5 + oxAuth/Server/src/main/webapp/js/gluu-auth.js | 201 + .../src/main/webapp/js/jquery-3.6.0.min.js | 2 + .../webapp/js/jquery-qrcode-0.17.0.min.js | 2 + .../src/main/webapp/js/jquery-ui.min.js | 6 + oxAuth/Server/src/main/webapp/js/passport.js | 33 + oxAuth/Server/src/main/webapp/js/platform.js | 14 + .../Server/src/main/webapp/js/popper.min.js | 5 + .../src/main/webapp/js/recorder_3buttons.js | 623 + .../Server/src/main/webapp/js/respond.min.js | 5 + .../Server/src/main/webapp/js/revocation.js | 32 + oxAuth/Server/src/main/webapp/login.xhtml | 174 + oxAuth/Server/src/main/webapp/logout.xhtml | 17 + oxAuth/Server/src/main/webapp/opiframe.xhtml | 49 + .../webapp/passwordless/alternative.xhtml | 128 + .../webapp/passwordless/bioid-template.xhtml | 68 + .../src/main/webapp/passwordless/bioid.xhtml | 1043 + .../src/main/webapp/passwordless/fido2.xhtml | 99 + .../webapp/passwordless/login-template.xhtml | 82 + .../src/main/webapp/passwordless/login.xhtml | 132 + .../src/main/webapp/passwordless/sg.xhtml | 107 + oxAuth/Server/src/main/webapp/postlogin.xhtml | 13 + .../src/main/webapp/selectAccount.xhtml | 99 + .../src/main/webapp/stylesheet/authorize.css | 226 + .../webapp/stylesheet/bootstrap-grid.min.css | 6 + .../stylesheet/bootstrap-reboot.min.css | 6 + .../stylesheet/bootstrap-responsive.css | 1109 + .../src/main/webapp/stylesheet/bootstrap.css | 12068 ++++++++ .../main/webapp/stylesheet/bootstrap.min.css | 6 + .../src/main/webapp/stylesheet/ciba.css | 38 + .../main/webapp/stylesheet/font-awesome.css | 2337 ++ .../webapp/stylesheet/fontawesome.min.css | 5 + .../main/webapp/stylesheet/form_window.css | 630 + .../main/webapp/stylesheet/jquery-ui.min.css | 7 + .../stylesheet/jquery-ui.structure.min.css | 5 + .../webapp/stylesheet/jquery-ui.theme.min.css | 5 + .../webapp/stylesheet/recorder_3buttons.css | 345 + .../webapp/stylesheet/responsive-media.css | 49 + .../src/main/webapp/stylesheet/site.css | 46 + .../src/main/webapp/stylesheet/style.css | 710 + .../src/main/webapp/stylesheet/theme.css | 841 + .../src/main/webapp/stylesheet/theme.xcss | 83 + .../src/main/webapp/uma2/sample/city.xhtml | 31 + .../webapp/uma2/sample/claims_resolved.xhtml | 28 + .../src/main/webapp/uma2/sample/country.xhtml | 31 + .../org/gluu/oxauth/BaseComponentTest.java | 26 + .../test/java/org/gluu/oxauth/BaseTest.java | 56 + .../org/gluu/oxauth/ConfigurableTest.java | 146 + .../gluu/oxauth/OxAuthUnitTestsListener.java | 78 + .../AuthorizeRestWebServiceValidatorTest.java | 121 + .../gluu/oxauth/comp/CleanUpClientTest.java | 70 + .../gluu/oxauth/comp/CleanerTimerTest.java | 444 + .../gluu/oxauth/comp/ConfigurationTest.java | 95 + .../gluu/oxauth/comp/CrossEncryptionTest.java | 493 + .../gluu/oxauth/comp/CryptoProviderTest.java | 457 + .../org/gluu/oxauth/comp/EncryptionTest.java | 353 + .../gluu/oxauth/comp/GrantServiceTest.java | 69 + .../gluu/oxauth/comp/IdGenServiceTest.java | 105 + .../gluu/oxauth/comp/InumGeneratorTest.java | 34 + .../gluu/oxauth/comp/JwtCrossCheckTest.java | 194 + .../gluu/oxauth/comp/KeyGenerationTest.java | 33 + .../java/org/gluu/oxauth/comp/LocaleTest.java | 76 + .../oxauth/comp/SessionIdServiceTest.java | 103 + .../org/gluu/oxauth/comp/SignatureTest.java | 176 + .../oxauth/comp/UmaResourceServiceTest.java | 100 + .../gluu/oxauth/comp/UtilityMethodsTest.java | 26 + .../org/gluu/oxauth/dev/CacheGrantManual.java | 168 + .../gluu/oxauth/dev/ConfSerialization.java | 104 + .../test/java/org/gluu/oxauth/dev/Manual.java | 60 + .../java/org/gluu/oxauth/dev/TestUUID.java | 42 + .../oxauth/dev/duo/PythonToLdapString.java | 102 + .../gluu/ws/rs/GluuConfigurationWSTest.java | 58 + .../oxauth/json/JsonApplierServerTest.java | 41 + .../org/gluu/oxauth/model/CIBAGrantTest.java | 120 + .../org/gluu/oxauth/model/TClientService.java | 40 + .../gluu/oxauth/model/WebServiceFactory.java | 31 + .../JwtAuthorizationRequestTest.java | 77 + .../RegisterParamsValidatorTest.java | 55 + .../gluu/oxauth/model/uma/TConfiguration.java | 71 + .../oxauth/model/uma/TRegisterPermission.java | 76 + .../oxauth/model/uma/TRegisterResource.java | 182 + .../gluu/oxauth/model/uma/TTokenRequest.java | 352 + .../java/org/gluu/oxauth/model/uma/TUma.java | 101 + .../service/DateFormatterServiceTest.java | 84 + .../service/RedirectionUriServiceTest.java | 25 + .../gluu/oxauth/service/ScopeServiceTest.java | 274 + .../service/TestResteasyInitializer.java | 64 + .../u2f/RawAuthenticationServiceTest.java | 32 + .../u2f/RawAuthenticationServiceUnitTest.java | 165 + .../fido/u2f/RawRegistrationServiceTest.java | 51 + .../servlet/OpenIdConfigurationTest.java | 46 + .../rs/EndSessionRestWebServiceImplTest.java | 196 + .../rs/AccessProtectedResourceFlowWSTest.java | 156 + .../oxauth/uma/ws/rs/ObtainPatWSTest.java | 47 + .../oxauth/uma/ws/rs/ObtainRptWSTest.java | 41 + .../uma/ws/rs/RegisterPermissionWSTest.java | 128 + .../uma/ws/rs/UmaConfigurationWSTest.java | 35 + .../uma/ws/rs/UmaRegisterResourceWSTest.java | 87 + .../gluu/oxauth/uma/ws/rs/UmaScopeWSTest.java | 64 + .../org/gluu/oxauth/util/Deployments.java | 52 + .../org/gluu/oxauth/util/ServerUtilTest.java | 34 + ...pplicationTypeRestrictionEmbeddedTest.java | 440 + .../rs/AuthorizationCodeFlowEmbeddedTest.java | 693 + .../AuthorizeRestWebServiceEmbeddedTest.java | 1211 + ...AuthorizeWithResponseModeEmbeddedTest.java | 297 + ...lientAuthenticationFilterEmbeddedTest.java | 228 + .../ClientInfoRestWebServiceEmbeddedTest.java | 382 + .../EndSessionBackchannelRestServerTest.java | 135 + .../EndSessionRestWebServiceEmbeddedTest.java | 256 + .../IntrospectionWebServiceEmbeddedTest.java | 87 + .../ws/rs/JwkRestWebServiceEmbeddedTest.java | 117 + .../rs/OpenIDRequestObjectEmbeddedTest.java | 1147 + ...nIDRequestObjectWithESAlgEmbeddedTest.java | 688 + ...nIDRequestObjectWithHSAlgEmbeddedTest.java | 388 + ...nIDRequestObjectWithRSAlgEmbeddedTest.java | 689 + ...egistrationRestWebServiceEmbeddedTest.java | 288 + ...jectSigningAlgRestrictionEmbeddedTest.java | 2014 ++ .../ResponseTypesRestrictionEmbeddedTest.java | 785 + ...IdentifierUrlVerificationEmbeddedTest.java | 259 + ...ointAuthMethodRestrictionEmbeddedTest.java | 1425 + .../rs/TokenRestWebServiceEmbeddedTest.java | 276 + ...enRestWebServiceWithESAlgEmbeddedTest.java | 621 + ...enRestWebServiceWithHSAlgEmbeddedTest.java | 325 + ...enRestWebServiceWithRSAlgEmbeddedTest.java | 623 + .../UserAuthenticationFilterEmbeddedTest.java | 553 + .../UserInfoRestWebServiceEmbeddedTest.java | 962 + .../test/resources/77-customAttributes.ldif | 17 + .../Server/src/test/resources/arquillian.xml | 20 + .../src/test/resources/client/jwks.json | 188 + .../resources/client/sector_identifier.js | 7 + .../resources/id/gen/SampleIdGenerator.py | 41 + .../Server/src/test/resources/jetty-env.xml | 21 + .../src/test/resources/testng-benchmark.xml | 4 + .../src/test/resources/testng-multi-authz.xml | 4 + .../src/test/resources/testng.properties | 70 + oxAuth/Server/src/test/resources/testng.xml | 374 + oxAuth/Server/src/test/resources/web.xml | 100 + oxAuth/Server/uma/UmaClientAuthzRptPolicy.py | 83 + .../Server/uma/sample/UmaClaimsGathering.py | 123 + oxAuth/Server/uma/sample/UmaRptPolicy.py | 77 + oxAuth/Tests/selenium/ReadMe.txt | 23 + .../selenium/selenium_login. t1_user.html | 86 + .../selenium/selenium_login. t2_user.html | 86 + .../selenium/selenium_login. t3_user.html | 86 + oxAuth/Tests/selenium/while.js | 126 + oxAuth/common/pom.xml | 247 + .../cert/fingerprint/FingerprintHelper.java | 83 + .../validation/CRLCertificateVerifier.java | 302 + .../cert/validation/CertificateVerifier.java | 21 + .../GenericCertificateVerifier.java | 67 + .../validation/OCSPCertificateVerifier.java | 246 + .../validation/PathCertificateVerifier.java | 196 + .../validation/model/ValidationStatus.java | 113 + .../java/org/gluu/oxauth/claims/Audience.java | 22 + .../gluu/oxauth/model/common/SimpleUser.java | 50 + .../org/gluu/oxauth/model/common/User.java | 84 + .../model/config/BaseDnConfiguration.java | 217 + .../model/config/StaticConfiguration.java | 37 + .../model/event/CryptoProviderEvent.java | 29 + .../oxauth/model/registration/Client.java | 1233 + .../gluu/oxauth/model/session/SessionId.java | 342 + .../model/session/SessionIdAccessMap.java | 89 + .../oxauth/model/session/SessionIdState.java | 49 + .../java/org/gluu/oxauth/model/stat/Stat.java | 56 + .../org/gluu/oxauth/model/stat/StatEntry.java | 60 + .../service/common/ApplicationFactory.java | 122 + .../service/common/ConfigurationService.java | 124 + .../service/common/EncryptionService.java | 82 + .../oxauth/service/common/InumService.java | 49 + .../oxauth/service/common/UserService.java | 547 + .../service/common/api/IdGenerator.java | 31 + .../fido2/RegistrationPersistenceService.java | 206 + .../org/gluu/oxauth/util/RedirectUri.java | 167 + .../src/main/resources/META-INF/beans.xml | 8 + .../org/gluu/oxauth/claims/AudienceTest.java | 44 + .../org/gluu/oxauth/util/RedirectUriTest.java | 25 + oxAuth/common/src/test/resources/testng.xml | 14 + oxAuth/docs/Authorization Code Flow.png | Bin 0 -> 29087 bytes oxAuth/docs/Authorize Endpoint.png | Bin 0 -> 802792 bytes oxAuth/docs/Authorize Page.png | Bin 0 -> 21692 bytes oxAuth/docs/Class Diagram.png | Bin 0 -> 172288 bytes oxAuth/docs/Client Credentials Flow.png | Bin 0 -> 6978 bytes oxAuth/docs/Implicit Flow.png | Bin 0 -> 19849 bytes ...source Owner Password Credentials Flow.png | Bin 0 -> 12412 bytes oxAuth/docs/oxAuth.vpp | Bin 0 -> 537116 bytes oxAuth/docs/oxAuthSwagger.yaml | 4492 +++ oxAuth/install.bat | 1 + .../jmeter/test/Authorization Code Flow.jmx | 925 + .../test/Authorization Code Flow_amazon.jmx | 1070 + ...on Code Flow_amazon_client_credentials.jmx | 625 + .../test/Authorization Code Flow_ce-dev5.jmx | 1317 + .../test/Authorization Code Flow_xeon.jmx | 1242 + ...tion Code Flow_xeon_client_credentials.jmx | 625 + .../test/Implicit_Flow_benchmark_gluu_org.jmx | 1018 + .../Implicit_Flow_c7_horizontal_scale.jmx | 1150 + ...Test user authorization - testing data.csv | 3 + .../oxServer - Test user authorization.jmx | 712 + oxAuth/oxAuthStatic/.gitignore | 1 + oxAuth/oxAuthStatic/pom.xml | 26 + .../META-INF/resources/img/buttons_bg.jpg | Bin 0 -> 14804 bytes .../META-INF/resources/img/footer_bg.jpg | Bin 0 -> 13094 bytes .../resources/img/panel_header_bg.png | Bin 0 -> 197 bytes .../META-INF/resources/js/Duo-Web-v1.min.js | 5 + .../META-INF/resources/stylesheet/site.css | 46 + .../META-INF/resources/stylesheet/theme.css | 841 + .../META-INF/resources/stylesheet/theme.xcss | 83 + oxAuth/persistence-model/pom.xml | 85 + .../persistence/model/ClientAttributes.java | 209 + .../persistence/model/PairwiseIdentifier.java | 78 + .../org/oxauth/persistence/model/Scope.java | 194 + .../persistence/model/ScopeAttributes.java | 56 + .../persistence/model/SectorIdentifier.java | 60 + .../oxauth/persistence/model/base/Entry.java | 49 + .../model/configuration/CustomProperty.java | 54 + .../configuration/GluuConfiguration.java | 122 + .../model/configuration/InumEntry.java | 48 + .../model/configuration/oxIDPAuthConf.java | 110 + .../src/main/resources/META-INF/beans.xml | 8 + oxAuth/pom.xml | 752 + oxAuth/release.properties | 7 + oxAuth/server-fips/.gitignore | 4 + oxAuth/server-fips/pom.xml | 161 + oxAuth/stat-exporter/pom.xml | 102 + .../gluu/stat/exporter/DiscoveryResponse.java | 51 + .../java/org/gluu/stat/exporter/Months.java | 35 + .../gluu/stat/exporter/RegisterRequest.java | 76 + .../gluu/stat/exporter/RegisterResponse.java | 62 + .../org/gluu/stat/exporter/StatExporter.java | 294 + .../stat/exporter/StatExporterConfig.java | 43 + .../stat/exporter/StatExporterResponse.java | 35 + .../org/gluu/stat/exporter/TokenResponse.java | 40 + .../test/resources/stat-exporter-config.json | 5 + 1607 files changed, 338405 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/CODEOWNERS create mode 100644 .github/ISSUE_TEMPLATE/issue-report.md create mode 100644 .github/workflows/central_code_quality_check.yml create mode 100644 oxAuth/.gitignore create mode 100644 oxAuth/CODE_OF_CONDUCT.md create mode 100644 oxAuth/Client/.gitignore create mode 100644 oxAuth/Client/pom.xml create mode 100644 oxAuth/Client/profiles/.gitignore create mode 100644 oxAuth/Client/profiles/default/client_keystore.pkcs12 create mode 100644 oxAuth/Client/profiles/default/config-oxauth-test-data.properties create mode 100644 oxAuth/Client/src/main/assembly/jar-without-provider-dependencies.xml create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/AuthorizationRequest.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/AuthorizationResponse.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/AuthorizeClient.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/BackchannelAuthenticationClient.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/BackchannelAuthenticationRequest.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/BackchannelAuthenticationResponse.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/BaseClient.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/BaseRequest.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/BaseResponse.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/BaseResponseWithErrors.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/ClientAuthnEnabler.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/ClientAuthnRequest.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/ClientInfoClient.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/ClientInfoRequest.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/ClientInfoResponse.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/ClientUtils.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/DeviceAuthzClient.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/DeviceAuthzRequest.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/DeviceAuthzResponse.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/EndSessionClient.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/EndSessionRequest.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/EndSessionResponse.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/GluuConfigurationClient.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/GluuConfigurationRequest.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/GluuConfigurationResponse.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/JwkClient.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/JwkRequest.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/JwkResponse.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/OpenIdConfigurationClient.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/OpenIdConfigurationRequest.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/OpenIdConfigurationResponse.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/OpenIdConnectDiscoveryClient.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/OpenIdConnectDiscoveryRequest.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/OpenIdConnectDiscoveryResponse.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/QueryStringDecoder.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/RegisterClient.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/RegisterRequest.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/RegisterResponse.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/RevokeSessionClient.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/RevokeSessionRequest.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/RevokeSessionResponse.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/TokenClient.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/TokenRequest.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/TokenResponse.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/TokenRevocationClient.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/TokenRevocationRequest.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/TokenRevocationResponse.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/UserInfoClient.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/UserInfoRequest.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/UserInfoResponse.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/fcm/FirebaseCloudMessagingClient.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/fcm/FirebaseCloudMessagingRequest.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/fcm/FirebaseCloudMessagingResponse.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/ping/PingCallbackClient.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/ping/PingCallbackRequest.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/ping/PingCallbackResponse.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/push/PushErrorClient.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/push/PushErrorRequest.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/push/PushErrorResponse.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/push/PushTokenDeliveryClient.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/push/PushTokenDeliveryRequest.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/push/PushTokenDeliveryResponse.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/fido/u2f/AuthenticationRequestService.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/fido/u2f/FidoU2fClientFactory.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/fido/u2f/RegistrationRequestService.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/fido/u2f/U2fConfigurationService.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/model/JwtState.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/model/SoftwareStatement.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/model/authorize/Claim.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/model/authorize/ClaimValue.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/model/authorize/IdTokenMember.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/model/authorize/JwtAuthorizationRequest.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/model/authorize/UserInfoMember.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/service/ClientFactory.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/service/IntrospectionService.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/service/StatService.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/UmaClientFactory.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/UmaMetadataService.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/UmaPermissionService.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/UmaResourceService.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/UmaRptIntrospectionService.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/UmaScopeService.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/UmaTokenService.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/exception/UmaException.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/wrapper/UmaClient.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/util/ClientUtil.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/util/KeyExporter.java create mode 100644 oxAuth/Client/src/main/java/org/gluu/oxauth/util/KeyGenerator.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/BaseTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/BackchannelAuthenticationExpiredRequestsTests.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/BackchannelAuthenticationPingMode.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/BackchannelAuthenticationPollMode.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/BackchannelAuthenticationPushMode.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/CibaPingModeJwtAuthRequestTests.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/CibaPollModeJwtAuthRequestTests.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/ConfigurationTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/RegistrationTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/client/Asserter.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/client/JSONObjectAsserter.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/client/RegisterRequestTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/client/ResponseAsserter.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/dev/.gitignore create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/dev/HostnameVerifierType.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/dev/TestSessionWorkflow.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/dev/manual/AccessTokenManualTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/dev/manual/BCFIPSTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/dev/manual/MTSLClientAuthenticationTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/AcceptRequestWithoutRedirectUriWhenOneRegistered.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/AcceptValidAsymmetricIdTokenSignature.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/AcceptValidSymmetricIdTokenSignature.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanDiscoverIdentifiersUsingEMailSyntax.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanDiscoverIdentifiersUsingUrlSyntax.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanMakeAccessTokenRequestWithClientSecretBasicAuthentication.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanMakeAccessTokenRequestWithClientSecretJwtAuthentication.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanMakeAccessTokenRequestWithClientSecretPostAuthentication.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanMakeAccessTokenRequestWithPrivateKeyJwtAuthentication.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanProvideEncryptedIdTokenResponse.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanProvideEncryptedUserInfoResponse.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanProvideSignedUserInfoResponse.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanRequestAndUseClaimsInIdToken.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanRequestAndUseEncryptedIdTokenResponse.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanRequestAndUseEncryptedUserInfoResponse.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanRequestAndUseSignedUserInfoResponse.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/DisplaysLogoInLoginPage.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/DisplaysPolicyUriInLoginPage.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/EnablesDynamicRegistration.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/IgnoresExtraQueryComponentInRequest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/IncludesAtHashInIdTokenWhenImplicitFlowUsed.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/IncludesCHashInIdTokenWhenCodeFlowUsed.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/OPRegistrationJwks.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/ProvidingAcrValues.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/ProvidingIdTokenWithEssentialAuthTimeClaim.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/ProvidingIdTokenWithMaxAgeRestriction.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/ProvidingIndividuallyRequestedEssentialAndVoluntaryClaims.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/ProvidingIndividuallyRequestedEssentialClaims.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/ProvidingIndividuallyRequestedVoluntaryClaims.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/PublishOpenIdConfigurationDiscoveryInformation.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectInvalidAsymmetricIdTokenSignature.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectInvalidSymmetricIdTokenSignature.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectRedirectUriNotMatchingARegisteredRedirectUri.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectRegistrationOfRedirectUriWithFragment.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectRequestWithoutRedirectUriWhenMultipleRegistered.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectRequestWithoutResponseType.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectRequestsWithoutNonceUsingImplicitFlow.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectsIncorrectAtHashWhenImplicitFlowUsed.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectsIncorrectCHashWhenCodeFlowUsed.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectsRedirectUriWhenQueryParameterDoesNotMatch.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectsSecondUseOfAccessCode.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectsSectorIdentifierNotContainingRegisteredRedirectUriValues.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RequestingUserInfoClaimsWithOpenIdRequestObject.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RequestingUserInfoClaimsWithScopeValues.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SecondUseOfAccessCodeRevokesPreviouslyIssuedAccessToken.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportAuthenticationToTokenEndpointUsingFormEncodedClientCredentialsInPostBody.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportAuthenticationToTokenEndpointUsingHttpBasicWithPost.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportAuthenticationToTokenEndpointWithAsymmetricallySignedJWTs.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportAuthenticationToTokenEndpointWithSymmetricallySignedJWTs.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportClaimsRequestSpecifyingSubValue.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportCodeResponseType.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportCombinationOfCodeIdTokenTokenResponseTypes.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportCombinationOfCodeTokenResponseTypes.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportCombinationOfIdTokenCodeResponseTypes.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportCombinationOfIdTokenTokenResponseTypes.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportDisplayValuePage.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportDisplayValuePopup.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportIdTokenResponseType.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportPromptValueLogin.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportPromptValueNone.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportRegistrationRead.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportRequestFile.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportRequestsContainingNonce.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportRequestsWithoutNonce.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportScopeRequestingAddressClaims.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportScopeRequestingAllBasicClaims.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportScopeRequestingEmailClaims.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportScopeRequestingNoSpecificClaims.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportScopeRequestingPhoneClaims.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportScopeRequestingProfileClaims.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportTokenResponseType.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportWebFingerDiscovery.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/Supports3rdPartyInitLogin.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/Supports3rdPartyInitLoginNoHttps.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportsCombiningClaimsRequestedWithScopeAndRequestObject.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportsReturningClaimsInIdToken.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportsReturningDifferentClaimsInIdTokenAndUserInfoEndpoint.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/UserInfoEndpoint.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/UserInfoEndpointAccessWithFormEncodedBodyMethod.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/UserInfoEndpointAccessWithHeaderMethod.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/UsesAsymmetricIdTokenSignatures.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/UsesDiscovery.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/UsesDynamicRegistration.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/UsesSymmetricIdTokenSignatures.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/VerifiesCorrectAtHashWhenImplicitFlowUsed.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/interop/VerifiesCorrectCHashWhenCodeFlowUsed.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/json/JsonApplierTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/load/LoadConstants.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/load/ObtainAccessTokenLoadTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/load/RegistrationLoadTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/load/UserInfoLoadTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/load/benchmark/BenchmarkRequestAccessToken.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/load/benchmark/BenchmarkRequestAuthorization.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/load/benchmark/suite/BenchmarkTestListener.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/load/benchmark/suite/BenchmarkTestSuiteListener.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/page/AbstractPage.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/page/DeviceAuthzPage.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/page/LoginPage.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/page/Page.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/page/PageConfig.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/page/SelectPage.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AccessTokenAsJwtHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AddressClaimsTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ApplicationTypeRestrictionHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AuthorizationCodeFlowHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AuthorizationResponseCustomHeaderTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AuthorizationResponseModeHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AuthorizationSupportCustomParams.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AuthorizeRestWebServiceHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AuthorizeSessionIdRestWebServiceHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ClientAuthenticationFilterHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ClientCredentialsGrantHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ClientInfoRestWebServiceHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ClientSecretBasicTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ClientSpecificAccessTokenExpiration.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ClientTestUtil.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ClientWhiteListBlackListRedirectUris.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ConfigurationRestWebServiceHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/EnableClientToRestrictJavascriptOrigin.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/EncodeClaimsInStateParameter.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/EndSessionRestWebServiceHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/GluuConfigurationWebServiceHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/GrantTypesRestrictionHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/IndividualClaimsRequestsTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/IntrospectionWsHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/JwkRestWebServiceHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/MultiStepAuthorizationCodeFlowHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/MultivaluedClaims.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/OpenIDConnectDiscoveryHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/OpenIDRequestObjectHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/PersistClientAuthorizationsHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/PkceHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/RegistrationRestWebServiceHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/RegistrationWithSoftwareStatement.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ResponseTypesRestrictionHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/RevokeSessionHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/SSOWithMultipleBackendServicesHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/SectorIdentifierUrlVerificationHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/SelectAccountHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/SpontaneousScopeHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/TokenBindingHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/TokenEncryptionHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/TokenEndpointAuthMethodRestrictionHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/TokenRestWebServiceHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/TokenRevocationTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/TokenSignaturesHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/UILocales.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/UserAuthenticationFilterHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/UserInfoRestWebServiceHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ValidateIdTokenHashesTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/WebKeysTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/deviceauthz/DeviceAuthzFlowHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/deviceauthz/DeviceAuthzRequestRegistrationTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/internal/StatWSTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/AccessProtectedResourceFlowHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/ClientAuthenticationByAccessTokenHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/MetaDataFlowHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/ObtainPatTokenFlowHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/RegisterResourceFlowHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/ScopeHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/UmaRegisterPermissionFlowHttpTest.java create mode 100644 oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/UmaSpontaneousScopeHttpTest.java create mode 100644 oxAuth/Client/src/test/resources/clientkeystore.jks create mode 100644 oxAuth/Client/src/test/resources/interop_Gluu_OX.properties create mode 100644 oxAuth/Client/src/test/resources/interop_NRI_PHP.properties create mode 100644 oxAuth/Client/src/test/resources/interop_Nov_Matake_Test.properties create mode 100644 oxAuth/Client/src/test/resources/interop_Oreo.properties create mode 100644 oxAuth/Client/src/test/resources/interop_Roland_Hedberg_Test.properties create mode 100644 oxAuth/Client/src/test/resources/interop_Ryo_Ito_Test.properties create mode 100644 oxAuth/Client/src/test/resources/interop_Wenou.properties create mode 100644 oxAuth/Client/src/test/resources/interop_rohe_config.py create mode 100644 oxAuth/Client/src/test/resources/interop_rohe_python_config.json create mode 100644 oxAuth/Client/src/test/resources/oxauth_test_client_keys.zip create mode 100644 oxAuth/Client/src/test/resources/testng-benchmark.xml create mode 100644 oxAuth/Client/src/test/resources/testng-multi-authz.xml create mode 100644 oxAuth/Client/src/test/resources/testng.properties create mode 100644 oxAuth/Client/src/test/resources/testng.xml create mode 100644 oxAuth/LICENSE create mode 100644 oxAuth/Model/.gitignore create mode 100644 oxAuth/Model/pom.xml create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/authorize/AuthorizeErrorResponseType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/authorize/AuthorizeRequestParam.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/authorize/AuthorizeResponseParam.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/authorize/CodeVerifier.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/authorize/DeviceAuthorizationRequestParam.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/authorize/DeviceAuthorizationResponseParam.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/authorize/DeviceAuthzErrorResponseType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/BackchannelAuthenticationErrorResponseType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/BackchannelAuthenticationRequestParam.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/BackchannelAuthenticationResponseParam.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/BackchannelDeviceRegistrationErrorResponseType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/FirebaseCloudMessagingRequestParam.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/FirebaseCloudMessagingResponseParam.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/PushErrorRequestParam.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/PushErrorResponseType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/PushTokenDeliveryRequestParam.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/AuthenticationMethod.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/AuthorizationMethod.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/BackchannelTokenDeliveryMode.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/CallerType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/Display.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/GrantType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/HasParamName.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/Holder.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/Id.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/IdType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/IntrospectionResponse.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/JSONable.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/PairwiseIdType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/ProgrammingLanguage.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/Prompt.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/ResponseMode.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/ResponseType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/ScopeConstants.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/ScopeType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/SoftwareStatementValidationType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/SubjectType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/TokenType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/TokenTypeHint.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/WebKeyStorage.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/converter/ListConverter.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/AppConfiguration.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/AuthenticationFilter.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/AuthenticationProtectionConfiguration.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/BaseFilter.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/CIBAEndUserNotificationConfig.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/ClientAuthenticationFilter.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/Configuration.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/ConfigurationResponseClaim.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/CorsConfigurationFilter.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/AbstractCryptoProvider.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/Certificate.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/CryptoProviderFactory.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/Key.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/KeyFactory.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/OxAuthCryptoProvider.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/OxElevenCryptoProvider.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/PrivateKey.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/PublicKey.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBinding.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingExtension.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingExtensionType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingID.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingKeyParameters.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingMessage.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingMessageParser.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingParseException.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingStream.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/encryption/BlockEncryptionAlgorithm.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/encryption/KeyEncryptionAlgorithm.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/AbstractSigner.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/AlgorithmFamily.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/AsymmetricSignatureAlgorithm.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/ECDSAKeyFactory.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/ECDSAPrivateKey.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/ECDSAPublicKey.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/ECEllipticCurve.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/RSAKeyFactory.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/RSAPrivateKey.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/RSAPublicKey.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/SignatureAlgorithm.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/Signer.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/discovery/OAuth2Discovery.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/discovery/WebFingerLink.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/discovery/WebFingerParam.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/error/DefaultErrorResponse.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/error/ErrorHandlingMethod.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/error/ErrorResponse.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/error/IErrorType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/exception/InvalidClaimException.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/exception/InvalidJweException.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/exception/InvalidJwtException.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/exception/InvalidParameterException.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/exception/SignatureException.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/DeviceRegistrationStatus.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/U2fConfiguration.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/U2fConstants.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/U2fErrorResponseType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/exception/BadInputException.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/exception/RegistrationNotAllowed.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/message/RawAuthenticateResponse.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/message/RawRegisterResponse.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/AuthenticateRequest.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/AuthenticateRequestMessage.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/AuthenticateResponse.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/AuthenticateStatus.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/ClientData.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/DeviceData.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/DeviceNotificationConf.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/RegisterRequest.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/RegisterRequestMessage.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/RegisterResponse.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/RegisterStatus.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/gluu/GluuConfiguration.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/gluu/GluuErrorResponseType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/json/JsonApplier.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/json/PropertyDefinition.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/AbstractJweDecrypter.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/AbstractJweEncrypter.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/Jwe.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/JweDecrypter.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/JweDecrypterImpl.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/JweEncrypter.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/JweEncrypterImpl.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/KeyDerivationFunction.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwk/Algorithm.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwk/JSONWebKey.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwk/JSONWebKeySet.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwk/JWKParameter.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwk/KeySelectionStrategy.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwk/KeyType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwk/Use.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jws/AbstractJwsSigner.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jws/ECDSASigner.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jws/HMACSigner.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jws/JwsSigner.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jws/PlainTextSignature.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jws/RSASigner.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/Jwt.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtClaimName.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtClaimSet.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtClaims.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtHeader.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtHeaderName.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtStateClaimName.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtSubClaimObject.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/PureJwt.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/register/ApplicationType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/register/RegisterErrorResponseType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/register/RegisterRequestParam.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/register/RegisterResponseParam.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/session/EndSessionErrorResponseType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/session/EndSessionRequestParam.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/session/EndSessionResponseParam.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/token/ClientAssertionType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/token/JsonWebResponse.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/token/TokenErrorResponseType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/token/TokenRevocationErrorResponseType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/token/TokenRevocationRequestParam.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/ClaimTokenFormatType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/JsonLogic.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/JsonLogicNode.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/JsonLogicNodeParser.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/PermissionTicket.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/RPTResponse.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/RptIntrospectionResponse.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/RptProfiles.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaConstants.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaErrorResponseType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaMetadata.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaNeedInfoResponse.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaPermission.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaPermissionList.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaResource.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaResourceResponse.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaResourceWithId.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaScopeDescription.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaScopeType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaTokenResponse.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/persistence/UmaPermission.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/persistence/UmaResource.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/wrapper/Token.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/userinfo/Schema.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/userinfo/UserInfoErrorResponseType.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/Base64Util.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/ByteUtils.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/CertUtils.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/HashUtil.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/JwtUtil.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/Pair.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/QueryBuilder.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/StringUtils.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/SubjectIdentifierGenerator.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/URLPatternList.java create mode 100644 oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/Util.java create mode 100644 oxAuth/Model/src/main/resources/json_logic.js create mode 100644 oxAuth/Model/src/test/java/org/gluu/oxauth/model/authorize/CodeVerifierTest.java create mode 100644 oxAuth/Model/src/test/java/org/gluu/oxauth/model/crypto/binding/TokenBindingParserTest.java create mode 100644 oxAuth/Model/src/test/java/org/gluu/oxauth/model/jwt/JwtClaimsTest.java create mode 100644 oxAuth/Model/src/test/java/org/gluu/oxauth/model/jwt/JwtTypeTest.java create mode 100644 oxAuth/Model/src/test/java/org/gluu/oxauth/model/uma/JsonLogicNodeParserTest.java create mode 100644 oxAuth/Model/src/test/java/org/gluu/oxauth/model/uma/JsonLogicTest.java create mode 100644 oxAuth/Model/src/test/java/org/gluu/oxauth/model/uma/TestUtil.java create mode 100644 oxAuth/Model/src/test/java/org/gluu/oxauth/model/uma/UmaTestUtil.java create mode 100644 oxAuth/Model/src/test/java/org/gluu/oxauth/model/util/CertUtilsTest.java create mode 100644 oxAuth/Model/src/test/java/org/gluu/oxauth/model/util/HashUtilTest.java create mode 100644 oxAuth/Model/src/test/java/org/gluu/oxauth/model/util/JwtUtilTest.java create mode 100644 oxAuth/Model/src/test/java/org/gluu/oxauth/model/util/Tester.java create mode 100644 oxAuth/Model/src/test/java/org/gluu/oxauth/model/util/URLPatternListTest.java create mode 100644 oxAuth/Model/src/test/resources/json-logic-node.json create mode 100644 oxAuth/Model/src/test/resources/testng-benchmark.xml create mode 100644 oxAuth/Model/src/test/resources/testng-multi-authz.xml create mode 100644 oxAuth/Model/src/test/resources/testng.xml create mode 100644 oxAuth/README create mode 100644 oxAuth/README.md create mode 100644 oxAuth/Server/.gitignore create mode 100644 oxAuth/Server/.metadata/src/main/webapp/WEB-INF/.gitignore create mode 100644 oxAuth/Server/conf/gluu-couchbase.properties create mode 100644 oxAuth/Server/conf/gluu-ldap.properties create mode 100644 oxAuth/Server/conf/gluu-spanner.properties create mode 100644 oxAuth/Server/conf/gluu-sql.properties create mode 100644 oxAuth/Server/conf/gluu.properties create mode 100644 oxAuth/Server/conf/keystore.jks create mode 100644 oxAuth/Server/conf/oxauth-config.json create mode 100644 oxAuth/Server/conf/oxauth-errors.json create mode 100644 oxAuth/Server/conf/oxauth-static-conf.json create mode 100644 oxAuth/Server/conf/oxauth-web-keys.json create mode 100644 oxAuth/Server/conf/salt create mode 100644 oxAuth/Server/integrations.deprecatred/inwebo/InWeboExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations.deprecatred/oneid/OneIdExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations.deprecatred/oneid/docs/workflow.png create mode 100644 oxAuth/Server/integrations.deprecatred/oneid/docs/workflow.txt create mode 100644 oxAuth/Server/integrations.deprecatred/oneid/lib/oneid.py create mode 100644 oxAuth/Server/integrations.deprecatred/oneid/lib/stringprep.py create mode 100644 oxAuth/Server/integrations.deprecatred/oxpush/oxPushExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations.deprecatred/phonefactor/PhoneFactorExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations.deprecatred/phonefactor/repository/com/phonefactor/PhoneFactorSDK/2.13/PhoneFactorSDK-2.13.jar create mode 100644 oxAuth/Server/integrations.deprecatred/phonefactor/repository/com/phonefactor/PhoneFactorSDK/2.13/PhoneFactorSDK-2.13.pom create mode 100644 oxAuth/Server/integrations.deprecatred/phonefactor/repository/com/phonefactor/PhoneFactorSDK/maven-metadata-local.xml create mode 100644 oxAuth/Server/integrations.deprecatred/toopher/ToopherExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations.deprecatred/toopher/repository/com/toopher/ToopherSDK/1.0.0/ToopherSDK-1.0.0.jar create mode 100644 oxAuth/Server/integrations.deprecatred/toopher/repository/com/toopher/ToopherSDK/1.0.0/ToopherSDK-1.0.0.pom create mode 100644 oxAuth/Server/integrations.deprecatred/toopher/repository/com/toopher/ToopherSDK/maven-metadata-local.xml create mode 100644 oxAuth/Server/integrations.deprecatred/toopher/sdk/pom.xml create mode 100644 oxAuth/Server/integrations.deprecatred/toopher/sdk/src/main/java/com/toopher/AuthenticationStatus.java create mode 100644 oxAuth/Server/integrations.deprecatred/toopher/sdk/src/main/java/com/toopher/PairingStatus.java create mode 100644 oxAuth/Server/integrations.deprecatred/toopher/sdk/src/main/java/com/toopher/RequestError.java create mode 100644 oxAuth/Server/integrations.deprecatred/toopher/sdk/src/main/java/com/toopher/ToopherAPI.java create mode 100644 oxAuth/Server/integrations.deprecatred/wikid/README.txt create mode 100644 oxAuth/Server/integrations.deprecatred/wikid/WikidExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations/Migration_stepts_to_3.1.x.txt create mode 100644 oxAuth/Server/integrations/Migration_stepts_to_4.0.txt create mode 100644 oxAuth/Server/integrations/ThumbSignIn/README.md create mode 100644 oxAuth/Server/integrations/ThumbSignIn/ThumbSignInExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations/acr_router/Readme.txt create mode 100644 oxAuth/Server/integrations/acr_router/acr_router_authenticator.py create mode 100644 oxAuth/Server/integrations/acr_saml_router/Readme.txt create mode 100644 oxAuth/Server/integrations/acr_saml_router/acr_smal_router_authenticator.py create mode 100644 oxAuth/Server/integrations/allowed_countries/allowed_countries.py create mode 100644 oxAuth/Server/integrations/allowed_countries/readme.txt create mode 100644 oxAuth/Server/integrations/authz/ConsentGatheringSample.py create mode 100644 oxAuth/Server/integrations/authz/docs/Authz design.dia create mode 100644 oxAuth/Server/integrations/authz/docs/Authz design.png create mode 100644 oxAuth/Server/integrations/azuread/AzureADAuthenticationForGluu.py create mode 100644 oxAuth/Server/integrations/azuread/README.md create mode 100644 oxAuth/Server/integrations/basic.change_password/BasicPassowrdUpdateExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations/basic.change_password/auth/pwd/new-password.xhtml create mode 100644 oxAuth/Server/integrations/basic.client_group/BasicClientGroupExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations/basic.client_group/README.txt create mode 100644 oxAuth/Server/integrations/basic.client_group/client_group.json create mode 100644 oxAuth/Server/integrations/basic.external_logout/BasicExternalAuthenticatorWithExternalLogout.py create mode 100644 oxAuth/Server/integrations/basic.external_logout/README.txt create mode 100644 oxAuth/Server/integrations/basic.ldap_auth_confs/LdapAuthConfExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations/basic.ldap_auth_confs/README.txt create mode 100644 oxAuth/Server/integrations/basic.lock.account/BasicLockAccountExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations/basic.lock.account/README.txt create mode 100644 oxAuth/Server/integrations/basic.multi_auth_conf/BasicMultiAuthConfExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations/basic.multi_auth_conf/INSTALLATION.txt create mode 100644 oxAuth/Server/integrations/basic.multi_auth_conf/README.txt create mode 100644 oxAuth/Server/integrations/basic.multi_login/BasicMultiLoginExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations/basic.multi_login/INSTALLATION.txt create mode 100644 oxAuth/Server/integrations/basic.multi_login/README.txt create mode 100644 oxAuth/Server/integrations/basic.multiple_test_email_addresses/BasicMultipleTestEmailAddressesExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations/basic.multiple_test_email_addresses/README.txt create mode 100644 oxAuth/Server/integrations/basic.one_session/BasicOneSessionExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations/basic.one_session/README.txt create mode 100644 oxAuth/Server/integrations/basic.password_expiration/PasswordExpiration.py create mode 100644 oxAuth/Server/integrations/basic.password_expiration/README.md create mode 100644 oxAuth/Server/integrations/basic.passwordchangewithvalidations/INSTALLATION.txt create mode 100644 oxAuth/Server/integrations/basic.passwordchangewithvalidations/PasswordChangeWithValidations.py create mode 100644 oxAuth/Server/integrations/basic.passwordchangewithvalidations/README.md create mode 100644 oxAuth/Server/integrations/basic.passwordchangewithvalidations/pwd/newpassword.xhtml create mode 100644 oxAuth/Server/integrations/basic.recaptcha/BasicRecaptchaExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations/basic.recaptcha/README.md create mode 100644 oxAuth/Server/integrations/basic.recaptcha/cert_creds.json create mode 100644 oxAuth/Server/integrations/basic.recaptcha/login-page.png create mode 100644 oxAuth/Server/integrations/basic.reset_to_step/BasicResetToStepExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations/basic.reset_to_step/README.txt create mode 100644 oxAuth/Server/integrations/basic/BasicExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations/basic/INSTALLATION.txt create mode 100644 oxAuth/Server/integrations/basic/README.txt create mode 100644 oxAuth/Server/integrations/basicMultiAuth_Duo/Readme.md create mode 100644 oxAuth/Server/integrations/basicMultiAuth_Duo/basicmultiauthduo.py create mode 100644 oxAuth/Server/integrations/bcrypt_ssha_migration/pwd_migration.py create mode 100644 oxAuth/Server/integrations/bioid/BioIDExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations/bioid/README.md create mode 100644 oxAuth/Server/integrations/bioid/README.txt create mode 100644 oxAuth/Server/integrations/cas2/Cas2ExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations/cas2_duo/Cas2DuoExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations/cas2_duo/Readme.md create mode 100644 oxAuth/Server/integrations/cas2_duo/docs/joinded_flows.dia create mode 100644 oxAuth/Server/integrations/cas2_duo/docs/joinded_flows.jpg create mode 100644 oxAuth/Server/integrations/casa/Casa.py create mode 100644 oxAuth/Server/integrations/casa/README.md create mode 100644 oxAuth/Server/integrations/casa_external_user/casa_external.py create mode 100644 oxAuth/Server/integrations/casa_external_user/readme.md create mode 100644 oxAuth/Server/integrations/cert/Generate certs guide.md create mode 100644 oxAuth/Server/integrations/cert/Generate certs without configs.md create mode 100644 oxAuth/Server/integrations/cert/Quick certs guide for testing.md create mode 100644 oxAuth/Server/integrations/cert/README.txt create mode 100644 oxAuth/Server/integrations/cert/UserCertExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations/cert/docs/Cert design.dia create mode 100644 oxAuth/Server/integrations/cert/docs/Cert design.jpg create mode 100644 oxAuth/Server/integrations/cert/sample/cert_creds.json create mode 100644 oxAuth/Server/integrations/cert/sample/generated_certs.zip create mode 100644 oxAuth/Server/integrations/ciba/FirebaseEndUserNotification.py create mode 100644 oxAuth/Server/integrations/compromised_password/compromised_password.py create mode 100644 oxAuth/Server/integrations/compromised_password/readme.txt create mode 100644 oxAuth/Server/integrations/consent-gathering-with-redirection/ConsentGatheringSample_redirect.py create mode 100644 oxAuth/Server/integrations/consent-gathering-with-redirection/README create mode 100644 oxAuth/Server/integrations/consent-gathering-with-redirection/postauthorize.xhtml create mode 100644 oxAuth/Server/integrations/consent-gathering-with-redirection/redirect.xhtml create mode 100644 oxAuth/Server/integrations/consent-gathering-with-redirection/third-party-mock-app/README.txt.txt create mode 100644 oxAuth/Server/integrations/consent-gathering-with-redirection/third-party-mock-app/consent_page.html create mode 100644 oxAuth/Server/integrations/custom_registration/Attributes.json create mode 100644 oxAuth/Server/integrations/custom_registration/README.md create mode 100644 oxAuth/Server/integrations/custom_registration/reg.xhtml create mode 100644 oxAuth/Server/integrations/custom_registration/register.py create mode 100644 oxAuth/Server/integrations/deduce/PasswordlessAuthenticationWithDeduceImpossTravel.py create mode 100644 oxAuth/Server/integrations/deduce/README_PASSWORDLESS_AUTHN_WITH_DEDUCE.md create mode 100644 oxAuth/Server/integrations/duo-universal-prompt/DuoUniversalPromptExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations/duo-universal-prompt/README.txt create mode 100644 oxAuth/Server/integrations/duo.passport.combine/Passport_and_Duo-Universal.py create mode 100644 oxAuth/Server/integrations/duo.passport.combine/Passport_and_Duo-Universal_ignore_empty.py create mode 100644 oxAuth/Server/integrations/duo.passport.combine/readme.md create mode 100644 oxAuth/Server/integrations/duo/DuoExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations/duo/INSTALLATION.txt create mode 100644 oxAuth/Server/integrations/duo/README.txt create mode 100644 oxAuth/Server/integrations/duo/lib/duo_web.py create mode 100644 oxAuth/Server/integrations/duo2_Gluu3/DUO_Universal_3.1.7.py create mode 100644 oxAuth/Server/integrations/duo2_Gluu3/readme.md create mode 100644 oxAuth/Server/integrations/email2FA/README.md create mode 100644 oxAuth/Server/integrations/email2FA/email2FAExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations/email2FA/entertoken.xhtml create mode 100644 oxAuth/Server/integrations/fido2/Fido2ExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations/forgot_password/README.md create mode 100644 oxAuth/Server/integrations/forgot_password/forgot_password.py create mode 100644 oxAuth/Server/integrations/fortinet/FortinetExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations/fortinet/README.txt create mode 100644 oxAuth/Server/integrations/gplus/GooglePlusExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations/gplus/README.txt create mode 100644 oxAuth/Server/integrations/gplus/sample/custom_script_entry.ldif create mode 100644 oxAuth/Server/integrations/gplus/sample/gplus_client_secrets.json create mode 100644 oxAuth/Server/integrations/gplus/workflow.txt create mode 100644 oxAuth/Server/integrations/idfirst/README_idfirst.md create mode 100644 oxAuth/Server/integrations/idfirst/idfirst.py create mode 100644 oxAuth/Server/integrations/inwebo/inwebo.py create mode 100644 oxAuth/Server/integrations/inwebo/iw_creds.json create mode 100644 oxAuth/Server/integrations/inwebo/iw_va.xhtml create mode 100644 oxAuth/Server/integrations/inwebo/iwauthenticate.xhtml create mode 100644 oxAuth/Server/integrations/inwebo/iwlogin.xhtml create mode 100644 oxAuth/Server/integrations/inwebo/iwlogin_without_password.xhtml create mode 100644 oxAuth/Server/integrations/inwebo/iwpushnotification.xhtml create mode 100644 oxAuth/Server/integrations/inwebo/oxauth.properties create mode 100644 oxAuth/Server/integrations/new_acr_link/README.md create mode 100644 oxAuth/Server/integrations/new_acr_link/new_acr_link.py create mode 100644 oxAuth/Server/integrations/otp/Installation.md create mode 100644 oxAuth/Server/integrations/otp/OtpExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations/otp/Properties description.md create mode 100644 oxAuth/Server/integrations/otp/Readme.md create mode 100644 oxAuth/Server/integrations/otp/img/gluu_otp_integration_authentication_workflow.png create mode 100644 oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp-keyprovisioning/0.0.1-SNAPSHOT/_remote.repositories create mode 100644 oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp-keyprovisioning/0.0.1-SNAPSHOT/maven-metadata-local.xml create mode 100644 oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp-keyprovisioning/0.0.1-SNAPSHOT/oath-otp-keyprovisioning-0.0.1-SNAPSHOT-javadoc.jar create mode 100644 oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp-keyprovisioning/0.0.1-SNAPSHOT/oath-otp-keyprovisioning-0.0.1-SNAPSHOT-sources.jar create mode 100644 oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp-keyprovisioning/0.0.1-SNAPSHOT/oath-otp-keyprovisioning-0.0.1-SNAPSHOT.jar create mode 100644 oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp-keyprovisioning/0.0.1-SNAPSHOT/oath-otp-keyprovisioning-0.0.1-SNAPSHOT.pom create mode 100644 oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp-keyprovisioning/maven-metadata-local.xml create mode 100644 oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp/0.0.1-SNAPSHOT/_remote.repositories create mode 100644 oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp/0.0.1-SNAPSHOT/maven-metadata-local.xml create mode 100644 oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp/0.0.1-SNAPSHOT/oath-otp-0.0.1-SNAPSHOT-javadoc.jar create mode 100644 oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp/0.0.1-SNAPSHOT/oath-otp-0.0.1-SNAPSHOT-sources.jar create mode 100644 oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp/0.0.1-SNAPSHOT/oath-otp-0.0.1-SNAPSHOT.jar create mode 100644 oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp/0.0.1-SNAPSHOT/oath-otp-0.0.1-SNAPSHOT.pom create mode 100644 oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp/maven-metadata-local.xml create mode 100644 oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-parent/0.0.1-SNAPSHOT/_remote.repositories create mode 100644 oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-parent/0.0.1-SNAPSHOT/maven-metadata-local.xml create mode 100644 oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-parent/0.0.1-SNAPSHOT/oath-parent-0.0.1-SNAPSHOT.pom create mode 100644 oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-parent/maven-metadata-local.xml create mode 100644 oxAuth/Server/integrations/otp/sample/otp_configuration.json create mode 100644 oxAuth/Server/integrations/otp/sequence_diagram.txt create mode 100644 oxAuth/Server/integrations/passport/PassportExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations/passport/README.md create mode 100644 oxAuth/Server/integrations/passport/sample/passport_script_entry.ldif create mode 100644 oxAuth/Server/integrations/passwordless/PasswordlessAuthentication.py create mode 100644 oxAuth/Server/integrations/passwordless/README.md create mode 100644 oxAuth/Server/integrations/passwordless/bundle.zip create mode 100644 oxAuth/Server/integrations/passwurd/PasswurdAuthentication.py create mode 100644 oxAuth/Server/integrations/passwurd/README.md create mode 100644 oxAuth/Server/integrations/passwurd/bundle-bak.zip create mode 100644 oxAuth/Server/integrations/passwurd/bundle.zip create mode 100644 oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/i18n/oxauth.properties create mode 100644 oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/pages/passwurd/enterPwd.xhtml create mode 100644 oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/pages/passwurd/login-template.xhtml create mode 100644 oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/pages/passwurd/login.xhtml create mode 100644 oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/pages/passwurd/savePwd.xhtml create mode 100644 oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/static/passwurd/font-awesome-5.12.1.all.min.js create mode 100644 oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/static/passwurd/logger_pwd.js create mode 100644 oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/static/passwurd/logger_username.js create mode 100644 oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/static/passwurd/style.css create mode 100644 oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/static/passwurd/tachyons.min.css create mode 100644 oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/python/libs/passwurd-external_bioid.py create mode 100644 oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/python/libs/passwurd-external_fido2.py create mode 100644 oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/python/libs/passwurd-external_otp.py create mode 100644 oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/python/libs/passwurd-external_super_gluu.py create mode 100644 oxAuth/Server/integrations/pingid/README.md create mode 100644 oxAuth/Server/integrations/pingid/bundle.zip create mode 100644 oxAuth/Server/integrations/pingid/flow.png create mode 100644 oxAuth/Server/integrations/pingid/oxauth-pingid-1.0.jar create mode 100644 oxAuth/Server/integrations/pingid/pingIDAuthenticator.py create mode 100644 oxAuth/Server/integrations/pingid/pom.xml create mode 100644 oxAuth/Server/integrations/pingid/src/main/java/org/gluu/oxauth/ping/HttpException.java create mode 100644 oxAuth/Server/integrations/pingid/src/main/java/org/gluu/oxauth/ping/PPMRequestBroker.java create mode 100644 oxAuth/Server/integrations/pingid/src/main/java/org/gluu/oxauth/ping/RequestToken.java create mode 100644 oxAuth/Server/integrations/pingid/src/main/java/org/gluu/oxauth/ping/ResponseTokenParser.java create mode 100644 oxAuth/Server/integrations/pingid/src/main/java/org/gluu/oxauth/ping/TokenProcessingException.java create mode 100644 oxAuth/Server/integrations/pingid/src/main/java/org/gluu/oxauth/ping/UserManagerBroker.java create mode 100644 oxAuth/Server/integrations/pingid/src/main/java/org/gluu/oxauth/ping/Utils.java create mode 100644 oxAuth/Server/integrations/postauthn/postauthn.py create mode 100644 oxAuth/Server/integrations/privacyidea/README.md create mode 100644 oxAuth/Server/integrations/privacyidea/pages/auth/privacyidea/privacyidea.xhtml create mode 100644 oxAuth/Server/integrations/privacyidea/privacyidea.py create mode 100644 oxAuth/Server/integrations/registration/read.txt create mode 100644 oxAuth/Server/integrations/registration/register.py create mode 100644 oxAuth/Server/integrations/saml-passport/SamlPassportAuthenticator.py create mode 100644 oxAuth/Server/integrations/saml.alias.attribute/README.md create mode 100644 oxAuth/Server/integrations/saml.alias.attribute/idp_script.py create mode 100644 oxAuth/Server/integrations/saml/Gluu Inbound SAML Design (no Session).png create mode 100644 oxAuth/Server/integrations/saml/INSTALLATION.txt create mode 100644 oxAuth/Server/integrations/saml/README.txt create mode 100644 oxAuth/Server/integrations/saml/SamlExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations/saml2oidc_acr_router/README.md create mode 100644 oxAuth/Server/integrations/saml2oidc_acr_router/saml2oidc_acr_router.py create mode 100644 oxAuth/Server/integrations/smpp/smpp2FA.py create mode 100644 oxAuth/Server/integrations/stytch/README.md create mode 100644 oxAuth/Server/integrations/stytch/stytchExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations/super_gluu/SuperGluuExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations/super_gluu/repository/com/google/gcm/gcm-server/1.0.1.gluu/gcm-server-1.0.1.gluu.jar create mode 100644 oxAuth/Server/integrations/super_gluu/repository/com/google/gcm/gcm-server/1.0.1.gluu/gcm-server-1.0.1.gluu.jar.md5 create mode 100644 oxAuth/Server/integrations/super_gluu/repository/com/google/gcm/gcm-server/1.0.1.gluu/gcm-server-1.0.1.gluu.jar.sha1 create mode 100644 oxAuth/Server/integrations/super_gluu/repository/com/google/gcm/gcm-server/1.0.1.gluu/gcm-server-1.0.1.gluu.pom create mode 100644 oxAuth/Server/integrations/super_gluu/repository/com/google/gcm/gcm-server/1.0.1.gluu/gcm-server-1.0.1.gluu.pom.md5 create mode 100644 oxAuth/Server/integrations/super_gluu/repository/com/google/gcm/gcm-server/1.0.1.gluu/gcm-server-1.0.1.gluu.pom.sha1 create mode 100644 oxAuth/Server/integrations/super_gluu/repository/com/google/gcm/gcm-server/maven-metadata.xml create mode 100644 oxAuth/Server/integrations/super_gluu/repository/com/google/gcm/gcm-server/maven-metadata.xml.md5 create mode 100644 oxAuth/Server/integrations/super_gluu/repository/com/google/gcm/gcm-server/maven-metadata.xml.sha1 create mode 100644 oxAuth/Server/integrations/super_gluu/sample/super_gluu_creds.json create mode 100644 oxAuth/Server/integrations/twilio_sms/README.txt create mode 100644 oxAuth/Server/integrations/twilio_sms/twilio2FA.py create mode 100644 oxAuth/Server/integrations/u2f/U2fExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations/uaf/Installation.md create mode 100644 oxAuth/Server/integrations/uaf/Properties description.md create mode 100644 oxAuth/Server/integrations/uaf/Readme.md create mode 100644 oxAuth/Server/integrations/uaf/UafExternalAuthenticator.py create mode 100644 oxAuth/Server/integrations/uaf/img/gluu_uaf_integration_authentication_workflow.png create mode 100644 oxAuth/Server/integrations/uaf/img/oob_qr_code.png create mode 100644 oxAuth/Server/integrations/uaf/img/typical_uaf_architecture.png create mode 100644 oxAuth/Server/integrations/uaf/img/uaf_device_integration_models.png create mode 100644 oxAuth/Server/integrations/uaf/sequence_diagram.txt create mode 100644 oxAuth/Server/integrations/whispeak/README.md create mode 100644 oxAuth/Server/integrations/whispeak/whispeak_open_v1.py create mode 100644 oxAuth/Server/integrations/wwpass/INSTALLATION.md create mode 100644 oxAuth/Server/integrations/wwpass/README.md create mode 100644 oxAuth/Server/integrations/wwpass/pages/auth/wwpass/checkemail.xhtml create mode 100644 oxAuth/Server/integrations/wwpass/pages/auth/wwpass/wwpass.xhtml create mode 100644 oxAuth/Server/integrations/wwpass/pages/auth/wwpass/wwpassbind.xhtml create mode 100644 oxAuth/Server/integrations/wwpass/static/js/wwpass-frontend.js create mode 100644 oxAuth/Server/integrations/wwpass/ticket.json create mode 100644 oxAuth/Server/integrations/wwpass/wwpass.ca.crt create mode 100644 oxAuth/Server/integrations/wwpass/wwpass.py create mode 100644 oxAuth/Server/integrations/wwpass/wwpassauth.py create mode 100644 oxAuth/Server/integrations/yubicloud/README.txt create mode 100644 oxAuth/Server/integrations/yubicloud/YubicloudExternalAuthenticator.py create mode 100644 oxAuth/Server/pom.xml create mode 100644 oxAuth/Server/profiles/.gitignore create mode 100644 oxAuth/Server/profiles/default/client_keystore.pkcs12 create mode 100644 oxAuth/Server/profiles/default/config-build.properties create mode 100644 oxAuth/Server/profiles/default/config-oxauth-test-data.properties create mode 100644 oxAuth/Server/profiles/default/config-oxauth-test.properties create mode 100644 oxAuth/Server/profiles/default/config-oxauth.properties create mode 100644 oxAuth/Server/src-remove/pages.xml create mode 100644 oxAuth/Server/src/main/docs/mustache/markdown.mustache create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/audit/ApplicationAuditLogger.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/audit/debug/ServletLoggingFilter.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/audit/debug/entity/HttpRequest.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/audit/debug/entity/HttpResponse.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/audit/debug/wrapper/RequestWrapper.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/audit/debug/wrapper/ResponseWrapper.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/auth/AuthenticationFilter.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/auth/Authenticator.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/auth/MTLSService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/auth/SelectAccountAction.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/AuthorizeAction.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/AuthorizeRestWebService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/AuthorizeRestWebServiceImpl.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/AuthorizeRestWebServiceValidator.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/ConsentGathererService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/ConsentGatheringSessionService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/DeviceAuthorizationAction.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/DeviceAuthorizationRestWebService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/DeviceAuthorizationRestWebServiceImpl.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/LoginAction.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/LogoutAction.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/bcauthorize/ws/rs/BackchannelAuthorizeRestWebService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/bcauthorize/ws/rs/BackchannelAuthorizeRestWebServiceImpl.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/bcauthorize/ws/rs/BackchannelDeviceRegistrationRestWebService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/bcauthorize/ws/rs/BackchannelDeviceRegistrationRestWebServiceImpl.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/bcauthorize/ws/rs/CIBAAuthorizeAction.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBAAuthorizeParamsValidatorService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBAConfigurationService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBADeviceRegistrationValidatorService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBAEndUserNotificationService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBAPingCallbackService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBAPushErrorService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBAPushTokenDeliveryService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBARegisterClientMetadataService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBARegisterClientResponseService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBARegisterParamsValidatorService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/clientinfo/ws/rs/ClientInfoRestWebService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/clientinfo/ws/rs/ClientInfoRestWebServiceImpl.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/crypto/cert/CertificateParser.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/crypto/random/ChallengeGenerator.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/crypto/random/RandomChallengeGenerator.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/crypto/signature/SHA256withECDSASignatureVerification.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/crypto/signature/SignatureVerification.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/exception/GlobalExceptionHandler.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/exception/GlobalExceptionHandlerFactory.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/exception/InvalidSchemaUpdateException.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/exception/UncaughtException.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/exception/fido/u2f/BadConfigurationException.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/exception/fido/u2f/DeviceCompromisedException.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/exception/fido/u2f/InvalidDeviceCounterException.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/exception/fido/u2f/InvalidKeyHandleDeviceException.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/exception/fido/u2f/NoEligableDevicesException.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/filter/CorsFilter.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/filter/CorsFilterConfig.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/gluu/ws/rs/GluuConfigurationWS.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/i18n/ApplicationFacesLocalizationConfigPopulator.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/i18n/CustomResourceBundle.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/i18n/LanguageBean.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/idgen/ws/rs/IdGenService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/idgen/ws/rs/InumGenerator.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/introspection/ws/rs/IntrospectionWebService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/jwk/ws/rs/JwkRestWebService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/jwk/ws/rs/JwkRestWebServiceImpl.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/GluuOrganization.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/audit/Action.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/audit/OAuth2AuditLog.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/auth/AuthenticationMode.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/AuthorizeParamsValidator.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/Claim.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/ClaimValue.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/ClaimValueType.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/IdTokenMember.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/JwtAuthorizationRequest.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/ScopeChecker.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/UserInfoMember.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/clientinfo/ClientInfoErrorResponseType.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/clientinfo/ClientInfoParamsValidator.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AbstractAuthorizationGrant.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AbstractToken.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AccessToken.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AuthorizationCode.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AuthorizationCodeGrant.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AuthorizationGrant.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AuthorizationGrantList.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AuthorizationGrantType.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/CIBAGrant.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/CacheGrant.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/CibaRequestCacheControl.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/CibaRequestStatus.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/ClientCredentialsGrant.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/ClientTokens.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/DefaultScope.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/DeviceAuthorizationCacheControl.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/DeviceAuthorizationStatus.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/DeviceCodeGrant.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/ExecutionContext.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/IAuthorizationGrant.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/IAuthorizationGrantList.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/IdToken.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/ImplicitGrant.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/RefreshToken.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/ResourceOwnerPasswordCredentialsGrant.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/SessionTokens.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/SimpleAuthorizationGrant.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/UnmodifiableAuthorizationGrant.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/config/Conf.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/config/ConfigurationFactory.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/config/Constants.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/config/WebKeysConfiguration.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/discovery/OpenIdConnectDiscoveryParamsValidator.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/error/ErrorMessageList.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/error/ErrorMessages.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/error/ErrorResponseFactory.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/error/JsonErrorResponse.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/exception/AcrChangedException.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/exception/InvalidSessionStateException.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/exception/InvalidStateException.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/fido/u2f/AuthenticateRequestMessageLdap.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/fido/u2f/DeviceRegistration.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/fido/u2f/DeviceRegistrationConfiguration.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/fido/u2f/DeviceRegistrationResult.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/fido/u2f/RegisterRequestMessageLdap.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/fido/u2f/RequestMessageLdap.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/ldap/CIBARequest.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/ldap/ClientAuthorization.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/ldap/SchemaEntry.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/ldap/TokenAttributes.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/ldap/TokenLdap.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/ldap/TokenType.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/ldap/UserGroup.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/registration/RegisterParamsValidator.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/session/SessionClient.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/ClientAssertion.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/HandleTokenFactory.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/HttpAuthTokenType.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/IdTokenFactory.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/JwrService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/JwtSigner.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/PersistentJwt.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/TokenParamsValidator.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/ValidateTokenParamsValidator.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/model/userinfo/UserInfoParamsValidator.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/register/ws/rs/RegisterRestWebService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/register/ws/rs/RegisterRestWebServiceImpl.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/revoke/RevokeRestWebService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/revoke/RevokeRestWebServiceImpl.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/revoke/RevokeSessionRestWebService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/security/Identity.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/AppInitializer.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/AttributeService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/AuthenticationFilterService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/AuthenticationProtectionService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/AuthenticationService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/AuthorizeService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/BaseAuthFilterService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/CleanerTimer.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/ClientAuthorizationsService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/ClientFilterService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/ClientService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/CookieService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/CryptoProviderProviderFactory.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/DeviceAuthorizationService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/ErrorHandlerService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/GrantService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/KeyGeneratorTimer.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/LdapCustomAuthenticationConfigurationService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/LocalResponseCache.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/MetricService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/OrganizationService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/OxAuthConfigurationService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/PairwiseIdentifierService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/RedirectUriResponse.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/RedirectionUriService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/RequestParameterService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/ResteasyInitializer.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/ScopeService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/SectorIdentifierService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/ServerCryptoProvider.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/SessionIdService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/SpontaneousScopeService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/UserGroupService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/UserService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/cdi/event/AuthConfigurationEvent.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/cdi/event/ExpirationEvent.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/cdi/event/KeyGenerationEvent.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/cdi/event/ReloadAuthScript.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/cdi/event/StatEvent.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/ciba/CibaEncryptionService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/ciba/CibaRequestService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/ciba/CibaRequestsProcessorJob.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/custom/CustomScriptService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/date/DateFormatterService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/expiration/ExpId.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/expiration/ExpType.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/expiration/ExpirationNotificatorTimer.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalApplicationSessionService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalAuthenticationService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalCibaEndUserNotificationService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalConsentGatheringService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalDynamicClientRegistrationService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalDynamicScopeService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalEndSessionService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalIdGeneratorService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalIntrospectionService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalPostAuthnService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalResourceOwnerPasswordCredentialsService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalRevokeTokenService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalSpontaneousScopeService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalUmaClaimsGatheringService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalUmaRptClaimsService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalUmaRptPolicyService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalUpdateTokenService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ConsentGatheringContext.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/DynamicClientRegistrationContext.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/DynamicScopeExternalContext.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/EndSessionContext.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ExternalCibaEndUserNotificationContext.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ExternalIntrospectionContext.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ExternalPostAuthnContext.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ExternalResourceOwnerPasswordCredentialsContext.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ExternalScriptContext.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ExternalUmaRptClaimsContext.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ExternalUpdateTokenContext.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/RevokeTokenContext.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/SpontaneousScopeExternalContext.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/internal/InternalDefaultPersonAuthenticationType.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/session/SessionEvent.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/session/SessionEventType.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/ApplicationService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/AuthenticationService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/ClientDataValidationService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/DeviceRegistrationService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/RawAuthenticationService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/RawRegistrationService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/RegistrationService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/RequestService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/UserSessionIdService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/ValidationService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/util/KeyGenerator.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/logger/LoggerService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/net/HttpService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/net/HttpService2.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/push/sns/PushPlatform.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/push/sns/PushSnsService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/stat/StatService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/stat/StatTimer.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/status/ldap/LdapStatusTimer.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/service/token/TokenService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/servlet/BcFirebaseMessagingSwServlet.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/servlet/OpenIdConfiguration.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/servlet/OxAuthFaviconServlet.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/servlet/OxAuthLogoServlet.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/servlet/SectorIdentifier.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/servlet/WebFinger.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/session/ws/rs/CheckSessionStatusRestWebServiceImpl.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/session/ws/rs/EndSessionRestWebService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/session/ws/rs/EndSessionRestWebServiceImpl.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/session/ws/rs/EndSessionUtils.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/session/ws/rs/LogoutTokenFactory.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/token/ws/rs/TokenRestWebService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/token/ws/rs/TokenRestWebServiceImpl.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/Claims.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/IPolicyExternalAuthorization.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/PolicyExternalAuthorizationEnum.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/UmaAuthorizationContext.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/UmaAuthorizationContextBuilder.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/UmaGatherContext.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/UmaPCT.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/UmaRPT.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/UmaScriptByScope.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/UmaWebException.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/RedirectParameters.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaExpressionService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaGatherer.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaNeedsInfoService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaPctService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaPermissionService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaResourceService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaRptService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaScopeService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaSessionService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaTokenService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaValidationService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/uma/ws/rs/UmaGatheringWS.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/uma/ws/rs/UmaMetadataWS.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/uma/ws/rs/UmaPermissionRegistrationWS.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/uma/ws/rs/UmaResourceRegistrationWS.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/uma/ws/rs/UmaRptIntrospectionWS.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/uma/ws/rs/UmaScopeIconWS.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/uma/ws/rs/UmaScopeWS.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/userinfo/ws/rs/UserInfoRestWebService.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/userinfo/ws/rs/UserInfoRestWebServiceImpl.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/util/CertUtil.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/util/PasswordValidator.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/util/QueryStringDecoder.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/util/RedirectUtil.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/util/ServerUtil.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/util/TokenHashUtil.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/ws/rs/controller/HealthCheckController.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/ws/rs/fido/u2f/U2fAuthenticationWS.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/ws/rs/fido/u2f/U2fConfigurationWS.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/ws/rs/fido/u2f/U2fRegistrationWS.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/ws/rs/stat/StatResponse.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/ws/rs/stat/StatResponseItem.java create mode 100644 oxAuth/Server/src/main/java/org/gluu/oxauth/ws/rs/stat/StatWS.java create mode 100644 oxAuth/Server/src/main/resources/META-INF/beans.xml create mode 100644 oxAuth/Server/src/main/resources/META-INF/navigation/cas2.navigation.xml create mode 100644 oxAuth/Server/src/main/resources/META-INF/navigation/cert.navigation.xml create mode 100644 oxAuth/Server/src/main/resources/META-INF/navigation/duo.navigation.xml create mode 100644 oxAuth/Server/src/main/resources/META-INF/navigation/gplus.navigation.xml create mode 100644 oxAuth/Server/src/main/resources/META-INF/navigation/otp.navigation.xml create mode 100644 oxAuth/Server/src/main/resources/META-INF/navigation/oxpush.navigation.xml create mode 100644 oxAuth/Server/src/main/resources/META-INF/navigation/passport.navigation.xml create mode 100644 oxAuth/Server/src/main/resources/META-INF/navigation/saml.navigation.xml create mode 100644 oxAuth/Server/src/main/resources/META-INF/navigation/super-gluu.navigation.xml create mode 100644 oxAuth/Server/src/main/resources/META-INF/navigation/u2f.navigation.xml create mode 100644 oxAuth/Server/src/main/resources/META-INF/navigation/uaf.navigation.xml create mode 100644 oxAuth/Server/src/main/resources/META-INF/navigation/uma2.sample.navigation.xml create mode 100644 oxAuth/Server/src/main/resources/META-INF/services/javax.faces.application.ApplicationConfigurationPopulator create mode 100644 oxAuth/Server/src/main/resources/ehcache.xml create mode 100644 oxAuth/Server/src/main/resources/faces-config.xml create mode 100644 oxAuth/Server/src/main/resources/log4j2.xml create mode 100644 oxAuth/Server/src/main/resources/oxauth.properties create mode 100644 oxAuth/Server/src/main/resources/oxauth_bg.properties create mode 100644 oxAuth/Server/src/main/resources/oxauth_de.properties create mode 100644 oxAuth/Server/src/main/resources/oxauth_en.properties create mode 100644 oxAuth/Server/src/main/resources/oxauth_es.properties create mode 100644 oxAuth/Server/src/main/resources/oxauth_fr.properties create mode 100644 oxAuth/Server/src/main/resources/oxauth_it.properties create mode 100644 oxAuth/Server/src/main/resources/oxauth_ru.properties create mode 100644 oxAuth/Server/src/main/resources/oxauth_tr.properties create mode 100644 oxAuth/Server/src/main/resources/quartz.properties create mode 100644 oxAuth/Server/src/main/resources/validation_messages.properties create mode 100644 oxAuth/Server/src/main/webapp-jetty/WEB-INF/jetty-env.xml create mode 100644 oxAuth/Server/src/main/webapp-jetty/WEB-INF/web.xml create mode 100644 oxAuth/Server/src/main/webapp-tomcat/META-INF/context.xml create mode 100644 oxAuth/Server/src/main/webapp/META-INF/MANIFEST.MF create mode 100644 oxAuth/Server/src/main/webapp/WEB-INF/faces-config.xml create mode 100644 oxAuth/Server/src/main/webapp/WEB-INF/firebase-messaging-sw.js create mode 100644 oxAuth/Server/src/main/webapp/WEB-INF/incl/layout/authorize-extended-template.xhtml create mode 100644 oxAuth/Server/src/main/webapp/WEB-INF/incl/layout/authorize-template.xhtml create mode 100644 oxAuth/Server/src/main/webapp/WEB-INF/incl/layout/bioid-template.xhtml create mode 100644 oxAuth/Server/src/main/webapp/WEB-INF/incl/layout/ciba-authorize-extended-template.xhtml create mode 100644 oxAuth/Server/src/main/webapp/WEB-INF/incl/layout/ciba-authorize-template.xhtml create mode 100644 oxAuth/Server/src/main/webapp/WEB-INF/incl/layout/login-extended-template.xhtml create mode 100644 oxAuth/Server/src/main/webapp/WEB-INF/incl/layout/login-template.xhtml create mode 100644 oxAuth/Server/src/main/webapp/WEB-INF/incl/layout/template.xhtml create mode 100644 oxAuth/Server/src/main/webapp/WEB-INF/incl/layout/whispeak-open-template.xhtml create mode 100644 oxAuth/Server/src/main/webapp/WEB-INF/static/favicon.ico create mode 100644 oxAuth/Server/src/main/webapp/WEB-INF/static/logo.png create mode 100644 oxAuth/Server/src/main/webapp/auth/bioid/bioid.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/bioid/css/uui.css create mode 100644 oxAuth/Server/src/main/webapp/auth/bioid/images/back.svg create mode 100644 oxAuth/Server/src/main/webapp/auth/bioid/images/logo.svg create mode 100644 oxAuth/Server/src/main/webapp/auth/bioid/images/perfect.png create mode 100644 oxAuth/Server/src/main/webapp/auth/bioid/images/tooclose.png create mode 100644 oxAuth/Server/src/main/webapp/auth/bioid/images/toofaraway.png create mode 100644 oxAuth/Server/src/main/webapp/auth/bioid/js/bws.capture.js create mode 100644 oxAuth/Server/src/main/webapp/auth/bioid/js/getUserMedia.js create mode 100644 oxAuth/Server/src/main/webapp/auth/bioid/js/getUserMedia.min.js create mode 100644 oxAuth/Server/src/main/webapp/auth/bioid/js/jquery-3.5.1.min.js create mode 100644 oxAuth/Server/src/main/webapp/auth/bioid/js/objLoader.js create mode 100644 oxAuth/Server/src/main/webapp/auth/bioid/js/objLoader.min.js create mode 100644 oxAuth/Server/src/main/webapp/auth/bioid/js/three.min.js create mode 100644 oxAuth/Server/src/main/webapp/auth/bioid/js/uui.js create mode 100644 oxAuth/Server/src/main/webapp/auth/bioid/language/de.json create mode 100644 oxAuth/Server/src/main/webapp/auth/bioid/language/en.json create mode 100644 oxAuth/Server/src/main/webapp/auth/bioid/model/head.obj create mode 100644 oxAuth/Server/src/main/webapp/auth/bioid/video/nodyourhead.mp4 create mode 100644 oxAuth/Server/src/main/webapp/auth/cas2/cas2login.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/cas2/cas2postlogin.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/cert/cert-invalid.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/cert/cert-login.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/cert/cert-not-selected.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/cert/login.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/compromised/complogin.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/compromised/newpassword.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/deduce/loginD.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/duo/duologin.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/duo/js/Duo-Web-v2.min.js create mode 100644 oxAuth/Server/src/main/webapp/auth/email_auth/entertoken.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/fido2/js/base64js.js create mode 100644 oxAuth/Server/src/main/webapp/auth/fido2/js/base64url.js create mode 100644 oxAuth/Server/src/main/webapp/auth/fido2/js/webauthn.js create mode 100644 oxAuth/Server/src/main/webapp/auth/fido2/login.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/fido2/platform.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/fido2/secKeys.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/fido2/step1.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/forgot_password/entertoken.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/forgot_password/forgot.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/forgot_password/newpassword.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/gplus/gpluslogin.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/gplus/gpluspostlogin.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/idfirst/alter_login.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/idfirst/idfirst_login.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/inwebo/iwauthenticate.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/inwebo/iwlogin.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/oneid/oneidlogin.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/oneid/oneidpostlogin.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/otp/enroll.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/otp/otplogin.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/otp_sms/otp_sms.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/oxpush/oxauthenticate.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/oxpush/oxlogin.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/oxpush/oxpair.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/passport/img/apple.png create mode 100644 oxAuth/Server/src/main/webapp/auth/passport/img/dropbox.png create mode 100644 oxAuth/Server/src/main/webapp/auth/passport/img/facebook.png create mode 100644 oxAuth/Server/src/main/webapp/auth/passport/img/github.png create mode 100644 oxAuth/Server/src/main/webapp/auth/passport/img/google.png create mode 100644 oxAuth/Server/src/main/webapp/auth/passport/img/linkedin.png create mode 100644 oxAuth/Server/src/main/webapp/auth/passport/img/openidconnect.png create mode 100644 oxAuth/Server/src/main/webapp/auth/passport/img/tumblr.png create mode 100644 oxAuth/Server/src/main/webapp/auth/passport/img/twitter.png create mode 100644 oxAuth/Server/src/main/webapp/auth/passport/img/windowslive.png create mode 100755 oxAuth/Server/src/main/webapp/auth/passport/passportlogin.xhtml create mode 100755 oxAuth/Server/src/main/webapp/auth/passport/passportpostlogin.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/passport/sample-redirector.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/phonefactor/pflogin.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/pwd/newpassword.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/recaptcha/login.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/register/register.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/saml/samllogin.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/saml/samlpostlogin.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/super-gluu/login.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/thumbsignin/README.md create mode 100644 oxAuth/Server/src/main/webapp/auth/thumbsignin/expired.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/thumbsignin/js/authenticate-bundle.js create mode 100644 oxAuth/Server/src/main/webapp/auth/thumbsignin/js/thumbsign_widget.js create mode 100644 oxAuth/Server/src/main/webapp/auth/thumbsignin/js/thumbsignin_widget.css create mode 100644 oxAuth/Server/src/main/webapp/auth/thumbsignin/tsLogin.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/thumbsignin/tsRegister.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/thumbsignin/tsRegistrationSuccess.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/toopher/tpauthenticate.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/toopher/tppair.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/u2f/login.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/u2f/scripts/u2f-api.js create mode 100644 oxAuth/Server/src/main/webapp/auth/uaf/js/uaf-api.js create mode 100644 oxAuth/Server/src/main/webapp/auth/uaf/login.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/whispeak/whispeak_open_ask_enroll.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/whispeak/whispeak_open_authentication_submit.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/whispeak/whispeak_open_enrollment_submit.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/whispeak/whispeak_open_identification.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/whispeak/whispeak_open_passport.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/whispeak/whispeak_open_passport_fallback.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/whispeak/whispeak_open_passport_loading.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/whispeak/whispeak_open_revocation_data_show.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/wikid/wikidlogin.xhtml create mode 100644 oxAuth/Server/src/main/webapp/auth/wikid/wikidregister.xhtml create mode 100644 oxAuth/Server/src/main/webapp/authorize.xhtml create mode 100644 oxAuth/Server/src/main/webapp/authz/authorize.xhtml create mode 100644 oxAuth/Server/src/main/webapp/authz/transaction.xhtml create mode 100644 oxAuth/Server/src/main/webapp/casa/bioid-template.xhtml create mode 100644 oxAuth/Server/src/main/webapp/casa/bioid.xhtml create mode 100644 oxAuth/Server/src/main/webapp/casa/casa.xhtml create mode 100644 oxAuth/Server/src/main/webapp/casa/cert.xhtml create mode 100644 oxAuth/Server/src/main/webapp/casa/duologin.xhtml create mode 100644 oxAuth/Server/src/main/webapp/casa/fido2.xhtml create mode 100644 oxAuth/Server/src/main/webapp/casa/fullwidth-template.xhtml create mode 100644 oxAuth/Server/src/main/webapp/casa/login-template.xhtml create mode 100644 oxAuth/Server/src/main/webapp/casa/login.xhtml create mode 100644 oxAuth/Server/src/main/webapp/casa/otp.xhtml create mode 100644 oxAuth/Server/src/main/webapp/casa/otp_email.xhtml create mode 100644 oxAuth/Server/src/main/webapp/casa/otp_email_prompt.xhtml create mode 100644 oxAuth/Server/src/main/webapp/casa/otp_sms.xhtml create mode 100644 oxAuth/Server/src/main/webapp/casa/otp_sms_prompt.xhtml create mode 100644 oxAuth/Server/src/main/webapp/casa/sg.xhtml create mode 100644 oxAuth/Server/src/main/webapp/casa/u2f.xhtml create mode 100644 oxAuth/Server/src/main/webapp/ciba/authorizeResponse.xhtml create mode 100644 oxAuth/Server/src/main/webapp/ciba/home.xhtml create mode 100644 oxAuth/Server/src/main/webapp/ciba/index.jsp create mode 100644 oxAuth/Server/src/main/webapp/ciba/manifest.json create mode 100644 oxAuth/Server/src/main/webapp/device_authorization.xhtml create mode 100644 oxAuth/Server/src/main/webapp/error.xhtml create mode 100644 oxAuth/Server/src/main/webapp/error_service.xhtml create mode 100644 oxAuth/Server/src/main/webapp/error_session.xhtml create mode 100644 oxAuth/Server/src/main/webapp/fonts/FontAwesome.otf create mode 100644 oxAuth/Server/src/main/webapp/fonts/fontawesome-webfont.eot create mode 100644 oxAuth/Server/src/main/webapp/fonts/fontawesome-webfont.svg create mode 100644 oxAuth/Server/src/main/webapp/fonts/fontawesome-webfont.ttf create mode 100644 oxAuth/Server/src/main/webapp/fonts/fontawesome-webfont.woff create mode 100644 oxAuth/Server/src/main/webapp/fonts/fontawesome-webfont.woff2 create mode 100644 oxAuth/Server/src/main/webapp/img/IcoArrow.svg create mode 100644 oxAuth/Server/src/main/webapp/img/IcoArrowRed.svg create mode 100644 oxAuth/Server/src/main/webapp/img/IcoCopy.png create mode 100644 oxAuth/Server/src/main/webapp/img/IcoMic.svg create mode 100644 oxAuth/Server/src/main/webapp/img/IcoPlay.svg create mode 100644 oxAuth/Server/src/main/webapp/img/IcoRetry.svg create mode 100644 oxAuth/Server/src/main/webapp/img/IcoStop.svg create mode 100644 oxAuth/Server/src/main/webapp/img/Logo_Whispeak_color.svg create mode 100644 oxAuth/Server/src/main/webapp/img/android.png create mode 100644 oxAuth/Server/src/main/webapp/img/android1.png create mode 100644 oxAuth/Server/src/main/webapp/img/background_wave.svg create mode 100644 oxAuth/Server/src/main/webapp/img/buttons_bg.jpg create mode 100644 oxAuth/Server/src/main/webapp/img/close.png create mode 100644 oxAuth/Server/src/main/webapp/img/email-ver.png create mode 100644 oxAuth/Server/src/main/webapp/img/eye.png create mode 100644 oxAuth/Server/src/main/webapp/img/favicon_icosahedron.ico create mode 100644 oxAuth/Server/src/main/webapp/img/footer_bg.jpg create mode 100644 oxAuth/Server/src/main/webapp/img/glu_icon.png create mode 100644 oxAuth/Server/src/main/webapp/img/ios.png create mode 100644 oxAuth/Server/src/main/webapp/img/iphone.png create mode 100644 oxAuth/Server/src/main/webapp/img/logo.png create mode 100644 oxAuth/Server/src/main/webapp/img/logo_white.png create mode 100644 oxAuth/Server/src/main/webapp/img/panel_header_bg.png create mode 100644 oxAuth/Server/src/main/webapp/img/phone-ver.png create mode 100644 oxAuth/Server/src/main/webapp/img/qr_code.png create mode 100644 oxAuth/Server/src/main/webapp/img/securitykey.jpg create mode 100644 oxAuth/Server/src/main/webapp/img/sg.png create mode 100644 oxAuth/Server/src/main/webapp/img/step_ver.png create mode 100644 oxAuth/Server/src/main/webapp/img/step_ver_touchid.png create mode 100644 oxAuth/Server/src/main/webapp/img/touchid.png create mode 100644 oxAuth/Server/src/main/webapp/img/tube_1.jpg create mode 100644 oxAuth/Server/src/main/webapp/img/ver_code.png create mode 100644 oxAuth/Server/src/main/webapp/img/ver_code_1.png create mode 100644 oxAuth/Server/src/main/webapp/index.jsp create mode 100644 oxAuth/Server/src/main/webapp/js/bootstrap.bundle.min.js create mode 100644 oxAuth/Server/src/main/webapp/js/bootstrap.min.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/CONTRIBUTING.md create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/LICENSE create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/README.md create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/aes.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/bower.json create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/cipher-core.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/core.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/crypto-js.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/docs/QuickStartGuide.wiki create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/enc-base64.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/enc-base64url.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/enc-hex.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/enc-latin1.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/enc-utf16.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/enc-utf8.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/evpkdf.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/format-hex.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/format-openssl.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/hmac-md5.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/hmac-ripemd160.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/hmac-sha1.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/hmac-sha224.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/hmac-sha256.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/hmac-sha3.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/hmac-sha384.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/hmac-sha512.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/hmac.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/index.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/lib-typedarrays.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/md5.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/mode-cfb.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/mode-ctr-gladman.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/mode-ctr.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/mode-ecb.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/mode-ofb.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/package.json create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/pad-ansix923.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/pad-iso10126.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/pad-iso97971.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/pad-nopadding.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/pad-pkcs7.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/pad-zeropadding.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/pbkdf2.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/rabbit-legacy.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/rabbit.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/rc4.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/ripemd160.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/sha1.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/sha224.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/sha256.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/sha3.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/sha384.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/sha512.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/tripledes.js create mode 100644 oxAuth/Server/src/main/webapp/js/crypto-js-4.1.1/x64-core.js create mode 100644 oxAuth/Server/src/main/webapp/js/fontawesome.min.js create mode 100644 oxAuth/Server/src/main/webapp/js/gluu-auth.js create mode 100644 oxAuth/Server/src/main/webapp/js/jquery-3.6.0.min.js create mode 100644 oxAuth/Server/src/main/webapp/js/jquery-qrcode-0.17.0.min.js create mode 100644 oxAuth/Server/src/main/webapp/js/jquery-ui.min.js create mode 100644 oxAuth/Server/src/main/webapp/js/passport.js create mode 100644 oxAuth/Server/src/main/webapp/js/platform.js create mode 100644 oxAuth/Server/src/main/webapp/js/popper.min.js create mode 100644 oxAuth/Server/src/main/webapp/js/recorder_3buttons.js create mode 100644 oxAuth/Server/src/main/webapp/js/respond.min.js create mode 100644 oxAuth/Server/src/main/webapp/js/revocation.js create mode 100644 oxAuth/Server/src/main/webapp/login.xhtml create mode 100644 oxAuth/Server/src/main/webapp/logout.xhtml create mode 100644 oxAuth/Server/src/main/webapp/opiframe.xhtml create mode 100644 oxAuth/Server/src/main/webapp/passwordless/alternative.xhtml create mode 100644 oxAuth/Server/src/main/webapp/passwordless/bioid-template.xhtml create mode 100644 oxAuth/Server/src/main/webapp/passwordless/bioid.xhtml create mode 100644 oxAuth/Server/src/main/webapp/passwordless/fido2.xhtml create mode 100644 oxAuth/Server/src/main/webapp/passwordless/login-template.xhtml create mode 100644 oxAuth/Server/src/main/webapp/passwordless/login.xhtml create mode 100644 oxAuth/Server/src/main/webapp/passwordless/sg.xhtml create mode 100644 oxAuth/Server/src/main/webapp/postlogin.xhtml create mode 100644 oxAuth/Server/src/main/webapp/selectAccount.xhtml create mode 100644 oxAuth/Server/src/main/webapp/stylesheet/authorize.css create mode 100644 oxAuth/Server/src/main/webapp/stylesheet/bootstrap-grid.min.css create mode 100644 oxAuth/Server/src/main/webapp/stylesheet/bootstrap-reboot.min.css create mode 100644 oxAuth/Server/src/main/webapp/stylesheet/bootstrap-responsive.css create mode 100644 oxAuth/Server/src/main/webapp/stylesheet/bootstrap.css create mode 100644 oxAuth/Server/src/main/webapp/stylesheet/bootstrap.min.css create mode 100644 oxAuth/Server/src/main/webapp/stylesheet/ciba.css create mode 100644 oxAuth/Server/src/main/webapp/stylesheet/font-awesome.css create mode 100644 oxAuth/Server/src/main/webapp/stylesheet/fontawesome.min.css create mode 100644 oxAuth/Server/src/main/webapp/stylesheet/form_window.css create mode 100644 oxAuth/Server/src/main/webapp/stylesheet/jquery-ui.min.css create mode 100644 oxAuth/Server/src/main/webapp/stylesheet/jquery-ui.structure.min.css create mode 100644 oxAuth/Server/src/main/webapp/stylesheet/jquery-ui.theme.min.css create mode 100644 oxAuth/Server/src/main/webapp/stylesheet/recorder_3buttons.css create mode 100644 oxAuth/Server/src/main/webapp/stylesheet/responsive-media.css create mode 100644 oxAuth/Server/src/main/webapp/stylesheet/site.css create mode 100644 oxAuth/Server/src/main/webapp/stylesheet/style.css create mode 100644 oxAuth/Server/src/main/webapp/stylesheet/theme.css create mode 100644 oxAuth/Server/src/main/webapp/stylesheet/theme.xcss create mode 100644 oxAuth/Server/src/main/webapp/uma2/sample/city.xhtml create mode 100644 oxAuth/Server/src/main/webapp/uma2/sample/claims_resolved.xhtml create mode 100644 oxAuth/Server/src/main/webapp/uma2/sample/country.xhtml create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/BaseComponentTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/BaseTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/ConfigurableTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/OxAuthUnitTestsListener.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/authorize/ws/rs/AuthorizeRestWebServiceValidatorTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/comp/CleanUpClientTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/comp/CleanerTimerTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/comp/ConfigurationTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/comp/CrossEncryptionTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/comp/CryptoProviderTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/comp/EncryptionTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/comp/GrantServiceTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/comp/IdGenServiceTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/comp/InumGeneratorTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/comp/JwtCrossCheckTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/comp/KeyGenerationTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/comp/LocaleTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/comp/SessionIdServiceTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/comp/SignatureTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/comp/UmaResourceServiceTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/comp/UtilityMethodsTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/dev/CacheGrantManual.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/dev/ConfSerialization.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/dev/Manual.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/dev/TestUUID.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/dev/duo/PythonToLdapString.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/gluu/ws/rs/GluuConfigurationWSTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/json/JsonApplierServerTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/model/CIBAGrantTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/model/TClientService.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/model/WebServiceFactory.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/model/authorize/JwtAuthorizationRequestTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/model/registration/RegisterParamsValidatorTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/model/uma/TConfiguration.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/model/uma/TRegisterPermission.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/model/uma/TRegisterResource.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/model/uma/TTokenRequest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/model/uma/TUma.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/service/DateFormatterServiceTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/service/RedirectionUriServiceTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/service/ScopeServiceTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/service/TestResteasyInitializer.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/service/fido/u2f/RawAuthenticationServiceTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/service/fido/u2f/RawAuthenticationServiceUnitTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/service/fido/u2f/RawRegistrationServiceTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/servlet/OpenIdConfigurationTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/session/ws/rs/EndSessionRestWebServiceImplTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/uma/ws/rs/AccessProtectedResourceFlowWSTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/uma/ws/rs/ObtainPatWSTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/uma/ws/rs/ObtainRptWSTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/uma/ws/rs/RegisterPermissionWSTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/uma/ws/rs/UmaConfigurationWSTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/uma/ws/rs/UmaRegisterResourceWSTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/uma/ws/rs/UmaScopeWSTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/util/Deployments.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/util/ServerUtilTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/ws/rs/ApplicationTypeRestrictionEmbeddedTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/ws/rs/AuthorizationCodeFlowEmbeddedTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/ws/rs/AuthorizeRestWebServiceEmbeddedTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/ws/rs/AuthorizeWithResponseModeEmbeddedTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/ws/rs/ClientAuthenticationFilterEmbeddedTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/ws/rs/ClientInfoRestWebServiceEmbeddedTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/ws/rs/EndSessionBackchannelRestServerTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/ws/rs/EndSessionRestWebServiceEmbeddedTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/ws/rs/IntrospectionWebServiceEmbeddedTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/ws/rs/JwkRestWebServiceEmbeddedTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/ws/rs/OpenIDRequestObjectEmbeddedTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/ws/rs/OpenIDRequestObjectWithESAlgEmbeddedTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/ws/rs/OpenIDRequestObjectWithHSAlgEmbeddedTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/ws/rs/OpenIDRequestObjectWithRSAlgEmbeddedTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/ws/rs/RegistrationRestWebServiceEmbeddedTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/ws/rs/RequestObjectSigningAlgRestrictionEmbeddedTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/ws/rs/ResponseTypesRestrictionEmbeddedTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/ws/rs/SectorIdentifierUrlVerificationEmbeddedTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/ws/rs/TokenEndpointAuthMethodRestrictionEmbeddedTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/ws/rs/TokenRestWebServiceEmbeddedTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/ws/rs/TokenRestWebServiceWithESAlgEmbeddedTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/ws/rs/TokenRestWebServiceWithHSAlgEmbeddedTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/ws/rs/TokenRestWebServiceWithRSAlgEmbeddedTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/ws/rs/UserAuthenticationFilterEmbeddedTest.java create mode 100644 oxAuth/Server/src/test/java/org/gluu/oxauth/ws/rs/UserInfoRestWebServiceEmbeddedTest.java create mode 100644 oxAuth/Server/src/test/resources/77-customAttributes.ldif create mode 100644 oxAuth/Server/src/test/resources/arquillian.xml create mode 100644 oxAuth/Server/src/test/resources/client/jwks.json create mode 100644 oxAuth/Server/src/test/resources/client/sector_identifier.js create mode 100644 oxAuth/Server/src/test/resources/id/gen/SampleIdGenerator.py create mode 100644 oxAuth/Server/src/test/resources/jetty-env.xml create mode 100644 oxAuth/Server/src/test/resources/testng-benchmark.xml create mode 100644 oxAuth/Server/src/test/resources/testng-multi-authz.xml create mode 100644 oxAuth/Server/src/test/resources/testng.properties create mode 100644 oxAuth/Server/src/test/resources/testng.xml create mode 100644 oxAuth/Server/src/test/resources/web.xml create mode 100644 oxAuth/Server/uma/UmaClientAuthzRptPolicy.py create mode 100644 oxAuth/Server/uma/sample/UmaClaimsGathering.py create mode 100644 oxAuth/Server/uma/sample/UmaRptPolicy.py create mode 100644 oxAuth/Tests/selenium/ReadMe.txt create mode 100644 oxAuth/Tests/selenium/selenium_login. t1_user.html create mode 100644 oxAuth/Tests/selenium/selenium_login. t2_user.html create mode 100644 oxAuth/Tests/selenium/selenium_login. t3_user.html create mode 100644 oxAuth/Tests/selenium/while.js create mode 100644 oxAuth/common/pom.xml create mode 100644 oxAuth/common/src/main/java/org/gluu/oxauth/cert/fingerprint/FingerprintHelper.java create mode 100644 oxAuth/common/src/main/java/org/gluu/oxauth/cert/validation/CRLCertificateVerifier.java create mode 100644 oxAuth/common/src/main/java/org/gluu/oxauth/cert/validation/CertificateVerifier.java create mode 100644 oxAuth/common/src/main/java/org/gluu/oxauth/cert/validation/GenericCertificateVerifier.java create mode 100644 oxAuth/common/src/main/java/org/gluu/oxauth/cert/validation/OCSPCertificateVerifier.java create mode 100644 oxAuth/common/src/main/java/org/gluu/oxauth/cert/validation/PathCertificateVerifier.java create mode 100644 oxAuth/common/src/main/java/org/gluu/oxauth/cert/validation/model/ValidationStatus.java create mode 100644 oxAuth/common/src/main/java/org/gluu/oxauth/claims/Audience.java create mode 100644 oxAuth/common/src/main/java/org/gluu/oxauth/model/common/SimpleUser.java create mode 100644 oxAuth/common/src/main/java/org/gluu/oxauth/model/common/User.java create mode 100644 oxAuth/common/src/main/java/org/gluu/oxauth/model/config/BaseDnConfiguration.java create mode 100644 oxAuth/common/src/main/java/org/gluu/oxauth/model/config/StaticConfiguration.java create mode 100644 oxAuth/common/src/main/java/org/gluu/oxauth/model/event/CryptoProviderEvent.java create mode 100644 oxAuth/common/src/main/java/org/gluu/oxauth/model/registration/Client.java create mode 100644 oxAuth/common/src/main/java/org/gluu/oxauth/model/session/SessionId.java create mode 100644 oxAuth/common/src/main/java/org/gluu/oxauth/model/session/SessionIdAccessMap.java create mode 100644 oxAuth/common/src/main/java/org/gluu/oxauth/model/session/SessionIdState.java create mode 100644 oxAuth/common/src/main/java/org/gluu/oxauth/model/stat/Stat.java create mode 100644 oxAuth/common/src/main/java/org/gluu/oxauth/model/stat/StatEntry.java create mode 100644 oxAuth/common/src/main/java/org/gluu/oxauth/service/common/ApplicationFactory.java create mode 100644 oxAuth/common/src/main/java/org/gluu/oxauth/service/common/ConfigurationService.java create mode 100644 oxAuth/common/src/main/java/org/gluu/oxauth/service/common/EncryptionService.java create mode 100644 oxAuth/common/src/main/java/org/gluu/oxauth/service/common/InumService.java create mode 100644 oxAuth/common/src/main/java/org/gluu/oxauth/service/common/UserService.java create mode 100644 oxAuth/common/src/main/java/org/gluu/oxauth/service/common/api/IdGenerator.java create mode 100644 oxAuth/common/src/main/java/org/gluu/oxauth/service/common/fido2/RegistrationPersistenceService.java create mode 100644 oxAuth/common/src/main/java/org/gluu/oxauth/util/RedirectUri.java create mode 100644 oxAuth/common/src/main/resources/META-INF/beans.xml create mode 100644 oxAuth/common/src/test/java/org/gluu/oxauth/claims/AudienceTest.java create mode 100644 oxAuth/common/src/test/java/org/gluu/oxauth/util/RedirectUriTest.java create mode 100644 oxAuth/common/src/test/resources/testng.xml create mode 100644 oxAuth/docs/Authorization Code Flow.png create mode 100644 oxAuth/docs/Authorize Endpoint.png create mode 100644 oxAuth/docs/Authorize Page.png create mode 100644 oxAuth/docs/Class Diagram.png create mode 100644 oxAuth/docs/Client Credentials Flow.png create mode 100644 oxAuth/docs/Implicit Flow.png create mode 100644 oxAuth/docs/Resource Owner Password Credentials Flow.png create mode 100644 oxAuth/docs/oxAuth.vpp create mode 100644 oxAuth/docs/oxAuthSwagger.yaml create mode 100644 oxAuth/install.bat create mode 100644 oxAuth/jmeter/test/Authorization Code Flow.jmx create mode 100644 oxAuth/jmeter/test/Authorization Code Flow_amazon.jmx create mode 100644 oxAuth/jmeter/test/Authorization Code Flow_amazon_client_credentials.jmx create mode 100644 oxAuth/jmeter/test/Authorization Code Flow_ce-dev5.jmx create mode 100644 oxAuth/jmeter/test/Authorization Code Flow_xeon.jmx create mode 100644 oxAuth/jmeter/test/Authorization Code Flow_xeon_client_credentials.jmx create mode 100644 oxAuth/jmeter/test/Implicit_Flow_benchmark_gluu_org.jmx create mode 100644 oxAuth/jmeter/test/Implicit_Flow_c7_horizontal_scale.jmx create mode 100644 oxAuth/jmeter/test/authorization/oxServer - Test user authorization - testing data.csv create mode 100644 oxAuth/jmeter/test/authorization/oxServer - Test user authorization.jmx create mode 100644 oxAuth/oxAuthStatic/.gitignore create mode 100644 oxAuth/oxAuthStatic/pom.xml create mode 100644 oxAuth/oxAuthStatic/src/main/resources/META-INF/resources/img/buttons_bg.jpg create mode 100644 oxAuth/oxAuthStatic/src/main/resources/META-INF/resources/img/footer_bg.jpg create mode 100644 oxAuth/oxAuthStatic/src/main/resources/META-INF/resources/img/panel_header_bg.png create mode 100644 oxAuth/oxAuthStatic/src/main/resources/META-INF/resources/js/Duo-Web-v1.min.js create mode 100644 oxAuth/oxAuthStatic/src/main/resources/META-INF/resources/stylesheet/site.css create mode 100644 oxAuth/oxAuthStatic/src/main/resources/META-INF/resources/stylesheet/theme.css create mode 100644 oxAuth/oxAuthStatic/src/main/resources/META-INF/resources/stylesheet/theme.xcss create mode 100644 oxAuth/persistence-model/pom.xml create mode 100644 oxAuth/persistence-model/src/main/java/org/oxauth/persistence/model/ClientAttributes.java create mode 100644 oxAuth/persistence-model/src/main/java/org/oxauth/persistence/model/PairwiseIdentifier.java create mode 100644 oxAuth/persistence-model/src/main/java/org/oxauth/persistence/model/Scope.java create mode 100644 oxAuth/persistence-model/src/main/java/org/oxauth/persistence/model/ScopeAttributes.java create mode 100644 oxAuth/persistence-model/src/main/java/org/oxauth/persistence/model/SectorIdentifier.java create mode 100644 oxAuth/persistence-model/src/main/java/org/oxauth/persistence/model/base/Entry.java create mode 100644 oxAuth/persistence-model/src/main/java/org/oxauth/persistence/model/configuration/CustomProperty.java create mode 100644 oxAuth/persistence-model/src/main/java/org/oxauth/persistence/model/configuration/GluuConfiguration.java create mode 100644 oxAuth/persistence-model/src/main/java/org/oxauth/persistence/model/configuration/InumEntry.java create mode 100644 oxAuth/persistence-model/src/main/java/org/oxauth/persistence/model/configuration/oxIDPAuthConf.java create mode 100644 oxAuth/persistence-model/src/main/resources/META-INF/beans.xml create mode 100644 oxAuth/pom.xml create mode 100644 oxAuth/release.properties create mode 100644 oxAuth/server-fips/.gitignore create mode 100644 oxAuth/server-fips/pom.xml create mode 100644 oxAuth/stat-exporter/pom.xml create mode 100644 oxAuth/stat-exporter/src/main/java/org/gluu/stat/exporter/DiscoveryResponse.java create mode 100644 oxAuth/stat-exporter/src/main/java/org/gluu/stat/exporter/Months.java create mode 100644 oxAuth/stat-exporter/src/main/java/org/gluu/stat/exporter/RegisterRequest.java create mode 100644 oxAuth/stat-exporter/src/main/java/org/gluu/stat/exporter/RegisterResponse.java create mode 100644 oxAuth/stat-exporter/src/main/java/org/gluu/stat/exporter/StatExporter.java create mode 100644 oxAuth/stat-exporter/src/main/java/org/gluu/stat/exporter/StatExporterConfig.java create mode 100644 oxAuth/stat-exporter/src/main/java/org/gluu/stat/exporter/StatExporterResponse.java create mode 100644 oxAuth/stat-exporter/src/main/java/org/gluu/stat/exporter/TokenResponse.java create mode 100644 oxAuth/stat-exporter/src/test/resources/stat-exporter-config.json diff --git a/.github/ISSUE_TEMPLATE/CODEOWNERS b/.github/ISSUE_TEMPLATE/CODEOWNERS new file mode 100644 index 00000000..09f21304 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/CODEOWNERS @@ -0,0 +1,36 @@ +# Please do not attempt to edit this file without the direct consent from the DevOps team. This file is managed centrally. +# Contact @moabu + +# These owners will be the default owners for everything in this branch of +# the repo. Unless a later match takes precedence +/.github/ @moabu +/community-edition-setup/ @devrimyatar @yuriyz @yurem @yuriyzz +/oxAuth/ @yurem @yuriyz @yuriyzz + +/jans-keycloak-integration/ @uprightech +/jans-keycloak-link/ @shekhar16 +/docker-jans-*/ @moabu @iromli +/automation/ @moabu @iromli +/charts/ @moabu @iromli + +/jans-*/version.txt @moabu @mo-auto +/jans-*/CHANGELOG.md @moabu @mo-auto +/jans-pycloudlib/ @moabu +/docker-jans-*/requirments.txt @iromli @mo-auto +/jans-bom/ @yurem @yuriyz @yuriyzz +/jans-core/ @yurem @yuriyz @yuriyzz +/jans-orm/ @yurem @yuriyz +/jans-auth-server/ @yurem @yuriyz @yuriyzz +/jans-fido2/ @yurem @yackermann +/jans-lock/ @yurem +/jans-scim/ @jgomer2001 +/jans-config-api/ @pujavs @yuriyz @yurem +/jans-cli-tui/ @devrimyatar +/jans-linux-setup/ @devrimyatar @yuriyz @yurem @yuriyzz +/jans-linux-setup/jans_setup/setup_app/version.py @moabu @mo-auto +/jans-linux-setup/static/scripts/admin_ui_plugin.py @devrimyatar @duttarnab +/jans-cache-refresh/ @yurem @shekhar16 +/jans-link/ @yurem @yuriyz +/agama/ @jgomer2001 +/jans-casa/ @jgomer2001 @maduvena +/demos/jans-tarp/ @duttarnab diff --git a/.github/ISSUE_TEMPLATE/issue-report.md b/.github/ISSUE_TEMPLATE/issue-report.md new file mode 100644 index 00000000..82e2df78 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue-report.md @@ -0,0 +1,48 @@ +--- +name: Issue report +about: Welcome at Gluu. Inc, please create an issue to help us improve +title: '' +labels: '' +assignees: '' + +--- + +!!!Note +Unless you are Gluu staff, please first review and open an issue on https://support.gluu.org before opening an issue here. Thanks! + + +## Describe the issue +A clear and concise description of what the issue is. + +## Steps To Reproduce +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +## Expected behavior +A clear and concise description of what you expected to happen. +## Actual behavior +A clear and concise description of what happen. + +## Screenshots +If applicable, add screenshots to help explain your problem. + +## Desktop (please complete the following information): + - OS: [e.g. Ubuntu16.04LTS] + - Gluu version(If applicable) + - Casa version(If applicable) + - SuperGluu version(If applicable) + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +## Smartphone (please complete the following information if applicable): +If applicable + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +## Additional context +Add any other context about the problem here. diff --git a/.github/workflows/central_code_quality_check.yml b/.github/workflows/central_code_quality_check.yml new file mode 100644 index 00000000..2df45a3f --- /dev/null +++ b/.github/workflows/central_code_quality_check.yml @@ -0,0 +1,164 @@ +# Please do not attempt to edit this flow without the direct consent from the DevOps team. This file is managed centrally. +# Contact @moabu +name: Code quality check + +on: + push: + branches: + - main + paths: + - 'community-edition-setup/**' + - 'oxAuth/**' + - '!**/CHANGELOG.md' + - '!**.txt' + + pull_request: + branches: + - master + - main + - '!update-pycloud-in-**' + types: + - opened + - synchronize + paths: + - 'community-edition-setup/**' + - 'oxAuth/**' + - '!**/CHANGELOG.md' + - '!**.txt' + + workflow_dispatch: + +jobs: + sonar-scan: + name: sonar scan + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + module: [jans-keycloak-link, jans-keycloak-integration, jans-auth-server, agama, jans-config-api, jans-core, jans-linux-setup, jans-cli-tui, jans-fido2, jans-orm, jans-scim, jans-pycloudlib, jans-link, jans-casa, jans-lock] + env: + JVM_PROJECTS: | + oxAuth + jans-keycloak-link + jans-link + jans-auth-server + jans-lock + jans-orm + jans-config-api + jans-scim + jans-core + jans-fido2 + jans-casa + agama + NON_JVM_PROJECTS: | + community-edition-setup + steps: + - name: Harden Runner + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 + with: + egress-policy: audit + + - name: check out code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of sonarqube analysis + + - name: find changed directories + run: | + if [ $GITHUB_BASE_REF ]; then + # Pull Request + echo "Triggering event: pull request" + echo Pull request base ref: $GITHUB_BASE_REF + git fetch origin $GITHUB_BASE_REF --depth=1 + if [ ${{ github.event.action }} = "opened" ]; then + echo "Triggering action: opened" + echo "CHANGED_DIR=$( git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} | cut -d/ -f1 | sort -u | sed -z 's/\n/,/g;s/^/[/;s/,$/]/;s/$/\n/')" >> ${GITHUB_ENV} + fi + if [ ${{ github.event.action }} = "synchronize" ]; then + echo "Triggering action: synchronize" + echo "CHANGED_DIR=$( git diff --name-only ${{ github.event.before }} ${{ github.event.pull_request.head.sha }} | cut -d/ -f1 | sort -u | sed -z 's/\n/,/g;s/^/[/;s/,$/]/;s/$/\n/')" >>${GITHUB_ENV} + fi + else + # Push + echo "Triggerring event: push" + git fetch origin ${{ github.event.before }} --depth=1 + echo "CHANGED_DIR=$( git diff --name-only ${{ github.event.before }} $GITHUB_SHA | cut -d/ -f1 | sort -u | sed -z 's/\n/,/g;s/^/[/;s/,$/]/;s/$/\n/')" >> ${GITHUB_ENV} + fi + + - name: check env + run: | + echo changed dir list: ${{ env.CHANGED_DIR }} + echo Matrix module: ${{ matrix.module }} + echo GH event action: ${{ github.event.action }} + echo PR base sha: ${{ github.event.pull_request.base.sha }} + echo PR head sha: ${{ github.event.pull_request.head.sha }} + echo event before: ${{ github.event.before }} + echo GH sha: $GITHUB_SHA + + - name: Set up JDK 17 + if: contains(env.CHANGED_DIR, matrix.module) && contains(env.JVM_PROJECTS, matrix.module) + uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0 + with: + java-version: '11' + distribution: 'adopt' + + - name: Cache SonarCloud packages for JVM based project + if: contains(env.CHANGED_DIR, matrix.module) && contains(env.JVM_PROJECTS, matrix.module) + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + + - name: Build and analyze JVM based project + if: contains(env.CHANGED_DIR, matrix.module) && contains(env.JVM_PROJECTS, matrix.module) + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: | + cd ${{ matrix.module }} + case ${{ matrix.module }} in + "opendj4") + echo "Build opendj-sdk first for gluu-opendj4" + mvn -B -f opendj-sdk/pom.xml -DskipTests clean install + ;& + "oxAuth") + ;& + "oxTrust") + ;& + "scim") + ;& + "casa") + ;& + "oxd") + echo "Run Sonar analysis without test execution" + mvn -B -DskipTests=true install org.sonarsource.scanner.maven:sonar-maven-plugin:sonar + ;; + *) + echo "Run Sonar analysis with test execution" + mvn -B install org.sonarsource.scanner.maven:sonar-maven-plugin:sonar + ;; + echo "Run Sonar analysis with test execution" + mvn -B install org.sonarsource.scanner.maven:sonar-maven-plugin:sonar + ;; + esac + + + - name: Convert repo org name to lowercase for non JVM projects + if: contains(env.CHANGED_DIR, matrix.module) && contains(env.NON_JVM_PROJECTS, matrix.module) + env: + REPO_OWNER: ${{ github.repository_owner }} + run: | + echo "REPO_ORG=${REPO_OWNER,,}" >>${GITHUB_ENV} + + - name: SonarCloud Scan for non-JVM project + if: contains(env.CHANGED_DIR, matrix.module) && contains(env.NON_JVM_PROJECTS, matrix.module) + uses: SonarSource/sonarcloud-github-action@49e6cd3b187936a73b8280d59ffd9da69df63ec9 # master + with: + args: > + -Dsonar.organization=${{ env.REPO_ORG }} + -Dsonar.projectKey=${{ github.repository_owner }}_${{ matrix.module }} + projectBaseDir: ${{ matrix.module }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/oxAuth/.gitignore b/oxAuth/.gitignore new file mode 100644 index 00000000..d9363d75 --- /dev/null +++ b/oxAuth/.gitignore @@ -0,0 +1,17 @@ +# Eclipse project files +.settings +.project +.classpath +.pydevproject +.tern-project +.faces-config.xml.jsfdia + +# IntelliJ IDEA project files +.idea +*.iml + +# Maven +target + +#Mac +.DS_Store diff --git a/oxAuth/CODE_OF_CONDUCT.md b/oxAuth/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..55d40067 --- /dev/null +++ b/oxAuth/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at sales@gluu.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/oxAuth/Client/.gitignore b/oxAuth/Client/.gitignore new file mode 100644 index 00000000..1dab039f --- /dev/null +++ b/oxAuth/Client/.gitignore @@ -0,0 +1,2 @@ +/target/ +test-output diff --git a/oxAuth/Client/pom.xml b/oxAuth/Client/pom.xml new file mode 100644 index 00000000..da40d579 --- /dev/null +++ b/oxAuth/Client/pom.xml @@ -0,0 +1,349 @@ + + + 4.0.0 + oxauth-client + oxAuth Client + jar + + + org.gluu + oxauth + 4.5.6-SNAPSHOT + + + + ${maven.min-version} + + + + oxauth-client + + + profiles/${cfg}/config-oxauth-test-data.properties + + + + + src/main/resources + true + + + + + + src/test/resources + true + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.maven.plugins + maven-clean-plugin + + + org.apache.maven.plugins + maven-deploy-plugin + + + org.apache.maven.plugins + maven-install-plugin + + + install-main-artifact + install + + install-file + + + ${project.groupId} + ${project.artifactId} + ${project.version} + jar + ${project.build.directory}/${project.artifactId}.jar + + + + + install-jar-with-dependencies-artifact + install + + install-file + + + ${project.groupId} + ${project.artifactId}-jar-with-dependencies + ${project.version} + jar + ${project.build.directory}/${project.artifactId}-jar-with-dependencies.jar + + + + + install-jar-without-provider-dependencies-artifact + install + + install-file + + + ${project.groupId} + ${project.artifactId}-jar-without-provider-dependencies + ${project.version} + jar + ${project.build.directory}/${project.artifactId}-jar-without-provider-dependencies.jar + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + org.apache.maven.plugins + maven-resources-plugin + + + org.apache.maven.plugins + maven-site-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + org.codehaus.mojo + findbugs-maven-plugin + + + org.apache.maven.plugins + maven-source-plugin + + + maven-assembly-plugin + 3.3.0 + + + assembly-jar-with-dependencies-artifact + package + + single + + + true + + jar-with-dependencies + + + + org.gluu.oxauth.util.KeyGenerator + + + true + + + + + + + assembly-jar-without-provider-dependencies-artifact + package + + single + + + true + + src/main/assembly/jar-without-provider-dependencies.xml + + + + org.gluu.oxauth.util.KeyGenerator + + + true + + + + + + + + + + + + com.google.guava + guava + + + org.gluu + oxauth-model + + + + org.jboss.resteasy + resteasy-client + + + org.jboss.resteasy + resteasy-jackson2-provider + + + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-databind + + + + commons-codec + commons-codec + + + commons-lang + commons-lang + + + commons-cli + commons-cli + + + org.apache.httpcomponents + httpclient + + + org.apache.httpcomponents + httpcore + + + + + org.bouncycastle + bcprov-jdk18on + + + org.bouncycastle + bcpkix-jdk18on + + + + + org.apache.logging.log4j + log4j-api + + + org.apache.logging.log4j + log4j-1.2-api + + + org.apache.logging.log4j + log4j-core + + + + org.testng + testng + test + + + org.gluu + oxauth-model + test-jar + test + + + + junit + junit + test + + + + org.seleniumhq.selenium + htmlunit3-driver + test + + + org.seleniumhq.selenium + selenium-java + test + + + + + + generic-provider-libs + + true + + !fips.enable + + + + + + org.bouncycastle + bcprov-jdk18on + + + org.bouncycastle + bcpkix-jdk18on + + + + + + fips-provider-libs + + + fips.enable + + + + + + + org.bouncycastle + bc-fips + + + org.bouncycastle + bcpkix-fips + + + + + + test-dependencies + + + maven.test.skip + false + + + + + org.gluu + oxauth-model + test-jar + test + + + + + + diff --git a/oxAuth/Client/profiles/.gitignore b/oxAuth/Client/profiles/.gitignore new file mode 100644 index 00000000..0f7fa195 --- /dev/null +++ b/oxAuth/Client/profiles/.gitignore @@ -0,0 +1,6 @@ +/hudson +/seed21/ +/seed22/ +/ce-dev.gluu.org/ +/ce-dev5.gluu.org/ +/jenkins-ldap.gluu.org/ diff --git a/oxAuth/Client/profiles/default/client_keystore.pkcs12 b/oxAuth/Client/profiles/default/client_keystore.pkcs12 new file mode 100644 index 0000000000000000000000000000000000000000..83704fd6155d2c0e847771f04c8feb178f724cba GIT binary patch literal 23345 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 z + + + jar-without-provider-dependencies + + jar + + false + + + + / + false + true + runtime + + org.bouncycastle:bcpkix-jdk18on + org.bouncycastle:bcprov-jdk18on + org.bouncycastle:bcutil-jdk18on + + + + + + + / + ${project.build.outputDirectory} + + + diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/AuthorizationRequest.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/AuthorizationRequest.java new file mode 100644 index 00000000..78859139 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/AuthorizationRequest.java @@ -0,0 +1,738 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.authorize.AuthorizeRequestParam; +import org.gluu.oxauth.model.authorize.CodeVerifier; +import org.gluu.oxauth.model.common.Display; +import org.gluu.oxauth.model.common.Prompt; +import org.gluu.oxauth.model.common.ResponseMode; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.util.Util; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Represents an authorization request to send to the authorization server. + * + * @author Javier Rojas Blum + * @version October 7, 2019 + */ +public class AuthorizationRequest extends BaseRequest { + + public static String NO_REDIRECT_HEADER = "X-Gluu-NoRedirect"; + + private List responseTypes; + private String clientId; + private List scopes; + private String redirectUri; + private String state; + + private ResponseMode responseMode; + private String nonce; + private Display display; + private List prompts; + private Integer maxAge; + private List uiLocales; + private List claimsLocales; + private String idTokenHint; + private String loginHint; + private List acrValues; + private JSONObject claims; + private String registration; + private String request; + private String requestUri; + + private boolean requestSessionId; + private String sessionId; + + private String accessToken; + private boolean useNoRedirectHeader; + + // code verifier according to PKCE spec + private String codeChallenge; + private String codeChallengeMethod; + + private Map customResponseHeaders; + + /** + * Constructs an authorization request. + * + * @param responseTypes The response type informs the authorization server of the desired response type: + * code, token, id_token + * a combination of them. The response type parameter is mandatory. + * @param clientId The client identifier is mandatory. + * @param scopes The scope of the access request. + * @param redirectUri Redirection URI + * @param nonce A string value used to associate a user agent session with an ID Token, + * and to mitigate replay attacks. + */ + public AuthorizationRequest(List responseTypes, String clientId, List scopes, + String redirectUri, String nonce) { + super(); + this.responseTypes = responseTypes; + this.clientId = clientId; + this.scopes = scopes; + this.redirectUri = redirectUri; + this.nonce = nonce; + prompts = new ArrayList(); + useNoRedirectHeader = false; + } + + public CodeVerifier generateAndSetCodeChallengeWithMethod() { + CodeVerifier verifier = new CodeVerifier(CodeVerifier.CodeChallengeMethod.S256); + codeChallenge = verifier.getCodeChallenge(); + codeChallengeMethod = verifier.getTransformationType().getPkceString(); + return verifier; + } + + public String getCodeChallenge() { + return codeChallenge; + } + + public String getCodeChallengeMethod() { + return codeChallengeMethod; + } + + public void setCodeChallenge(String codeChallenge) { + this.codeChallenge = codeChallenge; + } + + public void setCodeChallengeMethod(String codeChallengeMethod) { + this.codeChallengeMethod = codeChallengeMethod; + } + + /** + * Returns the response types. + * + * @return The response types. + */ + public List getResponseTypes() { + return responseTypes; + } + + /** + * Sets the response types. + * + * @param responseTypes The response types. + */ + public void setResponseTypes(List responseTypes) { + this.responseTypes = responseTypes; + } + + /** + * Returns the client identifier. + * + * @return The client identifier. + */ + public String getClientId() { + return clientId; + } + + /** + * Sets the client identifier. + * + * @param clientId The client identifier. + */ + public void setClientId(String clientId) { + this.clientId = clientId; + } + + /** + * Returns the scopes of the access request. The authorization endpoint allow + * the client to specify the scope of the access request using the scope + * request parameter. In turn, the authorization server uses the scope + * response parameter to inform the client of the scope of the access token + * issued. The value of the scope parameter is expressed as a list of + * space-delimited, case sensitive strings. + * + * @return The scopes of the access request. + */ + public List getScopes() { + return scopes; + } + + /** + * Sets the scope of the access request. The authorization endpoint allow + * the client to specify the scope of the access request using the scope + * request parameter. In turn, the authorization server uses the scope + * response parameter to inform the client of the scope of the access token + * issued. The value of the scope parameter is expressed as a list of + * space-delimited, case sensitive strings. + * + * @param scopes The scope of the access request. + */ + public void setScopes(List scopes) { + this.scopes = scopes; + } + + /** + * Returns the redirection URI. + * + * @return The redirection URI. + */ + public String getRedirectUri() { + return redirectUri; + } + + /** + * Sets the redirection URI. + * + * @param redirectUri The redirection URI. + */ + public void setRedirectUri(String redirectUri) { + this.redirectUri = redirectUri; + } + + /** + * Returns the state. The state is an opaque value used by the client to + * maintain state between the request and callback. The authorization server + * includes this value when redirecting the user-agent back to the client. + * The parameter should be used for preventing cross-site request forgery. + * + * @return The state. + */ + public String getState() { + return state; + } + + /** + * Sets the state. The state is an opaque value used by the client to + * maintain state between the request and callback. The authorization server + * includes this value when redirecting the user-agent back to the client. + * The parameter should be used for preventing cross-site request forgery. + * + * @param state The state. + */ + public void setState(String state) { + this.state = state; + } + + public ResponseMode getResponseMode() { + return responseMode; + } + + public void setResponseMode(ResponseMode responseMode) { + this.responseMode = responseMode; + } + + /** + * Returns a string value used to associate a user agent session with an ID Token, + * and to mitigate replay attacks. + * + * @return The nonce value. + */ + public String getNonce() { + return nonce; + } + + /** + * Sets a string value used to associate a user agent session with an ID Token, + * and to mitigate replay attacks. + * + * @param nonce The nonce value. + */ + public void setNonce(String nonce) { + this.nonce = nonce; + } + + /** + * Returns an ASCII string value that specifies how the Authorization Server displays the + * authentication page to the End-User. + * + * @return The display value. + */ + public Display getDisplay() { + return display; + } + + /** + * Sets an ASCII string value that specifies how the Authorization Server displays the + * authentication page to the End-User. + * + * @param display The display value. + */ + public void setDisplay(Display display) { + this.display = display; + } + + /** + * Returns a space delimited list of ASCII strings that can contain the values login, consent, + * select_account, and none. + * + * @return The prompt list. + */ + public List getPrompts() { + return prompts; + } + + public void setPrompts(List prompts) { + this.prompts = prompts; + } + + public Integer getMaxAge() { + return maxAge; + } + + public void setMaxAge(Integer maxAge) { + this.maxAge = maxAge; + } + + public List getUiLocales() { + return uiLocales; + } + + public void setUiLocales(List uiLocales) { + this.uiLocales = uiLocales; + } + + public List getClaimsLocales() { + return claimsLocales; + } + + public void setClaimsLocales(List claimsLocales) { + this.claimsLocales = claimsLocales; + } + + public String getIdTokenHint() { + return idTokenHint; + } + + public void setIdTokenHint(String idTokenHint) { + this.idTokenHint = idTokenHint; + } + + public String getLoginHint() { + return loginHint; + } + + public void setLoginHint(String loginHint) { + this.loginHint = loginHint; + } + + public List getAcrValues() { + return acrValues; + } + + public void setAcrValues(List acrValues) { + this.acrValues = acrValues; + } + + public JSONObject getClaims() { + return claims; + } + + public void setClaims(JSONObject claims) { + this.claims = claims; + } + + public String getRegistration() { + return registration; + } + + public void setRegistration(String registration) { + this.registration = registration; + } + + /** + * Returns a JWT encoded OpenID Request Object. + * + * @return A JWT encoded OpenID Request Object. + */ + public String getRequest() { + return request; + } + + /** + * Sets a JWT encoded OpenID Request Object. + * + * @param request A JWT encoded OpenID Request Object. + */ + public void setRequest(String request) { + this.request = request; + } + + /** + * Returns an URL that points to an OpenID Request Object. + * + * @return An URL that points to an OpenID Request Object. + */ + public String getRequestUri() { + return requestUri; + } + + /** + * Sets an URL that points to an OpenID Request Object. + * + * @param requestUri An URL that points to an OpenID Request Object. + */ + public void setRequestUri(String requestUri) { + this.requestUri = requestUri; + } + + /** + * Returns whether session id is requested. + * + * @return whether session id is requested + */ + public boolean isRequestSessionId() { + return requestSessionId; + } + + /** + * Sets whether session id should be requested. + * + * @param p_requestSessionId session id. + */ + public void setRequestSessionId(boolean p_requestSessionId) { + requestSessionId = p_requestSessionId; + } + + /** + * Gets session id. + * + * @return session id. + */ + public String getSessionId() { + return sessionId; + } + + /** + * Sets session id. + * + * @param p_sessionId session id + */ + public void setSessionId(String p_sessionId) { + sessionId = p_sessionId; + } + + public String getAccessToken() { + return accessToken; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public boolean isUseNoRedirectHeader() { + return useNoRedirectHeader; + } + + public void setUseNoRedirectHeader(boolean useNoRedirectHeader) { + this.useNoRedirectHeader = useNoRedirectHeader; + } + + public String getResponseTypesAsString() { + return Util.asString(responseTypes); + } + + public String getScopesAsString() { + return Util.listAsString(scopes); + } + + public String getPromptsAsString() { + return Util.asString(prompts); + } + + public String getUiLocalesAsString() { + return Util.listAsString(uiLocales); + } + + public String getClaimsLocalesAsString() { + return Util.listAsString(claimsLocales); + } + + public String getAcrValuesAsString() { + return Util.listAsString(acrValues); + } + + public String getCustomResponseHeadersAsString() throws JSONException { + String header = Util.mapAsString(customResponseHeaders); + if (header == null) { + return null; + } + + try { + return URLEncoder.encode(header, "UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + + return null; + } + + public Map getCustomResponseHeaders() { + return customResponseHeaders; + } + + public void setCustomResponseHeaders(Map customResponseHeaders) { + this.customResponseHeaders = customResponseHeaders; + } + + public String getClaimsAsString() { + if (claims != null) { + return claims.toString(); + } else { + return null; + } + } + + /** + * Returns a query string with the parameters of the authorization request. + * Any null or empty parameter will be omitted. + * + * @return A query string of parameters. + */ + @Override + public String getQueryString() { + StringBuilder queryStringBuilder = new StringBuilder(); + + try { + // OAuth 2.0 request parameters + final String responseTypesAsString = getResponseTypesAsString(); + final String scopesAsString = getScopesAsString(); + final String promptsAsString = getPromptsAsString(); + final String customResponseHeadersAsString = getCustomResponseHeadersAsString(); + + if (StringUtils.isNotBlank(responseTypesAsString)) { + queryStringBuilder.append(AuthorizeRequestParam.RESPONSE_TYPE) + .append("=").append(URLEncoder.encode(responseTypesAsString, Util.UTF8_STRING_ENCODING)); + } + if (StringUtils.isNotBlank(clientId)) { + queryStringBuilder.append("&").append(AuthorizeRequestParam.CLIENT_ID) + .append("=").append(URLEncoder.encode(clientId, Util.UTF8_STRING_ENCODING)); + } + if (StringUtils.isNotBlank(scopesAsString)) { + queryStringBuilder.append("&").append(AuthorizeRequestParam.SCOPE) + .append("=").append(URLEncoder.encode(scopesAsString, Util.UTF8_STRING_ENCODING)); + } + if (StringUtils.isNotBlank(redirectUri)) { + queryStringBuilder.append("&").append(AuthorizeRequestParam.REDIRECT_URI) + .append("=").append(URLEncoder.encode(redirectUri, Util.UTF8_STRING_ENCODING)); + } + if (StringUtils.isNotBlank(state)) { + queryStringBuilder.append("&").append(AuthorizeRequestParam.STATE) + .append("=").append(URLEncoder.encode(state, Util.UTF8_STRING_ENCODING)); + } + + // OpenID Connect request parameters + final String uiLocalesAsString = getUiLocalesAsString(); + final String claimLocalesAsString = getClaimsLocalesAsString(); + final String acrValuesAsString = getAcrValuesAsString(); + final String claimsAsString = getClaimsAsString(); + + if (responseMode != null) { + queryStringBuilder.append("&").append(AuthorizeRequestParam.RESPONSE_MODE) + .append("=").append(URLEncoder.encode(responseMode.toString(), Util.UTF8_STRING_ENCODING)); + } + if (StringUtils.isNotBlank(nonce)) { + queryStringBuilder.append("&").append(AuthorizeRequestParam.NONCE) + .append("=").append(URLEncoder.encode(nonce, Util.UTF8_STRING_ENCODING)); + } + if (display != null) { + queryStringBuilder.append("&").append(AuthorizeRequestParam.DISPLAY) + .append("=").append(URLEncoder.encode(display.toString(), Util.UTF8_STRING_ENCODING)); + } + if (StringUtils.isNotBlank(promptsAsString)) { + queryStringBuilder.append("&").append(AuthorizeRequestParam.PROMPT) + .append("=").append(URLEncoder.encode(promptsAsString, Util.UTF8_STRING_ENCODING)); + } + if (maxAge != null) { + queryStringBuilder.append("&").append(AuthorizeRequestParam.MAX_AGE) + .append("=").append(maxAge); + } + if (StringUtils.isNotBlank(uiLocalesAsString)) { + queryStringBuilder.append("&").append(AuthorizeRequestParam.UI_LOCALES) + .append("=").append(URLEncoder.encode(uiLocalesAsString, Util.UTF8_STRING_ENCODING)); + } + if (StringUtils.isNotBlank(claimLocalesAsString)) { + queryStringBuilder.append("&").append(AuthorizeRequestParam.CLAIMS_LOCALES) + .append("=").append(URLEncoder.encode(claimLocalesAsString, Util.UTF8_STRING_ENCODING)); + } + if (StringUtils.isNotBlank(idTokenHint)) { + queryStringBuilder.append("&").append(AuthorizeRequestParam.ID_TOKEN_HINT) + .append("=").append(idTokenHint); + } + if (StringUtils.isNotBlank(loginHint)) { + queryStringBuilder.append("&").append(AuthorizeRequestParam.LOGIN_HINT) + .append("=").append(loginHint); + } + if (StringUtils.isNotBlank(acrValuesAsString)) { + queryStringBuilder.append("&").append(AuthorizeRequestParam.ACR_VALUES) + .append("=").append(URLEncoder.encode(acrValuesAsString, Util.UTF8_STRING_ENCODING)); + } + if (StringUtils.isNotBlank(claimsAsString)) { + queryStringBuilder.append("&").append(AuthorizeRequestParam.CLAIMS) + .append("=").append(URLEncoder.encode(claimsAsString, Util.UTF8_STRING_ENCODING)); + } + if (StringUtils.isNotBlank(registration)) { + queryStringBuilder.append("&").append(AuthorizeRequestParam.REGISTRATION) + .append("=").append(registration); + } + if (StringUtils.isNotBlank(request)) { + queryStringBuilder.append("&").append(AuthorizeRequestParam.REQUEST) + .append("=").append(URLEncoder.encode(request, Util.UTF8_STRING_ENCODING)); + } + if (StringUtils.isNotBlank(requestUri)) { + queryStringBuilder.append("&").append(AuthorizeRequestParam.REQUEST_URI) + .append("=").append(URLEncoder.encode(requestUri, Util.UTF8_STRING_ENCODING)); + } + if (requestSessionId) { + queryStringBuilder.append("&").append(AuthorizeRequestParam.REQUEST_SESSION_ID) + .append("=").append(URLEncoder.encode(Boolean.toString(requestSessionId), Util.UTF8_STRING_ENCODING)); + } + if (StringUtils.isNotBlank(sessionId)) { + queryStringBuilder.append("&").append(AuthorizeRequestParam.SESSION_ID) + .append("=").append(URLEncoder.encode(sessionId, Util.UTF8_STRING_ENCODING)); + } + if (StringUtils.isNotBlank(accessToken)) { + queryStringBuilder.append("&").append(AuthorizeRequestParam.ACCESS_TOKEN) + .append("=").append(URLEncoder.encode(accessToken, Util.UTF8_STRING_ENCODING)); + } + if (StringUtils.isNotBlank(codeChallenge)) { + queryStringBuilder.append("&").append(AuthorizeRequestParam.CODE_CHALLENGE) + .append("=").append(codeChallenge); + } + if (StringUtils.isNotBlank(codeChallengeMethod)) { + queryStringBuilder.append("&").append(AuthorizeRequestParam.CODE_CHALLENGE_METHOD) + .append("=").append(codeChallengeMethod); + } + if (StringUtils.isNotBlank(customResponseHeadersAsString)) { + queryStringBuilder.append("&").append(AuthorizeRequestParam.CUSTOM_RESPONSE_HEADERS) + .append("=").append(URLEncoder.encode(customResponseHeadersAsString, Util.UTF8_STRING_ENCODING)); + } + for (String key : getCustomParameters().keySet()) { + queryStringBuilder.append("&"); + queryStringBuilder.append(key).append("=").append(getCustomParameters().get(key)); + } + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } catch (JSONException e) { + e.printStackTrace(); + } + + return queryStringBuilder.toString(); + } + + /** + * Returns a collection of parameters of the authorization request. Any + * null or empty parameter will be omitted. + * + * @return A collection of parameters. + */ + public Map getParameters() { + Map parameters = new HashMap(); + + try { + // OAuth 2.0 request parameters + final String responseTypesAsString = getResponseTypesAsString(); + final String scopesAsString = getScopesAsString(); + final String promptsAsString = getPromptsAsString(); + final String customResponseHeadersAsString = getCustomResponseHeadersAsString(); + + if (StringUtils.isNotBlank(responseTypesAsString)) { + parameters.put(AuthorizeRequestParam.RESPONSE_TYPE, responseTypesAsString); + } + if (StringUtils.isNotBlank(clientId)) { + parameters.put(AuthorizeRequestParam.CLIENT_ID, clientId); + } + if (StringUtils.isNotBlank(scopesAsString)) { + parameters.put(AuthorizeRequestParam.SCOPE, scopesAsString); + } + if (StringUtils.isNotBlank(redirectUri)) { + parameters.put(AuthorizeRequestParam.REDIRECT_URI, redirectUri); + } + if (StringUtils.isNotBlank(state)) { + parameters.put(AuthorizeRequestParam.STATE, state); + } + + // OpenID Connect request parameters + final String uiLocalesAsString = getUiLocalesAsString(); + final String claimLocalesAsString = getClaimsLocalesAsString(); + final String acrValuesAsString = getAcrValuesAsString(); + final String claimsAsString = getClaimsAsString(); + + if (responseMode != null) { + parameters.put(AuthorizeRequestParam.RESPONSE_MODE, responseMode.toString()); + } + if (StringUtils.isNotBlank(nonce)) { + parameters.put(AuthorizeRequestParam.NONCE, nonce); + } + if (display != null) { + parameters.put(AuthorizeRequestParam.DISPLAY, display.toString()); + } + if (StringUtils.isNotBlank(promptsAsString)) { + parameters.put(AuthorizeRequestParam.PROMPT, promptsAsString); + } + if (maxAge != null) { + parameters.put(AuthorizeRequestParam.MAX_AGE, maxAge.toString()); + } + if (StringUtils.isNotBlank(uiLocalesAsString)) { + parameters.put(AuthorizeRequestParam.UI_LOCALES, uiLocalesAsString); + } + if (StringUtils.isNotBlank(claimLocalesAsString)) { + parameters.put(AuthorizeRequestParam.CLAIMS_LOCALES, claimLocalesAsString); + } + if (StringUtils.isNotBlank(idTokenHint)) { + parameters.put(AuthorizeRequestParam.ID_TOKEN_HINT, idTokenHint); + } + if (StringUtils.isNotBlank(loginHint)) { + parameters.put(AuthorizeRequestParam.LOGIN_HINT, loginHint); + } + if (StringUtils.isNotBlank(acrValuesAsString)) { + parameters.put(AuthorizeRequestParam.ACR_VALUES, acrValuesAsString); + } + if (StringUtils.isNotBlank(claimsAsString)) { + parameters.put(AuthorizeRequestParam.CLAIMS, claimsAsString); + } + if (StringUtils.isNotBlank(registration)) { + parameters.put(AuthorizeRequestParam.REGISTRATION, registration); + } + if (StringUtils.isNotBlank(request)) { + parameters.put(AuthorizeRequestParam.REQUEST, request); + } + if (StringUtils.isNotBlank(requestUri)) { + parameters.put(AuthorizeRequestParam.REQUEST_URI, requestUri); + } + if (requestSessionId) { + parameters.put(AuthorizeRequestParam.REQUEST_SESSION_ID, Boolean.toString(requestSessionId)); + } + if (StringUtils.isNotBlank(sessionId)) { + parameters.put(AuthorizeRequestParam.SESSION_ID, sessionId); + } + if (StringUtils.isNotBlank(accessToken)) { + parameters.put(AuthorizeRequestParam.ACCESS_TOKEN, accessToken); + } + if (StringUtils.isNotBlank(codeChallenge)) { + parameters.put(AuthorizeRequestParam.CODE_CHALLENGE, codeChallenge); + } + if (StringUtils.isNotBlank(codeChallengeMethod)) { + parameters.put(AuthorizeRequestParam.CODE_CHALLENGE_METHOD, codeChallengeMethod); + } + if (StringUtils.isNotBlank(customResponseHeadersAsString)) { + parameters.put(AuthorizeRequestParam.CUSTOM_RESPONSE_HEADERS, customResponseHeadersAsString); + } + + for (String key : getCustomParameters().keySet()) { + parameters.put(key, getCustomParameters().get(key)); + } + } catch (JSONException e) { + e.printStackTrace(); + } + + return parameters; + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/AuthorizationResponse.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/AuthorizationResponse.java new file mode 100644 index 00000000..9c402408 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/AuthorizationResponse.java @@ -0,0 +1,413 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import static org.gluu.oxauth.model.authorize.AuthorizeResponseParam.ACCESS_TOKEN; +import static org.gluu.oxauth.model.authorize.AuthorizeResponseParam.CODE; +import static org.gluu.oxauth.model.authorize.AuthorizeResponseParam.EXPIRES_IN; +import static org.gluu.oxauth.model.authorize.AuthorizeResponseParam.ID_TOKEN; +import static org.gluu.oxauth.model.authorize.AuthorizeResponseParam.SCOPE; +import static org.gluu.oxauth.model.authorize.AuthorizeResponseParam.SESSION_ID; +import static org.gluu.oxauth.model.authorize.AuthorizeResponseParam.SID; +import static org.gluu.oxauth.model.authorize.AuthorizeResponseParam.STATE; +import static org.gluu.oxauth.model.authorize.AuthorizeResponseParam.TOKEN_TYPE; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import javax.ws.rs.core.Response; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.authorize.AuthorizeErrorResponseType; +import org.gluu.oxauth.model.common.ResponseMode; +import org.gluu.oxauth.model.common.TokenType; +import org.gluu.oxauth.model.util.Util; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Represents an authorization response received from the authorization server. + * + * @author Javier Rojas Blum + * @version August 9, 2017 + */ +public class AuthorizationResponse extends BaseResponse { + + private String code; + private String accessToken; + private TokenType tokenType; + private Integer expiresIn; + private String scope; + private String idToken; + private String state; + private String sessionId; + private String sid; + private Map customParams; + private ResponseMode responseMode; + + private AuthorizeErrorResponseType errorType; + private String errorDescription; + private String errorUri; + + /** + * Constructs an authorization response. + */ + public AuthorizationResponse(Response clientResponse) { + super(clientResponse); + customParams = new HashMap(); + + if (StringUtils.isNotBlank(entity)) { + try { + JSONObject jsonObj = new JSONObject(entity); + if (jsonObj.has("error")) { + errorType = AuthorizeErrorResponseType.fromString(jsonObj.getString("error")); + } + if (jsonObj.has("error_description")) { + errorDescription = jsonObj.getString("error_description"); + } + if (jsonObj.has("error_uri")) { + errorUri = jsonObj.getString("error_uri"); + } + if (jsonObj.has("state")) { + state = jsonObj.getString("state"); + } + if (jsonObj.has("redirect")) { + location = jsonObj.getString("redirect"); + } + } catch (JSONException e) { + e.printStackTrace(); + } + } + processLocation(); + } + + public AuthorizationResponse(String location) { + this.location = location; + customParams = new HashMap(); + + processLocation(); + } + + private void processLocation() { + try { + if (StringUtils.isNotBlank(location)) { + Map params = null; + int fragmentIndex = location.indexOf("#"); + if (fragmentIndex != -1) { + responseMode = ResponseMode.FRAGMENT; + String fragment = location.substring(fragmentIndex + 1); + params = QueryStringDecoder.decode(fragment); + } else { + int queryStringIndex = location.indexOf("?"); + if (queryStringIndex != -1) { + responseMode = ResponseMode.QUERY; + String queryString = location.substring(queryStringIndex + 1); + params = QueryStringDecoder.decode(queryString); + } + } + + if (params != null) { + if (params.containsKey(CODE)) { + code = params.get(CODE); + params.remove(CODE); + } + if (params.containsKey(SESSION_ID)) { + sessionId = params.get(SESSION_ID); + params.remove(SESSION_ID); + } + if (params.containsKey(SID)) { + sid = params.get(SID); + params.remove(SID); + } + if (params.containsKey(ACCESS_TOKEN)) { + accessToken = params.get(ACCESS_TOKEN); + params.remove(ACCESS_TOKEN); + } + if (params.containsKey(TOKEN_TYPE)) { + tokenType = TokenType.fromString(params.get(TOKEN_TYPE)); + params.remove(TOKEN_TYPE); + } + if (params.containsKey(EXPIRES_IN)) { + expiresIn = Integer.parseInt(params.get(EXPIRES_IN)); + params.remove(EXPIRES_IN); + } + if (params.containsKey(SCOPE)) { + scope = URLDecoder.decode(params.get(SCOPE), Util.UTF8_STRING_ENCODING); + params.remove(SCOPE); + } + if (params.containsKey(ID_TOKEN)) { + idToken = params.get(ID_TOKEN); + params.remove(ID_TOKEN); + } + if (params.containsKey(STATE)) { + state = params.get(STATE); + params.remove(STATE); + } + if (params.containsKey("error")) { + errorType = AuthorizeErrorResponseType.fromString(params.get("error")); + params.remove("error"); + } + if (params.containsKey("error_description")) { + errorDescription = URLDecoder.decode(params.get("error_description"), Util.UTF8_STRING_ENCODING); + params.remove("error_description"); + } + if (params.containsKey("error_uri")) { + errorUri = URLDecoder.decode(params.get("error_uri"), Util.UTF8_STRING_ENCODING); + params.remove("error_uri"); + } + + for (Iterator it = params.keySet().iterator(); it.hasNext(); ) { + String key = it.next(); + getCustomParams().put(key, params.get(key)); + } + } + } + } catch (UnsupportedEncodingException e) { + } + } + + /** + * Returns the authorization code generated by the authorization server. + * + * @return The authorization code. + */ + public String getCode() { + return code; + } + + /** + * Sets the authorization code generated by the authorization server. + * + * @param code The authorization code. + */ + public void setCode(String code) { + this.code = code; + } + + /** + * Returns the access token issued by the authorization server. + * + * @return The access token. + */ + public String getAccessToken() { + return accessToken; + } + + /** + * Sets the access token issued by the authorization server. + * + * @param accessToken The access token. + */ + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public String getSid() { + return sid; + } + + public void setSid(String sid) { + this.sid = sid; + } + + /** + * Gets session id. + * + * @return session id. + */ + public String getSessionId() { + return sessionId; + } + + /** + * Sets session id. + * + * @param p_sessionId session id. + */ + public void setSessionId(String p_sessionId) { + sessionId = p_sessionId; + } + + public Map getCustomParams() { + return customParams; + } + + public void setCustomParams(Map customParams) { + this.customParams = customParams; + } + + public ResponseMode getResponseMode() { + return responseMode; + } + + public void setResponseMode(ResponseMode responseMode) { + this.responseMode = responseMode; + } + + /** + * Returns the type of the token issued (value is case insensitive). + * + * @return The type of the token. + */ + public TokenType getTokenType() { + return tokenType; + } + + /** + * Sets the type of the token issued (value is case insensitive). + * + * @param tokenType The type of the token. + */ + public void setTokenType(TokenType tokenType) { + this.tokenType = tokenType; + } + + /** + * Returns the lifetime in seconds of the access token. For example, the + * value 3600 denotes that the access token will expire in one hour from the + * time the response was generated. + * + * @return The lifetime in seconds of the access token. + */ + public Integer getExpiresIn() { + return expiresIn; + } + + /** + * Sets the lifetime in seconds of the access token. For example, the value + * 3600 denotes that the access token will expire in one hour from the time + * the response was generated. + * + * @param expiresIn The lifetime in seconds of the access token. + */ + public void setExpiresIn(Integer expiresIn) { + this.expiresIn = expiresIn; + } + + /** + * Returns the scope of the access token. + * + * @return The scope of the access token. + */ + public String getScope() { + return scope; + } + + /** + * Sets the scope of the access token. + * + * @param scope The scope of the access token. + */ + public void setScope(String scope) { + this.scope = scope; + } + + /** + * Returns the ID Token of the for the authentication session. + * + * @return The ID Token. + */ + public String getIdToken() { + return idToken; + } + + /** + * Sets the ID Token of the for the authentication session. + * + * @param idToken The ID Token. + */ + public void setIdToken(String idToken) { + this.idToken = idToken; + } + + /** + * Returns the state. If the state parameter was present in the client + * authorization request, the exact value received from the client. + * + * @return The state. + */ + public String getState() { + return state; + } + + /** + * Sets the state. If the state parameter was present in the client + * authorization request, the exact value received from the client. + * + * @param state The state. + */ + public void setState(String state) { + this.state = state; + } + + /** + * Returns the error code when the request fails, otherwise will return + * null. + * + * @return The error code when the request fails. + */ + public AuthorizeErrorResponseType getErrorType() { + return errorType; + } + + /** + * Sets the error code when the request fails, otherwise will return + * null. + * + * @param errorType The error code when the request fails. + */ + public void setErrorType(AuthorizeErrorResponseType errorType) { + this.errorType = errorType; + } + + /** + * Returns a human-readable UTF-8 encoded text providing additional + * information, used to assist the client developer in understanding the + * error that occurred. + * + * @return The error description. + */ + public String getErrorDescription() { + return errorDescription; + } + + /** + * Sets a human-readable UTF-8 encoded text providing additional + * information, used to assist the client developer in understanding the + * error that occurred. + * + * @param errorDescription The error description. + */ + public void setErrorDescription(String errorDescription) { + this.errorDescription = errorDescription; + } + + /** + * Returns a URI identifying a human-readable web page with information + * about the error, used to provide the client developer with additional + * information about the error. + * + * @return A URI with information about the error. + */ + public String getErrorUri() { + return errorUri; + } + + /** + * Sets a URI identifying a human-readable web page with information about + * the error, used to provide the client developer with additional + * information about the error. + * + * @param errorUri A URI with information about the error. + */ + public void setErrorUri(String errorUri) { + this.errorUri = errorUri; + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/AuthorizeClient.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/AuthorizeClient.java new file mode 100644 index 00000000..19876fff --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/AuthorizeClient.java @@ -0,0 +1,277 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import static org.gluu.oxauth.client.AuthorizationRequest.NO_REDIRECT_HEADER; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation.Builder; +import javax.ws.rs.core.MediaType; + +import org.apache.log4j.Logger; +import org.gluu.oxauth.model.authorize.AuthorizeRequestParam; +import org.gluu.oxauth.model.common.AuthorizationMethod; +import org.gluu.oxauth.model.common.Display; +import org.gluu.oxauth.model.common.Prompt; +import org.gluu.oxauth.model.common.ResponseType; +import org.jboss.resteasy.client.jaxrs.ClientHttpEngine; +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; + +/** + * Encapsulates functionality to make authorization request calls to an authorization server via REST Services. + * + * @author Javier Rojas Blum + * @version October 7, 2019 + */ +public class AuthorizeClient extends BaseClient { + + private static final Logger LOG = Logger.getLogger(AuthorizeClient.class); + + /** + * Constructs an authorize client by providing a REST url where the + * authorize service is located. + * + * @param url The REST Service location. + */ + public AuthorizeClient(String url) { + super(url); + } + + @Override + public String getHttpMethod() { + if (request.getAuthorizationMethod() == null + || request.getAuthorizationMethod() == AuthorizationMethod.AUTHORIZATION_REQUEST_HEADER_FIELD + || request.getAuthorizationMethod() == AuthorizationMethod.FORM_ENCODED_BODY_PARAMETER) { + return HttpMethod.POST; + } else { // AuthorizationMethod.URL_QUERY_PARAMETER + return HttpMethod.GET; + } + } + + /** + * The authorization code grant type is used to obtain both access tokens + * and refresh tokens and is optimized for confidential clients. As a + * redirection-based flow, the client must be capable of interacting with + * the resource owner's user-agent (typically a web browser) and capable of + * receiving incoming requests (via redirection) from the authorization + * server. + * + * @param clientId The client identifier. This parameter is required. + * @param scopes The scope of the access request. This parameter is optional. + * @param redirectUri The redirection URI. This parameter is optional. + * @param nonce A string value used to associate a user agent session with an ID Token, + * and to mitigate replay attacks. + * forgery. This parameter is recommended. + * @param state An opaque value used by the client to maintain state between + * the request and callback. The authorization server includes + * this value when redirecting the user-agent back to the client. + * The parameter should be used for preventing cross-site request + * forgery. + * @param req A JWT encoded OpenID Request Object. + * @param reqUri An URL that points to an OpenID Request Object. + * @param display An ASCII string value that specifies how the Authorization Server displays the + * authentication page to the End-User. + * @param prompt A space delimited list of ASCII strings that can contain the values login, consent, + * select_account, and none. + * @return The authorization response. + */ + public AuthorizationResponse execAuthorizationCodeGrant( + String clientId, List scopes, String redirectUri, String nonce, + String state, String req, String reqUri, Display display, List prompt) { + List responseTypes = new ArrayList(); + responseTypes.add(ResponseType.CODE); + setRequest(new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce)); + getRequest().setRedirectUri(redirectUri); + getRequest().setState(state); + getRequest().setRequest(req); + getRequest().setRedirectUri(reqUri); + getRequest().setDisplay(display); + getRequest().getPrompts().addAll(prompt); + + return exec(); + } + + /** + *

+ * The implicit grant type is used to obtain access tokens (it does not + * support the issuance of refresh tokens) and is optimized for public + * clients known to operate a particular redirection URI. These clients are + * typically implemented in a browser using a scripting language such as + * JavaScript. + *

+ *

+ * As a redirection-based flow, the client must be capable of interacting + * with the resource owner's user-agent (typically a web browser) and + * capable of receiving incoming requests (via redirection) from the + * authorization server. + *

+ *

+ * Unlike the authorization code grant type in which the client makes + * separate requests for authorization and access token, the client receives + * the access token as the result of the authorization request. + *

+ *

+ * The implicit grant type does not include client authentication, and + * relies on the presence of the resource owner and the registration of the + * redirection URI. Because the access token is encoded into the redirection + * URI, it may be exposed to the resource owner and other applications + * residing on its device. + *

+ * + * @param clientId The client identifier. This parameter is required. + * @param scopes The scope of the access request. This parameter is optional. + * @param redirectUri The redirection URI. This parameter is optional. + * @param nonce A string value used to associate a user agent session with an ID Token, + * and to mitigate replay attacks. + * forgery. This parameter is recommended. + * @param state An opaque value used by the client to maintain state between + * the request and callback. The authorization server includes + * this value when redirecting the user-agent back to the client. + * The parameter should be used for preventing cross-site request + * forgery. + * @param req A JWT encoded OpenID Request Object. + * @param reqUri An URL that points to an OpenID Request Object. + * @param display An ASCII string value that specifies how the Authorization Server displays the + * authentication page to the End-User. + * @param prompt A space delimited list of ASCII strings that can contain the values login, consent, + * select_account, and none. + * @return The authorization response. + */ + @Deprecated // it produces confusion since we have parameters and request object at the same time + public AuthorizationResponse execImplicitGrant( + String clientId, List scopes, String redirectUri, String nonce, + String state, String req, String reqUri, Display display, List prompt) { + List responseTypes = new ArrayList(); + responseTypes.add(ResponseType.TOKEN); + setRequest(new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce)); + getRequest().setRedirectUri(redirectUri); + getRequest().setState(state); + getRequest().setRequest(req); + getRequest().setRedirectUri(reqUri); + getRequest().setDisplay(display); + getRequest().getPrompts().addAll(prompt); + + return exec(); + } + + /** + * Executes the call to the REST Service and processes the response. + * + * @return The authorization response. + */ + public AuthorizationResponse exec() { + AuthorizationResponse response = null; + + try { + initClientRequest(); + response = exec_(); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } finally { + closeConnection(); + } + + return response; + } + + @Deprecated + public AuthorizationResponse exec(ClientHttpEngine engine) { + AuthorizationResponse response = null; + + try { + resteasyClient = ((ResteasyClientBuilder) ResteasyClientBuilder.newBuilder()).httpEngine(engine).build(); + webTarget = resteasyClient.target(getUrl()); + + response = exec_(); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } + // Do not close the connection for this case. + + return response; + } + + private AuthorizationResponse exec_() throws Exception { + final String responseTypesAsString = getRequest().getResponseTypesAsString(); + final String scopesAsString = getRequest().getScopesAsString(); + final String promptsAsString = getRequest().getPromptsAsString(); + final String uiLocalesAsString = getRequest().getUiLocalesAsString(); + final String claimLocalesAsString = getRequest().getClaimsLocalesAsString(); + final String acrValuesAsString = getRequest().getAcrValuesAsString(); + final String claimsAsString = getRequest().getClaimsAsString(); + + addReqParam(AuthorizeRequestParam.RESPONSE_TYPE, responseTypesAsString); + addReqParam(AuthorizeRequestParam.CLIENT_ID, getRequest().getClientId()); + addReqParam(AuthorizeRequestParam.SCOPE, scopesAsString); + addReqParam(AuthorizeRequestParam.REDIRECT_URI, getRequest().getRedirectUri()); + addReqParam(AuthorizeRequestParam.STATE, getRequest().getState()); + + addReqParam(AuthorizeRequestParam.NONCE, getRequest().getNonce()); + addReqParam(AuthorizeRequestParam.DISPLAY, getRequest().getDisplay()); + addReqParam(AuthorizeRequestParam.PROMPT, promptsAsString); + if (getRequest().getMaxAge() != null) { + addReqParam(AuthorizeRequestParam.MAX_AGE, getRequest().getMaxAge().toString()); + } + addReqParam(AuthorizeRequestParam.UI_LOCALES, uiLocalesAsString); + addReqParam(AuthorizeRequestParam.CLAIMS_LOCALES, claimLocalesAsString); + addReqParam(AuthorizeRequestParam.ID_TOKEN_HINT, getRequest().getIdTokenHint()); + addReqParam(AuthorizeRequestParam.LOGIN_HINT, getRequest().getLoginHint()); + addReqParam(AuthorizeRequestParam.ACR_VALUES, acrValuesAsString); + addReqParam(AuthorizeRequestParam.CLAIMS, claimsAsString); + addReqParam(AuthorizeRequestParam.REGISTRATION, getRequest().getRegistration()); + addReqParam(AuthorizeRequestParam.REQUEST, getRequest().getRequest()); + addReqParam(AuthorizeRequestParam.REQUEST_URI, getRequest().getRequestUri()); + addReqParam(AuthorizeRequestParam.ACCESS_TOKEN, getRequest().getAccessToken()); + addReqParam(AuthorizeRequestParam.CUSTOM_RESPONSE_HEADERS, getRequest().getCustomResponseHeadersAsString()); + + // PKCE + addReqParam(AuthorizeRequestParam.CODE_CHALLENGE, getRequest().getCodeChallenge()); + addReqParam(AuthorizeRequestParam.CODE_CHALLENGE_METHOD, getRequest().getCodeChallengeMethod()); + + if (getRequest().isRequestSessionId()) { + addReqParam(AuthorizeRequestParam.REQUEST_SESSION_ID, Boolean.toString(getRequest().isRequestSessionId())); + } + addReqParam(AuthorizeRequestParam.SESSION_ID, getRequest().getSessionId()); + + // Custom params + for (String key : request.getCustomParameters().keySet()) { + addReqParam(key, request.getCustomParameters().get(key)); + } + + Builder clientRequest = webTarget.request(); + applyCookies(clientRequest); + + // Prepare request parameters + clientRequest.header("Content-Type", MediaType.APPLICATION_FORM_URLENCODED); +//// clientRequest.setHttpMethod(getHttpMethod()); + + if (getRequest().isUseNoRedirectHeader()) { + clientRequest.header(NO_REDIRECT_HEADER, "true"); + } + + if (request.getAuthorizationMethod() != AuthorizationMethod.FORM_ENCODED_BODY_PARAMETER && request.hasCredentials()) { + clientRequest.header("Authorization", "Basic " + request.getEncodedCredentials()); + } + + // Call REST Service and handle response + if (request.getAuthorizationMethod() == AuthorizationMethod.FORM_ENCODED_BODY_PARAMETER) { + clientResponse = clientRequest.buildPost(Entity.form(requestForm)).invoke(); + } else { + clientResponse = clientRequest.buildGet().invoke(); + } + + setResponse(new AuthorizationResponse(clientResponse)); + + return getResponse(); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/BackchannelAuthenticationClient.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/BackchannelAuthenticationClient.java new file mode 100644 index 00000000..590b4421 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/BackchannelAuthenticationClient.java @@ -0,0 +1,154 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import static org.gluu.oxauth.model.ciba.BackchannelAuthenticationRequestParam.ACR_VALUES; +import static org.gluu.oxauth.model.ciba.BackchannelAuthenticationRequestParam.BINDING_MESSAGE; +import static org.gluu.oxauth.model.ciba.BackchannelAuthenticationRequestParam.CLIENT_ID; +import static org.gluu.oxauth.model.ciba.BackchannelAuthenticationRequestParam.CLIENT_NOTIFICATION_TOKEN; +import static org.gluu.oxauth.model.ciba.BackchannelAuthenticationRequestParam.ID_TOKEN_HINT; +import static org.gluu.oxauth.model.ciba.BackchannelAuthenticationRequestParam.LOGIN_HINT; +import static org.gluu.oxauth.model.ciba.BackchannelAuthenticationRequestParam.LOGIN_HINT_TOKEN; +import static org.gluu.oxauth.model.ciba.BackchannelAuthenticationRequestParam.REQUEST; +import static org.gluu.oxauth.model.ciba.BackchannelAuthenticationRequestParam.REQUESTED_EXPIRY; +import static org.gluu.oxauth.model.ciba.BackchannelAuthenticationRequestParam.REQUEST_URI; +import static org.gluu.oxauth.model.ciba.BackchannelAuthenticationRequestParam.SCOPE; +import static org.gluu.oxauth.model.ciba.BackchannelAuthenticationRequestParam.USER_CODE; +import static org.gluu.oxauth.model.ciba.BackchannelAuthenticationResponseParam.AUTH_REQ_ID; +import static org.gluu.oxauth.model.ciba.BackchannelAuthenticationResponseParam.EXPIRES_IN; +import static org.gluu.oxauth.model.ciba.BackchannelAuthenticationResponseParam.INTERVAL; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation.Builder; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.util.Util; +import org.json.JSONObject; + +/** + * Encapsulates functionality to make backchannel authentication request calls to an authorization server via REST Services. + * + * @author Javier Rojas Blum + * @version September 4, 2019 + */ +public class BackchannelAuthenticationClient extends BaseClient { + + private static final Logger LOG = Logger.getLogger(BackchannelAuthenticationClient.class); + + /** + * Constructs a backchannel authentication client by providing a REST url where the + * backchannel authentication service is located. + * + * @param url The REST Service location. + */ + public BackchannelAuthenticationClient(String url) { + super(url); + } + + @Override + public String getHttpMethod() { + return HttpMethod.POST; + } + + /** + * Executes the call to the REST Service and processes the response. + * + * @return The authorization response. + */ + public BackchannelAuthenticationResponse exec() { + BackchannelAuthenticationResponse response = null; + + try { + initClientRequest(); + response = exec_(); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } finally { + closeConnection(); + } + + return response; + } + + private BackchannelAuthenticationResponse exec_() throws Exception { + final String scopesAsString = Util.listAsString(getRequest().getScope()); + final String acrValuesAsString = Util.listAsString(getRequest().getAcrValues()); + + if (StringUtils.isNotBlank(scopesAsString)) { + requestForm.param(SCOPE, scopesAsString); + } + if (StringUtils.isNotBlank(getRequest().getClientNotificationToken())) { + requestForm.param(CLIENT_NOTIFICATION_TOKEN, getRequest().getClientNotificationToken()); + } + if (StringUtils.isNotBlank(acrValuesAsString)) { + requestForm.param(ACR_VALUES, acrValuesAsString); + } + if (StringUtils.isNotBlank(getRequest().getLoginHintToken())) { + requestForm.param(LOGIN_HINT_TOKEN, getRequest().getLoginHintToken()); + } + if (StringUtils.isNotBlank(getRequest().getIdTokenHint())) { + requestForm.param(ID_TOKEN_HINT, getRequest().getIdTokenHint()); + } + if (StringUtils.isNotBlank(getRequest().getLoginHint())) { + requestForm.param(LOGIN_HINT, getRequest().getLoginHint()); + } + if (StringUtils.isNotBlank(getRequest().getBindingMessage())) { + requestForm.param(BINDING_MESSAGE, getRequest().getBindingMessage()); + } + if (StringUtils.isNotBlank(getRequest().getUserCode())) { + requestForm.param(USER_CODE, getRequest().getUserCode()); + } + if (getRequest().getRequestedExpiry() != null) { + requestForm.param(REQUESTED_EXPIRY, getRequest().getRequestedExpiry().toString()); + } + if (StringUtils.isNotBlank(getRequest().getClientId())) { + requestForm.param(CLIENT_ID, getRequest().getClientId()); + } + if (StringUtils.isNotBlank(getRequest().getRequest())) { + requestForm.param(REQUEST, getRequest().getRequest()); + } + if (StringUtils.isNotBlank(getRequest().getRequestUri())) { + requestForm.param(REQUEST_URI, getRequest().getRequestUri()); + } + + Builder clientRequest = webTarget.request(); + applyCookies(clientRequest); + + // Prepare request parameters +//// clientRequest.setHttpMethod(getHttpMethod()); + clientRequest.header("Content-Type", request.getContentType()); + if (request.getAuthenticationMethod() == AuthenticationMethod.CLIENT_SECRET_BASIC && request.hasCredentials()) { + clientRequest.header("Authorization", "Basic " + request.getEncodedCredentials()); + } + + + new ClientAuthnEnabler(clientRequest, requestForm).exec(getRequest()); + + // Call REST Service and handle response + clientResponse = clientRequest.buildPost(Entity.form(requestForm)).invoke(); + + setResponse(new BackchannelAuthenticationResponse(clientResponse)); + if (StringUtils.isNotBlank(response.getEntity())) { + JSONObject jsonObj = new JSONObject(response.getEntity()); + + if (jsonObj.has(AUTH_REQ_ID)) { + getResponse().setAuthReqId(jsonObj.getString(AUTH_REQ_ID)); + } + if (jsonObj.has(EXPIRES_IN)) { + getResponse().setExpiresIn(jsonObj.getInt(EXPIRES_IN)); + } + if (jsonObj.has(INTERVAL)) { + getResponse().setInterval(jsonObj.getInt(INTERVAL)); + } + } + + return getResponse(); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/BackchannelAuthenticationRequest.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/BackchannelAuthenticationRequest.java new file mode 100644 index 00000000..45d18c02 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/BackchannelAuthenticationRequest.java @@ -0,0 +1,163 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import java.util.List; + +import javax.ws.rs.core.MediaType; + +import org.gluu.oxauth.model.ciba.BackchannelAuthenticationRequestParam; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.util.QueryBuilder; +import org.gluu.oxauth.model.util.Util; + +/** + * Represents a CIBA backchannel authorization request to send to the authorization server. + * + * @author Javier Rojas Blum + * @version May 28, 2020 + */ +public class BackchannelAuthenticationRequest extends ClientAuthnRequest { + + private List scope; + private String clientNotificationToken; + private List acrValues; + private String loginHintToken; + private String idTokenHint; + private String loginHint; + private String bindingMessage; + private String userCode; + private Integer requestedExpiry; + private String clientId; + private String request; + private String requestUri; + + public BackchannelAuthenticationRequest() { + setContentType(MediaType.APPLICATION_FORM_URLENCODED); + setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + } + + public List getScope() { + return scope; + } + + public void setScope(List scope) { + this.scope = scope; + } + + public String getClientNotificationToken() { + return clientNotificationToken; + } + + public void setClientNotificationToken(String clientNotificationToken) { + this.clientNotificationToken = clientNotificationToken; + } + + public List getAcrValues() { + return acrValues; + } + + public void setAcrValues(List acrValues) { + this.acrValues = acrValues; + } + + public String getLoginHintToken() { + return loginHintToken; + } + + public void setLoginHintToken(String loginHintToken) { + this.loginHintToken = loginHintToken; + } + + public String getIdTokenHint() { + return idTokenHint; + } + + public void setIdTokenHint(String idTokenHint) { + this.idTokenHint = idTokenHint; + } + + public String getLoginHint() { + return loginHint; + } + + public void setLoginHint(String loginHint) { + this.loginHint = loginHint; + } + + public String getBindingMessage() { + return bindingMessage; + } + + public void setBindingMessage(String bindingMessage) { + this.bindingMessage = bindingMessage; + } + + public String getUserCode() { + return userCode; + } + + public void setUserCode(String userCode) { + this.userCode = userCode; + } + + public Integer getRequestedExpiry() { + return requestedExpiry; + } + + public void setRequestedExpiry(Integer requestedExpiry) { + this.requestedExpiry = requestedExpiry; + } + + public String getRequest() { + return request; + } + + public void setRequest(String request) { + this.request = request; + } + + public String getRequestUri() { + return requestUri; + } + + public void setRequestUri(String requestUri) { + this.requestUri = requestUri; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + @Override + public String getQueryString() { + QueryBuilder builder = QueryBuilder.instance(); + + final String scopesAsString = Util.listAsString(scope); + final String acrValuesAsString = Util.listAsString(acrValues); + + builder.append(BackchannelAuthenticationRequestParam.SCOPE, scopesAsString); + builder.append(BackchannelAuthenticationRequestParam.CLIENT_NOTIFICATION_TOKEN, clientNotificationToken); + builder.append(BackchannelAuthenticationRequestParam.ACR_VALUES, acrValuesAsString); + builder.append(BackchannelAuthenticationRequestParam.LOGIN_HINT_TOKEN, loginHintToken); + builder.append(BackchannelAuthenticationRequestParam.ID_TOKEN_HINT, idTokenHint); + builder.append(BackchannelAuthenticationRequestParam.LOGIN_HINT, loginHint); + builder.append(BackchannelAuthenticationRequestParam.BINDING_MESSAGE, bindingMessage); + builder.append(BackchannelAuthenticationRequestParam.USER_CODE, userCode); + builder.appendIfNotNull(BackchannelAuthenticationRequestParam.REQUESTED_EXPIRY, requestedExpiry); + builder.appendIfNotNull(BackchannelAuthenticationRequestParam.CLIENT_ID, clientId); + builder.appendIfNotNull(BackchannelAuthenticationRequestParam.REQUEST, request); + builder.appendIfNotNull(BackchannelAuthenticationRequestParam.REQUEST_URI, requestUri); + + appendClientAuthnToQuery(builder); + return builder.toString(); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/BackchannelAuthenticationResponse.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/BackchannelAuthenticationResponse.java new file mode 100644 index 00000000..2473756d --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/BackchannelAuthenticationResponse.java @@ -0,0 +1,100 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import static org.gluu.oxauth.model.ciba.BackchannelAuthenticationResponseParam.AUTH_REQ_ID; +import static org.gluu.oxauth.model.ciba.BackchannelAuthenticationResponseParam.EXPIRES_IN; +import static org.gluu.oxauth.model.ciba.BackchannelAuthenticationResponseParam.INTERVAL; + +import javax.ws.rs.core.Response; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.gluu.oxauth.model.ciba.BackchannelAuthenticationErrorResponseType; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Represents a CIBA backchannel authorization response. + * + * @author Javier Rojas Blum + * @version August 20, 2019 + */ +public class BackchannelAuthenticationResponse extends BaseResponseWithErrors { + + private static final Logger LOG = Logger.getLogger(BackchannelAuthenticationResponse.class); + + private String authReqId; + private Integer expiresIn; + private Integer interval; + + /** + * Constructs a backchannel authentication response. + */ + public BackchannelAuthenticationResponse() { + } + + /** + * Constructs a backchannel authentication response. + */ + public BackchannelAuthenticationResponse(Response clientResponse) { + super(clientResponse); + } + + @Override + public BackchannelAuthenticationErrorResponseType fromString(String p_str) { + return BackchannelAuthenticationErrorResponseType.fromString(p_str); + } + + public void injectDataFromJson() { + injectDataFromJson(entity); + } + + @Override + public void injectDataFromJson(String p_json) { + if (StringUtils.isNotBlank(entity)) { + try { + JSONObject jsonObj = new JSONObject(entity); + if (jsonObj.has(AUTH_REQ_ID)) { + setAuthReqId(jsonObj.getString(AUTH_REQ_ID)); + } + if (jsonObj.has(EXPIRES_IN)) { + setExpiresIn(jsonObj.getInt(EXPIRES_IN)); + } + if (jsonObj.has(INTERVAL)) { + setInterval(jsonObj.getInt(INTERVAL)); + } + } catch (JSONException e) { + LOG.error(e.getMessage(), e); + } + } + } + + public String getAuthReqId() { + return authReqId; + } + + public void setAuthReqId(String authReqId) { + this.authReqId = authReqId; + } + + public Integer getExpiresIn() { + return expiresIn; + } + + public void setExpiresIn(Integer expiresIn) { + this.expiresIn = expiresIn; + } + + public Integer getInterval() { + return interval; + } + + public void setInterval(Integer interval) { + this.interval = interval; + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/BaseClient.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/BaseClient.java new file mode 100644 index 00000000..0446e2c5 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/BaseClient.java @@ -0,0 +1,277 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import static org.gluu.oxauth.client.AuthorizationRequest.NO_REDIRECT_HEADER; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.client.Invocation.Builder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Cookie; +import javax.ws.rs.core.Form; +import javax.ws.rs.core.Response; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.AuthorizationMethod; +import org.gluu.oxauth.model.common.HasParamName; +import org.gluu.oxauth.model.util.Util; +import org.jboss.resteasy.client.jaxrs.ClientHttpEngine; +import org.jboss.resteasy.client.jaxrs.ResteasyClient; +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; + +/** + * Allows to retrieve HTTP requests to the authorization server and responses from it for display purposes. + * + * @author Javier Rojas Blum + * @version May 28, 2020 + */ +public abstract class BaseClient { + + private static final Logger LOG = Logger.getLogger(BaseClient.class); + + private String url; + + protected T request; + protected V response; + protected ResteasyClient resteasyClient = null; + protected WebTarget webTarget = null; + protected Form requestForm = new Form(); + protected Response clientResponse = null; + private final List cookies = new ArrayList(); + private final Map headers = new HashMap(); + + protected ClientHttpEngine executor = null; + + public BaseClient() { + } + + public BaseClient(String url) { + this.url = url; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public T getRequest() { + return request; + } + + public void setRequest(T request) { + this.request = request; + } + + public V getResponse() { + return response; + } + + public void setResponse(V response) { + this.response = response; + } + + public ClientHttpEngine getExecutor() { + return executor; + } + + public void setExecutor(ClientHttpEngine executor) { + this.executor = executor; + } + + protected void addReqParam(String p_key, HasParamName p_value) { + if (p_value != null) { + addReqParam(p_key, p_value.getParamName()); + } + } + + protected void addReqParam(String p_key, String p_value) { + if (Util.allNotBlank(p_key, p_value)) { + if (request.getAuthorizationMethod() == AuthorizationMethod.FORM_ENCODED_BODY_PARAMETER) { + requestForm.param(p_key, p_value); + } else { + webTarget = webTarget.queryParam(p_key, p_value); + } + } + } +/* + public static void putAllFormParameters(ClientRequest p_clientRequest, BaseRequest p_request) { + if (p_clientRequest != null && p_request != null) { + final Map parameters = p_request.getParameters(); + if (parameters != null && !parameters.isEmpty()) { + for (Map.Entry e : parameters.entrySet()) { + p_requestForm.param(e.getKey(), e.getValue()); + } + } + } + } +*/ + public String getRequestAsString() { + StringBuilder sb = new StringBuilder(); + + try { + URL theUrl = new URL(url); + + if (getHttpMethod().equals(HttpMethod.POST) || getHttpMethod().equals(HttpMethod.PUT) || getHttpMethod().equals(HttpMethod.DELETE)) { + sb.append(getHttpMethod()).append(" ").append(theUrl.getPath()).append(" HTTP/1.1"); + if (StringUtils.isNotBlank(request.getContentType())) { + sb.append("\n"); + sb.append("Content-Type: ").append(request.getContentType()); + } + if (StringUtils.isNotBlank(request.getMediaType())) { + sb.append("\n"); + sb.append("Accept: ").append(request.getMediaType()); + } + sb.append("\n"); + sb.append("Host: ").append(theUrl.getHost()); + + if (request instanceof AuthorizationRequest) { + AuthorizationRequest authorizationRequest = (AuthorizationRequest) request; + if (authorizationRequest.isUseNoRedirectHeader()) { + sb.append("\n"); + sb.append(NO_REDIRECT_HEADER + ": true"); + } + } + if (request.getAuthorizationMethod() == null) { + if (request.getAuthenticationMethod() == null + || request.getAuthenticationMethod() == AuthenticationMethod.CLIENT_SECRET_BASIC) { + if (request.hasCredentials()) { + String encodedCredentials = request.getEncodedCredentials(); + sb.append("\n"); + sb.append("Authorization: Basic ").append(encodedCredentials); + } + } + } else if (request.getAuthorizationMethod() == AuthorizationMethod.AUTHORIZATION_REQUEST_HEADER_FIELD) { + if (request instanceof UserInfoRequest) { + String accessToken = ((UserInfoRequest) request).getAccessToken(); + sb.append("\n"); + sb.append("Authorization: Bearer ").append(accessToken); + } + } + + sb.append("\n"); + sb.append("\n"); + sb.append(request.getQueryString()); + } else if (getHttpMethod().equals(HttpMethod.GET)) { + sb.append(getHttpMethod()).append(" ").append(theUrl.getPath()).append(" HTTP/1.1"); + if (StringUtils.isNotBlank(request.getQueryString())) { + sb.append("?").append(request.getQueryString()); + } + sb.append(" HTTP/1.1"); + sb.append("\n"); + sb.append("Host: ").append(theUrl.getHost()); + + if (request instanceof AuthorizationRequest) { + AuthorizationRequest authorizationRequest = (AuthorizationRequest) request; + if (authorizationRequest.isUseNoRedirectHeader()) { + sb.append("\n"); + sb.append(NO_REDIRECT_HEADER + ": true"); + } + } + if (request.getAuthorizationMethod() == null) { + if (request.hasCredentials()) { + String encodedCredentials = request.getEncodedCredentials(); + sb.append("\n"); + sb.append("Authorization: Basic ").append(encodedCredentials); + } else if (request instanceof RegisterRequest) { + RegisterRequest r = (RegisterRequest) request; + String registrationAccessToken = r.getRegistrationAccessToken(); + sb.append("\n"); + sb.append("Authorization: Bearer ").append(registrationAccessToken); + } + } else if (request.getAuthorizationMethod() == AuthorizationMethod.AUTHORIZATION_REQUEST_HEADER_FIELD) { + if (request instanceof UserInfoRequest) { + String accessToken = ((UserInfoRequest) request).getAccessToken(); + sb.append("\n"); + sb.append("Authorization: Bearer ").append(accessToken); + } + } + } + } catch (MalformedURLException e) { + LOG.error(e.getMessage(), e); + } + + return sb.toString(); + } + + public String getResponseAsString() { + StringBuilder sb = new StringBuilder(); + + if (response != null) { + sb.append("HTTP/1.1 ").append(response.getStatus()); + if (response.getHeaders() != null) { + for (String key : response.getHeaders().keySet()) { + sb.append("\n") + .append(key) + .append(": ") + .append(response.getHeaders().get(key).get(0)); + } + } + if (response.getEntity() != null) { + sb.append("\n"); + sb.append("\n"); + sb.append(response.getEntity()); + } + } + return sb.toString(); + } + + protected void initClientRequest() { + if (this.executor == null) { + resteasyClient = (ResteasyClient) ResteasyClientBuilder.newClient(); + } else { + resteasyClient = ((ResteasyClientBuilder) ResteasyClientBuilder.newBuilder()).httpEngine(executor).build(); + } + + webTarget = resteasyClient.target(getUrl()); + } + + protected void applyCookies(Builder clientRequest) { + for (Cookie cookie : cookies) { + clientRequest.cookie(cookie); + } + for (Map.Entry headerEntry : headers.entrySet()) { + clientRequest.header(headerEntry.getKey(), headerEntry.getValue()); + } + } + + public void closeConnection() { + try { + if (clientResponse != null) { + clientResponse.close(); + } + // Why we should close engine after processing response? +// if (resteasyClient != null && resteasyClient.httpEngine() != null) { +// resteasyClient.httpEngine().close(); +// } + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } + } + + public abstract String getHttpMethod(); + + public List getCookies() { + return cookies; + } + + public Map getHeaders() { + return headers; + } + +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/BaseRequest.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/BaseRequest.java new file mode 100644 index 00000000..37180ea4 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/BaseRequest.java @@ -0,0 +1,145 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.codec.binary.Base64; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.AuthorizationMethod; +import org.gluu.oxauth.model.util.Util; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * @author Javier Rojas Blum + * @version January 26. 2018 + */ +public abstract class BaseRequest { + + private static final Map EMPTY_MAP = new HashMap(); + private static final JSONObject EMPTY_JSON_OBJECT = new JSONObject(); + + private String contentType; + private String mediaType; + private String authUsername; + private String authPassword; + private AuthenticationMethod authenticationMethod; + private AuthorizationMethod authorizationMethod; + private Map customParameters; + + protected BaseRequest() { + customParameters = new HashMap(); + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public String getMediaType() { + return mediaType; + } + + public void setMediaType(String mediaType) { + this.mediaType = mediaType; + } + + public String getAuthUsername() { + return authUsername; + } + + public void setAuthUsername(String authUsername) { + this.authUsername = authUsername; + } + + public String getAuthPassword() { + return authPassword; + } + + public void setAuthPassword(String authPassword) { + this.authPassword = authPassword; + } + + public AuthenticationMethod getAuthenticationMethod() { + return authenticationMethod; + } + + public void setAuthenticationMethod(AuthenticationMethod authenticationMethod) { + this.authenticationMethod = authenticationMethod; + } + + public AuthorizationMethod getAuthorizationMethod() { + return authorizationMethod; + } + + public void setAuthorizationMethod(AuthorizationMethod authorizationMethod) { + this.authorizationMethod = authorizationMethod; + } + + public Map getCustomParameters() { + return customParameters; + } + + public void addCustomParameter(String paramName, String paramValue) { + customParameters.put(paramName, paramValue); + } + + public boolean hasCredentials() { + return authUsername != null && authPassword != null + && !authUsername.isEmpty() + && !authPassword.isEmpty(); + } + + /** + * Returns the client credentials (URL encoded). + * + * @return The client credentials. + */ + public String getCredentials() throws UnsupportedEncodingException { + return URLEncoder.encode(authUsername, Util.UTF8_STRING_ENCODING) + + ":" + + URLEncoder.encode(authPassword, Util.UTF8_STRING_ENCODING); + } + + /** + * Returns the client credentials encoded using base64. + * + * @return The encoded client credentials. + */ + public String getEncodedCredentials() { + try { + if (hasCredentials()) { + return Base64.encodeBase64String(Util.getBytes(getCredentials())); + } + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + + return null; + } + + public static String getEncodedCredentials(String clientId, String clientSecret) throws UnsupportedEncodingException { + return Base64.encodeBase64String(Util.getBytes(URLEncoder.encode(clientId, Util.UTF8_STRING_ENCODING) + ":" + URLEncoder.encode(clientSecret, Util.UTF8_STRING_ENCODING))); + } + + public Map getParameters() { + return EMPTY_MAP; + } + + public JSONObject getJSONParameters() throws JSONException { + return EMPTY_JSON_OBJECT; + } + + public abstract String getQueryString(); +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/BaseResponse.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/BaseResponse.java new file mode 100644 index 00000000..c32da21d --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/BaseResponse.java @@ -0,0 +1,104 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; + +/** + * @author Javier Rojas Blum + * @version December 26, 2016 + */ +public abstract class BaseResponse { + + protected int status; + protected String location; + protected String entity; + protected MultivaluedMap headers; + + public BaseResponse() { + } + + // TODO: remove + @Deprecated + public BaseResponse(int status) { + this.status = status; + } + + public BaseResponse(Response clientResponse) { + if (clientResponse != null) { + status = clientResponse.getStatus(); + if (clientResponse.getLocation() != null) { + location = clientResponse.getLocation().toString(); + } + entity = clientResponse.readEntity(String.class); + headers = clientResponse.getMetadata(); + } + } + + /** + * Returns the HTTP status code of the response. + * + * @return The HTTP status code. + */ + public int getStatus() { + return status; + } + + /** + * Returns the location of the response in the header. + * + * @return The location of the response. + */ + public String getLocation() { + return location; + } + + /** + * Sets the location of the response in the header. + * + * @param location The location of the response. + */ + public void setLocation(String location) { + this.location = location; + } + + /** + * Sets the HTTP status code of the response. + * + * @param status The HTTP status code. + */ + public void setStatus(int status) { + this.status = status; + } + + /** + * Returns the entity or body content of the response. + * + * @return The entity or body content of the response. + */ + public String getEntity() { + return entity; + } + + /** + * Sets the entity or body content of the response. + * + * @param entity The entity or body content of the response. + */ + public void setEntity(String entity) { + this.entity = entity; + } + + public MultivaluedMap getHeaders() { + return headers; + } + + public void setHeaders(MultivaluedMap headers) { + this.headers = headers; + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/BaseResponseWithErrors.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/BaseResponseWithErrors.java new file mode 100644 index 00000000..d0c2e306 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/BaseResponseWithErrors.java @@ -0,0 +1,89 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import javax.ws.rs.core.Response; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.error.IErrorType; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 09/10/2012 + */ + +public abstract class BaseResponseWithErrors extends BaseResponse { + +// private static final Logger LOG = Logger.getLogger(BaseResponseWithErrors.class); + + private T errorType; + private String errorDescription; + private String errorUri; + + public BaseResponseWithErrors() { + super(); + } + + public BaseResponseWithErrors(Response clientResponse) { + super(clientResponse); + final String entity = getEntity(); + if (StringUtils.isNotBlank(entity)) { + injectErrorIfExistSilently(entity); + } + } + + public String getErrorDescription() { + return errorDescription; + } + + public void setErrorDescription(String p_errorDescription) { + errorDescription = p_errorDescription; + } + + public T getErrorType() { + return errorType; + } + + public void setErrorType(T p_errorType) { + errorType = p_errorType; + } + + public String getErrorUri() { + return errorUri; + } + + public void setErrorUri(String p_errorUri) { + errorUri = p_errorUri; + } + + public abstract T fromString(String p_str); + + public void injectDataFromJson(String p_json) { + } + + public void injectErrorIfExistSilently(JSONObject jsonObj) throws JSONException { + if (jsonObj.has("error")) { + errorType = fromString(jsonObj.getString("error")); + } + if (jsonObj.has("error_description")) { + errorDescription = jsonObj.getString("error_description"); + } + if (jsonObj.has("error_uri")) { + errorUri = jsonObj.getString("error_uri"); + } + } + + public void injectErrorIfExistSilently(String p_entity) { + try { + injectErrorIfExistSilently(new JSONObject(p_entity)); + } catch (JSONException e) { + // ignore : it's ok to skip exception because entity string can be json array or just trash + } + } +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ClientAuthnEnabler.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ClientAuthnEnabler.java new file mode 100644 index 00000000..39b5cc88 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ClientAuthnEnabler.java @@ -0,0 +1,53 @@ +package org.gluu.oxauth.client; + +import javax.ws.rs.client.Invocation.Builder; +import javax.ws.rs.core.Form; + +import org.apache.log4j.Logger; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.token.ClientAssertionType; + + +/** + * @author Yuriy Zabrovarnyy + */ +public class ClientAuthnEnabler { + + private static final Logger LOG = Logger.getLogger(ClientAuthnEnabler.class); + + private Builder clientRequest; + private Form requestForm; + + public ClientAuthnEnabler(Builder clientRequest, Form requestForm) { + this.clientRequest = clientRequest; + this.requestForm = requestForm; + } + + public void exec(ClientAuthnRequest request){ + if (request.getAuthenticationMethod() == AuthenticationMethod.CLIENT_SECRET_BASIC + && request.hasCredentials()) { + clientRequest.header("Authorization", "Basic " + request.getEncodedCredentials()); + return; + } + + if (request.getAuthenticationMethod() == AuthenticationMethod.CLIENT_SECRET_POST) { + if (request.getAuthUsername() != null && !request.getAuthUsername().isEmpty()) { + requestForm.param("client_id", request.getAuthUsername()); + } + if (request.getAuthPassword() != null && !request.getAuthPassword().isEmpty()) { + requestForm.param("client_secret", request.getAuthPassword()); + } + return; + } + if (request.getAuthenticationMethod() == AuthenticationMethod.CLIENT_SECRET_JWT || + request.getAuthenticationMethod() == AuthenticationMethod.PRIVATE_KEY_JWT) { + requestForm.param("client_assertion_type", ClientAssertionType.JWT_BEARER.toString()); + if (request.getClientAssertion() != null) { + requestForm.param("client_assertion", request.getClientAssertion()); + } + if (request.getAuthUsername() != null && !request.getAuthUsername().isEmpty()) { + requestForm.param("client_id", request.getAuthUsername()); + } + } + } +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ClientAuthnRequest.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ClientAuthnRequest.java new file mode 100644 index 00000000..cdfd8eb1 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ClientAuthnRequest.java @@ -0,0 +1,130 @@ +package org.gluu.oxauth.client; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; +import java.util.UUID; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.crypto.AbstractCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtType; +import org.gluu.oxauth.model.token.ClientAssertionType; +import org.gluu.oxauth.model.util.QueryBuilder; + +/** + * @author Yuriy Zabrovarnyy + */ +public abstract class ClientAuthnRequest extends BaseRequest { + + private static final Logger LOG = Logger.getLogger(ClientAuthnRequest.class); + + private SignatureAlgorithm algorithm; + private String sharedKey; + private String audience; + private AbstractCryptoProvider cryptoProvider; + private String keyId; + + public ClientAuthnRequest() { + } + + public AbstractCryptoProvider getCryptoProvider() { + return cryptoProvider; + } + + public void setCryptoProvider(AbstractCryptoProvider cryptoProvider) { + this.cryptoProvider = cryptoProvider; + } + + public String getKeyId() { + return keyId; + } + + public void setKeyId(String keyId) { + this.keyId = keyId; + } + + public SignatureAlgorithm getAlgorithm() { + return algorithm; + } + + public void setAlgorithm(SignatureAlgorithm algorithm) { + this.algorithm = algorithm; + } + + public String getSharedKey() { + return sharedKey; + } + + public void setSharedKey(String sharedKey) { + this.sharedKey = sharedKey; + } + + public String getAudience() { + return audience; + } + + public void setAudience(String audience) { + this.audience = audience; + } + + public void appendClientAuthnToQuery(QueryBuilder builder) { + if (getAuthenticationMethod() == AuthenticationMethod.CLIENT_SECRET_POST) { + builder.append("client_id", getAuthUsername()); + builder.append("client_secret", getAuthPassword()); + } else if (getAuthenticationMethod() == AuthenticationMethod.CLIENT_SECRET_JWT || + getAuthenticationMethod() == AuthenticationMethod.PRIVATE_KEY_JWT) { + builder.append("client_assertion_type", ClientAssertionType.JWT_BEARER.toString()); + builder.append("client_assertion", getClientAssertion()); + } + } + + public String getClientAssertion() { + if (cryptoProvider == null) { + LOG.error("Crypto provider is not specified"); + return null; + } + + if (algorithm == null) { + algorithm = SignatureAlgorithm.HS256; + } + + GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + Date issuedAt = calendar.getTime(); + calendar.add(Calendar.MINUTE, 5); + Date expirationTime = calendar.getTime(); + + Jwt clientAssertion = new Jwt(); + // Header + clientAssertion.getHeader().setType(JwtType.JWT); + clientAssertion.getHeader().setAlgorithm(algorithm); + if (StringUtils.isNotBlank(keyId)) { + clientAssertion.getHeader().setKeyId(keyId); + } + + // Claims + clientAssertion.getClaims().setIssuer(getAuthUsername()); + clientAssertion.getClaims().setSubjectIdentifier(getAuthUsername()); + clientAssertion.getClaims().setAudience(audience); + clientAssertion.getClaims().setJwtId(UUID.randomUUID()); + clientAssertion.getClaims().setExpirationTime(expirationTime); + clientAssertion.getClaims().setIssuedAt(issuedAt); + + // Signature + try { + if (sharedKey == null) { + sharedKey = getAuthPassword(); + } + String signature = cryptoProvider.sign(clientAssertion.getSigningInput(), keyId, sharedKey, algorithm); + clientAssertion.setEncodedSignature(signature); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } + + return clientAssertion.toString(); + } +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ClientInfoClient.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ClientInfoClient.java new file mode 100644 index 00000000..348a9e2f --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ClientInfoClient.java @@ -0,0 +1,183 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation.Builder; +import javax.ws.rs.core.MediaType; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.gluu.oxauth.model.common.AuthorizationMethod; +import org.gluu.oxauth.model.userinfo.UserInfoErrorResponseType; +import org.jboss.resteasy.client.jaxrs.ClientHttpEngine; +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Encapsulates functionality to make client info request calls to an authorization server via REST Services. + * + * @author Javier Rojas Blum + * @version December 26, 2016 + */ +public class ClientInfoClient extends BaseClient { + + private static final Logger LOG = Logger.getLogger(ClientInfoClient.class); + + /** + * Constructs an Client Info client by providing a REST url where the service is located. + * + * @param url The REST Service location. + */ + public ClientInfoClient(String url) { + super(url); + } + + @Override + public String getHttpMethod() { + if (getRequest().getAuthorizationMethod() == null + || getRequest().getAuthorizationMethod() == AuthorizationMethod.AUTHORIZATION_REQUEST_HEADER_FIELD + || getRequest().getAuthorizationMethod() == AuthorizationMethod.FORM_ENCODED_BODY_PARAMETER) { + return HttpMethod.POST; + } else { // AuthorizationMethod.URL_QUERY_PARAMETER + return HttpMethod.GET; + } + } + + /** + * Executes the call to the REST Service and processes the response. + * + * @param accessToken The access token obtained from the oxAuth authorization request. + * @return The service response. + */ + public ClientInfoResponse execClientInfo(String accessToken) { + setRequest(new ClientInfoRequest(accessToken)); + + return exec(); + } + + public ClientInfoResponse exec() { + initClientRequest(); + return _exec(); + } + + + @Deprecated + public ClientInfoResponse exec(ClientHttpEngine engine) { + resteasyClient = ((ResteasyClientBuilder) ResteasyClientBuilder.newBuilder()).httpEngine(engine).build(); + webTarget = resteasyClient.target(getUrl()); + return _exec(); + } + + + /** + * Executes the call to the REST Service and processes the response. + * + * @return The service response. + */ + private ClientInfoResponse _exec() { + // Prepare request parameters + + Builder clientRequest = null; + if (getRequest().getAuthorizationMethod() == null + || getRequest().getAuthorizationMethod() == AuthorizationMethod.AUTHORIZATION_REQUEST_HEADER_FIELD) { + if (StringUtils.isNotBlank(getRequest().getAccessToken())) { + clientRequest = webTarget.request(); + clientRequest.header("Authorization", "Bearer " + getRequest().getAccessToken()); + } + } else if (getRequest().getAuthorizationMethod() == AuthorizationMethod.FORM_ENCODED_BODY_PARAMETER) { + if (StringUtils.isNotBlank(getRequest().getAccessToken())) { + requestForm.param("access_token", getRequest().getAccessToken()); + } + } else if (getRequest().getAuthorizationMethod() == AuthorizationMethod.URL_QUERY_PARAMETER) { + if (StringUtils.isNotBlank(getRequest().getAccessToken())) { + addReqParam("access_token", getRequest().getAccessToken()); + } + } + + if (clientRequest == null) { + clientRequest = webTarget.request(); + } + + clientRequest.header("Content-Type", MediaType.APPLICATION_FORM_URLENCODED); +// clientRequest.setHttpMethod(getHttpMethod()); + + // Call REST Service and handle response + try { + if (getRequest().getAuthorizationMethod() == null + || getRequest().getAuthorizationMethod() == AuthorizationMethod.AUTHORIZATION_REQUEST_HEADER_FIELD + || getRequest().getAuthorizationMethod() == AuthorizationMethod.FORM_ENCODED_BODY_PARAMETER) { + clientResponse = clientRequest.buildPost(Entity.form(requestForm)).invoke(); + } else { //AuthorizationMethod.URL_QUERY_PARAMETER + clientResponse = clientRequest.buildGet().invoke(); + } + + int status = clientResponse.getStatus(); + + setResponse(new ClientInfoResponse(status)); + + String entity = clientResponse.readEntity(String.class); + getResponse().setEntity(entity); + getResponse().setHeaders(clientResponse.getMetadata()); + if (StringUtils.isNotBlank(entity)) { + try { + JSONObject jsonObj = new JSONObject(entity); + + if (jsonObj.has("error")) { + getResponse().setErrorType(UserInfoErrorResponseType.fromString(jsonObj.getString("error"))); + jsonObj.remove("error"); + } + if (jsonObj.has("error_description")) { + getResponse().setErrorDescription(jsonObj.getString("error_description")); + jsonObj.remove("error_description"); + } + if (jsonObj.has("error_uri")) { + getResponse().setErrorUri(jsonObj.getString("error_uri")); + jsonObj.remove("error_uri"); + } + + for (Iterator iterator = jsonObj.keys(); iterator.hasNext(); ) { + String key = iterator.next(); + List values = new ArrayList(); + + JSONArray jsonArray = jsonObj.optJSONArray(key); + if (jsonArray != null) { + for (int i = 0; i < jsonArray.length(); i++) { + String value = jsonArray.optString(i); + if (value != null) { + values.add(value); + } + } + } else { + String value = jsonObj.optString(key); + if (value != null) { + values.add(value); + } + } + + getResponse().getClaims().put(key, values); + } + } catch (JSONException e) { + e.printStackTrace(); + } + } + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } finally { + closeConnection(); + } + + return getResponse(); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ClientInfoRequest.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ClientInfoRequest.java new file mode 100644 index 00000000..58c35cbf --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ClientInfoRequest.java @@ -0,0 +1,89 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import java.util.HashMap; +import java.util.Map; + +import org.gluu.oxauth.model.common.AuthorizationMethod; + +/** + * Represents a Client Info request to send to the authorization server. + * + * @author Javier Rojas Blum Date: 07.19.2012 + */ +public class ClientInfoRequest extends BaseRequest { + + private String accessToken; + + /** + * Constructs a Client Info Request. + * + * @param accessToken The access token obtained from the oxAuth authorization request. + */ + public ClientInfoRequest(String accessToken) { + this.accessToken = accessToken; + setAuthorizationMethod(AuthorizationMethod.AUTHORIZATION_REQUEST_HEADER_FIELD); + } + + /** + * Returns the access token obtained from oxAuth authorization request. + * + * @return The access token obtained from oxAuth authorization request. + */ + public String getAccessToken() { + return accessToken; + } + + /** + * Sets the access token obtained from oxAuth authorization request. + * + * @param accessToken The access token obtained from oxAuth authorization request. + */ + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + /** + * Returns a query string with the parameters of the Client Info request. + * Any null or empty parameter will be omitted. + * + * @return A query string of parameters. + */ + @Override + public String getQueryString() { + StringBuilder queryStringBuilder = new StringBuilder(); + + if (accessToken != null && !accessToken.isEmpty()) { + if (getAuthorizationMethod() == AuthorizationMethod.FORM_ENCODED_BODY_PARAMETER + || getAuthorizationMethod() == AuthorizationMethod.URL_QUERY_PARAMETER) { + queryStringBuilder.append("access_token=").append(accessToken); + } + } + + return queryStringBuilder.toString(); + } + + /** + * Returns a collection of parameters of the client info request. Any + * null or empty parameter will be omitted. + * + * @return A collection of parameters. + */ + public Map getParameters() { + Map parameters = new HashMap(); + + if (accessToken != null && !accessToken.isEmpty()) { + if (getAuthorizationMethod() == AuthorizationMethod.FORM_ENCODED_BODY_PARAMETER + || getAuthorizationMethod() == AuthorizationMethod.URL_QUERY_PARAMETER) { + parameters.put("access_token", accessToken); + } + } + + return parameters; + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ClientInfoResponse.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ClientInfoResponse.java new file mode 100644 index 00000000..d170e382 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ClientInfoResponse.java @@ -0,0 +1,116 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.gluu.oxauth.model.userinfo.UserInfoErrorResponseType; + +/** + * Represents an client info response received from the authorization server. + * + * @author Javier Rojas Blum Date: 07.19.2012 + */ +public class ClientInfoResponse extends BaseResponse { + + private Map> claims; + + private UserInfoErrorResponseType errorType; + private String errorDescription; + private String errorUri; + + /** + * Constructs a Client Info response. + * + * @param status The response status code. + */ + public ClientInfoResponse(int status) { + super(status); + claims = new HashMap>(); + } + + public Map> getClaims() { + return claims; + } + + public void setClaims(Map> claims) { + this.claims = claims; + } + + /** + * Returns the error code when the request fails, otherwise will return null. + * + * @return The error code when the request fails. + */ + public UserInfoErrorResponseType getErrorType() { + return errorType; + } + + /** + * Sets the error code when the request fails, otherwise will return + * null. + * + * @param errorType The error code when the request fails. + */ + public void setErrorType(UserInfoErrorResponseType errorType) { + this.errorType = errorType; + } + + /** + * Returns a human-readable UTF-8 encoded text providing additional + * information, used to assist the client developer in understanding the + * error that occurred. + * + * @return The error description. + */ + public String getErrorDescription() { + return errorDescription; + } + + /** + * Sets a human-readable UTF-8 encoded text providing additional + * information, used to assist the client developer in understanding the + * error that occurred. + * + * @param errorDescription The error description. + */ + public void setErrorDescription(String errorDescription) { + this.errorDescription = errorDescription; + } + + /** + * Returns a URI identifying a human-readable web page with information + * about the error, used to provide the client developer with additional + * information about the error. + * + * @return A URI with information about the error. + */ + public String getErrorUri() { + return errorUri; + } + + /** + * Sets a URI identifying a human-readable web page with information about + * the error, used to provide the client developer with additional + * information about the error. + * + * @param errorUri A URI with information about the error. + */ + public void setErrorUri(String errorUri) { + this.errorUri = errorUri; + } + + public List getClaim(String claimName) { + if (claims.containsKey(claimName)) { + return claims.get(claimName); + } + + return null; + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ClientUtils.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ClientUtils.java new file mode 100644 index 00000000..1cecc5fc --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ClientUtils.java @@ -0,0 +1,57 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import org.apache.http.client.CookieStore; + +/** + * @author Yuriy Zabrovarnyy + * @author Javier Rojas Blum + * @version December 26, 2016 + */ + +public class ClientUtils { + private ClientUtils() { + } + + public static void showHeader(String header) { + System.out.println("-------------------------------------------------------"); + System.out.println(header); + System.out.println("-------------------------------------------------------"); + } + + public static void showClient(BaseClient client) { + showHeader("REQUEST:"); + System.out.println(client.getRequestAsString()); + System.out.println(); + + showHeader("RESPONSE:"); + System.out.println(client.getResponseAsString()); + System.out.println(); + } + + public static void showClientUserAgent(BaseClient client) { + showHeader("REQUEST:"); + System.out.println(client.getUrl() + "?" + client.getRequest().getQueryString()); + System.out.println(); + + if (client.getResponse() != null) { + showHeader("RESPONSE:"); + System.out.println("HTTP/1.1 302 Found"); + System.out.println("Location: " + client.getResponse().getLocation()); + System.out.println(); + } + } + + public static void showClient(BaseClient client, CookieStore cookieStore) { + showClient(client); + + showHeader("COOKIES:"); + System.out.println(cookieStore.getCookies()); + System.out.println(); + } +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/DeviceAuthzClient.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/DeviceAuthzClient.java new file mode 100644 index 00000000..42d50ab4 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/DeviceAuthzClient.java @@ -0,0 +1,116 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import static org.gluu.oxauth.model.authorize.DeviceAuthorizationRequestParam.CLIENT_ID; +import static org.gluu.oxauth.model.authorize.DeviceAuthorizationRequestParam.SCOPE; +import static org.gluu.oxauth.model.authorize.DeviceAuthorizationResponseParam.DEVICE_CODE; +import static org.gluu.oxauth.model.authorize.DeviceAuthorizationResponseParam.EXPIRES_IN; +import static org.gluu.oxauth.model.authorize.DeviceAuthorizationResponseParam.INTERVAL; +import static org.gluu.oxauth.model.authorize.DeviceAuthorizationResponseParam.USER_CODE; +import static org.gluu.oxauth.model.authorize.DeviceAuthorizationResponseParam.VERIFICATION_URI; +import static org.gluu.oxauth.model.authorize.DeviceAuthorizationResponseParam.VERIFICATION_URI_COMPLETE; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation.Builder; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.gluu.oxauth.model.util.Util; +import org.jboss.resteasy.client.jaxrs.ClientHttpEngine; +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; +import org.json.JSONObject; + +/** + * Encapsulates functionality to make Device Authz request calls to an authorization server via REST Services. + */ +public class DeviceAuthzClient extends BaseClient { + + private static final Logger LOG = Logger.getLogger(DeviceAuthzClient.class); + + /** + * Construct a device authz client by providing an URL where the REST service is located. + * + * @param url The REST service location. + */ + public DeviceAuthzClient(String url) { + super(url); + } + + @Override + public String getHttpMethod() { + return HttpMethod.POST; + } + + public DeviceAuthzResponse exec() { + initClientRequest(); + return _exec(); + } + + @Deprecated + public DeviceAuthzResponse exec(ClientHttpEngine engine) { + resteasyClient = ((ResteasyClientBuilder) ResteasyClientBuilder.newBuilder()).httpEngine(engine).build(); + webTarget = resteasyClient.target(getUrl()); + + return _exec(); + } + + private DeviceAuthzResponse _exec() { + try { + // clientRequest.setHttpMethod(getHttpMethod()); + Builder clientRequest = webTarget.request(); + applyCookies(clientRequest); + + clientRequest.header("Content-Type", request.getContentType()); + new ClientAuthnEnabler(clientRequest, requestForm).exec(getRequest()); + + final String scopesAsString = Util.listAsString(getRequest().getScopes()); + + if (StringUtils.isNotBlank(scopesAsString)) { + requestForm.param(SCOPE, scopesAsString); + } + if (StringUtils.isNotBlank(getRequest().getClientId())) { + requestForm.param(CLIENT_ID, getRequest().getClientId()); + } + + // Call REST Service and handle response + clientResponse = clientRequest.buildPost(Entity.form(requestForm)).invoke(); + + setResponse(new DeviceAuthzResponse(clientResponse)); + if (StringUtils.isNotBlank(response.getEntity())) { + JSONObject jsonObj = new JSONObject(response.getEntity()); + + if (jsonObj.has(USER_CODE)) { + getResponse().setUserCode(jsonObj.getString(USER_CODE)); + } + if (jsonObj.has(DEVICE_CODE)) { + getResponse().setDeviceCode(jsonObj.getString(DEVICE_CODE)); + } + if (jsonObj.has(INTERVAL)) { + getResponse().setInterval(jsonObj.getInt(INTERVAL)); + } + if (jsonObj.has(VERIFICATION_URI)) { + getResponse().setVerificationUri(jsonObj.getString(VERIFICATION_URI)); + } + if (jsonObj.has(VERIFICATION_URI_COMPLETE)) { + getResponse().setVerificationUriComplete(jsonObj.getString(VERIFICATION_URI_COMPLETE)); + } + if (jsonObj.has(EXPIRES_IN)) { + getResponse().setExpiresIn(jsonObj.getInt(EXPIRES_IN)); + } + } + + return getResponse(); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + return null; + } finally { + closeConnection(); + } + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/DeviceAuthzRequest.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/DeviceAuthzRequest.java new file mode 100644 index 00000000..b85839ab --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/DeviceAuthzRequest.java @@ -0,0 +1,116 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.ws.rs.core.MediaType; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.gluu.oxauth.model.authorize.AuthorizeRequestParam; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.util.Util; +import org.json.JSONException; + +/** + * Represents a device authorization request to send to the authorization server. + */ +public class DeviceAuthzRequest extends ClientAuthnRequest { + + private static final Logger LOG = Logger.getLogger(DeviceAuthzRequest.class); + + private String clientId; + private List scopes; + + public DeviceAuthzRequest(String clientId, List scopes) { + setContentType(MediaType.APPLICATION_FORM_URLENCODED); + setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + this.clientId = clientId; + this.scopes = scopes; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public List getScopes() { + return scopes; + } + + public void setScopes(List scopes) { + this.scopes = scopes; + } + + public String getScopesAsString() { + return Util.listAsString(scopes); + } + + /** + * Returns a collection of parameters of the authorization request. Any + * null or empty parameter will be omitted. + * + * @return A collection of parameters. + */ + public Map getParameters() { + Map parameters = new HashMap(); + + try { + // OAuth 2.0 request parameters + final String scopesAsString = getScopesAsString(); + + if (StringUtils.isNotBlank(clientId)) { + parameters.put(AuthorizeRequestParam.CLIENT_ID, clientId); + } + if (StringUtils.isNotBlank(scopesAsString)) { + parameters.put(AuthorizeRequestParam.SCOPE, scopesAsString); + } + + for (String key : getCustomParameters().keySet()) { + parameters.put(key, getCustomParameters().get(key)); + } + } catch (JSONException e) { + LOG.error(e.getMessage(), e); + } + + return parameters; + } + + @Override + public String getQueryString() { + StringBuilder queryStringBuilder = new StringBuilder(); + try { + final String scopesAsString = getScopesAsString(); + + if (StringUtils.isNotBlank(clientId)) { + queryStringBuilder.append("&").append(AuthorizeRequestParam.CLIENT_ID) + .append("=").append(URLEncoder.encode(clientId, Util.UTF8_STRING_ENCODING)); + } + if (StringUtils.isNotBlank(scopesAsString)) { + queryStringBuilder.append("&").append(AuthorizeRequestParam.SCOPE) + .append("=").append(URLEncoder.encode(scopesAsString, Util.UTF8_STRING_ENCODING)); + } + + for (String key : getCustomParameters().keySet()) { + queryStringBuilder.append("&"); + queryStringBuilder.append(key).append("=").append(getCustomParameters().get(key)); + } + } catch (UnsupportedEncodingException | JSONException e) { + LOG.error(e.getMessage(), e); + } + + return queryStringBuilder.toString(); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/DeviceAuthzResponse.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/DeviceAuthzResponse.java new file mode 100644 index 00000000..cce9f0e0 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/DeviceAuthzResponse.java @@ -0,0 +1,122 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import javax.ws.rs.core.Response; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.gluu.oxauth.model.authorize.DeviceAuthorizationResponseParam; +import org.gluu.oxauth.model.authorize.DeviceAuthzErrorResponseType; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Represents a device authz response received from the authorization server. + */ +public class DeviceAuthzResponse extends BaseResponseWithErrors { + + private static final Logger LOG = Logger.getLogger(DeviceAuthzResponse.class); + + private String userCode; + private String deviceCode; + private Integer interval; + private String verificationUri; + private String verificationUriComplete; + private Integer expiresIn; + + public DeviceAuthzResponse(Response clientResponse) { + super(clientResponse); + } + + @Override + public DeviceAuthzErrorResponseType fromString(String p_string) { + return DeviceAuthzErrorResponseType.fromString(p_string); + } + + public void injectDataFromJson(String json) { + if (StringUtils.isNotBlank(json)) { + try { + JSONObject jsonObj = new JSONObject(json); + if (jsonObj.has(DeviceAuthorizationResponseParam.USER_CODE)) { + setUserCode(jsonObj.getString(DeviceAuthorizationResponseParam.USER_CODE)); + jsonObj.remove(DeviceAuthorizationResponseParam.USER_CODE); + } + if (jsonObj.has(DeviceAuthorizationResponseParam.DEVICE_CODE)) { + setDeviceCode(jsonObj.getString(DeviceAuthorizationResponseParam.DEVICE_CODE)); + jsonObj.remove(DeviceAuthorizationResponseParam.DEVICE_CODE); + } + if (jsonObj.has(DeviceAuthorizationResponseParam.INTERVAL)) { + setInterval(jsonObj.getInt(DeviceAuthorizationResponseParam.INTERVAL)); + jsonObj.remove(DeviceAuthorizationResponseParam.INTERVAL); + } + if (jsonObj.has(DeviceAuthorizationResponseParam.VERIFICATION_URI)) { + setVerificationUri(jsonObj.getString(DeviceAuthorizationResponseParam.VERIFICATION_URI)); + jsonObj.remove(DeviceAuthorizationResponseParam.VERIFICATION_URI); + } + if (jsonObj.has(DeviceAuthorizationResponseParam.VERIFICATION_URI_COMPLETE)) { + setVerificationUriComplete(jsonObj.getString(DeviceAuthorizationResponseParam.VERIFICATION_URI_COMPLETE)); + jsonObj.remove(DeviceAuthorizationResponseParam.VERIFICATION_URI_COMPLETE); + } + if (jsonObj.has(DeviceAuthorizationResponseParam.EXPIRES_IN)) { + setExpiresIn(jsonObj.getInt(DeviceAuthorizationResponseParam.EXPIRES_IN)); + jsonObj.remove(DeviceAuthorizationResponseParam.EXPIRES_IN); + } + } catch (JSONException e) { + LOG.error(e.getMessage(), e); + } + } + } + + public String getUserCode() { + return userCode; + } + + public void setUserCode(String userCode) { + this.userCode = userCode; + } + + public String getDeviceCode() { + return deviceCode; + } + + public void setDeviceCode(String deviceCode) { + this.deviceCode = deviceCode; + } + + public Integer getInterval() { + return interval; + } + + public void setInterval(Integer interval) { + this.interval = interval; + } + + public String getVerificationUri() { + return verificationUri; + } + + public void setVerificationUri(String verificationUri) { + this.verificationUri = verificationUri; + } + + public String getVerificationUriComplete() { + return verificationUriComplete; + } + + public void setVerificationUriComplete(String verificationUriComplete) { + this.verificationUriComplete = verificationUriComplete; + } + + public Integer getExpiresIn() { + return expiresIn; + } + + public void setExpiresIn(Integer expiresIn) { + this.expiresIn = expiresIn; + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/EndSessionClient.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/EndSessionClient.java new file mode 100644 index 00000000..47228547 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/EndSessionClient.java @@ -0,0 +1,139 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import java.util.Map; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.client.Invocation.Builder; +import javax.ws.rs.core.MediaType; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.session.EndSessionErrorResponseType; +import org.gluu.oxauth.model.session.EndSessionRequestParam; +import org.gluu.oxauth.model.session.EndSessionResponseParam; +import org.gluu.oxauth.model.util.Util; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Encapsulates functionality to make end session request calls to an + * authorization server via REST Services. + * + * @author Javier Rojas Blum + * @version August 9, 2017 + */ +public class EndSessionClient extends BaseClient { + + private static final String mediaType = MediaType.TEXT_PLAIN; + + /** + * Constructs an end session client by providing an URL where the REST service is located. + * + * @param url The REST service location. + */ + public EndSessionClient(String url) { + super(url); + } + + @Override + public String getHttpMethod() { + return HttpMethod.GET; + } + + /** + * Executes the call to the REST Service requesting to end session and processes the response. + * + * @param idTokenHint The issued ID Token. + * @param postLogoutRedirectUri The URL to which the RP is requesting that the End-User's User-Agent be redirected + * after a logout has been performed. + * @param state The state. + * @return The service response. + */ + public EndSessionResponse execEndSession(String idTokenHint, String postLogoutRedirectUri, String state) { + setRequest(new EndSessionRequest(idTokenHint, postLogoutRedirectUri, state)); + + return exec(); + } + + /** + * Executes the call to the REST Service and processes the response. + * + * @return The service response. + */ + public EndSessionResponse exec() { + // Prepare request parameters + initClientRequest(); + + if (StringUtils.isNotBlank(getRequest().getIdTokenHint())) { + addReqParam(EndSessionRequestParam.ID_TOKEN_HINT, getRequest().getIdTokenHint()); + } + if (StringUtils.isNotBlank(getRequest().getPostLogoutRedirectUri())) { + addReqParam(EndSessionRequestParam.POST_LOGOUT_REDIRECT_URI, getRequest().getPostLogoutRedirectUri()); + } + if (StringUtils.isNotBlank(getRequest().getState())) { + addReqParam(EndSessionRequestParam.STATE, getRequest().getState()); + } + if (StringUtils.isNotBlank(getRequest().getSid())) { + addReqParam(EndSessionRequestParam.SID, getRequest().getSid()); + } + + // Call REST Service and handle response + try { + Builder clientRequest = webTarget.request(); + applyCookies(clientRequest); + + clientRequest.accept(mediaType); +// clientRequest.setHttpMethod(getHttpMethod()); + + clientResponse = clientRequest.buildGet().invoke(); + int status = clientResponse.getStatus(); + + setResponse(new EndSessionResponse(status)); + String entity = clientResponse.readEntity(String.class); + getResponse().setEntity(entity); + getResponse().setHeaders(clientResponse.getMetadata()); + if (clientResponse.getLocation() != null) { + String location = clientResponse.getLocation().toString(); + getResponse().setLocation(location); + + int queryStringIndex = location.indexOf("?"); + if (queryStringIndex != -1) { + String queryString = location + .substring(queryStringIndex + 1); + Map params = QueryStringDecoder.decode(queryString); + if (params.containsKey(EndSessionResponseParam.STATE)) { + getResponse().setState(params.get(EndSessionResponseParam.STATE)); + } + } + } + + if (!Util.isNullOrEmpty(entity) && !entity.contains("")) { + try { + JSONObject jsonObj = new JSONObject(entity); + if (jsonObj.has("error")) { + getResponse().setErrorType(EndSessionErrorResponseType.fromString(jsonObj.getString("error"))); + } + if (jsonObj.has("error_description")) { + getResponse().setErrorDescription(jsonObj.getString("error_description")); + } + if (jsonObj.has("error_uri")) { + getResponse().setErrorUri(jsonObj.getString("error_uri")); + } + } catch (JSONException e) { + e.printStackTrace(); + } + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + closeConnection(); + } + + return getResponse(); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/EndSessionRequest.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/EndSessionRequest.java new file mode 100644 index 00000000..6eed5d3d --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/EndSessionRequest.java @@ -0,0 +1,149 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.session.EndSessionRequestParam; +import org.gluu.oxauth.model.util.Util; + +/** + * Represents an end session request to send to the authorization server. + * + * @author Javier Rojas Blum + * @version August 9, 2017 + */ +public class EndSessionRequest extends BaseRequest { + + private String idTokenHint; + private String postLogoutRedirectUri; + private String sid; + private String state; + + /** + * Constructs an end session request. + */ + public EndSessionRequest(String idTokenHint, String postLogoutRedirectUri, String state) { + this.idTokenHint = idTokenHint; + this.postLogoutRedirectUri = postLogoutRedirectUri; + this.state = state; + } + + /** + * Returns the issued ID Token. + * + * @return The issued ID Token. + */ + public String getIdTokenHint() { + return idTokenHint; + } + + /** + * Sets the issued ID Token. + * + * @param idTokenHint The issued ID Token. + */ + public void setAccessToken(String idTokenHint) { + this.idTokenHint = idTokenHint; + } + + /** + * Returns the URL to which the RP is requesting that the End-User's User-Agent be redirected after a logout + * has been performed. + * + * @return The post logout redirection URI. + */ + public String getPostLogoutRedirectUri() { + return postLogoutRedirectUri; + } + + /** + * Sets the URL to which the RP is requesting that the End-User's User-Agent be redirected after a logout + * has been performed. + * + * @param postLogoutRedirectUri The post logout redirection URI. + */ + public void setPostLogoutRedirectUri(String postLogoutRedirectUri) { + this.postLogoutRedirectUri = postLogoutRedirectUri; + } + + public String getSid() { + return sid; + } + + public void setSid(String sid) { + this.sid = sid; + } + + /** + * Returns the state. The state is an opaque value used by the RP to maintain state between the logout request and + * the callback to the endpoint specified by the post_logout_redirect_uri parameter. If included in the logout + * request, the OP passes this value back to the RP using the state query parameter when redirecting the User Agent + * back to the RP. + * + * @return The state. + */ + public String getState() { + return state; + } + + /** + * Sets the state. The state is an opaque value used by the RP to maintain state between the logout request and the + * callback to the endpoint specified by the post_logout_redirect_uri parameter. If included in the logout request, + * the OP passes this value back to the RP using the state query parameter when redirecting the User Agent back to + * the RP. + * + * @param state he state. + */ + public void setState(String state) { + this.state = state; + } + + /** + * Returns a query string with the parameters of the end session request. + * Any null or empty parameter will be omitted. + * + * @return A query string of parameters. + */ + @Override + public String getQueryString() { + StringBuilder queryStringBuilder = new StringBuilder(); + + try { + if (StringUtils.isNotBlank(idTokenHint)) { + queryStringBuilder.append(EndSessionRequestParam.ID_TOKEN_HINT) + .append("=") + .append(idTokenHint); + } + if (StringUtils.isNotBlank(postLogoutRedirectUri)) { + queryStringBuilder.append("&") + .append(EndSessionRequestParam.POST_LOGOUT_REDIRECT_URI) + .append("=") + .append(URLEncoder.encode(postLogoutRedirectUri, Util.UTF8_STRING_ENCODING)); + } + if (StringUtils.isNotBlank(state)) { + queryStringBuilder.append("&") + .append(EndSessionRequestParam.STATE) + .append("=") + .append(URLEncoder.encode(state, Util.UTF8_STRING_ENCODING)); + } + + if (StringUtils.isNotBlank(sid)) { + queryStringBuilder.append("&") + .append(EndSessionRequestParam.SID) + .append("=") + .append(URLEncoder.encode(sid, Util.UTF8_STRING_ENCODING)); + } + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + + return queryStringBuilder.toString(); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/EndSessionResponse.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/EndSessionResponse.java new file mode 100644 index 00000000..1a2dad37 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/EndSessionResponse.java @@ -0,0 +1,147 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import org.gluu.oxauth.model.session.EndSessionErrorResponseType; + +/** + * Represents an end session response received from the authorization server. + * + * @author Javier Rojas Blum + * @version December 20, 2015 + */ +public class EndSessionResponse extends BaseResponse { + + private String location; + private String state; + + private EndSessionErrorResponseType errorType; + private String errorDescription; + private String errorUri; + + /** + * Constructs an end session response. + * + * @param status The response status code. + */ + public EndSessionResponse(int status) { + super(status); + } + + /** + * Returns the location of the response in the header. + * + * @return The location of the response. + */ + public String getLocation() { + return location; + } + + /** + * Sets the location of the response in the header. + * + * @param location The location of the response. + */ + public void setLocation(String location) { + this.location = location; + } + + /** + * Html page of http based logout + * + * @return html + */ + public String getHtmlPage() { + return entity; + } + + /** + * Returns the state. The state is an opaque value used by the RP to maintain state between the logout request and + * the callback to the endpoint specified by the post_logout_redirect_uri parameter. If included in the logout + * request, the OP passes this value back to the RP using the state query parameter when redirecting the User Agent + * back to the RP. + * + * @return The state. + */ + public String getState() { + return state; + } + + /** + * Sets the state. The state is an opaque value used by the RP to maintain state between the logout request and the + * callback to the endpoint specified by the post_logout_redirect_uri parameter. If included in the logout request, + * the OP passes this value back to the RP using the state query parameter when redirecting the User Agent back to + * the RP. + * + * @param state he state. + */ + public void setState(String state) { + this.state = state; + } + + /** + * Returns the error code when the request fails, otherwise will return null. + * + * @return The error code when the request fails. + */ + public EndSessionErrorResponseType getErrorType() { + return errorType; + } + + /** + * Sets the error code when the request fails, otherwise will return null. + * + * @param errorType The error code when the request fails. + */ + public void setErrorType(EndSessionErrorResponseType errorType) { + this.errorType = errorType; + } + + /** + * Returns a human-readable UTF-8 encoded text providing additional + * information, used to assist the client developer in understanding the + * error that occurred. + * + * @return The error description. + */ + public String getErrorDescription() { + return errorDescription; + } + + /** + * Sets a human-readable UTF-8 encoded text providing additional + * information, used to assist the client developer in understanding the + * error that occurred. + * + * @param errorDescription The error description. + */ + public void setErrorDescription(String errorDescription) { + this.errorDescription = errorDescription; + } + + /** + * Returns a URI identifying a human-readable web page with information + * about the error, used to provide the client developer with additional + * information about the error. + * + * @return A URI with information about the error. + */ + public String getErrorUri() { + return errorUri; + } + + /** + * Sets a URI identifying a human-readable web page with information about + * the error, used to provide the client developer with additional + * information about the error. + * + * @param errorUri A URI with information about the error. + */ + public void setErrorUri(String errorUri) { + this.errorUri = errorUri; + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/GluuConfigurationClient.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/GluuConfigurationClient.java new file mode 100644 index 00000000..18f76183 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/GluuConfigurationClient.java @@ -0,0 +1,128 @@ +package org.gluu.oxauth.client; + +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.AUTH_LEVEL_MAPPING; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.ID_GENERATION_ENDPOINT; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.INTROSPECTION_ENDPOINT; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.SCOPE_TO_CLAIMS_MAPPING; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.client.Invocation.Builder; +import javax.ws.rs.core.MediaType; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.json.JSONArray; +import org.json.JSONObject; + +/** + * Created by eugeniuparvan on 8/12/16. + * + * @version December 26, 2016 + */ +public class GluuConfigurationClient extends BaseClient { + + private static final Logger LOG = Logger.getLogger(GluuConfigurationClient.class); + + public GluuConfigurationClient(String url) { + super(url); + } + + @Override + public String getHttpMethod() { + return HttpMethod.GET; + } + + public GluuConfigurationResponse execGluuConfiguration() { + initClientRequest(); + + setRequest(new GluuConfigurationRequest()); + + Builder clientRequest = webTarget.request(); + applyCookies(clientRequest); + + // Prepare request parameters + clientRequest.header("Content-Type", MediaType.APPLICATION_JSON); +// clientRequest.setHttpMethod(getHttpMethod()); + + // Call REST Service and handle response + try { + clientResponse = clientRequest.buildGet().invoke(); + + setResponse(new GluuConfigurationResponse()); + + String entity = clientResponse.readEntity(String.class); + getResponse().setEntity(entity); + getResponse().setHeaders(clientResponse.getMetadata()); + getResponse().setStatus(clientResponse.getStatus()); + + if (StringUtils.isNotBlank(entity)) { + JSONObject jsonObj = new JSONObject(entity); + + if (jsonObj.has(ID_GENERATION_ENDPOINT)) { + getResponse().setIdGenerationEndpoint(jsonObj.getString(ID_GENERATION_ENDPOINT)); + } + if (jsonObj.has(INTROSPECTION_ENDPOINT)) { + getResponse().setIntrospectionEndpoint(jsonObj.getString(INTROSPECTION_ENDPOINT)); + } + if (jsonObj.has(AUTH_LEVEL_MAPPING)) { + getResponse().setAuthLevelMapping(mapJsonToAuthLevelMapping(jsonObj.getJSONObject(AUTH_LEVEL_MAPPING))); + } + if (jsonObj.has(SCOPE_TO_CLAIMS_MAPPING)) { + getResponse().setScopeToClaimsMapping(mapJsonToScopeToClaimsMapping(jsonObj.getJSONObject(SCOPE_TO_CLAIMS_MAPPING))); + } + } + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } finally { + closeConnection(); + } + + return getResponse(); + } + + private Map> mapJsonToAuthLevelMapping(JSONObject jsonObject) { + Map> authLevelMapping = new HashMap>(); + + Iterator keys = jsonObject.keys(); + while (keys.hasNext()) { + try { + String key = (String) keys.next(); + Integer level = new Integer(key); + + authLevelMapping.put(level, new HashSet()); + + JSONArray jsonArray = jsonObject.getJSONArray(key); + for (int i = 0; i < jsonArray.length(); i++) + authLevelMapping.get(level).add(jsonArray.getString(i)); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } + } + return authLevelMapping; + } + + private Map> mapJsonToScopeToClaimsMapping(JSONObject jsonObject) { + Map> scopeToClaimsMapping = new HashMap>(); + Iterator keys = jsonObject.keys(); + while (keys.hasNext()) { + try { + String scope = (String) keys.next(); + + scopeToClaimsMapping.put(scope, new HashSet()); + + JSONArray jsonArray = jsonObject.getJSONArray(scope); + for (int i = 0; i < jsonArray.length(); i++) + scopeToClaimsMapping.get(scope).add(jsonArray.getString(i)); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } + } + return scopeToClaimsMapping; + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/GluuConfigurationRequest.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/GluuConfigurationRequest.java new file mode 100644 index 00000000..efdb852e --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/GluuConfigurationRequest.java @@ -0,0 +1,12 @@ +package org.gluu.oxauth.client; + +/** + * Created by eugeniuparvan on 8/12/16. + */ +public class GluuConfigurationRequest extends BaseRequest { + + @Override + public String getQueryString() { + return null; + } +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/GluuConfigurationResponse.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/GluuConfigurationResponse.java new file mode 100644 index 00000000..7b5e41eb --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/GluuConfigurationResponse.java @@ -0,0 +1,61 @@ +package org.gluu.oxauth.client; + +import java.util.Map; +import java.util.Set; + +/** + * Created by eugeniuparvan on 8/12/16. + */ +public class GluuConfigurationResponse extends BaseResponse { + + private String idGenerationEndpoint; + + private String introspectionEndpoint; + + private Map> authLevelMapping; + + private Map> scopeToClaimsMapping; + + + public String getIdGenerationEndpoint() { + return idGenerationEndpoint; + } + + public void setIdGenerationEndpoint(String idGenerationEndpoint) { + this.idGenerationEndpoint = idGenerationEndpoint; + } + + public String getIntrospectionEndpoint() { + return introspectionEndpoint; + } + + public void setIntrospectionEndpoint(String introspectionEndpoint) { + this.introspectionEndpoint = introspectionEndpoint; + } + + public Map> getAuthLevelMapping() { + return authLevelMapping; + } + + public void setAuthLevelMapping(Map> authLevelMapping) { + this.authLevelMapping = authLevelMapping; + } + + public Map> getScopeToClaimsMapping() { + return scopeToClaimsMapping; + } + + public void setScopeToClaimsMapping(Map> scopeToClaimsMapping) { + this.scopeToClaimsMapping = scopeToClaimsMapping; + } + + @Override + public String toString() { + return "GluuConfigurationResponse{" + + "idGenerationEndpoint='" + idGenerationEndpoint + '\'' + + ", introspectionEndpoint='" + introspectionEndpoint + '\'' + + ", authLevelMapping=" + authLevelMapping + + ", scopeToClaimsMapping=" + scopeToClaimsMapping + + '}'; + } +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/JwkClient.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/JwkClient.java new file mode 100644 index 00000000..731d058f --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/JwkClient.java @@ -0,0 +1,140 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import static org.gluu.oxauth.model.jwk.JWKParameter.JSON_WEB_KEY_SET; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.client.Invocation.Builder; +import javax.ws.rs.core.MediaType; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.crypto.PublicKey; +import org.gluu.oxauth.model.crypto.signature.ECDSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.jwk.JSONWebKeySet; +import org.jboss.resteasy.client.jaxrs.ClientHttpEngine; +import org.json.JSONObject; + +/** + * Encapsulates functionality to make JWK request calls to an authorization + * server via REST Services. + * + * @author Javier Rojas Blum + * @version December 26, 2016 + */ +public class JwkClient extends BaseClient { + + private static final String mediaType = MediaType.APPLICATION_JSON; + + /** + * Constructs a JSON Web Key (JWK) client by providing a REST url where the + * validate token service is located. + * + * @param url The REST Service location. + */ + public JwkClient(String url) { + super(url); + } + + @Override + public String getHttpMethod() { + return HttpMethod.GET; + } + + /** + * Executes the call to the REST Service requesting the JWK and processes + * the response. + * + * @return The service response. + */ + public JwkResponse exec() { + if (getRequest() == null) { + setRequest(new JwkRequest()); + } + + // Prepare request parameters + initClientRequest(); + + Builder clientRequest = webTarget.request(); + applyCookies(clientRequest); + + if (getRequest().hasCredentials()) { + String encodedCredentials = getRequest().getEncodedCredentials(); + clientRequest.header("Authorization", "Basic " + encodedCredentials); + } + clientRequest.accept(mediaType); +// clientRequest.setHttpMethod(getHttpMethod()); + + // Call REST Service and handle response + try { + clientResponse = clientRequest.buildGet().invoke(); + int status = clientResponse.getStatus(); + + setResponse(new JwkResponse(status)); + getResponse().setHeaders(clientResponse.getMetadata()); + + String entity = clientResponse.readEntity(String.class); + getResponse().setEntity(entity); + if (StringUtils.isNotBlank(entity)) { + JSONObject jsonObj = new JSONObject(entity); + if (jsonObj.has(JSON_WEB_KEY_SET)) { + JSONWebKeySet jwks = JSONWebKeySet.fromJSONObject(jsonObj); + getResponse().setJwks(jwks); + } + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + closeConnection(); + } + + return getResponse(); + } + + public static RSAPublicKey getRSAPublicKey(String jwkSetUri, String keyId) { + return getRSAPublicKey(jwkSetUri, keyId, null); + } + + public static RSAPublicKey getRSAPublicKey(String jwkSetUri, String keyId, ClientHttpEngine engine) { + RSAPublicKey publicKey = null; + + JwkClient jwkClient = new JwkClient(jwkSetUri); + jwkClient.setExecutor(engine); + JwkResponse jwkResponse = jwkClient.exec(); + if (jwkResponse != null && jwkResponse.getStatus() == 200) { + PublicKey pk = jwkResponse.getPublicKey(keyId); + if (pk instanceof RSAPublicKey) { + publicKey = (RSAPublicKey) pk; + } + } + + return publicKey; + } + + public static ECDSAPublicKey getECDSAPublicKey(String jwkSetUrl, String keyId) { + return getECDSAPublicKey(jwkSetUrl, keyId, null); + } + + public static ECDSAPublicKey getECDSAPublicKey(String jwkSetUrl, String keyId, ClientHttpEngine engine) { + ECDSAPublicKey publicKey = null; + + JwkClient jwkClient = new JwkClient(jwkSetUrl); + if (engine != null) { + jwkClient.setExecutor(engine); + } + JwkResponse jwkResponse = jwkClient.exec(); + if (jwkResponse != null && jwkResponse.getStatus() == 200) { + PublicKey pk = jwkResponse.getPublicKey(keyId); + if (pk instanceof ECDSAPublicKey) { + publicKey = (ECDSAPublicKey) pk; + } + } + + return publicKey; + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/JwkRequest.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/JwkRequest.java new file mode 100644 index 00000000..ed42b278 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/JwkRequest.java @@ -0,0 +1,20 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +/** + * Represents a JSON Web Key (JWK) request to send to the authorization server. + * + * @author Javier Rojas Blum Date: 11.15.2011 + */ +public class JwkRequest extends BaseRequest { + + @Override + public String getQueryString() { + return null; + } +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/JwkResponse.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/JwkResponse.java new file mode 100644 index 00000000..d9ba8164 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/JwkResponse.java @@ -0,0 +1,121 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.gluu.oxauth.model.crypto.PublicKey; +import org.gluu.oxauth.model.crypto.signature.AlgorithmFamily; +import org.gluu.oxauth.model.crypto.signature.ECDSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jwk.Algorithm; +import org.gluu.oxauth.model.jwk.JSONWebKey; +import org.gluu.oxauth.model.jwk.JSONWebKeySet; + +/** + * Represents a JSON Web Key (JWK) received from the authorization server. + * + * @author Javier Rojas Blum + * @version February 12, 2019 + */ +public class JwkResponse extends BaseResponse { + + private JSONWebKeySet jwks; + + /** + * Constructs a JWK response. + * + * @param status The response status code. + */ + public JwkResponse(int status) { + super(status); + } + + public JSONWebKeySet getJwks() { + return jwks; + } + + public void setJwks(JSONWebKeySet jwks) { + this.jwks = jwks; + } + + /** + * Search and returns a {@link org.gluu.oxauth.model.jwk.JSONWebKey} given its keyId. + * + * @param keyId The key id. + * @return The JSONWebKey if found, otherwise null. + */ + @Deprecated + public JSONWebKey getKeyValue(String keyId) { + for (JSONWebKey JSONWebKey : jwks.getKeys()) { + if (JSONWebKey.getKid().equals(keyId)) { + return JSONWebKey; + } + } + + return null; + } + + @Deprecated + public PublicKey getPublicKey(String keyId) { + PublicKey publicKey = null; + JSONWebKey JSONWebKey = getKeyValue(keyId); + + if (JSONWebKey != null) { + switch (JSONWebKey.getKty()) { + case RSA: + publicKey = new RSAPublicKey( + JSONWebKey.getN(), + JSONWebKey.getE()); + break; + case EC: + publicKey = new ECDSAPublicKey( + SignatureAlgorithm.fromString(JSONWebKey.getAlg().getParamName()), + JSONWebKey.getX(), + JSONWebKey.getY()); + break; + default: + break; + } + } + + return publicKey; + } + + public List getKeys(Algorithm algorithm) { + List jsonWebKeys = new ArrayList(); + + if (AlgorithmFamily.RSA.equals(algorithm.getFamily())) { + for (JSONWebKey jsonWebKey : jwks.getKeys()) { + if (jsonWebKey.getAlg().equals(algorithm)) { + jsonWebKeys.add(jsonWebKey); + } + } + } else if (AlgorithmFamily.EC.equals(algorithm.getFamily())) { + for (JSONWebKey jsonWebKey : jwks.getKeys()) { + if (jsonWebKey.getAlg().equals(algorithm)) { + jsonWebKeys.add(jsonWebKey); + } + } + } + + Collections.sort(jsonWebKeys); + return jsonWebKeys; + } + + public String getKeyId(Algorithm algorithm) { + List jsonWebKeys = getKeys(algorithm); + if (jsonWebKeys.size() > 0) { + return jsonWebKeys.get(0).getKid(); + } else { + return null; + } + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/OpenIdConfigurationClient.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/OpenIdConfigurationClient.java new file mode 100644 index 00000000..7851ba91 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/OpenIdConfigurationClient.java @@ -0,0 +1,303 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.ACR_VALUES_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.AUTHORIZATION_ENDPOINT; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.BACKCHANNEL_AUTHENTICATION_ENDPOINT; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG_VALUES_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.BACKCHANNEL_LOGOUT_SESSION_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.BACKCHANNEL_LOGOUT_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.BACKCHANNEL_TOKEN_DELIVERY_MODES_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.BACKCHANNEL_USER_CODE_PAREMETER_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.CHECK_SESSION_IFRAME; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.CLAIMS_LOCALES_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.CLAIMS_PARAMETER_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.CLAIMS_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.CLAIM_TYPES_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.CLIENT_INFO_ENDPOINT; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.DEVICE_AUTHZ_ENDPOINT; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.DISPLAY_VALUES_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.END_SESSION_ENDPOINT; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.FRONTCHANNEL_LOGOUT_SESSION_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.FRONTCHANNEL_LOGOUT_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.GRANT_TYPES_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.ID_GENERATION_ENDPOINT; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.ID_TOKEN_ENCRYPTION_ALG_VALUES_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.ID_TOKEN_ENCRYPTION_ENC_VALUES_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.INTROSPECTION_ENDPOINT; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.ISSUER; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.JWKS_URI; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.OP_POLICY_URI; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.OP_TOS_URI; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.REGISTRATION_ENDPOINT; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.REQUEST_OBJECT_ENCRYPTION_ALG_VALUES_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.REQUEST_OBJECT_ENCRYPTION_ENC_VALUES_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.REQUEST_OBJECT_SIGNING_ALG_VALUES_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.REQUEST_PARAMETER_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.REQUEST_URI_PARAMETER_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.REQUIRE_REQUEST_URI_REGISTRATION; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.RESPONSE_MODES_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.RESPONSE_TYPES_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.REVOCATION_ENDPOINT; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.SCOPES_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.SCOPE_TO_CLAIMS_MAPPING; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.SERVICE_DOCUMENTATION; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.SESSION_REVOCATION_ENDPOINT; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.SUBJECT_TYPES_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.TLS_CLIENT_CERTIFICATE_BOUND_ACCESS_TOKENS; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.TOKEN_ENDPOINT; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.TOKEN_ENDPOINT_AUTH_SIGNING_ALG_VALUES_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.TOKEN_REVOCATION_ENDPOINT; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.UI_LOCALES_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.USER_INFO_ENCRYPTION_ALG_VALUES_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.USER_INFO_ENCRYPTION_ENC_VALUES_SUPPORTED; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.USER_INFO_ENDPOINT; +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.USER_INFO_SIGNING_ALG_VALUES_SUPPORTED; + +import java.io.IOException; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.client.Invocation.Builder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.gluu.oxauth.model.util.Util; +import org.jboss.resteasy.client.jaxrs.ClientHttpEngine; +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Encapsulates functionality to make OpenId Configuration request calls to an authorization server via REST Services. + * + * @author Javier Rojas Blum + * @version August 22, 2019 + */ +public class OpenIdConfigurationClient extends BaseClient { + + private static final Logger LOG = Logger.getLogger(OpenIdConfigurationClient.class); + + private static final String mediaTypes = String.join(",", MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON); + + /** + * Constructs an OpenID Configuration Client by providing an url where the REST service is located. + * + * @param url The REST service location. + */ + public OpenIdConfigurationClient(String url) { + super(url); + } + + @Override + public String getHttpMethod() { + return HttpMethod.GET; + } + + public OpenIdConfigurationResponse execOpenIdConfiguration() throws IOException { + initClientRequest(); + + return _execOpenIdConfiguration(); + } + + @Deprecated + public OpenIdConfigurationResponse execOpenIdConfiguration(ClientHttpEngine engine) throws IOException { + resteasyClient = ((ResteasyClientBuilder) ResteasyClientBuilder.newBuilder()).httpEngine(engine).build(); + webTarget = resteasyClient.target(getUrl()); + + return _execOpenIdConfiguration(); + } + + /** + * Executes the call to the REST Service requesting the OpenID Configuration and processes the response. + * + * @return The service response. + */ + private OpenIdConfigurationResponse _execOpenIdConfiguration() throws IOException { + setRequest(new OpenIdConfigurationRequest()); + + // Call REST Service and handle response + String entity = null; + try { + requestClientResponse(webTarget); + + int status = clientResponse.getStatus(); + // Support AWS LB which requires follow redirect + if (status == Response.Status.FOUND.getStatusCode()) { + webTarget = resteasyClient.target(clientResponse.getLocation()); + requestClientResponse(webTarget); + } + + setResponse(new OpenIdConfigurationResponse(status)); + + entity = clientResponse.readEntity(String.class); + getResponse().setEntity(entity); + getResponse().setHeaders(clientResponse.getMetadata()); + parse(entity, getResponse()); + } catch (JSONException e) { + LOG.error("There is an error in the JSON response. Check if there is a syntax error in the JSON response or there is a wrong key", e); + if (entity != null) { + LOG.error("Invalid JSON: " + entity); + } + } catch (Exception e) { + LOG.error(e.getMessage(), e); + LOG.error(e.getMessage(), e); // Unexpected exception. + } finally { + closeConnection(); + } + + return getResponse(); + } + + private void requestClientResponse(WebTarget webTarget) { + Builder clientRequest = webTarget.request(); + + applyCookies(clientRequest); + + // Prepare request parameters + clientRequest.accept(mediaTypes); +// clientRequest.setHttpMethod(getHttpMethod()); + + clientResponse = clientRequest.buildGet().invoke(); + } + + public static void parse(String json, OpenIdConfigurationResponse response) { + if (StringUtils.isBlank(json)) { + return; + } + + JSONObject jsonObj = new JSONObject(json); + + if (jsonObj.has(ISSUER)) { + response.setIssuer(jsonObj.getString(ISSUER)); + } + if (jsonObj.has(AUTHORIZATION_ENDPOINT)) { + response.setAuthorizationEndpoint(jsonObj.getString(AUTHORIZATION_ENDPOINT)); + } + if (jsonObj.has(TOKEN_ENDPOINT)) { + response.setTokenEndpoint(jsonObj.getString(TOKEN_ENDPOINT)); + } + if (jsonObj.has(TOKEN_REVOCATION_ENDPOINT)) { + response.setRevocationEndpoint(jsonObj.getString(TOKEN_REVOCATION_ENDPOINT)); + } + if (jsonObj.has(REVOCATION_ENDPOINT)) { + response.setRevocationEndpoint(jsonObj.getString(REVOCATION_ENDPOINT)); + } + if (jsonObj.has(SESSION_REVOCATION_ENDPOINT)) { + response.setSessionRevocationEndpoint(jsonObj.getString(SESSION_REVOCATION_ENDPOINT)); + } + if (jsonObj.has(USER_INFO_ENDPOINT)) { + response.setUserInfoEndpoint(jsonObj.getString(USER_INFO_ENDPOINT)); + } + if (jsonObj.has(CLIENT_INFO_ENDPOINT)) { + response.setClientInfoEndpoint(jsonObj.getString(CLIENT_INFO_ENDPOINT)); + } + if (jsonObj.has(CHECK_SESSION_IFRAME)) { + response.setCheckSessionIFrame(jsonObj.getString(CHECK_SESSION_IFRAME)); + } + if (jsonObj.has(END_SESSION_ENDPOINT)) { + response.setEndSessionEndpoint(jsonObj.getString(END_SESSION_ENDPOINT)); + } + if (jsonObj.has(JWKS_URI)) { + response.setJwksUri(jsonObj.getString(JWKS_URI)); + } + if (jsonObj.has(REGISTRATION_ENDPOINT)) { + response.setRegistrationEndpoint(jsonObj.getString(REGISTRATION_ENDPOINT)); + } + if (jsonObj.has(ID_GENERATION_ENDPOINT)) { + response.setIdGenerationEndpoint(jsonObj.getString(ID_GENERATION_ENDPOINT)); + } + if (jsonObj.has(INTROSPECTION_ENDPOINT)) { + response.setIntrospectionEndpoint(jsonObj.getString(INTROSPECTION_ENDPOINT)); + } + if (jsonObj.has(DEVICE_AUTHZ_ENDPOINT)) { + response.setDeviceAuthzEndpoint(jsonObj.getString(DEVICE_AUTHZ_ENDPOINT)); + } + if (jsonObj.has(SCOPE_TO_CLAIMS_MAPPING)) { + response.setScopeToClaimsMapping(OpenIdConfigurationResponse.parseScopeToClaimsMapping(jsonObj.getJSONArray(SCOPE_TO_CLAIMS_MAPPING))); + } + Util.addToListIfHas(response.getScopesSupported(), jsonObj, SCOPES_SUPPORTED); + Util.addToListIfHas(response.getResponseTypesSupported(), jsonObj, RESPONSE_TYPES_SUPPORTED); + Util.addToListIfHas(response.getResponseModesSupported(), jsonObj, RESPONSE_MODES_SUPPORTED); + Util.addToListIfHas(response.getGrantTypesSupported(), jsonObj, GRANT_TYPES_SUPPORTED); + Util.addToListIfHas(response.getAcrValuesSupported(), jsonObj, ACR_VALUES_SUPPORTED); + Util.addToListIfHas(response.getSubjectTypesSupported(), jsonObj, SUBJECT_TYPES_SUPPORTED); + Util.addToListIfHas(response.getUserInfoSigningAlgValuesSupported(), jsonObj, USER_INFO_SIGNING_ALG_VALUES_SUPPORTED); + Util.addToListIfHas(response.getUserInfoEncryptionAlgValuesSupported(), jsonObj, USER_INFO_ENCRYPTION_ALG_VALUES_SUPPORTED); + Util.addToListIfHas(response.getUserInfoEncryptionEncValuesSupported(), jsonObj, USER_INFO_ENCRYPTION_ENC_VALUES_SUPPORTED); + Util.addToListIfHas(response.getIdTokenSigningAlgValuesSupported(), jsonObj, ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED); + Util.addToListIfHas(response.getIdTokenEncryptionAlgValuesSupported(), jsonObj, ID_TOKEN_ENCRYPTION_ALG_VALUES_SUPPORTED); + Util.addToListIfHas(response.getIdTokenEncryptionEncValuesSupported(), jsonObj, ID_TOKEN_ENCRYPTION_ENC_VALUES_SUPPORTED); + Util.addToListIfHas(response.getRequestObjectSigningAlgValuesSupported(), jsonObj, REQUEST_OBJECT_SIGNING_ALG_VALUES_SUPPORTED); + Util.addToListIfHas(response.getRequestObjectEncryptionAlgValuesSupported(), jsonObj, REQUEST_OBJECT_ENCRYPTION_ALG_VALUES_SUPPORTED); + Util.addToListIfHas(response.getRequestObjectEncryptionEncValuesSupported(), jsonObj, REQUEST_OBJECT_ENCRYPTION_ENC_VALUES_SUPPORTED); + Util.addToListIfHas(response.getTokenEndpointAuthMethodsSupported(), jsonObj, TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED); + Util.addToListIfHas(response.getTokenEndpointAuthSigningAlgValuesSupported(), jsonObj, TOKEN_ENDPOINT_AUTH_SIGNING_ALG_VALUES_SUPPORTED); + Util.addToListIfHas(response.getDisplayValuesSupported(), jsonObj, DISPLAY_VALUES_SUPPORTED); + Util.addToListIfHas(response.getClaimTypesSupported(), jsonObj, CLAIM_TYPES_SUPPORTED); + Util.addToListIfHas(response.getClaimsSupported(), jsonObj, CLAIMS_SUPPORTED); + if (jsonObj.has(SERVICE_DOCUMENTATION)) { + response.setServiceDocumentation(jsonObj.getString(SERVICE_DOCUMENTATION)); + } + Util.addToListIfHas(response.getClaimsLocalesSupported(), jsonObj, CLAIMS_LOCALES_SUPPORTED); + Util.addToListIfHas(response.getUiLocalesSupported(), jsonObj, UI_LOCALES_SUPPORTED); + if (jsonObj.has(CLAIMS_PARAMETER_SUPPORTED)) { + response.setClaimsParameterSupported(jsonObj.getBoolean(CLAIMS_PARAMETER_SUPPORTED)); + } + if (jsonObj.has(REQUEST_PARAMETER_SUPPORTED)) { + response.setRequestParameterSupported(jsonObj.getBoolean(REQUEST_PARAMETER_SUPPORTED)); + } + if (jsonObj.has(REQUEST_URI_PARAMETER_SUPPORTED)) { + response.setRequestUriParameterSupported(jsonObj.getBoolean(REQUEST_URI_PARAMETER_SUPPORTED)); + } + if (jsonObj.has(TLS_CLIENT_CERTIFICATE_BOUND_ACCESS_TOKENS)) { + response.setTlsClientCertificateBoundAccessTokens(jsonObj.optBoolean(TLS_CLIENT_CERTIFICATE_BOUND_ACCESS_TOKENS)); + } + if (jsonObj.has(FRONTCHANNEL_LOGOUT_SUPPORTED)) { + response.setFrontChannelLogoutSupported(jsonObj.getBoolean(FRONTCHANNEL_LOGOUT_SUPPORTED)); + } + if (jsonObj.has(FRONTCHANNEL_LOGOUT_SESSION_SUPPORTED)) { + response.setFrontChannelLogoutSessionSupported(jsonObj.getBoolean(FRONTCHANNEL_LOGOUT_SESSION_SUPPORTED)); + } + if (jsonObj.has(BACKCHANNEL_LOGOUT_SUPPORTED)) { + response.setBackchannelLogoutSupported(jsonObj.optBoolean(BACKCHANNEL_LOGOUT_SUPPORTED)); + } + if (jsonObj.has(BACKCHANNEL_LOGOUT_SESSION_SUPPORTED)) { + response.setBackchannelLogoutSessionSupported(jsonObj.optBoolean(BACKCHANNEL_LOGOUT_SESSION_SUPPORTED)); + } + if (jsonObj.has(REQUIRE_REQUEST_URI_REGISTRATION)) { + response.setRequireRequestUriRegistration(jsonObj.getBoolean(REQUIRE_REQUEST_URI_REGISTRATION)); + } + if (jsonObj.has(OP_POLICY_URI)) { + response.setOpPolicyUri(jsonObj.getString(OP_POLICY_URI)); + } + if (jsonObj.has(OP_TOS_URI)) { + response.setOpTosUri(jsonObj.getString(OP_TOS_URI)); + } + + // CIBA + if (jsonObj.has(BACKCHANNEL_AUTHENTICATION_ENDPOINT)) { + response.setBackchannelAuthenticationEndpoint(jsonObj.getString(BACKCHANNEL_AUTHENTICATION_ENDPOINT)); + } + Util.addToListIfHas(response.getBackchannelTokenDeliveryModesSupported(), jsonObj, BACKCHANNEL_TOKEN_DELIVERY_MODES_SUPPORTED); + Util.addToListIfHas(response.getBackchannelAuthenticationRequestSigningAlgValuesSupported(), jsonObj, BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG_VALUES_SUPPORTED); + if (jsonObj.has(BACKCHANNEL_USER_CODE_PAREMETER_SUPPORTED)) { + response.setBackchannelUserCodeParameterSupported(jsonObj.getBoolean(BACKCHANNEL_USER_CODE_PAREMETER_SUPPORTED)); + } + } + + public static OpenIdConfigurationResponse parse(String json) { + OpenIdConfigurationResponse response = new OpenIdConfigurationResponse(); + parse(json, response); + return response; + } +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/OpenIdConfigurationRequest.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/OpenIdConfigurationRequest.java new file mode 100644 index 00000000..1107cfd4 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/OpenIdConfigurationRequest.java @@ -0,0 +1,26 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +/** + * Represents an OpenId Configuration request to send to the authorization server. + * + * @author Javier Rojas Blum Date: 12.6.2011 + */ +public class OpenIdConfigurationRequest extends BaseRequest{ + + /** + * Construct an OpenID Configuration Request. + */ + public OpenIdConfigurationRequest() { + } + + @Override + public String getQueryString() { + return null; + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/OpenIdConfigurationResponse.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/OpenIdConfigurationResponse.java new file mode 100644 index 00000000..ba3f302d --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/OpenIdConfigurationResponse.java @@ -0,0 +1,1152 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang.StringUtils; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Represents an OpenId Configuration received from the authorization server. + * + * @author Javier Rojas Blum + * @version August 22, 2019 + */ +public class OpenIdConfigurationResponse extends BaseResponse implements Serializable { + + private String issuer; + private String authorizationEndpoint; + private String tokenEndpoint; + private String revocationEndpoint; + private String sessionRevocationEndpoint; + private String userInfoEndpoint; + private String clientInfoEndpoint; + private String checkSessionIFrame; + private String endSessionEndpoint; + private String jwksUri; + private String registrationEndpoint; + private String idGenerationEndpoint; + private String introspectionEndpoint; + private String deviceAuthzEndpoint; + private List scopesSupported; + private List responseTypesSupported; + private List responseModesSupported; + private List grantTypesSupported; + private List acrValuesSupported; + private List subjectTypesSupported; + private List userInfoSigningAlgValuesSupported; + private List userInfoEncryptionAlgValuesSupported; + private List userInfoEncryptionEncValuesSupported; + private List idTokenSigningAlgValuesSupported; + private List idTokenEncryptionAlgValuesSupported; + private List idTokenEncryptionEncValuesSupported; + private List requestObjectSigningAlgValuesSupported; + private List requestObjectEncryptionAlgValuesSupported; + private List requestObjectEncryptionEncValuesSupported; + private List tokenEndpointAuthMethodsSupported; + private List tokenEndpointAuthSigningAlgValuesSupported; + private List displayValuesSupported; + private List claimTypesSupported; + private List claimsSupported; + private List idTokenTokenBindingCnfValuesSupported; + private String serviceDocumentation; + private List claimsLocalesSupported; + private List uiLocalesSupported; + private Boolean claimsParameterSupported; + private Boolean requestParameterSupported; + private Boolean requestUriParameterSupported; + private Boolean requireRequestUriRegistration; + private Boolean tlsClientCertificateBoundAccessTokens; + private Boolean frontChannelLogoutSupported; + private Boolean frontChannelLogoutSessionSupported; + private Boolean backchannelLogoutSupported; + private Boolean backchannelLogoutSessionSupported; + private String opPolicyUri; + private String opTosUri; + private Map> scopeToClaimsMapping = new HashMap>(); + + // CIBA + private String backchannelAuthenticationEndpoint; + private List backchannelTokenDeliveryModesSupported; + private List backchannelAuthenticationRequestSigningAlgValuesSupported; + private Boolean backchannelUserCodeParameterSupported; + + public OpenIdConfigurationResponse() { + } + + /** + * Constructs an OpenID Configuration Response. + * + * @param status The response status code. + */ + public OpenIdConfigurationResponse(int status) { + super(status); + + scopesSupported = new ArrayList(); + responseTypesSupported = new ArrayList(); + responseModesSupported = new ArrayList<>(); + grantTypesSupported = new ArrayList(); + acrValuesSupported = new ArrayList(); + subjectTypesSupported = new ArrayList(); + userInfoSigningAlgValuesSupported = new ArrayList(); + userInfoEncryptionAlgValuesSupported = new ArrayList(); + userInfoEncryptionEncValuesSupported = new ArrayList(); + idTokenSigningAlgValuesSupported = new ArrayList(); + idTokenEncryptionAlgValuesSupported = new ArrayList(); + idTokenEncryptionEncValuesSupported = new ArrayList(); + requestObjectSigningAlgValuesSupported = new ArrayList(); + requestObjectEncryptionAlgValuesSupported = new ArrayList(); + requestObjectEncryptionEncValuesSupported = new ArrayList(); + tokenEndpointAuthMethodsSupported = new ArrayList(); + tokenEndpointAuthSigningAlgValuesSupported = new ArrayList(); + displayValuesSupported = new ArrayList(); + claimTypesSupported = new ArrayList(); + claimsSupported = new ArrayList(); + idTokenTokenBindingCnfValuesSupported = new ArrayList(); + claimsLocalesSupported = new ArrayList(); + uiLocalesSupported = new ArrayList(); + backchannelTokenDeliveryModesSupported = new ArrayList<>(); + backchannelAuthenticationRequestSigningAlgValuesSupported = new ArrayList<>(); + } + + public static Map> parseScopeToClaimsMapping(String p_scopeToClaimsJson) throws JSONException { + return parseScopeToClaimsMapping(new JSONArray(p_scopeToClaimsJson)); + } + + public static Map> parseScopeToClaimsMapping(JSONArray p_jsonArray) throws JSONException { + final Map> map = new HashMap>(); + if (p_jsonArray != null) { + for (int i = 0; i < p_jsonArray.length(); i++) { + final JSONObject obj = p_jsonArray.getJSONObject(i); + final String scope = obj.names().getString(0); + final JSONArray claimsArray = obj.getJSONArray(scope); + final List claimsList = new ArrayList(); + for (int j = 0; j < claimsArray.length(); j++) { + final String claim = claimsArray.getString(j); + if (StringUtils.isNotBlank(claim)) { + claimsList.add(claim); + } + } + map.put(scope, claimsList); + } + + } + return map; + } + + /** + * Gets scopes to claims map. + * + * @return scopes to claims map + * @deprecated this parameter will be moved from /.well-known/openid-configuration to /.well-known/gluu-configuration + */ + @Deprecated + public Map> getScopeToClaimsMapping() { + return scopeToClaimsMapping; + } + + /** + * Sets scope to claim map. + * + * @param p_scopeToClaimsMapping scope to claim map + * @deprecated this parameter will be moved from /.well-known/openid-configuration to /.well-known/gluu-configuration + */ + @Deprecated + public void setScopeToClaimsMapping(Map> p_scopeToClaimsMapping) { + scopeToClaimsMapping = p_scopeToClaimsMapping; + } + + /** + * Returns the issuer identifier. + * + * @return The issuer identifier. + */ + public String getIssuer() { + return issuer; + } + + /** + * Sets the issuer identifier. + * + * @param issuer The issuer identifier. + */ + public void setIssuer(String issuer) { + this.issuer = issuer; + } + + /** + * Returns the URL of the Authentication and Authorization endpoint. + * + * @return The URL of the Authentication and Authorization endpoint. + */ + public String getAuthorizationEndpoint() { + return authorizationEndpoint; + } + + /** + * Sets the URL of the Authentication and Authorization endpoint. + * + * @param authorizationEndpoint The URL of the Authentication and Authorization endpoint. + */ + public void setAuthorizationEndpoint(String authorizationEndpoint) { + this.authorizationEndpoint = authorizationEndpoint; + } + + /** + * Returns the URL of the Token endpoint. + * + * @return The URL of the Token endpoint. + */ + public String getTokenEndpoint() { + return tokenEndpoint; + } + + /** + * Sets the URL of the Token endpoint. + * + * @param tokenEndpoint The URL of the Token endpoint. + */ + public void setTokenEndpoint(String tokenEndpoint) { + this.tokenEndpoint = tokenEndpoint; + } + + public String getSessionRevocationEndpoint() { + return sessionRevocationEndpoint; + } + + public void setSessionRevocationEndpoint(String sessionRevocationEndpoint) { + this.sessionRevocationEndpoint = sessionRevocationEndpoint; + } + + /** + * Returns the URL of the Token Revocation endpoint. + * + * @return The URL of the Token Revocation endpoint. + */ + public String getRevocationEndpoint() { + return revocationEndpoint; + } + + /** + * Sets the URL of the Token Revocation endpoint. + * + * @param revocationEndpoint The URL of the Token Revocation endpoint. + */ + public void setRevocationEndpoint(String revocationEndpoint) { + this.revocationEndpoint = revocationEndpoint; + } + + /** + * Returns the URL of the User Info endpoint. + * + * @return The URL of the User Info endpoint. + */ + public String getUserInfoEndpoint() { + return userInfoEndpoint; + } + + /** + * Sets the URL for the User Info endpoint. + * + * @param userInfoEndpoint The URL for the User Info endpoint. + */ + public void setUserInfoEndpoint(String userInfoEndpoint) { + this.userInfoEndpoint = userInfoEndpoint; + } + + /** + * Returns the URL of the Client Info endpoint. + * + * @return The URL of the Client Info endpoint. + */ + public String getClientInfoEndpoint() { + return clientInfoEndpoint; + } + + /** + * Sets the URL for the Client Info endpoint. + * + * @param clientInfoEndpoint The URL for the Client Info endpoint. + */ + public void setClientInfoEndpoint(String clientInfoEndpoint) { + this.clientInfoEndpoint = clientInfoEndpoint; + } + + /** + * Returns the URL of an OP endpoint that provides a page to support + * cross-origin communications for session state information with the RP + * client. + * + * @return The Check Session iFrame URL. + */ + public String getCheckSessionIFrame() { + return checkSessionIFrame; + } + + /** + * Sets the URL of an OP endpoint that provides a page to support + * cross-origin communications for session state information with the RP + * client. + * + * @param checkSessionIFrame The Check Session iFrame URL. + */ + public void setCheckSessionIFrame(String checkSessionIFrame) { + this.checkSessionIFrame = checkSessionIFrame; + } + + /** + * Returns the URL of the End Session endpoint. + * + * @return The URL of the End Session endpoint. + */ + public String getEndSessionEndpoint() { + return endSessionEndpoint; + } + + /** + * Sets the URL of the End Session endpoint. + * + * @param endSessionEndpoint The URL of the End Session endpoint. + */ + public void setEndSessionEndpoint(String endSessionEndpoint) { + this.endSessionEndpoint = endSessionEndpoint; + } + + /** + * Returns the URL of the OP's JSON Web Key Set (JWK) document that contains + * the Server's signing key(s) that are used for signing responses to the + * Client. The JWK Set may also contain the Server's encryption key(s) that + * are used by the Client to encrypt requests to the Server. + * + * @return The URL of the OP's JSON Web Key Set (JWK) document. + */ + public String getJwksUri() { + return jwksUri; + } + + /** + * Sets the URL of the OP's JSON Web Key Set (JWK) document that contains + * the Server's signing key(s) that are used for signing responses to the + * Client. The JWK Set may also contain the Server's encryption key(s) that + * are used by the Client to encrypt requests to the Server. + * + * @param jwksUri The URL of the OP's JSON Web Key Set (JWK) document. + */ + public void setJwksUri(String jwksUri) { + this.jwksUri = jwksUri; + } + + /** + * Returns the URL of the Dynamic Client Registration endpoint. + * + * @return The URL of the Dynamic Client Registration endpoint. + */ + public String getRegistrationEndpoint() { + return registrationEndpoint; + } + + /** + * Sets the URL of the Dynamic Client Registration endpoint. + * + * @param registrationEndpoint The URL of the Dynamic Client Registration endpoint. + */ + public void setRegistrationEndpoint(String registrationEndpoint) { + this.registrationEndpoint = registrationEndpoint; + } + + /** + * @deprecated this parameter will be moved from /.well-known/openid-configuration to /.well-known/gluu-configuration + */ + @Deprecated + public String getIdGenerationEndpoint() { + return idGenerationEndpoint; + } + + /** + * @deprecated this parameter will be moved from /.well-known/openid-configuration to /.well-known/gluu-configuration + */ + @Deprecated + public void setIdGenerationEndpoint(String p_idGenerationEndpoint) { + idGenerationEndpoint = p_idGenerationEndpoint; + } + + /** + * @deprecated this parameter will be moved from /.well-known/openid-configuration to /.well-known/gluu-configuration + */ + @Deprecated + public String getIntrospectionEndpoint() { + return introspectionEndpoint; + } + + /** + * @deprecated this parameter will be moved from /.well-known/openid-configuration to /.well-known/gluu-configuration + */ + @Deprecated + public void setIntrospectionEndpoint(String p_introspectionEndpoint) { + introspectionEndpoint = p_introspectionEndpoint; + } + + /** + * Returns a list of the OAuth 2.0 scopes that the server supports. + * + * @return A list of the OAuth 2.0 scopes that the server supports. + */ + public List getScopesSupported() { + return scopesSupported; + } + + /** + * Sets a list of the OAuth 2.0 scopes that the server supports. + * + * @param scopesSupported A list of the OAuth 2.0 scopes that the server supports. + */ + public void setScopesSupported(List scopesSupported) { + this.scopesSupported = scopesSupported; + } + + /** + * Returns a list of the response types that the server supports. + * + * @return A list of the response types that the server supports. + */ + public List getResponseTypesSupported() { + return responseTypesSupported; + } + + /** + * Sets a list of the response types that the server supports. + * + * @param responseTypesSupported A list of the response types that the server supports. + */ + public void setResponseTypesSupported(List responseTypesSupported) { + this.responseTypesSupported = responseTypesSupported; + } + + public List getResponseModesSupported() { + return responseModesSupported; + } + + public void setResponseModesSupported(List responseModesSupported) { + this.responseModesSupported = responseModesSupported; + } + + /** + * Returns a list of the OAuth 2.0 grant type values that this server + * supports. + * + * @return A list of the OAuth 2.0 grant type values that this server + * supports. + */ + public List getGrantTypesSupported() { + return grantTypesSupported; + } + + /** + * Sets a list of the OAuth 2.0 grant type values that this server supports. + * + * @param grantTypesSupported A list of the OAuth 2.0 grant type values that this server + * supports. + */ + public void setGrantTypesSupported(List grantTypesSupported) { + this.grantTypesSupported = grantTypesSupported; + } + + /** + * Returns a list of the Authentication Context Class References that this + * server supports. + * + * @return A list of the Authentication Context Class References + */ + public List getAcrValuesSupported() { + return acrValuesSupported; + } + + /** + * Sets a list of the Authentication Context Class References that this + * server supports. + * + * @param acrValuesSupported A list of the Authentication Context Class References + */ + public void setAcrValuesSupported(List acrValuesSupported) { + this.acrValuesSupported = acrValuesSupported; + } + + /** + * Returns a list of the subject identifier types that this server supports. + * Valid types include pairwise and public. + * + * @return A list of the subject identifier types that this server supports. + */ + public List getSubjectTypesSupported() { + return subjectTypesSupported; + } + + /** + * Sets a list of the subject identifier types that this server supports. + * Valid types include pairwise and public. + * + * @param subjectTypesSupported A list of the subject identifier types that this server + * supports. + */ + public void setSubjectTypesSupported(List subjectTypesSupported) { + this.subjectTypesSupported = subjectTypesSupported; + } + + /** + * Returns a list of the JWS signing algorithms (alg values JWA) supported + * by the UserInfo Endpoint to encode the claims in a JWT + * + * @return A list of the JWS signing algorithms. + */ + public List getUserInfoSigningAlgValuesSupported() { + return userInfoSigningAlgValuesSupported; + } + + /** + * Sets a list of the JWS signing algorithms (alg values JWA) supported by + * the UserInfo Endpoint to encode the claims in a JWT + * + * @param userInfoSigningAlgValuesSupported A list of the JWS signing algorithms. + */ + public void setUserInfoSigningAlgValuesSupported(List userInfoSigningAlgValuesSupported) { + this.userInfoSigningAlgValuesSupported = userInfoSigningAlgValuesSupported; + } + + /** + * Returns a list of the JWE encryption algorithms (alg values JWA) + * supported by the UserInfo Endpoint to encode the claims in a JWT. + * + * @return A list of the JWE encryption algorithms. + */ + public List getUserInfoEncryptionAlgValuesSupported() { + return userInfoEncryptionAlgValuesSupported; + } + + /** + * Sets a list of the JWE encryption algorithms (alg values JWA) supported + * by the UserInfo Endpoint to encode the claims in a JWT. + * + * @param userInfoEncryptionAlgValuesSupported A list of the JWE encryption algorithms. + */ + public void setUserInfoEncryptionAlgValuesSupported(List userInfoEncryptionAlgValuesSupported) { + this.userInfoEncryptionAlgValuesSupported = userInfoEncryptionAlgValuesSupported; + } + + /** + * Returns a list of the JWE encryption algorithms (enc values JWA) + * supported by the UserInfo Endpoint to encode the claims in a JWT. + * + * @return A list of the JWE encryption algorithms. + */ + public List getUserInfoEncryptionEncValuesSupported() { + return userInfoEncryptionEncValuesSupported; + } + + /** + * Sets a list of the JWE encryption algorithms (enc values JWA) supported + * by the UserInfo Endpoint to encode the claims in a JWT. + * + * @param userInfoEncryptionEncValuesSupported A list of the JWE encryption algorithms. + */ + public void setUserInfoEncryptionEncValuesSupported(List userInfoEncryptionEncValuesSupported) { + this.userInfoEncryptionEncValuesSupported = userInfoEncryptionEncValuesSupported; + } + + /** + * Returns a list of the JWS signing algorithms (alg values) supported by + * the Authorization Server for the ID Token to encode the claims in a JWT. + * + * @return A list of the JWS signing algorithms. + */ + public List getIdTokenSigningAlgValuesSupported() { + return idTokenSigningAlgValuesSupported; + } + + /** + * Sets a list of the JWS signing algorithms (alg values) supported by the + * Authorization Server for the ID Token to encode the claims in a JWT. + * + * @param idTokenSigningAlgValuesSupported A list of the JWS signing algorithms. + */ + public void setIdTokenSigningAlgValuesSupported(List idTokenSigningAlgValuesSupported) { + this.idTokenSigningAlgValuesSupported = idTokenSigningAlgValuesSupported; + } + + /** + * Returns a list of the JWE encryption algorithms (alg values) supported by + * the Authorization Server for the ID Token to encode the claims in a JWT. + * + * @return A list of the JWE encryption algorithms. + */ + public List getIdTokenEncryptionAlgValuesSupported() { + return idTokenEncryptionAlgValuesSupported; + } + + /** + * Sets a list of the JWE encryption algorithms (alg values) supported by + * the Authorization Server for the ID Token to encode the claims in a JWT. + * + * @param idTokenEncryptionAlgValuesSupported A list of the JWE encryption algorithms. + */ + public void setIdTokenEncryptionAlgValuesSupported(List idTokenEncryptionAlgValuesSupported) { + this.idTokenEncryptionAlgValuesSupported = idTokenEncryptionAlgValuesSupported; + } + + /** + * Returns a list of the JWE encryption algorithms (enc values) supported by + * the Authorization Server for the ID Token to encode the claims in a JWT. + * + * @return A list of the JWE encryption algorithms. + */ + public List getIdTokenEncryptionEncValuesSupported() { + return idTokenEncryptionEncValuesSupported; + } + + /** + * Sets a list of the JWE encryption algorithms (enc values) supported by + * the Authorization Server for the ID Token to encode the claims in a JWT. + * + * @param idTokenEncryptionEncValuesSupported A list of the JWE encryption algorithms. + */ + public void setIdTokenEncryptionEncValuesSupported(List idTokenEncryptionEncValuesSupported) { + this.idTokenEncryptionEncValuesSupported = idTokenEncryptionEncValuesSupported; + } + + /** + * Returns a list of the JWS signing algorithms (alg values) supported by + * the Authorization Server for the OpenID Request Object. + * + * @return A list of the JWS signing algorithms. + */ + public List getRequestObjectSigningAlgValuesSupported() { + return requestObjectSigningAlgValuesSupported; + } + + /** + * Sets a list of the JWS signing algorithms (alg values) supported by the + * Authorization Server for the OpenID Request Object. + * + * @param requestObjectSigningAlgValuesSupported A list of the JWS signing algorithms. + */ + public void setRequestObjectSigningAlgValuesSupported(List requestObjectSigningAlgValuesSupported) { + this.requestObjectSigningAlgValuesSupported = requestObjectSigningAlgValuesSupported; + } + + /** + * Returns a list of the JWE encryption algorithms (alg values) supported by + * the Authorization Server for the OpenID Request Object. + * + * @return A list of the JWE encryption algorithms. + */ + public List getRequestObjectEncryptionAlgValuesSupported() { + return requestObjectEncryptionAlgValuesSupported; + } + + /** + * Sets a list of the JWE encryption algorithms (alg values) supported by + * the Authorization Server for the OpenID Request Object. + * + * @param requestObjectEncryptionAlgValuesSupported A list of the JWE encryption algorithms. + */ + public void setRequestObjectEncryptionAlgValuesSupported(List requestObjectEncryptionAlgValuesSupported) { + this.requestObjectEncryptionAlgValuesSupported = requestObjectEncryptionAlgValuesSupported; + } + + /** + * Returns a list of the JWE encryption algorithms (enc values) supported by + * the Authorization Server for the OpenID Request Object. + * + * @return A list of the JWE encryption algorithms. + */ + public List getRequestObjectEncryptionEncValuesSupported() { + return requestObjectEncryptionEncValuesSupported; + } + + /** + * Sets a list of the JWE encryption algorithms (enc values) supported by + * the Authorization Server for the OpenID Request Object. + * + * @param requestObjectEncryptionEncValuesSupported A list of the JWE encryption algorithms. + */ + public void setRequestObjectEncryptionEncValuesSupported(List requestObjectEncryptionEncValuesSupported) { + this.requestObjectEncryptionEncValuesSupported = requestObjectEncryptionEncValuesSupported; + } + + /** + * Returns a list of authentication types supported by this Token Endpoint. + * The options are client_secret_post, client_secret_basic, + * client_secret_jwt, and private_key_jwt. Other authentication types may be + * defined by extension. If unspecified or omitted, the default is + * client_secret_basic, the HTTP Basic Authentication Scheme. + * + * @return A list of authentication types. + */ + public List getTokenEndpointAuthMethodsSupported() { + return tokenEndpointAuthMethodsSupported; + } + + /** + * Sets a list of authentication types supported by this Token Endpoint. The + * options are client_secret_post, client_secret_basic, client_secret_jwt, + * and private_key_jwt. Other authentication types may be defined by + * extension. If unspecified or omitted, the default is client_secret_basic, + * the HTTP Basic Authentication Scheme. + * + * @param tokenEndpointAuthMethodsSupported A list of authentication types. + */ + public void setTokenEndpointAuthMethodsSupported(List tokenEndpointAuthMethodsSupported) { + this.tokenEndpointAuthMethodsSupported = tokenEndpointAuthMethodsSupported; + } + + /** + * Returns a list of the JWS signing algorithms (alg values) supported by + * the Token Endpoint for the private_key_jwt and client_secret_jwt methods + * to encode the JWT. Servers SHOULD support RS256. + * + * @return A list of the JWS signing algorithms. + */ + public List getTokenEndpointAuthSigningAlgValuesSupported() { + return tokenEndpointAuthSigningAlgValuesSupported; + } + + /** + * Sets a list of the JWS signing algorithms (alg values) supported by the + * Token Endpoint for the private_key_jwt and client_secret_jwt methods to + * encode the JWT. Servers SHOULD support RS256. + * + * @param tokenEndpointAuthSigningAlgValuesSupported A list of the JWS signing algorithms. + */ + public void setTokenEndpointAuthSigningAlgValuesSupported(List tokenEndpointAuthSigningAlgValuesSupported) { + this.tokenEndpointAuthSigningAlgValuesSupported = tokenEndpointAuthSigningAlgValuesSupported; + } + + /** + * Returns a list of the display parameter values that the OpenID Provider + * supports. + * + * @return A list of the display parameter values. + */ + public List getDisplayValuesSupported() { + return displayValuesSupported; + } + + /** + * Sets a list of the display parameter values that the OpenID Provider + * supports. + * + * @param displayValuesSupported A list of the display parameter values. + */ + public void setDisplayValuesSupported(List displayValuesSupported) { + this.displayValuesSupported = displayValuesSupported; + } + + /** + * Returns a list of the claim types that the OpenID Provider supports. If + * not specified, the implementation supports only normal claims. + * + * @return A list of the claim types. + */ + public List getClaimTypesSupported() { + return claimTypesSupported; + } + + /** + * Sets a list of the claim types that the OpenID Provider supports. If not + * specified, the implementation supports only normal claims. + * + * @param claimTypesSupported A list of the claim types. + */ + public void setClaimTypesSupported(List claimTypesSupported) { + this.claimTypesSupported = claimTypesSupported; + } + + /** + * Returns a list of the Claim Names of the Claims that the OpenID Provider + * may be able to supply values for. Note that for privacy or other reasons, + * this may not be an exhaustive list. + * + * @return A list of Claim Names. + */ + public List getClaimsSupported() { + return claimsSupported; + } + + /** + * Sets a list of the Claim Names of the Claims that the OpenID Provider may + * be able to supply values for. Note that for privacy or other reasons, + * this may not be an exhaustive list. + * + * @param claimsSupported A list of Claim Names. + */ + public void setClaimsSupported(List claimsSupported) { + this.claimsSupported = claimsSupported; + } + + + public List getIdTokenTokenBindingCnfValuesSupported() { + return idTokenTokenBindingCnfValuesSupported; + } + + public void setIdTokenTokenBindingCnfValuesSupported(List idTokenTokenBindingCnfValuesSupported) { + this.idTokenTokenBindingCnfValuesSupported = idTokenTokenBindingCnfValuesSupported; + } + + /** + * Returns an URL of a page containing human-readable information that + * developers might want or need to know when using the OpenID Provider. In + * particular, if the OpenID Provider does not support dynamic client + * registration, then information on how to register clients should be + * provided in this documentation. + * + * @return An URL with information for developers. + */ + public String getServiceDocumentation() { + return serviceDocumentation; + } + + /** + * Sets an URL of a page containing human-readable information that + * developers might want or need to know when using the OpenID Provider. In + * particular, if the OpenID Provider does not support dynamic client + * registration, then information on how to register clients should be + * provided in this documentation. + * + * @param serviceDocumentation An URL with information for developers. + */ + public void setServiceDocumentation(String serviceDocumentation) { + this.serviceDocumentation = serviceDocumentation; + } + + /** + * Returns a list of languages and scripts supported for values in Claims + * being returned. + * + * @return A list of languages and scripts supported for values in Claims + * being returned. + */ + public List getClaimsLocalesSupported() { + return claimsLocalesSupported; + } + + /** + * Sets a list of languages and scripts supported for values in Claims being + * returned. + * + * @param claimsLocalesSupported A list of languages and scripts supported for values in Claims + * being returned. + */ + public void setClaimsLocalesSupported(List claimsLocalesSupported) { + this.claimsLocalesSupported = claimsLocalesSupported; + } + + /** + * Returns a list of languages and scripts supported for the user interface. + * + * @return A list of languages and scripts supported for the user interface. + */ + public List getUiLocalesSupported() { + return uiLocalesSupported; + } + + /** + * Sets a list of languages and scripts supported for the user interface. + * + * @param uiLocalesSupported A list of languages and scripts supported for the user + * interface. + */ + public void setUiLocalesSupported(List uiLocalesSupported) { + this.uiLocalesSupported = uiLocalesSupported; + } + + /** + * Returns a Boolean value specifying whether the OP supports use of the + * claims parameter, with true indicating support. If omitted, + * the default value is false. + * + * @return A Boolean value specifying whether the OP supports use of the + * claims parameter. + */ + public Boolean getClaimsParameterSupported() { + return claimsParameterSupported; + } + + /** + * Sets a Boolean value specifying whether the OP supports use of the claims + * parameter, with true indicating support. If omitted, the + * default value is false. + * + * @param claimsParameterSupported A Boolean value specifying whether the OP supports use of the + * claims parameter. + */ + public void setClaimsParameterSupported(Boolean claimsParameterSupported) { + this.claimsParameterSupported = claimsParameterSupported; + } + + /** + * Returns a Boolean value specifying whether the OP supports use of the + * request parameter, with true indicating support. If omitted, + * the default value is false. + * + * @return A Boolean value specifying whether the OP supports use of the + * request parameter. + */ + public Boolean getRequestParameterSupported() { + return requestParameterSupported; + } + + /** + * Sets a Boolean value specifying whether the OP supports use of the + * request parameter, with true indicating support. If omitted, + * the default value is false. + * + * @param requestParameterSupported A Boolean value specifying whether the OP supports use of the + * request parameter. + */ + public void setRequestParameterSupported(Boolean requestParameterSupported) { + this.requestParameterSupported = requestParameterSupported; + } + + /** + * Returns a Boolean value specifying whether the OP supports use of the + * request_uri parameter, with true indicating support. If + * omitted, the default value is true. + * + * @return A Boolean value specifying whether the OP supports use of the + * request_uri parameter. + */ + public Boolean getRequestUriParameterSupported() { + return requestUriParameterSupported; + } + + /** + * Sets a Boolean value specifying whether the OP supports use of the + * request_uri parameter, with true indicating support. If + * omitted, the default value is true. + * + * @param requestUriParameterSupported A Boolean value specifying whether the OP supports use of the + * request_uri parameter. + */ + public void setRequestUriParameterSupported(Boolean requestUriParameterSupported) { + this.requestUriParameterSupported = requestUriParameterSupported; + } + + /** + * Returns a Boolean value specifying whether the OP requires any + * request_uri values used to be pre-registered using the request_uris + * registration parameter. Pre-registration is required when the value is + * true. + * + * @return A Boolean value specifying whether the OP requires any + * request_uri values used to be pre-registered using the + * request_uris registration parameter. + */ + public Boolean getRequireRequestUriRegistration() { + return requireRequestUriRegistration; + } + + /** + * Sets a Boolean value specifying whether the OP requires any request_uri + * values used to be pre-registered using the request_uris registration + * parameter. Pre-registration is required when the value is + * true. + * + * @param requireRequestUriRegistration A Boolean value specifying whether the OP requires any + * request_uri values used to be pre-registered using the + * request_uris registration parameter. + */ + public void setRequireRequestUriRegistration(Boolean requireRequestUriRegistration) { + this.requireRequestUriRegistration = requireRequestUriRegistration; + } + + /** + * Returns a URL that the OpenID Provider provides to the person registering + * the Client to read about the OP's requirements on how the Relying Party + * may use the data provided by the OP. + * + * @return The OP's policy URI. + */ + public String getOpPolicyUri() { + return opPolicyUri; + } + + /** + * Sets a URL that the OpenID Provider provides to the person registering + * the Client to read about the OP's requirements on how the Relying Party + * may use the data provided by the OP. + * + * @param opPolicyUri The OP's policy URI. + */ + public void setOpPolicyUri(String opPolicyUri) { + this.opPolicyUri = opPolicyUri; + } + + /** + * Returns a URL that the OpenID Provider provides to the person registering + * the Client to read about OpenID Provider's terms of service. + * + * @return The OP's policy URI. + */ + public String getOpTosUri() { + return opTosUri; + } + + /** + * Sets a URL that the OpenID Provider provides to the person registering + * the Client to read about OpenID Provider's terms of service. + * + * @param opTosUri The OP's policy URI. + */ + public void setOpTosUri(String opTosUri) { + this.opTosUri = opTosUri; + } + + public Boolean getFrontChannelLogoutSupported() { + return frontChannelLogoutSupported; + } + + public void setFrontChannelLogoutSupported(Boolean frontChannelLogoutSupported) { + this.frontChannelLogoutSupported = frontChannelLogoutSupported; + } + + public Boolean getBackchannelLogoutSupported() { + return backchannelLogoutSupported; + } + + public void setBackchannelLogoutSupported(Boolean backchannelLogoutSupported) { + this.backchannelLogoutSupported = backchannelLogoutSupported; + } + + public Boolean getBackchannelLogoutSessionSupported() { + return backchannelLogoutSessionSupported; + } + + public void setBackchannelLogoutSessionSupported(Boolean backchannelLogoutSessionSupported) { + this.backchannelLogoutSessionSupported = backchannelLogoutSessionSupported; + } + + public Boolean getTlsClientCertificateBoundAccessTokens() { + return tlsClientCertificateBoundAccessTokens; + } + + public void setTlsClientCertificateBoundAccessTokens(Boolean tlsClientCertificateBoundAccessTokens) { + this.tlsClientCertificateBoundAccessTokens = tlsClientCertificateBoundAccessTokens; + } + + public Boolean getFrontChannelLogoutSessionSupported() { + return frontChannelLogoutSessionSupported; + } + + public void setFrontChannelLogoutSessionSupported(Boolean frontChannelLogoutSessionSupported) { + this.frontChannelLogoutSessionSupported = frontChannelLogoutSessionSupported; + } + + public String getBackchannelAuthenticationEndpoint() { + return backchannelAuthenticationEndpoint; + } + + public void setBackchannelAuthenticationEndpoint(String backchannelAuthenticationEndpoint) { + this.backchannelAuthenticationEndpoint = backchannelAuthenticationEndpoint; + } + + public List getBackchannelTokenDeliveryModesSupported() { + return backchannelTokenDeliveryModesSupported; + } + + public void setBackchannelTokenDeliveryModesSupported(List backchannelTokenDeliveryModesSupported) { + this.backchannelTokenDeliveryModesSupported = backchannelTokenDeliveryModesSupported; + } + + public List getBackchannelAuthenticationRequestSigningAlgValuesSupported() { + return backchannelAuthenticationRequestSigningAlgValuesSupported; + } + + public void setBackchannelAuthenticationRequestSigningAlgValuesSupported(List backchannelAuthenticationRequestSigningAlgValuesSupported) { + this.backchannelAuthenticationRequestSigningAlgValuesSupported = backchannelAuthenticationRequestSigningAlgValuesSupported; + } + + public Boolean getBackchannelUserCodeParameterSupported() { + return backchannelUserCodeParameterSupported; + } + + public void setBackchannelUserCodeParameterSupported(Boolean backchannelUserCodeParameterSupported) { + this.backchannelUserCodeParameterSupported = backchannelUserCodeParameterSupported; + } + + public String getDeviceAuthzEndpoint() { + return deviceAuthzEndpoint; + } + + public void setDeviceAuthzEndpoint(String deviceAuthzEndpoint) { + this.deviceAuthzEndpoint = deviceAuthzEndpoint; + } + + @Override + public String toString() { + return "OpenIdConfigurationResponse{" + + "issuer='" + issuer + '\'' + + ", authorizationEndpoint='" + authorizationEndpoint + '\'' + + ", tokenEndpoint='" + tokenEndpoint + '\'' + + ", revocationEndpoint='" + revocationEndpoint + '\'' + + ", userInfoEndpoint='" + userInfoEndpoint + '\'' + + ", clientInfoEndpoint='" + clientInfoEndpoint + '\'' + + ", checkSessionIFrame='" + checkSessionIFrame + '\'' + + ", endSessionEndpoint='" + endSessionEndpoint + '\'' + + ", jwksUri='" + jwksUri + '\'' + + ", registrationEndpoint='" + registrationEndpoint + '\'' + + ", idGenerationEndpoint='" + idGenerationEndpoint + '\'' + + ", introspectionEndpoint='" + introspectionEndpoint + '\'' + + ", deviceAuthzEndpoint='" + deviceAuthzEndpoint + '\'' + + ", scopesSupported=" + scopesSupported + + ", responseTypesSupported=" + responseTypesSupported + + ", responseModesSupported=" + responseModesSupported + + ", grantTypesSupported=" + grantTypesSupported + + ", acrValuesSupported=" + acrValuesSupported + + ", subjectTypesSupported=" + subjectTypesSupported + + ", userInfoSigningAlgValuesSupported=" + userInfoSigningAlgValuesSupported + + ", userInfoEncryptionAlgValuesSupported=" + userInfoEncryptionAlgValuesSupported + + ", userInfoEncryptionEncValuesSupported=" + userInfoEncryptionEncValuesSupported + + ", idTokenSigningAlgValuesSupported=" + idTokenSigningAlgValuesSupported + + ", idTokenEncryptionAlgValuesSupported=" + idTokenEncryptionAlgValuesSupported + + ", idTokenEncryptionEncValuesSupported=" + idTokenEncryptionEncValuesSupported + + ", requestObjectSigningAlgValuesSupported=" + requestObjectSigningAlgValuesSupported + + ", requestObjectEncryptionAlgValuesSupported=" + requestObjectEncryptionAlgValuesSupported + + ", requestObjectEncryptionEncValuesSupported=" + requestObjectEncryptionEncValuesSupported + + ", tokenEndpointAuthMethodsSupported=" + tokenEndpointAuthMethodsSupported + + ", tokenEndpointAuthSigningAlgValuesSupported=" + tokenEndpointAuthSigningAlgValuesSupported + + ", displayValuesSupported=" + displayValuesSupported + + ", claimTypesSupported=" + claimTypesSupported + + ", claimsSupported=" + claimsSupported + + ", idTokenTokenBindingCnfValuesSupported=" + idTokenTokenBindingCnfValuesSupported + + ", serviceDocumentation='" + serviceDocumentation + '\'' + + ", claimsLocalesSupported=" + claimsLocalesSupported + + ", uiLocalesSupported=" + uiLocalesSupported + + ", claimsParameterSupported=" + claimsParameterSupported + + ", requestParameterSupported=" + requestParameterSupported + + ", requestUriParameterSupported=" + requestUriParameterSupported + + ", tlsClientCertificateBoundAccessTokens=" + tlsClientCertificateBoundAccessTokens + + ", frontChannelLogoutSupported=" + frontChannelLogoutSupported + + ", frontChannelLogoutSessionSupported=" + frontChannelLogoutSessionSupported + + ", backchannelLogoutSupported=" + backchannelLogoutSupported + + ", backchannelLogoutSessionSupported=" + backchannelLogoutSessionSupported + + ", requireRequestUriRegistration=" + requireRequestUriRegistration + + ", opPolicyUri='" + opPolicyUri + '\'' + + ", opTosUri='" + opTosUri + '\'' + + ", scopeToClaimsMapping=" + scopeToClaimsMapping + '\'' + + ", backchannelAuthenticationEndpoint=" + backchannelAuthenticationEndpoint + '\'' + + ", backchannelTokenDeliveryModesSupported=" + backchannelTokenDeliveryModesSupported + '\'' + + ", backchannelAuthenticationRequestSigningAlgValuesSupported=" + backchannelAuthenticationRequestSigningAlgValuesSupported + '\'' + + ", backchannelUserCodeParameterSupported=" + backchannelUserCodeParameterSupported + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/OpenIdConnectDiscoveryClient.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/OpenIdConnectDiscoveryClient.java new file mode 100644 index 00000000..beef8f12 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/OpenIdConnectDiscoveryClient.java @@ -0,0 +1,130 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import static org.gluu.oxauth.model.discovery.WebFingerParam.HREF; +import static org.gluu.oxauth.model.discovery.WebFingerParam.LINKS; +import static org.gluu.oxauth.model.discovery.WebFingerParam.REL; +import static org.gluu.oxauth.model.discovery.WebFingerParam.REL_VALUE; +import static org.gluu.oxauth.model.discovery.WebFingerParam.RESOURCE; +import static org.gluu.oxauth.model.discovery.WebFingerParam.SUBJECT; + +import java.io.InputStream; +import java.net.URISyntaxException; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.client.Invocation.Builder; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.gluu.oxauth.model.discovery.WebFingerLink; +import org.jboss.resteasy.client.jaxrs.ClientHttpEngine; +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * @author Javier Rojas Blum + * @version December 26, 2016 + */ +public class OpenIdConnectDiscoveryClient extends BaseClient { + + private static final Logger LOG = Logger.getLogger(OpenIdConnectDiscoveryClient.class); + + private static final String MEDIA_TYPE = MediaType.APPLICATION_JSON; + private static final String SCHEMA = "https://"; + private static final String PATH = "/.well-known/webfinger"; + + public OpenIdConnectDiscoveryClient(String resource) throws URISyntaxException { + setRequest(new OpenIdConnectDiscoveryRequest(resource)); + setUrl(SCHEMA + getRequest().getHost() + PATH); + } + + @Override + public String getHttpMethod() { + return HttpMethod.GET; + } + + public OpenIdConnectDiscoveryResponse exec() { + initClientRequest(); + + return _exec(); + } + + @Deprecated + public OpenIdConnectDiscoveryResponse exec(ClientHttpEngine engine) { + resteasyClient = ((ResteasyClientBuilder) ResteasyClientBuilder.newBuilder()).httpEngine(engine).build(); + webTarget = resteasyClient.target(getUrl()); + + return _exec(); + } + + private OpenIdConnectDiscoveryResponse _exec() { + OpenIdConnectDiscoveryResponse response = null; + + try { + response = _exec2(); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } finally { + closeConnection(); + } + + return response; + } + + private OpenIdConnectDiscoveryResponse _exec2() { + // Prepare request parameters + if (StringUtils.isNotBlank(getRequest().getResource())) { + addReqParam(RESOURCE, getRequest().getResource()); + } + addReqParam(REL, REL_VALUE); + + // Call REST Service and handle response + Response clientResponse1; + try { + Builder clientRequest = webTarget.request(); + applyCookies(clientRequest); + + clientRequest.accept(MEDIA_TYPE); +// clientRequest.setHttpMethod(getHttpMethod()); + clientResponse1 = clientRequest.buildGet().invoke(); + + int status = clientResponse1.getStatus(); + + setResponse(new OpenIdConnectDiscoveryResponse(status)); + + String entity = clientResponse1.readEntity(String.class); + getResponse().setEntity(entity); + getResponse().setHeaders(clientResponse1.getMetadata()); + if (StringUtils.isNotBlank(entity)) { + JSONObject jsonObj = new JSONObject(entity); + getResponse().setSubject(jsonObj.getString(SUBJECT)); + JSONArray linksJsonArray = jsonObj.getJSONArray(LINKS); + for (int i = 0; i < linksJsonArray.length(); i++) { + WebFingerLink webFingerLink = new WebFingerLink(); + webFingerLink.setRel(linksJsonArray.getJSONObject(i).getString(REL)); + webFingerLink.setHref(linksJsonArray.getJSONObject(i).getString(HREF)); + + getResponse().getLinks().add(webFingerLink); + } + } + } catch (JSONException e) { + LOG.error(e.getMessage(), e); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } + + return getResponse(); + } + +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/OpenIdConnectDiscoveryRequest.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/OpenIdConnectDiscoveryRequest.java new file mode 100644 index 00000000..9d2397b2 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/OpenIdConnectDiscoveryRequest.java @@ -0,0 +1,133 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import static org.gluu.oxauth.model.discovery.WebFingerParam.REL; +import static org.gluu.oxauth.model.discovery.WebFingerParam.REL_VALUE; +import static org.gluu.oxauth.model.discovery.WebFingerParam.RESOURCE; +import static org.gluu.oxauth.model.util.StringUtils.addQueryStringParam; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; + +import org.apache.commons.lang.StringUtils; + +/** + * @author Javier Rojas Blum Date: 01.28.2013 + */ +public class OpenIdConnectDiscoveryRequest extends BaseRequest { + + private String resource; + private String host; + private String path; + + public OpenIdConnectDiscoveryRequest(String resource) throws URISyntaxException { + this.resource = resource; + + if (StringUtils.isBlank(resource)) { + throw new IllegalArgumentException("Resource cannot be null"); + } + if (resource.startsWith("=") || resource.startsWith("@") || resource.startsWith("!")) { // XRI + throw new UnsupportedOperationException("XRI is not supported"); // TODO: Add support for XRI + } else if (resource.contains("@")) { // email + this.host = resource.substring(resource.indexOf("@") + 1); + } else { + if (!resource.contains("://")) { + // If the user input Identifier does not have an RFC 3986 "scheme" portion, + // the string is interpreted as authority path-abempty [ "?" query ] [ "#" fragment ] of RFC 3986. + // In this case, the https scheme is assumed, and the normalized URL will be formed by prefixing + // https:// to the string. + resource = "https://" + resource; + } + + URI uri = new URI(resource); + this.host = uri.getHost(); + if (uri.getPort() != -1) { + this.host += ":" + uri.getPort(); + } + if (StringUtils.isNotBlank(uri.getPath()) && !uri.getPath().equals(uri.getHost()) && !uri.getPath().equals("/")) { + this.path = uri.getPath(); + } + } + } + + /** + * Returns the Identifier of the target End-User that is the subject of the discovery request. + * + * @return The Identifier of the target End-User that is the subject of the discovery request. + */ + public String getResource() { + return resource; + } + + /** + * Sets the Identifier of the target End-User that is the subject of the discovery request. + * + * @param resource The Identifier of the target End-User that is the subject of the discovery request. + */ + public void setResource(String resource) { + this.resource = resource; + } + + /** + * Returns the Server where a WebFinger service is hosted. + * + * @return The Server where a WebFinger service is hosted. + */ + public String getHost() { + return host; + } + + /** + * Sets the Server where a WebFinger service is hosted. + * + * @param host The Server where a WebFinger service is hosted. + */ + public void setHost(String host) { + this.host = host; + } + + /** + * If the Issuer value contains a path component, any terminating / must be removed before + * appending /.well-known/openid-configuration. Then the Client may make a new request using the path. + * + * @return The path component. + */ + public String getPath() { + return path; + } + + /** + * Sets the path component. + * + * @param path The path component. + */ + public void setPath(String path) { + this.path = path; + } + + /** + * Returns a query string with the parameters of the OpenID Connect Discovery request. + * Any null or empty parameter will be omitted. + * + * @return A query string of parameters. + */ + @Override + public String getQueryString() { + StringBuilder queryStringBuilder = new StringBuilder(); + + try { + addQueryStringParam(queryStringBuilder, RESOURCE, resource); + addQueryStringParam(queryStringBuilder, REL, REL_VALUE); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + + return queryStringBuilder.toString(); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/OpenIdConnectDiscoveryResponse.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/OpenIdConnectDiscoveryResponse.java new file mode 100644 index 00000000..d212ad30 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/OpenIdConnectDiscoveryResponse.java @@ -0,0 +1,47 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import java.util.ArrayList; +import java.util.List; + +import org.gluu.oxauth.model.discovery.WebFingerLink; + +/** + * @author Javier Rojas Blum Date: 01.28.2013 + */ +public class OpenIdConnectDiscoveryResponse extends BaseResponse { + + private String subject; + private List links; + + /** + * Constructs an OpenID Connect Discovery Response. + * + * @param status The response status code. + */ + public OpenIdConnectDiscoveryResponse(int status) { + super(status); + links = new ArrayList(); + } + + public String getSubject() { + return subject; + } + + public void setSubject(String subject) { + this.subject = subject; + } + + public List getLinks() { + return links; + } + + public void setLinks(List links) { + this.links = links; + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/QueryStringDecoder.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/QueryStringDecoder.java new file mode 100644 index 00000000..755b1d2a --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/QueryStringDecoder.java @@ -0,0 +1,51 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.lang.StringUtils; + +/** + * Provides functionality to parse query strings. + * + * @author Javier Rojas Blum Date: 09.29.2011 + */ +public class QueryStringDecoder { + + /** + * Avoid instance creation + */ + private QueryStringDecoder() { + } + + /** + * Decodes a query string and returns a map with the parsed query string + * parameters as keys and its values. + * + * @param queryString The query string. + * @return A map with the parsed query string parameters and its values. + */ + public static Map decode(String queryString) { + Map map = new HashMap(); + + if (queryString != null) { + String[] params = queryString.split("&"); + for (String param : params) { + String[] nameValue = param.split("="); + String name = nameValue.length > 0 ? nameValue[0] : ""; + String value = nameValue.length > 1 ? nameValue[1] : ""; + if (StringUtils.isNotBlank(name)) { + map.put(name, value); + } + } + } + + return map; + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/RegisterClient.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/RegisterClient.java new file mode 100644 index 00000000..eca73b62 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/RegisterClient.java @@ -0,0 +1,386 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import static org.gluu.oxauth.model.register.RegisterRequestParam.ACCESS_TOKEN_AS_JWT; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ACCESS_TOKEN_LIFETIME; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ACCESS_TOKEN_SIGNING_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ALLOW_SPONTANEOUS_SCOPES; +import static org.gluu.oxauth.model.register.RegisterRequestParam.APPLICATION_TYPE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.AUTHORIZED_ORIGINS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_LOGOUT_SESSION_REQUIRED; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_LOGOUT_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_TOKEN_DELIVERY_MODE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_USER_CODE_PARAMETER; +import static org.gluu.oxauth.model.register.RegisterRequestParam.CLAIMS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.CLAIMS_REDIRECT_URIS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.CLIENT_NAME; +import static org.gluu.oxauth.model.register.RegisterRequestParam.CLIENT_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.CONTACTS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.DEFAULT_ACR_VALUES; +import static org.gluu.oxauth.model.register.RegisterRequestParam.DEFAULT_MAX_AGE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED; +import static org.gluu.oxauth.model.register.RegisterRequestParam.FRONT_CHANNEL_LOGOUT_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.GRANT_TYPES; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ID_TOKEN_ENCRYPTED_RESPONSE_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ID_TOKEN_ENCRYPTED_RESPONSE_ENC; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ID_TOKEN_SIGNED_RESPONSE_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ID_TOKEN_TOKEN_BINDING_CNF; +import static org.gluu.oxauth.model.register.RegisterRequestParam.INITIATE_LOGIN_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.JWKS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.JWKS_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.KEEP_CLIENT_AUTHORIZATION_AFTER_EXPIRATION; +import static org.gluu.oxauth.model.register.RegisterRequestParam.LOGO_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.POLICY_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.POST_LOGOUT_REDIRECT_URIS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REDIRECT_URIS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REQUEST_OBJECT_ENCRYPTION_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REQUEST_OBJECT_ENCRYPTION_ENC; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REQUEST_OBJECT_SIGNING_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REQUEST_URIS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REQUIRE_AUTH_TIME; +import static org.gluu.oxauth.model.register.RegisterRequestParam.RESPONSE_TYPES; +import static org.gluu.oxauth.model.register.RegisterRequestParam.RPT_AS_JWT; +import static org.gluu.oxauth.model.register.RegisterRequestParam.RUN_INTROSPECTION_SCRIPT_BEFORE_ACCESS_TOKEN_CREATION_AS_JWT_AND_INCLUDE_CLAIMS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SCOPE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SCOPES; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SECTOR_IDENTIFIER_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SOFTWARE_ID; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SOFTWARE_STATEMENT; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SOFTWARE_VERSION; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SPONTANEOUS_SCOPES; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SUBJECT_TYPE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.TLS_CLIENT_AUTH_SUBJECT_DN; +import static org.gluu.oxauth.model.register.RegisterRequestParam.TOKEN_ENDPOINT_AUTH_METHOD; +import static org.gluu.oxauth.model.register.RegisterRequestParam.TOKEN_ENDPOINT_AUTH_SIGNING_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.TOS_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.USERINFO_ENCRYPTED_RESPONSE_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.USERINFO_ENCRYPTED_RESPONSE_ENC; +import static org.gluu.oxauth.model.register.RegisterRequestParam.USERINFO_SIGNED_RESPONSE_ALG; +import static org.gluu.oxauth.model.util.StringUtils.implode; + +import java.util.List; +import java.util.Map; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation.Builder; +import javax.ws.rs.core.MediaType; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.util.ClientUtil; +import org.jboss.resteasy.client.jaxrs.ClientHttpEngine; +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; +import org.json.JSONArray; +import org.json.JSONObject; + +/** + * Encapsulates functionality to make Register request calls to an authorization server via REST Services. + * + * @author Javier Rojas Blum + * @author Yuriy Zabrovarnyy + * @author Yuriy Movchan + * @version August 20, 2019 + */ +public class RegisterClient extends BaseClient { + + private static final Logger LOG = Logger.getLogger(RegisterClient.class); + + /** + * Construct a register client by providing an URL where the REST service is located. + * + * @param url The REST service location. + */ + public RegisterClient(String url) { + super(url); + } + + @Override + public String getHttpMethod() { + if (getRequest() != null) { + if (StringUtils.isNotBlank(getRequest().getHttpMethod())) { + return getRequest().getHttpMethod(); + } + if (getRequest().getRegistrationAccessToken() != null) { + return HttpMethod.GET; + } + } + + return HttpMethod.POST; + } + + /** + * Executes the call to the REST service requesting to register and process the response. + * + * @param applicationType The application type. + * @param clientName The client name. + * @param redirectUri A list of space-delimited redirection URIs. + * @return The service response. + */ + public RegisterResponse execRegister(ApplicationType applicationType, + String clientName, List redirectUri) { + setRequest(new RegisterRequest(applicationType, clientName, redirectUri)); + + return exec(); + } + + public RegisterResponse exec() { + initClientRequest(); + return _exec(); + } + + @Deprecated + public RegisterResponse exec(ClientHttpEngine engine) { + resteasyClient = ((ResteasyClientBuilder) ResteasyClientBuilder.newBuilder()).httpEngine(engine).build(); + webTarget = resteasyClient.target(getUrl()); + + return _exec(); + } + + private RegisterResponse _exec() { + try { + // Prepare request parameters + // clientRequest.setHttpMethod(getHttpMethod()); + + Entity requestEntity = null; + // POST - Client Register, PUT - update client + if (getHttpMethod().equals(HttpMethod.POST) || getHttpMethod().equals(HttpMethod.PUT)) { + JSONObject requestBody = new JSONObject(); + if (getRequest().getRedirectUris() != null && !getRequest().getRedirectUris().isEmpty()) { + requestBody.put(REDIRECT_URIS.toString(), new JSONArray(getRequest().getRedirectUris())); + } + if (getRequest().getClaimsRedirectUris() != null && !getRequest().getClaimsRedirectUris().isEmpty()) { + requestBody.put(CLAIMS_REDIRECT_URIS.toString(), new JSONArray(getRequest().getClaimsRedirectUris())); + } + if (getRequest().getResponseTypes() != null && !getRequest().getResponseTypes().isEmpty()) { + requestBody.put(RESPONSE_TYPES.toString(), new JSONArray(getRequest().getResponseTypes_())); + } + if (getRequest().getGrantTypes() != null && !getRequest().getGrantTypes().isEmpty()) { + requestBody.put(GRANT_TYPES.toString(), new JSONArray(getRequest().getGrantTypes())); + } + if (getRequest().getApplicationType() != null) { + requestBody.put(APPLICATION_TYPE.toString(), getRequest().getApplicationType()); + } + if (getRequest().getContacts() != null && !getRequest().getContacts().isEmpty()) { + requestBody.put(CONTACTS.toString(), new JSONArray(getRequest().getContacts())); + } + if (StringUtils.isNotBlank(getRequest().getClientName())) { + requestBody.put(CLIENT_NAME.toString(), getRequest().getClientName()); + } + if (StringUtils.isNotBlank(getRequest().getIdTokenTokenBindingCnf())) { + requestBody.put(ID_TOKEN_TOKEN_BINDING_CNF.toString(), getRequest().getIdTokenTokenBindingCnf()); + } + if (StringUtils.isNotBlank(getRequest().getLogoUri())) { + requestBody.put(LOGO_URI.toString(), getRequest().getLogoUri()); + } + if (StringUtils.isNotBlank(getRequest().getClientUri())) { + requestBody.put(CLIENT_URI.toString(), getRequest().getClientUri()); + } + if (StringUtils.isNotBlank(getRequest().getPolicyUri())) { + requestBody.put(POLICY_URI.toString(), getRequest().getPolicyUri()); + } + if (StringUtils.isNotBlank(getRequest().getTosUri())) { + requestBody.put(TOS_URI.toString(), getRequest().getTosUri()); + } + if (StringUtils.isNotBlank(getRequest().getJwksUri())) { + requestBody.put(JWKS_URI.toString(), getRequest().getJwksUri()); + } + if (StringUtils.isNotBlank(getRequest().getJwks())) { + requestBody.put(JWKS.toString(), getRequest().getJwks()); + } + if (StringUtils.isNotBlank(getRequest().getSectorIdentifierUri())) { + requestBody.put(SECTOR_IDENTIFIER_URI.toString(), getRequest().getSectorIdentifierUri()); + } + if (getRequest().getSubjectType() != null) { + requestBody.put(SUBJECT_TYPE.toString(), getRequest().getSubjectType()); + } + if (getRequest().getAccessTokenAsJwt() != null) { + requestBody.put(ACCESS_TOKEN_AS_JWT.toString(), getRequest().getAccessTokenAsJwt().toString()); + } + if (getRequest().getAccessTokenSigningAlg() != null) { + requestBody.put(ACCESS_TOKEN_SIGNING_ALG.toString(), getRequest().getAccessTokenSigningAlg().toString()); + } + if (getRequest().getRptAsJwt() != null) { + requestBody.put(RPT_AS_JWT.toString(), getRequest().getRptAsJwt().toString()); + } + if (getRequest().getTlsClientAuthSubjectDn() != null) { + requestBody.put(TLS_CLIENT_AUTH_SUBJECT_DN.toString(), getRequest().getTlsClientAuthSubjectDn()); + } + if (getRequest().getAllowSpontaneousScopes() != null) { + requestBody.put(ALLOW_SPONTANEOUS_SCOPES.toString(), getRequest().getAllowSpontaneousScopes()); + } + if (getRequest().getSpontaneousScopes() != null) { + requestBody.put(SPONTANEOUS_SCOPES.toString(), new JSONArray(getRequest().getSpontaneousScopes())); + } + if (getRequest().getRunIntrospectionScriptBeforeAccessTokenAsJwtCreationAndIncludeClaims() != null) { + requestBody.put(RUN_INTROSPECTION_SCRIPT_BEFORE_ACCESS_TOKEN_CREATION_AS_JWT_AND_INCLUDE_CLAIMS.toString(), getRequest().getRunIntrospectionScriptBeforeAccessTokenAsJwtCreationAndIncludeClaims().toString()); + } + if (getRequest().getKeepClientAuthorizationAfterExpiration() != null) { + requestBody.put(KEEP_CLIENT_AUTHORIZATION_AFTER_EXPIRATION.toString(), getRequest().getKeepClientAuthorizationAfterExpiration().toString()); + } + if (getRequest().getIdTokenSignedResponseAlg() != null) { + requestBody.put(ID_TOKEN_SIGNED_RESPONSE_ALG.toString(), getRequest().getIdTokenSignedResponseAlg().getName()); + } + if (getRequest().getIdTokenEncryptedResponseAlg() != null) { + requestBody.put(ID_TOKEN_ENCRYPTED_RESPONSE_ALG.toString(), getRequest().getIdTokenEncryptedResponseAlg().getName()); + } + if (getRequest().getIdTokenEncryptedResponseEnc() != null) { + requestBody.put(ID_TOKEN_ENCRYPTED_RESPONSE_ENC.toString(), getRequest().getIdTokenEncryptedResponseEnc().getName()); + } + if (getRequest().getUserInfoSignedResponseAlg() != null) { + requestBody.put(USERINFO_SIGNED_RESPONSE_ALG.toString(), getRequest().getUserInfoSignedResponseAlg().getName()); + } + if (getRequest().getUserInfoEncryptedResponseAlg() != null) { + requestBody.put(USERINFO_ENCRYPTED_RESPONSE_ALG.toString(), getRequest().getUserInfoEncryptedResponseAlg().getName()); + } + if (getRequest().getUserInfoEncryptedResponseEnc() != null) { + requestBody.put(USERINFO_ENCRYPTED_RESPONSE_ENC.toString(), getRequest().getUserInfoEncryptedResponseEnc().getName()); + } + if (getRequest().getRequestObjectSigningAlg() != null) { + requestBody.put(REQUEST_OBJECT_SIGNING_ALG.toString(), getRequest().getRequestObjectSigningAlg().getName()); + } + if (getRequest().getRequestObjectEncryptionAlg() != null) { + requestBody.put(REQUEST_OBJECT_ENCRYPTION_ALG.toString(), getRequest().getRequestObjectEncryptionAlg().getName()); + } + if (getRequest().getRequestObjectEncryptionEnc() != null) { + requestBody.put(REQUEST_OBJECT_ENCRYPTION_ENC.toString(), getRequest().getRequestObjectEncryptionEnc().getName()); + } + if (getRequest().getTokenEndpointAuthMethod() != null) { + requestBody.put(TOKEN_ENDPOINT_AUTH_METHOD.toString(), getRequest().getTokenEndpointAuthMethod()); + } + if (getRequest().getTokenEndpointAuthSigningAlg() != null) { + requestBody.put(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString(), getRequest().getTokenEndpointAuthSigningAlg()); + } + if (getRequest().getDefaultMaxAge() != null) { + requestBody.put(DEFAULT_MAX_AGE.toString(), getRequest().getDefaultMaxAge()); + } + if (getRequest().getRequireAuthTime() != null) { + requestBody.put(REQUIRE_AUTH_TIME.toString(), getRequest().getRequireAuthTime()); + } + if (getRequest().getDefaultAcrValues() != null && !getRequest().getDefaultAcrValues().isEmpty()) { + requestBody.put(DEFAULT_ACR_VALUES.toString(), getRequest().getDefaultAcrValues()); + } + if (StringUtils.isNotBlank(getRequest().getInitiateLoginUri())) { + requestBody.put(INITIATE_LOGIN_URI.toString(), getRequest().getInitiateLoginUri()); + } + if (getRequest().getPostLogoutRedirectUris() != null && !getRequest().getPostLogoutRedirectUris().isEmpty()) { + requestBody.put(POST_LOGOUT_REDIRECT_URIS.toString(), getRequest().getPostLogoutRedirectUris()); + } + if (getRequest().getFrontChannelLogoutUris() != null && !getRequest().getFrontChannelLogoutUris().isEmpty()) { + requestBody.put(FRONT_CHANNEL_LOGOUT_URI.getName(), getRequest().getFrontChannelLogoutUris()); + } + if (getRequest().getFrontChannelLogoutSessionRequired() != null) { + requestBody.put(FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED.getName(), getRequest().getFrontChannelLogoutSessionRequired()); + } + if (getRequest().getBackchannelLogoutUris() != null && !getRequest().getBackchannelLogoutUris().isEmpty()) { + requestBody.put(BACKCHANNEL_LOGOUT_URI.getName(), getRequest().getBackchannelLogoutUris()); + } + if (getRequest().getBackchannelLogoutSessionRequired() != null) { + requestBody.put(BACKCHANNEL_LOGOUT_SESSION_REQUIRED.getName(), getRequest().getBackchannelLogoutSessionRequired()); + } + if (getRequest().getRequestUris() != null && !getRequest().getRequestUris().isEmpty()) { + requestBody.put(REQUEST_URIS.toString(), new JSONArray(getRequest().getRequestUris())); + } + if (getRequest().getAuthorizedOrigins() != null && !getRequest().getAuthorizedOrigins().isEmpty()) { + requestBody.put(AUTHORIZED_ORIGINS.toString(), new JSONArray(getRequest().getAuthorizedOrigins())); + } + if (getRequest().getAccessTokenLifetime() != null) { + requestBody.put(ACCESS_TOKEN_LIFETIME.toString(), getRequest().getAccessTokenLifetime()); + } + if (StringUtils.isNotBlank(getRequest().getSoftwareId())) { + requestBody.put(SOFTWARE_ID.toString(), getRequest().getSoftwareId()); + } + if (StringUtils.isNotBlank(getRequest().getSoftwareVersion())) { + requestBody.put(SOFTWARE_VERSION.toString(), getRequest().getSoftwareVersion()); + } + if (StringUtils.isNotBlank(getRequest().getSoftwareStatement())) { + requestBody.put(SOFTWARE_STATEMENT.toString(), getRequest().getSoftwareStatement()); + } + + if (getRequest().getScopes() != null && !getRequest().getScopes().isEmpty()) { + requestBody.put(SCOPES.toString(), new JSONArray(getRequest().getScopes())); + } else if (getRequest().getScope() != null && !getRequest().getScope().isEmpty()) { + String spaceSeparatedScope = implode(getRequest().getScope(), " "); + requestBody.put(SCOPE.toString(), spaceSeparatedScope); + } + + if (getRequest().getClaims() != null && !getRequest().getClaims().isEmpty()) { + String spaceSeparatedClaims = implode(getRequest().getClaims(), " "); + requestBody.put(CLAIMS.toString(), spaceSeparatedClaims); + } + + // CIBA + if (getRequest().getBackchannelTokenDeliveryMode() != null) { + requestBody.put(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString(), getRequest().getBackchannelTokenDeliveryMode()); + } + if (StringUtils.isNotBlank(getRequest().getBackchannelClientNotificationEndpoint())) { + requestBody.put(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString(), getRequest().getBackchannelClientNotificationEndpoint()); + } + if (getRequest().getBackchannelAuthenticationRequestSigningAlg() != null) { + requestBody.put(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString(), getRequest().getBackchannelAuthenticationRequestSigningAlg()); + } + if (getRequest().getBackchannelUserCodeParameter() != null) { + requestBody.put(BACKCHANNEL_USER_CODE_PARAMETER.toString(), getRequest().getBackchannelUserCodeParameter()); + } + + // Custom params + final Map customAttributes = getRequest().getCustomAttributes(); + if (customAttributes != null && !customAttributes.isEmpty()) { + for (Map.Entry entry : customAttributes.entrySet()) { + final String name = entry.getKey(); + final String value = entry.getValue(); + if (StringUtils.isNotBlank(name) && StringUtils.isNotBlank(value)) { + requestBody.put(name, value); + } + } + } + requestEntity = Entity.json(ClientUtil.toPrettyJson(requestBody)); + } + + Builder clientRequest = webTarget.request(); + applyCookies(clientRequest); + + if (getHttpMethod().equals(HttpMethod.POST) || getHttpMethod().equals(HttpMethod.PUT)) { + clientRequest.header("Content-Type", getRequest().getContentType()); + clientRequest.accept(getRequest().getMediaType()); + + if (StringUtils.isNotBlank(getRequest().getRegistrationAccessToken())) { + clientRequest.header("Authorization", "Bearer " + getRequest().getRegistrationAccessToken()); + } + } else { // GET, Client Read + clientRequest.accept(MediaType.APPLICATION_JSON); + + if (StringUtils.isNotBlank(getRequest().getRegistrationAccessToken())) { + clientRequest.header("Authorization", "Bearer " + getRequest().getRegistrationAccessToken()); + } + } + + // Call REST Service and handle response + + if (getHttpMethod().equals(HttpMethod.POST)) { + clientResponse = clientRequest.buildPost(requestEntity).invoke(); + } else if (getHttpMethod().equals(HttpMethod.PUT)) { + clientResponse = clientRequest.buildPut(requestEntity).invoke(); + } else if (getHttpMethod().equals(HttpMethod.DELETE)) { + clientResponse = clientRequest.buildDelete().invoke(); + } else { // GET + clientResponse = clientRequest.buildGet().invoke(); + } + setResponse(new RegisterResponse(clientResponse)); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } finally { + closeConnection(); + } + + return getResponse(); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/RegisterRequest.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/RegisterRequest.java new file mode 100644 index 00000000..7d87cd17 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/RegisterRequest.java @@ -0,0 +1,1822 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import static org.gluu.oxauth.model.register.RegisterRequestParam.ACCESS_TOKEN_AS_JWT; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ACCESS_TOKEN_LIFETIME; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ACCESS_TOKEN_SIGNING_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ALLOW_SPONTANEOUS_SCOPES; +import static org.gluu.oxauth.model.register.RegisterRequestParam.APPLICATION_TYPE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.AUTHORIZED_ORIGINS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_LOGOUT_SESSION_REQUIRED; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_LOGOUT_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_TOKEN_DELIVERY_MODE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_USER_CODE_PARAMETER; +import static org.gluu.oxauth.model.register.RegisterRequestParam.CLAIMS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.CLAIMS_REDIRECT_URIS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.CLIENT_NAME; +import static org.gluu.oxauth.model.register.RegisterRequestParam.CLIENT_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.CONTACTS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.DEFAULT_ACR_VALUES; +import static org.gluu.oxauth.model.register.RegisterRequestParam.DEFAULT_MAX_AGE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED; +import static org.gluu.oxauth.model.register.RegisterRequestParam.FRONT_CHANNEL_LOGOUT_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.GRANT_TYPES; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ID_TOKEN_ENCRYPTED_RESPONSE_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ID_TOKEN_ENCRYPTED_RESPONSE_ENC; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ID_TOKEN_SIGNED_RESPONSE_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ID_TOKEN_TOKEN_BINDING_CNF; +import static org.gluu.oxauth.model.register.RegisterRequestParam.INITIATE_LOGIN_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.JWKS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.JWKS_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.KEEP_CLIENT_AUTHORIZATION_AFTER_EXPIRATION; +import static org.gluu.oxauth.model.register.RegisterRequestParam.LOGO_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.POLICY_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.POST_LOGOUT_REDIRECT_URIS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REDIRECT_URIS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REQUEST_OBJECT_ENCRYPTION_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REQUEST_OBJECT_ENCRYPTION_ENC; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REQUEST_OBJECT_SIGNING_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REQUEST_URIS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REQUIRE_AUTH_TIME; +import static org.gluu.oxauth.model.register.RegisterRequestParam.RESPONSE_TYPES; +import static org.gluu.oxauth.model.register.RegisterRequestParam.RPT_AS_JWT; +import static org.gluu.oxauth.model.register.RegisterRequestParam.RUN_INTROSPECTION_SCRIPT_BEFORE_ACCESS_TOKEN_CREATION_AS_JWT_AND_INCLUDE_CLAIMS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SCOPE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SCOPES; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SECTOR_IDENTIFIER_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SOFTWARE_ID; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SOFTWARE_STATEMENT; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SOFTWARE_VERSION; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SPONTANEOUS_SCOPES; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SUBJECT_TYPE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.TLS_CLIENT_AUTH_SUBJECT_DN; +import static org.gluu.oxauth.model.register.RegisterRequestParam.TOKEN_ENDPOINT_AUTH_METHOD; +import static org.gluu.oxauth.model.register.RegisterRequestParam.TOKEN_ENDPOINT_AUTH_SIGNING_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.TOS_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.USERINFO_ENCRYPTED_RESPONSE_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.USERINFO_ENCRYPTED_RESPONSE_ENC; +import static org.gluu.oxauth.model.register.RegisterRequestParam.USERINFO_SIGNED_RESPONSE_ALG; +import static org.gluu.oxauth.model.util.StringUtils.implode; +import static org.gluu.oxauth.model.util.StringUtils.toJSONArray; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.ws.rs.core.MediaType; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.BackchannelTokenDeliveryMode; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.common.SubjectType; +import org.gluu.oxauth.model.crypto.encryption.BlockEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.encryption.KeyEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.signature.AsymmetricSignatureAlgorithm; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.json.JsonApplier; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.register.RegisterRequestParam; +import org.gluu.oxauth.util.ClientUtil; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; + +/** + * Represents a register request to send to the authorization server. + * + * @author Javier Rojas Blum + * @author Yuriy Zabrovarnyy + * @version August 20, 2019 + */ +public class RegisterRequest extends BaseRequest { + + private static final Logger log = Logger.getLogger(RegisterRequest.class); + + private String registrationAccessToken; + private List redirectUris; + private List claimsRedirectUris; + + /** + * code: authorization_code + * id_token: implicit + * token id_token: implicit + * code id_token: authorization_code, implicit + * code token: authorization_code, implicit + * code token id_token: authorization_code, implicit + * + * https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata + */ + private List responseTypes; + private List grantTypes; + private ApplicationType applicationType; + private List contacts; + private String clientName; + private String logoUri; + private String clientUri; + private String policyUri; + private List frontChannelLogoutUris; + private Boolean frontChannelLogoutSessionRequired; + private List backchannelLogoutUris; + private Boolean backchannelLogoutSessionRequired; + private String tosUri; + private String jwksUri; + private String jwks; + private String sectorIdentifierUri; + private String idTokenTokenBindingCnf; + private String tlsClientAuthSubjectDn; + private Boolean allowSpontaneousScopes; + private List spontaneousScopes; + private Boolean runIntrospectionScriptBeforeAccessTokenAsJwtCreationAndIncludeClaims; + private Boolean keepClientAuthorizationAfterExpiration; + private SubjectType subjectType; + private Boolean rptAsJwt; + private Boolean accessTokenAsJwt; + private SignatureAlgorithm accessTokenSigningAlg; + private SignatureAlgorithm idTokenSignedResponseAlg; + private KeyEncryptionAlgorithm idTokenEncryptedResponseAlg; + private BlockEncryptionAlgorithm idTokenEncryptedResponseEnc; + private SignatureAlgorithm userInfoSignedResponseAlg; + private KeyEncryptionAlgorithm userInfoEncryptedResponseAlg; + private BlockEncryptionAlgorithm userInfoEncryptedResponseEnc; + private SignatureAlgorithm requestObjectSigningAlg; + private KeyEncryptionAlgorithm requestObjectEncryptionAlg; + private BlockEncryptionAlgorithm requestObjectEncryptionEnc; + private AuthenticationMethod tokenEndpointAuthMethod; + private SignatureAlgorithm tokenEndpointAuthSigningAlg; + private Integer defaultMaxAge; + private Boolean requireAuthTime; + private List defaultAcrValues; + private String initiateLoginUri; + private List postLogoutRedirectUris; + private List requestUris; + private List authorizedOrigins; + private Integer accessTokenLifetime; + private String softwareId; + private String softwareVersion; + private String softwareStatement; + private BackchannelTokenDeliveryMode backchannelTokenDeliveryMode; + private String backchannelClientNotificationEndpoint; + private AsymmetricSignatureAlgorithm backchannelAuthenticationRequestSigningAlg; + private Boolean backchannelUserCodeParameter; + private List additionalAudience; + + /** + * @deprecated This param will be removed in a future version because the correct is 'scope' not 'scopes', see (rfc7591). + */ + private List scopes; + + /** + * String containing a space-separated list of scope values. + */ + private List scope; + + /** + * String containing a space-separated list of claims that can be requested individually. + */ + private List claims; + + private Map customAttributes; + + // internal state + private JSONObject jsonObject; + private String httpMethod; + + /** + * Common constructor. + */ + public RegisterRequest() { + setContentType(MediaType.APPLICATION_JSON); + setMediaType(MediaType.APPLICATION_JSON); + + this.redirectUris = new ArrayList(); + this.claimsRedirectUris = new ArrayList(); + this.responseTypes = new ArrayList(); + this.grantTypes = new ArrayList(); + this.contacts = new ArrayList(); + this.defaultAcrValues = new ArrayList(); + this.postLogoutRedirectUris = new ArrayList(); + this.requestUris = new ArrayList(); + this.authorizedOrigins = new ArrayList(); + this.scopes = new ArrayList(); + this.scope = new ArrayList(); + this.claims = new ArrayList(); + this.customAttributes = new HashMap(); + } + + /** + * Constructs a request for Client Registration + * + * @param applicationType The application type. + * @param clientName The Client Name + * @param redirectUris A list of redirection URIs. + */ + public RegisterRequest(ApplicationType applicationType, String clientName, + List redirectUris) { + this(); + this.applicationType = applicationType; + this.clientName = clientName; + this.redirectUris = redirectUris; + } + + /** + * Constructs a request for Client Read + * + * @param registrationAccessToken The Registration Access Token. + */ + public RegisterRequest(String registrationAccessToken) { + this(); + this.registrationAccessToken = registrationAccessToken; + } + + public String getTlsClientAuthSubjectDn() { + return tlsClientAuthSubjectDn; + } + + public void setTlsClientAuthSubjectDn(String tlsClientAuthSubjectDn) { + this.tlsClientAuthSubjectDn = tlsClientAuthSubjectDn; + } + + public Boolean getAllowSpontaneousScopes() { + return allowSpontaneousScopes; + } + + public void setAllowSpontaneousScopes(Boolean allowSpontaneousScopes) { + this.allowSpontaneousScopes = allowSpontaneousScopes; + } + + public List getSpontaneousScopes() { + return spontaneousScopes; + } + + public void setSpontaneousScopes(List spontaneousScopes) { + this.spontaneousScopes = spontaneousScopes; + } + + public List getAdditionalAudience() { + return additionalAudience; + } + + public void setAdditionalAudience(List additionalAudience) { + this.additionalAudience = additionalAudience; + } + + public Boolean getRunIntrospectionScriptBeforeAccessTokenAsJwtCreationAndIncludeClaims() { + return runIntrospectionScriptBeforeAccessTokenAsJwtCreationAndIncludeClaims; + } + + public void setRunIntrospectionScriptBeforeAccessTokenAsJwtCreationAndIncludeClaims(Boolean runIntrospectionScriptBeforeAccessTokenAsJwtCreationAndIncludeClaims) { + this.runIntrospectionScriptBeforeAccessTokenAsJwtCreationAndIncludeClaims = runIntrospectionScriptBeforeAccessTokenAsJwtCreationAndIncludeClaims; + } + + public Boolean getKeepClientAuthorizationAfterExpiration() { + return keepClientAuthorizationAfterExpiration; + } + + public void setKeepClientAuthorizationAfterExpiration(Boolean keepClientAuthorizationAfterExpiration) { + this.keepClientAuthorizationAfterExpiration = keepClientAuthorizationAfterExpiration; + } + + /** + * Returns the Registration Access Token to authorize Client Read requests. + * + * @return The Registration Access Token. + */ + public String getRegistrationAccessToken() { + return registrationAccessToken; + } + + /** + * Sets the Registration Access Token to authorize Client Read requests. + * + * @param registrationAccessToken The Registration Access Token. + */ + public void setAccessToken(String registrationAccessToken) { + this.registrationAccessToken = registrationAccessToken; + } + + public List getBackchannelLogoutUris() { + return backchannelLogoutUris; + } + + public void setBackchannelLogoutUris(List backchannelLogoutUris) { + this.backchannelLogoutUris = backchannelLogoutUris; + } + + public Boolean getBackchannelLogoutSessionRequired() { + return backchannelLogoutSessionRequired; + } + + public void setBackchannelLogoutSessionRequired(Boolean backchannelLogoutSessionRequired) { + this.backchannelLogoutSessionRequired = backchannelLogoutSessionRequired; + } + + /** + * Gets logout uri. + * + * @return logout uri + */ + public List getFrontChannelLogoutUris() { + return frontChannelLogoutUris; + } + + /** + * Sets logout uri + * + * @param logoutUris logout uri + */ + public void setFrontChannelLogoutUris(List logoutUris) { + this.frontChannelLogoutUris = logoutUris; + } + + /** + * Gets logout session required. + * + * @return logout session required + */ + public Boolean getFrontChannelLogoutSessionRequired() { + return frontChannelLogoutSessionRequired; + } + + /** + * Sets front channel logout session required. + * + * @param frontChannelLogoutSessionRequired front channel logout session required + */ + public void setFrontChannelLogoutSessionRequired(Boolean frontChannelLogoutSessionRequired) { + this.frontChannelLogoutSessionRequired = frontChannelLogoutSessionRequired; + } + + /** + * Returns a list of redirection URIs. + * + * @return The redirection URIs. + */ + public List getRedirectUris() { + return redirectUris; + } + + /** + * Sets a list of redirection URIs. + * + * @param redirectUris The redirection URIs. + */ + public void setRedirectUris(List redirectUris) { + this.redirectUris = redirectUris; + } + + /** + * Returns claims redirect URIs. + * + * @return claims redirect URIs + */ + public List getClaimsRedirectUris() { + return claimsRedirectUris; + } + + /** + * Sets claims redirect URIs. + * + * @param claimsRedirectUris claims redirect URIs. + */ + public void setClaimsRedirectUris(List claimsRedirectUris) { + this.claimsRedirectUris = claimsRedirectUris; + } + + /** + * Returns a list of the OAuth 2.0 response_type values that the Client is declaring that it will restrict itself + * to using. + * + * @return A list of response types. + */ + public List getResponseTypes() { + Set types = Sets.newHashSet(); + responseTypes.forEach(s -> types.addAll(ResponseType.fromString(s, " "))); + return Lists.newArrayList(types); + } + + /** + * Sets a list of the OAuth 2.0 response_type values that the Client is declaring that it will restrict itself to + * using. If omitted, the default is that the Client will use only the code response type. + * + * @param responseTypes A list of response types. + */ + public void setResponseTypes(List responseTypes) { + this.responseTypes = ResponseType.toStringList(responseTypes); + } + + public List getResponseTypes_() { + return responseTypes; + } + + public void setResponseTypes_(List responseTypes) { + this.responseTypes = responseTypes; + } + + + /** + * Returns a list of the OAuth 2.0 grant types that the Client is declaring that it will restrict itself to using. + * + * @return A list of grant types. + */ + public List getGrantTypes() { + return grantTypes; + } + + /** + * Sets a list of the OAuth 2.0 grant types that the Client is declaring that it will restrict itself to using. + * + * @param grantTypes A list of grant types. + */ + public void setGrantTypes(List grantTypes) { + this.grantTypes = grantTypes; + } + + /** + * Returns the application type. + * + * @return The application type. + */ + public ApplicationType getApplicationType() { + return applicationType; + } + + /** + * Sets the application type. The default if not specified is web. + * + * @param applicationType The application type. + */ + public void setApplicationType(ApplicationType applicationType) { + this.applicationType = applicationType; + } + + public String getIdTokenTokenBindingCnf() { + return idTokenTokenBindingCnf; + } + + public void setIdTokenTokenBindingCnf(String idTokenTokenBindingCnf) { + this.idTokenTokenBindingCnf = idTokenTokenBindingCnf; + } + + /** + * Returns a list of e-mail addresses for people allowed to administer the information + * for this Client. + * + * @return A list of e-mail addresses. + */ + public List getContacts() { + return contacts; + } + + /** + * Sets a list of e-mail addresses for people allowed to administer the information for + * this Client. + * + * @param contacts A list of e-mail addresses. + */ + public void setContacts(List contacts) { + this.contacts = contacts; + } + + /** + * Returns the name of the Client to be presented to the user. + * + * @return The name of the Client to be presented to the user. + */ + public String getClientName() { + return clientName; + } + + /** + * Sets the name of the Client to be presented to the user. + * + * @param clientName The name of the Client to be presented to the user. + */ + public void setClientName(String clientName) { + this.clientName = clientName; + } + + /** + * Returns an URL that references a logo for the Client application. + * + * @return The URL that references a logo for the Client application. + */ + public String getLogoUri() { + return logoUri; + } + + /** + * Sets an URL that references a logo for the Client application. + * + * @param logoUri The URL that references a logo for the Client application. + */ + public void setLogoUri(String logoUri) { + this.logoUri = logoUri; + } + + /** + * Returns an URL of the home page of the Client. + * + * @return The URL of the home page of the Client. + */ + public String getClientUri() { + return clientUri; + } + + /** + * Sets an URL of the home page of the Client. + * + * @param clientUri The URL of the home page of the Client. + */ + public void setClientUri(String clientUri) { + this.clientUri = clientUri; + } + + /** + * Returns an URL that the Relying Party Client provides to the End-User to read about the how the profile data + * will be used. + * + * @return The policy URL. + */ + public String getPolicyUri() { + return policyUri; + } + + /** + * Sets an URL that the Relying Party Client provides to the End-User to read about the how the profile data will + * be used. + * + * @param policyUri The policy URL. + */ + public void setPolicyUri(String policyUri) { + this.policyUri = policyUri; + } + + /** + * Returns an URL that the Relying Party Client provides to the End-User to read about the Relying Party's terms + * of service. + * + * @return The tems of service URL. + */ + public String getTosUri() { + return tosUri; + } + + /** + * Sets an URL that the Relying Party Client provides to the End-User to read about the Relying Party's terms of + * service. + * + * @param tosUri The term of service URL. + */ + public void setTosUri(String tosUri) { + this.tosUri = tosUri; + } + + /** + * Returns the URL for the Client's JSON Web Key Set (JWK) document containing key(s) that are used for signing + * requests to the OP. The JWK Set may also contain the Client's encryption keys(s) that are used by the OP to + * encrypt the responses to the Client. When both signing and encryption keys are made available, a use (Key Use) + * parameter value is required for all keys in the document to indicate each key's intended usage. + * + * @return The URL for the Client's JSON Web Key Set (JWK) document. + */ + public String getJwksUri() { + return jwksUri; + } + + /** + * Sets the URL for the Client's JSON Web Key Set (JWK) document containing key(s) that are used for signing + * requests to the OP. The JWK Set may also contain the Client's encryption keys(s) that are used by the OP to + * encrypt the responses to the Client. When both signing and encryption keys are made available, a use (Key Use) + * parameter value is required for all keys in the document to indicate each key's intended usage. + * + * @param jwksUri The URL for the Client's JSON Web Key Set (JWK) document. + */ + public void setJwksUri(String jwksUri) { + this.jwksUri = jwksUri; + } + + /** + * Client's JSON Web Key Set (JWK) document, passed by value. The semantics of the jwks parameter are the same as + * the jwks_uri parameter, other than that the JWK Set is passed by value, rather than by reference. + * This parameter is intended only to be used by Clients that, for some reason, are unable to use the jwks_uri + * parameter, for instance, by native applications that might not have a location to host the contents of the JWK + * Set. If a Client can use jwks_uri, it must not use jwks. + * One significant downside of jwks is that it does not enable key rotation (which jwks_uri does, as described in + * Section 10 of OpenID Connect Core 1.0). The jwks_uri and jwks parameters must not be used together. + * + * @return The Client's JSON Web Key Set (JWK) document. + */ + public String getJwks() { + return jwks; + } + + /** + * Client's JSON Web Key Set (JWK) document, passed by value. The semantics of the jwks parameter are the same as + * the jwks_uri parameter, other than that the JWK Set is passed by value, rather than by reference. + * This parameter is intended only to be used by Clients that, for some reason, are unable to use the jwks_uri + * parameter, for instance, by native applications that might not have a location to host the contents of the JWK + * Set. If a Client can use jwks_uri, it must not use jwks. + * One significant downside of jwks is that it does not enable key rotation (which jwks_uri does, as described in + * Section 10 of OpenID Connect Core 1.0). The jwks_uri and jwks parameters must not be used together. + * + * @param jwks The Client's JSON Web Key Set (JWK) document. + */ + public void setJwks(String jwks) { + this.jwks = jwks; + } + + /** + * Returns the URL using the https scheme to be used in calculating Pseudonymous Identifiers by the OP. + * The URL references a file with a single JSON array of redirect_uri values. + * + * @return The sector identifier URL. + */ + public String getSectorIdentifierUri() { + return sectorIdentifierUri; + } + + /** + * Sets the URL using the https scheme to be used in calculating Pseudonymous Identifiers by the OP. + * The URL references a file with a single JSON array of redirect_uri values. + * + * @param sectorIdentifierUri The sector identifier URL. + */ + public void setSectorIdentifierUri(String sectorIdentifierUri) { + this.sectorIdentifierUri = sectorIdentifierUri; + } + + /** + * Returns the Subject Type. Valid types include pairwise and public. + * + * @return The Subject Type. + */ + public SubjectType getSubjectType() { + return subjectType; + } + + /** + * Sets the Subject Type. Valid types include pairwise and public. + * + * @param subjectType The Subject Type. + */ + public void setSubjectType(SubjectType subjectType) { + this.subjectType = subjectType; + } + + public Boolean getRptAsJwt() { + return rptAsJwt; + } + + public void setRptAsJwt(Boolean rptAsJwt) { + this.rptAsJwt = rptAsJwt; + } + + public Boolean getAccessTokenAsJwt() { + return accessTokenAsJwt; + } + + public void setAccessTokenAsJwt(Boolean accessTokenAsJwt) { + this.accessTokenAsJwt = accessTokenAsJwt; + } + + public SignatureAlgorithm getAccessTokenSigningAlg() { + return accessTokenSigningAlg; + } + + public void setAccessTokenSigningAlg(SignatureAlgorithm accessTokenSigningAlg) { + this.accessTokenSigningAlg = accessTokenSigningAlg; + } + + /** + * Returns th JWS alg algorithm (JWA) required for the ID Token issued to this client_id. + * + * @return The JWS algorithm (JWA). + */ + public SignatureAlgorithm getIdTokenSignedResponseAlg() { + return idTokenSignedResponseAlg; + } + + /** + * Sets the JWS alg algorithm (JWA) required for the ID Token issued to this client_id. + * + * @param idTokenSignedResponseAlg The JWS algorithm (JWA). + */ + public void setIdTokenSignedResponseAlg(SignatureAlgorithm idTokenSignedResponseAlg) { + this.idTokenSignedResponseAlg = idTokenSignedResponseAlg; + } + + /** + * Returns the JWE alg algorithm (JWA) required for encrypting the ID Token issued to this client_id. + * + * @return The JWE algorithm (JWA). + */ + public KeyEncryptionAlgorithm getIdTokenEncryptedResponseAlg() { + return idTokenEncryptedResponseAlg; + } + + /** + * Sets the JWE alg algorithm (JWA) required for encrypting the ID Token issued to this client_id. + * + * @param idTokenEncryptedResponseAlg The JWE algorithm (JWA). + */ + public void setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm idTokenEncryptedResponseAlg) { + this.idTokenEncryptedResponseAlg = idTokenEncryptedResponseAlg; + } + + /** + * Returns the JWE enc algorithm (JWA) required for symmetric encryption of the ID Token issued to this client_id. + * + * @return The JWE algorithm (JWA). + */ + public BlockEncryptionAlgorithm getIdTokenEncryptedResponseEnc() { + return idTokenEncryptedResponseEnc; + } + + /** + * Sets the JWE enc algorithm (JWA) required for symmetric encryption of the ID Token issued to this client_id. + * + * @param idTokenEncryptedResponseEnc The JWE algorithm (JWA). + */ + public void setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm idTokenEncryptedResponseEnc) { + this.idTokenEncryptedResponseEnc = idTokenEncryptedResponseEnc; + } + + /** + * Returns the JWS alg algorithm (JWA) required for UserInfo responses. + * + * @return The JWS algorithm (JWA). + */ + public SignatureAlgorithm getUserInfoSignedResponseAlg() { + return userInfoSignedResponseAlg; + } + + /** + * Sets the JWS alg algorithm (JWA) required for UserInfo responses. + * + * @param userInfoSignedResponseAlg The JWS algorithm (JWA). + */ + public void setUserInfoSignedResponseAlg(SignatureAlgorithm userInfoSignedResponseAlg) { + this.userInfoSignedResponseAlg = userInfoSignedResponseAlg; + } + + /** + * Returns the JWE alg algorithm (JWA) required for encrypting UserInfo responses. + * + * @return The JWE algorithm (JWA). + */ + public KeyEncryptionAlgorithm getUserInfoEncryptedResponseAlg() { + return userInfoEncryptedResponseAlg; + } + + /** + * Sets the JWE alg algorithm (JWA) required for encrypting UserInfo responses. + * + * @param userInfoEncryptedResponseAlg The JWE algorithm (JWA). + */ + public void setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm userInfoEncryptedResponseAlg) { + this.userInfoEncryptedResponseAlg = userInfoEncryptedResponseAlg; + } + + /** + * Returns the JWE enc algorithm (JWA) required for symmetric encryption of UserInfo responses. + * + * @return The JWE algorithm (JWA). + */ + public BlockEncryptionAlgorithm getUserInfoEncryptedResponseEnc() { + return userInfoEncryptedResponseEnc; + } + + /** + * Sets the JWE enc algorithm (JWA) required for symmetric encryption of UserInfo responses. + * + * @param userInfoEncryptedResponseEnc The JWE algorithm (JWA). + */ + public void setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm userInfoEncryptedResponseEnc) { + this.userInfoEncryptedResponseEnc = userInfoEncryptedResponseEnc; + } + + /** + * Returns the JWS alg algorithm (JWA) that must be required by the Authorization Server. + * + * @return The JWS algorithm (JWA). + */ + public SignatureAlgorithm getRequestObjectSigningAlg() { + return requestObjectSigningAlg; + } + + /** + * Sets the JWS alg algorithm (JWA) that must be required by the Authorization Server. + * + * @param requestObjectSigningAlg The JWS algorithm (JWA). + */ + public void setRequestObjectSigningAlg(SignatureAlgorithm requestObjectSigningAlg) { + this.requestObjectSigningAlg = requestObjectSigningAlg; + } + + /** + * Returns the JWE alg algorithm (JWA) the RP is declaring that it may use for encrypting Request Objects + * sent to the OP. + * + * @return The JWE alg algorithm (JWA). + */ + public KeyEncryptionAlgorithm getRequestObjectEncryptionAlg() { + return requestObjectEncryptionAlg; + } + + /** + * Sets the JWE alg algorithm (JWA) the RP is declaring that it may use for encrypting Request Objects + * sent to the OP. + * + * @param requestObjectEncryptionAlg The JWE alg algorithm (JWA). + */ + public void setRequestObjectEncryptionAlg(KeyEncryptionAlgorithm requestObjectEncryptionAlg) { + this.requestObjectEncryptionAlg = requestObjectEncryptionAlg; + } + + /** + * Returns the JWE enc algorithm (JWA) the RP is declaring that it may use for encrypting Request Objects + * sent to the OP. + * + * @return The JWE enc algorithm (JWA). + */ + public BlockEncryptionAlgorithm getRequestObjectEncryptionEnc() { + return requestObjectEncryptionEnc; + } + + /** + * Sets the JWE enc algorithm (JWA) the RP is declaring that it may use for encrypting Request Objects + * sent to the OP. + * + * @param requestObjectEncryptionEnc The JWE enc algorithm (JWA). + */ + public void setRequestObjectEncryptionEnc(BlockEncryptionAlgorithm requestObjectEncryptionEnc) { + this.requestObjectEncryptionEnc = requestObjectEncryptionEnc; + } + + /** + * Returns the requested authentication method for the Token Endpoint. + * + * @return The requested authentication method for the Token Endpoint. + */ + public AuthenticationMethod getTokenEndpointAuthMethod() { + return tokenEndpointAuthMethod; + } + + /** + * Sets the requested authentication method for the Token Endpoint. + * + * @param tokenEndpointAuthMethod The requested authentication method for the Token Endpoint. + */ + public void setTokenEndpointAuthMethod(AuthenticationMethod tokenEndpointAuthMethod) { + this.tokenEndpointAuthMethod = tokenEndpointAuthMethod; + } + + /** + * Returns the Requested Client Authentication method for the Token Endpoint. + * + * @return The Requested Client Authentication method for the Token Endpoint. + */ + public SignatureAlgorithm getTokenEndpointAuthSigningAlg() { + return tokenEndpointAuthSigningAlg; + } + + /** + * Sets the Requested Client Authentication method for the Token Endpoint. + * + * @param tokenEndpointAuthSigningAlg The Requested Client Authentication method for the Token Endpoint. + */ + public void setTokenEndpointAuthSigningAlg(SignatureAlgorithm tokenEndpointAuthSigningAlg) { + this.tokenEndpointAuthSigningAlg = tokenEndpointAuthSigningAlg; + } + + /** + * Returns the Default Maximum Authentication Age. + * + * @return The Default Maximum Authentication Age. + */ + public Integer getDefaultMaxAge() { + return defaultMaxAge; + } + + /** + * Sets the Default Maximum Authentication Age. + * + * @param defaultMaxAge The Default Maximum Authentication Age. + */ + public void setDefaultMaxAge(Integer defaultMaxAge) { + this.defaultMaxAge = defaultMaxAge; + } + + /** + * Returns the Boolean value specifying whether the auth_time claim in the id_token is required. + * It is required when the value is true. The auth_time claim request in the request object overrides this setting. + * + * @return The Boolean value specifying whether the auth_time claim in the id_token is required. + */ + public Boolean getRequireAuthTime() { + return requireAuthTime; + } + + /** + * Sets the Boolean value specifying whether the auth_time claim in the id_token is required. + * Ir is required when the value is true. The auth_time claim request in the request object overrides this setting. + * + * @param requireAuthTime The Boolean value specifying whether the auth_time claim in the id_token is required. + */ + public void setRequireAuthTime(Boolean requireAuthTime) { + this.requireAuthTime = requireAuthTime; + } + + /** + * Returns the Default requested Authentication Context Class Reference values. + * + * @return The Default requested Authentication Context Class Reference values. + */ + public List getDefaultAcrValues() { + return defaultAcrValues; + } + + /** + * Sets the Default requested Authentication Context Class Reference values. + * + * @param defaultAcrValues The Default requested Authentication Context Class Reference values. + */ + public void setDefaultAcrValues(List defaultAcrValues) { + this.defaultAcrValues = defaultAcrValues; + } + + /** + * Returns the URI using the https: scheme that the authorization server can call to initiate a login at the client. + * + * @return The URI using the https: scheme that the authorization server can call to initiate a login at the client. + */ + public String getInitiateLoginUri() { + return initiateLoginUri; + } + + /** + * Sets the URI using the https: scheme that the authorization server can call to initiate a login at the client. + * + * @param initiateLoginUri The URI using the https: scheme that the authorization server can call to initiate a + * login at the client. + */ + public void setInitiateLoginUri(String initiateLoginUri) { + this.initiateLoginUri = initiateLoginUri; + } + + /** + * Returns the URLs supplied by the RP to request that the user be redirected to this location after a logout has + * been performed. + * + * @return The URLs supplied by the RP to request that the user be redirected to this location after a logout has + * been performed. + */ + public List getPostLogoutRedirectUris() { + return postLogoutRedirectUris; + } + + /** + * Sets the URLs supplied by the RP to request that the user be redirected to this location after a logout has + * been performed. + * + * @param postLogoutRedirectUris The URLs supplied by the RP to request that the user be redirected to this location + * after a logout has been performed. + */ + public void setPostLogoutRedirectUris(List postLogoutRedirectUris) { + this.postLogoutRedirectUris = postLogoutRedirectUris; + } + + /** + * Returns a list of request_uri values that are pre-registered by the Client for use at the Authorization Server. + * + * @return A list of request URIs. + */ + public List getRequestUris() { + return requestUris; + } + + /** + * Sets a list of request_uri values that are pre-registered by the Client for use at the Authorization Server. + * + * @param requestUris A list of request URIs. + */ + public void setRequestUris(List requestUris) { + this.requestUris = requestUris; + } + + /** + * Returns authorized JavaScript origins. + * + * @return Authorized JavaScript origins. + */ + public List getAuthorizedOrigins() { + return authorizedOrigins; + } + + /** + * Sets authorized JavaScript origins. + * + * @param authorizedOrigins Authorized JavaScript origins. + */ + public void setAuthorizedOrigins(List authorizedOrigins) { + this.authorizedOrigins = authorizedOrigins; + } + + /** + * @deprecated This function will be removed in a future version because the correct is 'scope' not 'scopes', see (rfc7591). + */ + public List getScopes() { + return scopes; + } + + /** + * @deprecated This method will be removed in a future version because the correct is 'scope' not 'scopes', see (rfc7591). + */ + public void setScopes(List scopes) { + this.scopes = scopes; + } + + public List getScope() { + return scope; + } + + public void setScope(List scope) { + this.scope = scope; + } + + public List getClaims() { + return claims; + } + + public void setClaims(List claims) { + this.claims = claims; + } + + /** + * Returns the Client-specific access token expiration. + * + * @return The Client-specific access token expiration. + */ + public Integer getAccessTokenLifetime() { + return accessTokenLifetime; + } + + /** + * Sets the Client-specific access token expiration (in seconds). Set it to Null or Zero to use the system default value. + * + * @param accessTokenLifetime The Client-specific access token expiration. + */ + public void setAccessTokenLifetime(Integer accessTokenLifetime) { + this.accessTokenLifetime = accessTokenLifetime; + } + + /** + * Returns a unique identifier string (UUID) assigned by the client developer or software publisher used by + * registration endpoints to identify the client software to be dynamically registered. + * + * @return The software identifier. + */ + public String getSoftwareId() { + return softwareId; + } + + /** + * Sets a unique identifier string (UUID) assigned by the client developer or software publisher used by + * registration endpoints to identify the client software to be dynamically registered. + * + * @param softwareId The software identifier. + */ + public void setSoftwareId(String softwareId) { + this.softwareId = softwareId; + } + + /** + * Returns a version identifier string for the client software identified by "software_id". + * The value of the "software_version" should change on any update to the client software identified by the same + * "software_id". + * + * @return The version identifier. + */ + public String getSoftwareVersion() { + return softwareVersion; + } + + /** + * Sets a version identifier string for the client software identified by "software_id". + * The value of the "software_version" should change on any update to the client software identified by the same + * "software_id". + * + * @param softwareVersion The version identifier. + */ + public void setSoftwareVersion(String softwareVersion) { + this.softwareVersion = softwareVersion; + } + + /** + * Returns a software statement containing client metadata values about the client software as claims. + * This is a string value containing the entire signed JWT. + * + * @return The software statement. + */ + public String getSoftwareStatement() { + return softwareStatement; + } + + /** + * Sets a software statement containing client metadata values about the client software as claims. + * This is a string value containing the entire signed JWT. + * + * @param softwareStatement The software statement. + */ + public void setSoftwareStatement(String softwareStatement) { + this.softwareStatement = softwareStatement; + } + + public BackchannelTokenDeliveryMode getBackchannelTokenDeliveryMode() { + return backchannelTokenDeliveryMode; + } + + public void setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode backchannelTokenDeliveryMode) { + this.backchannelTokenDeliveryMode = backchannelTokenDeliveryMode; + } + + public String getBackchannelClientNotificationEndpoint() { + return backchannelClientNotificationEndpoint; + } + + public void setBackchannelClientNotificationEndpoint(String backchannelClientNotificationEndpoint) { + this.backchannelClientNotificationEndpoint = backchannelClientNotificationEndpoint; + } + + public AsymmetricSignatureAlgorithm getBackchannelAuthenticationRequestSigningAlg() { + return backchannelAuthenticationRequestSigningAlg; + } + + public void setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm backchannelAuthenticationRequestSigningAlg) { + this.backchannelAuthenticationRequestSigningAlg = backchannelAuthenticationRequestSigningAlg; + } + + public Boolean getBackchannelUserCodeParameter() { + return backchannelUserCodeParameter; + } + + public void setBackchannelUserCodeParameter(Boolean backchannelUserCodeParameter) { + this.backchannelUserCodeParameter = backchannelUserCodeParameter; + } + + public String getHttpMethod() { + return httpMethod; + } + + public void setHttpMethod(String p_httpMethod) { + httpMethod = p_httpMethod; + } + + /** + * Gets custom attribute map copy. + * + * @return custom attribute map copy + */ + public Map getCustomAttributes() { + // return unmodifiable map to force add custom attribute via addCustomAttribute() that has validation + return Collections.unmodifiableMap(this.customAttributes); + } + + public void addCustomAttribute(String p_name, String p_value) { + if (RegisterRequestParam.isCustomParameterValid(p_name)) { + this.customAttributes.put(p_name, p_value); + } + } + + /** + * Returns a collection of parameters of the register request. + * Any null or empty parameter will be omitted. + * + * @return A collection of parameters. + */ + @Override + public Map getParameters() { + Map parameters = new HashMap<>(); + + JsonApplier.getInstance().apply(this, parameters); + + if (redirectUris != null && !redirectUris.isEmpty()) { + parameters.put(REDIRECT_URIS.toString(), toJSONArray(redirectUris).toString()); + } + if (claimsRedirectUris != null && !claimsRedirectUris.isEmpty()) { + parameters.put(CLAIMS_REDIRECT_URIS.toString(), toJSONArray(claimsRedirectUris).toString()); + } + if (responseTypes != null && !responseTypes.isEmpty()) { + parameters.put(RESPONSE_TYPES.toString(), toJSONArray(responseTypes).toString()); + } + if (grantTypes != null && !grantTypes.isEmpty()) { + parameters.put(GRANT_TYPES.toString(), toJSONArray(grantTypes).toString()); + } + if (applicationType != null) { + parameters.put(APPLICATION_TYPE.toString(), applicationType.toString()); + } + if (contacts != null && !contacts.isEmpty()) { + parameters.put(CONTACTS.toString(), toJSONArray(contacts).toString()); + } + if (StringUtils.isNotBlank(clientName)) { + parameters.put(CLIENT_NAME.toString(), clientName); + } + if (StringUtils.isNotBlank(logoUri)) { + parameters.put(LOGO_URI.toString(), logoUri); + } + if (StringUtils.isNotBlank(clientUri)) { + parameters.put(CLIENT_URI.toString(), clientUri); + } + if (StringUtils.isNotBlank(policyUri)) { + parameters.put(POLICY_URI.toString(), policyUri); + } + if (StringUtils.isNotBlank(tosUri)) { + parameters.put(TOS_URI.toString(), tosUri); + } + if (StringUtils.isNotBlank(jwksUri)) { + parameters.put(JWKS_URI.toString(), jwksUri); + } + if (StringUtils.isNotBlank(jwks)) { + parameters.put(JWKS.toString(), jwks); + } + if (StringUtils.isNotBlank(sectorIdentifierUri)) { + parameters.put(SECTOR_IDENTIFIER_URI.toString(), sectorIdentifierUri); + } + if (subjectType != null) { + parameters.put(SUBJECT_TYPE.toString(), subjectType.toString()); + } + if (rptAsJwt != null) { + parameters.put(RPT_AS_JWT.toString(), rptAsJwt.toString()); + } + if (accessTokenAsJwt != null) { + parameters.put(ACCESS_TOKEN_AS_JWT.toString(), accessTokenAsJwt.toString()); + } + if (accessTokenSigningAlg != null) { + parameters.put(ACCESS_TOKEN_SIGNING_ALG.toString(), accessTokenSigningAlg.toString()); + } + if (idTokenSignedResponseAlg != null) { + parameters.put(ID_TOKEN_SIGNED_RESPONSE_ALG.toString(), idTokenSignedResponseAlg.getName()); + } + if (idTokenEncryptedResponseAlg != null) { + parameters.put(ID_TOKEN_ENCRYPTED_RESPONSE_ALG.toString(), idTokenEncryptedResponseAlg.getName()); + } + if (idTokenEncryptedResponseEnc != null) { + parameters.put(ID_TOKEN_ENCRYPTED_RESPONSE_ENC.toString(), idTokenEncryptedResponseEnc.getName()); + } + if (userInfoSignedResponseAlg != null) { + parameters.put(USERINFO_SIGNED_RESPONSE_ALG.toString(), userInfoSignedResponseAlg.getName()); + } + if (userInfoEncryptedResponseAlg != null) { + parameters.put(USERINFO_ENCRYPTED_RESPONSE_ALG.toString(), userInfoEncryptedResponseAlg.getName()); + } + if (userInfoEncryptedResponseEnc != null) { + parameters.put(USERINFO_ENCRYPTED_RESPONSE_ENC.toString(), userInfoEncryptedResponseEnc.getName()); + } + if (requestObjectSigningAlg != null) { + parameters.put(REQUEST_OBJECT_SIGNING_ALG.toString(), requestObjectSigningAlg.getName()); + } + if (requestObjectEncryptionAlg != null) { + parameters.put(REQUEST_OBJECT_ENCRYPTION_ALG.toString(), requestObjectEncryptionAlg.getName()); + } + if (requestObjectEncryptionEnc != null) { + parameters.put(REQUEST_OBJECT_ENCRYPTION_ENC.toString(), requestObjectEncryptionEnc.getName()); + } + if (tokenEndpointAuthMethod != null) { + parameters.put(TOKEN_ENDPOINT_AUTH_METHOD.toString(), tokenEndpointAuthMethod.toString()); + } + if (tokenEndpointAuthSigningAlg != null) { + parameters.put(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString(), tokenEndpointAuthSigningAlg.toString()); + } + if (defaultMaxAge != null) { + parameters.put(DEFAULT_MAX_AGE.toString(), defaultMaxAge.toString()); + } + if (requireAuthTime != null) { + parameters.put(REQUIRE_AUTH_TIME.toString(), requireAuthTime.toString()); + } + if (defaultAcrValues != null && !defaultAcrValues.isEmpty()) { + parameters.put(DEFAULT_ACR_VALUES.toString(), toJSONArray(defaultAcrValues).toString()); + } + if (StringUtils.isNotBlank(initiateLoginUri)) { + parameters.put(INITIATE_LOGIN_URI.toString(), initiateLoginUri); + } + if (postLogoutRedirectUris != null && !postLogoutRedirectUris.isEmpty()) { + parameters.put(POST_LOGOUT_REDIRECT_URIS.toString(), toJSONArray(postLogoutRedirectUris).toString()); + } + if (frontChannelLogoutUris != null && !frontChannelLogoutUris.isEmpty()) { + parameters.put(FRONT_CHANNEL_LOGOUT_URI.toString(), toJSONArray(frontChannelLogoutUris).toString()); + } + if (frontChannelLogoutSessionRequired != null) { + parameters.put(FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED.toString(), frontChannelLogoutSessionRequired.toString()); + } + if (backchannelLogoutUris != null && !backchannelLogoutUris.isEmpty()) { + parameters.put(BACKCHANNEL_LOGOUT_URI.toString(), toJSONArray(backchannelLogoutUris).toString()); + } + if (backchannelLogoutSessionRequired != null) { + parameters.put(BACKCHANNEL_LOGOUT_SESSION_REQUIRED.toString(), backchannelLogoutSessionRequired.toString()); + } + if (requestUris != null && !requestUris.isEmpty()) { + parameters.put(REQUEST_URIS.toString(), toJSONArray(requestUris).toString()); + } + if (authorizedOrigins != null && !authorizedOrigins.isEmpty()) { + parameters.put(AUTHORIZED_ORIGINS.toString(), toJSONArray(authorizedOrigins).toString()); + } + if (scopes != null && !scopes.isEmpty()) { + parameters.put(SCOPES.toString(), toJSONArray(scopes).toString()); + } + if (scope != null && !scope.isEmpty()) { + parameters.put(SCOPE.toString(), implode(scope, " ")); + } + if (StringUtils.isNotBlank(idTokenTokenBindingCnf)) { + parameters.put(ID_TOKEN_TOKEN_BINDING_CNF.toString(), idTokenTokenBindingCnf); + } + if (StringUtils.isNotBlank(tlsClientAuthSubjectDn)) { + parameters.put(TLS_CLIENT_AUTH_SUBJECT_DN.toString(), tlsClientAuthSubjectDn); + } + if (allowSpontaneousScopes != null) { + parameters.put(ALLOW_SPONTANEOUS_SCOPES.toString(), allowSpontaneousScopes.toString()); + } + if (spontaneousScopes != null && !spontaneousScopes.isEmpty()) { + parameters.put(SPONTANEOUS_SCOPES.toString(), implode(spontaneousScopes, " ")); + } + if (runIntrospectionScriptBeforeAccessTokenAsJwtCreationAndIncludeClaims != null) { + parameters.put(RUN_INTROSPECTION_SCRIPT_BEFORE_ACCESS_TOKEN_CREATION_AS_JWT_AND_INCLUDE_CLAIMS.toString(), runIntrospectionScriptBeforeAccessTokenAsJwtCreationAndIncludeClaims.toString()); + } + if (keepClientAuthorizationAfterExpiration != null) { + parameters.put(KEEP_CLIENT_AUTHORIZATION_AFTER_EXPIRATION.toString(), keepClientAuthorizationAfterExpiration.toString()); + } + if (claims != null && !claims.isEmpty()) { + parameters.put(CLAIMS.toString(), implode(claims, " ")); + } + if (accessTokenLifetime != null) { + parameters.put(ACCESS_TOKEN_LIFETIME.toString(), accessTokenLifetime.toString()); + } + if (StringUtils.isNotBlank(softwareId)) { + parameters.put(SOFTWARE_ID.toString(), softwareId); + } + if (StringUtils.isNotBlank(softwareVersion)) { + parameters.put(SOFTWARE_VERSION.toString(), softwareVersion); + } + if (StringUtils.isNotBlank(softwareStatement)) { + parameters.put(SOFTWARE_STATEMENT.toString(), softwareStatement); + } + if (backchannelTokenDeliveryMode != null) { + parameters.put(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString(), backchannelTokenDeliveryMode.toString()); + } + if (StringUtils.isNotBlank(backchannelClientNotificationEndpoint)) { + parameters.put(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString(), backchannelClientNotificationEndpoint); + } + if (backchannelAuthenticationRequestSigningAlg != null) { + parameters.put(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString(), backchannelAuthenticationRequestSigningAlg.toString()); + } + if (backchannelUserCodeParameter != null && backchannelUserCodeParameter) { + parameters.put(BACKCHANNEL_USER_CODE_PARAMETER.toString(), backchannelUserCodeParameter.toString()); + } + + // Custom params + if (customAttributes != null && !customAttributes.isEmpty()) { + for (Map.Entry entry : customAttributes.entrySet()) { + final String name = entry.getKey(); + final String value = entry.getValue(); + if (RegisterRequestParam.isCustomParameterValid(name) && StringUtils.isNotBlank(value)) { + parameters.put(name, value); + } + } + } + return parameters; + } + + public static RegisterRequest fromJson(String p_json, boolean authorizationRequestCustomAllowedParameters) throws JSONException { + return fromJson(new JSONObject(p_json), authorizationRequestCustomAllowedParameters); + } + + public static RegisterRequest fromJson(JSONObject requestObject, boolean authorizationRequestCustomAllowedParameters) throws JSONException { + final List redirectUris = new ArrayList(); + if (requestObject.has(REDIRECT_URIS.toString())) { + JSONArray redirectUrisJsonArray = requestObject.getJSONArray(REDIRECT_URIS.toString()); + for (int i = 0; i < redirectUrisJsonArray.length(); i++) { + String redirectionUri = redirectUrisJsonArray.getString(i); + redirectUris.add(redirectionUri); + } + } + + final List claimRedirectUris = new ArrayList(); + if (requestObject.has(CLAIMS_REDIRECT_URIS.toString())) { + JSONArray jsonArray = requestObject.getJSONArray(CLAIMS_REDIRECT_URIS.toString()); + for (int i = 0; i < jsonArray.length(); i++) { + String uri = jsonArray.getString(i); + claimRedirectUris.add(uri); + } + } + + final Set responseTypes = new HashSet(); + if (requestObject.has(RESPONSE_TYPES.toString())) { + JSONArray responseTypesJsonArray = requestObject.getJSONArray(RESPONSE_TYPES.toString()); + for (int i = 0; i < responseTypesJsonArray.length(); i++) { + responseTypes.add(responseTypesJsonArray.getString(i)); + } + } + + final Set grantTypes = new HashSet(); + if (requestObject.has(GRANT_TYPES.toString())) { + JSONArray grantTypesJsonArray = requestObject.getJSONArray(GRANT_TYPES.toString()); + for (int i = 0; i < grantTypesJsonArray.length(); i++) { + GrantType gt = GrantType.fromString(grantTypesJsonArray.getString(i)); + if (gt != null) { + grantTypes.add(gt); + } + } + } + + final List contacts = new ArrayList(); + if (requestObject.has(CONTACTS.toString())) { + JSONArray contactsJsonArray = requestObject.getJSONArray(CONTACTS.toString()); + for (int i = 0; i < contactsJsonArray.length(); i++) { + contacts.add(contactsJsonArray.getString(i)); + } + } + + final List defaultAcrValues = new ArrayList(); + if (requestObject.has(DEFAULT_ACR_VALUES.toString())) { + JSONArray defaultAcrValuesJsonArray = requestObject.getJSONArray(DEFAULT_ACR_VALUES.toString()); + for (int i = 0; i < defaultAcrValuesJsonArray.length(); i++) { + defaultAcrValues.add(defaultAcrValuesJsonArray.getString(i)); + } + } + + final List postLogoutRedirectUris = new ArrayList(); + if (requestObject.has(POST_LOGOUT_REDIRECT_URIS.toString())) { + JSONArray postLogoutRedirectUrisJsonArray = requestObject.getJSONArray(POST_LOGOUT_REDIRECT_URIS.toString()); + for (int i = 0; i < postLogoutRedirectUrisJsonArray.length(); i++) { + postLogoutRedirectUris.add(postLogoutRedirectUrisJsonArray.getString(i)); + } + } + + final List requestUris = new ArrayList(); + if (requestObject.has(REQUEST_URIS.toString())) { + JSONArray requestUrisJsonArray = requestObject.getJSONArray(REQUEST_URIS.toString()); + for (int i = 0; i < requestUrisJsonArray.length(); i++) { + requestUris.add(requestUrisJsonArray.getString(i)); + } + } + + final List authorizedOrigins = new ArrayList(); + if (requestObject.has(AUTHORIZED_ORIGINS.toString())) { + JSONArray authorizedOriginsJsonArray = requestObject.getJSONArray((AUTHORIZED_ORIGINS.toString())); + for (int i = 0; i < authorizedOriginsJsonArray.length(); i++) { + authorizedOrigins.add(authorizedOriginsJsonArray.getString(i)); + } + } + + final List scope = new ArrayList(); + if (authorizationRequestCustomAllowedParameters && requestObject.has(SCOPES.toString())) { + JSONArray scopesJsonArray = requestObject.getJSONArray(SCOPES.toString()); + for (int i = 0; i < scopesJsonArray.length(); i++) { + scope.add(scopesJsonArray.getString(i)); + } + } else if (requestObject.has(SCOPE.toString())) { + String scopeString = requestObject.getString(SCOPE.toString()); + String[] scopeArray = scopeString.split(" "); + for (String s : scopeArray) { + if (StringUtils.isNotBlank(s)) { + scope.add(s); + } + } + } + + final List claims = new ArrayList(); + if (requestObject.has(CLAIMS.toString())) { + String claimsString = requestObject.getString(CLAIMS.toString()); + String[] claimsArray = claimsString.split(" "); + for (String c : claimsArray) { + if (StringUtils.isNotBlank(c)) { + claims.add(c); + } + } + } + + + final RegisterRequest result = new RegisterRequest(); + + JsonApplier.getInstance().apply(requestObject, result); + + result.setJsonObject(requestObject); + result.setRequestUris(requestUris); + result.setAuthorizedOrigins(authorizedOrigins); + result.setClaimsRedirectUris(claimRedirectUris); + result.setInitiateLoginUri(requestObject.optString(INITIATE_LOGIN_URI.toString())); + result.setPostLogoutRedirectUris(postLogoutRedirectUris); + result.setDefaultAcrValues(defaultAcrValues); + result.setRequireAuthTime(requestObject.has(REQUIRE_AUTH_TIME.toString()) && requestObject.getBoolean(REQUIRE_AUTH_TIME.toString())); + result.setFrontChannelLogoutUris(extractList(requestObject, FRONT_CHANNEL_LOGOUT_URI.toString())); + result.setFrontChannelLogoutSessionRequired(requestObject.optBoolean(FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED.toString())); + result.setBackchannelLogoutUris(extractList(requestObject, BACKCHANNEL_LOGOUT_URI.toString())); + result.setBackchannelLogoutSessionRequired(requestObject.optBoolean(BACKCHANNEL_LOGOUT_SESSION_REQUIRED.toString())); + result.setAccessTokenLifetime(requestObject.has(ACCESS_TOKEN_LIFETIME.toString()) ? + requestObject.getInt(ACCESS_TOKEN_LIFETIME.toString()) : null); + result.setDefaultMaxAge(requestObject.has(DEFAULT_MAX_AGE.toString()) ? + requestObject.getInt(DEFAULT_MAX_AGE.toString()) : null); + result.setTlsClientAuthSubjectDn(requestObject.optString(TLS_CLIENT_AUTH_SUBJECT_DN.toString())); + result.setAllowSpontaneousScopes(requestObject.optBoolean(ALLOW_SPONTANEOUS_SCOPES.toString())); + result.setSpontaneousScopes(ClientUtil.extractListByKey(requestObject, SPONTANEOUS_SCOPES.toString())); + result.setRunIntrospectionScriptBeforeAccessTokenAsJwtCreationAndIncludeClaims(requestObject.optBoolean(RUN_INTROSPECTION_SCRIPT_BEFORE_ACCESS_TOKEN_CREATION_AS_JWT_AND_INCLUDE_CLAIMS.toString())); + result.setKeepClientAuthorizationAfterExpiration(requestObject.optBoolean(KEEP_CLIENT_AUTHORIZATION_AFTER_EXPIRATION.toString())); + result.setRptAsJwt(requestObject.optBoolean(RPT_AS_JWT.toString())); + result.setAccessTokenAsJwt(requestObject.optBoolean(ACCESS_TOKEN_AS_JWT.toString())); + result.setAccessTokenSigningAlg(SignatureAlgorithm.fromString(requestObject.optString(ACCESS_TOKEN_SIGNING_ALG.toString()))); + result.setIdTokenSignedResponseAlg(requestObject.has(ID_TOKEN_SIGNED_RESPONSE_ALG.toString()) ? + SignatureAlgorithm.fromString(requestObject.getString(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())) : null); + result.setIdTokenEncryptedResponseAlg(requestObject.has(ID_TOKEN_ENCRYPTED_RESPONSE_ALG.toString()) ? + KeyEncryptionAlgorithm.fromName(requestObject.getString(ID_TOKEN_ENCRYPTED_RESPONSE_ALG.toString())) : null); + result.setIdTokenEncryptedResponseEnc(requestObject.has(ID_TOKEN_ENCRYPTED_RESPONSE_ENC.toString()) ? + BlockEncryptionAlgorithm.fromName(requestObject.getString(ID_TOKEN_ENCRYPTED_RESPONSE_ENC.toString())) : null); + result.setUserInfoSignedResponseAlg(requestObject.has(USERINFO_SIGNED_RESPONSE_ALG.toString()) ? + SignatureAlgorithm.fromString(requestObject.getString(USERINFO_SIGNED_RESPONSE_ALG.toString())) : null); + result.setUserInfoEncryptedResponseAlg(requestObject.has(USERINFO_ENCRYPTED_RESPONSE_ALG.toString()) ? + KeyEncryptionAlgorithm.fromName(requestObject.getString(USERINFO_ENCRYPTED_RESPONSE_ALG.toString())) : null); + result.setUserInfoEncryptedResponseEnc(requestObject.has(USERINFO_ENCRYPTED_RESPONSE_ENC.toString()) ? + BlockEncryptionAlgorithm.fromName(requestObject.getString(USERINFO_ENCRYPTED_RESPONSE_ENC.toString())) : null); + result.setRequestObjectSigningAlg(requestObject.has(REQUEST_OBJECT_SIGNING_ALG.toString()) ? + SignatureAlgorithm.fromString(requestObject.getString(REQUEST_OBJECT_SIGNING_ALG.toString())) : null); + result.setRequestObjectEncryptionAlg(requestObject.has(REQUEST_OBJECT_ENCRYPTION_ALG.toString()) ? + KeyEncryptionAlgorithm.fromName(requestObject.getString(REQUEST_OBJECT_ENCRYPTION_ALG.toString())) : null); + result.setRequestObjectEncryptionEnc(requestObject.has(REQUEST_OBJECT_ENCRYPTION_ENC.toString()) ? + BlockEncryptionAlgorithm.fromName(requestObject.getString(REQUEST_OBJECT_ENCRYPTION_ENC.toString())) : null); + result.setTokenEndpointAuthMethod(requestObject.has(TOKEN_ENDPOINT_AUTH_METHOD.toString()) ? + AuthenticationMethod.fromString(requestObject.getString(TOKEN_ENDPOINT_AUTH_METHOD.toString())) : null); + result.setTokenEndpointAuthSigningAlg(requestObject.has(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()) ? + SignatureAlgorithm.fromString(requestObject.getString(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())) : null); + result.setRedirectUris(redirectUris); + result.setScopes(scope); + result.setScope(scope); + result.setClaims(claims); + result.setResponseTypes_(new ArrayList(responseTypes)); + result.setGrantTypes(new ArrayList(grantTypes)); + result.setApplicationType(requestObject.has(APPLICATION_TYPE.toString()) ? + ApplicationType.fromString(requestObject.getString(APPLICATION_TYPE.toString())) : ApplicationType.WEB); + result.setContacts(contacts); + result.setClientName(requestObject.optString(CLIENT_NAME.toString())); + result.setIdTokenTokenBindingCnf(requestObject.optString(ID_TOKEN_TOKEN_BINDING_CNF.toString(), "")); + result.setLogoUri(requestObject.optString(LOGO_URI.toString())); + result.setClientUri(requestObject.optString(CLIENT_URI.toString())); + result.setPolicyUri(requestObject.optString(POLICY_URI.toString())); + result.setTosUri(requestObject.optString(TOS_URI.toString())); + result.setJwksUri(requestObject.optString(JWKS_URI.toString())); + result.setJwks(requestObject.optString(JWKS.toString())); + result.setSectorIdentifierUri(requestObject.optString(SECTOR_IDENTIFIER_URI.toString())); + result.setSubjectType(requestObject.has(SUBJECT_TYPE.toString()) ? + SubjectType.fromString(requestObject.getString(SUBJECT_TYPE.toString())) : null); + result.setSoftwareId(requestObject.optString(SOFTWARE_ID.toString())); + result.setSoftwareVersion(requestObject.optString(SOFTWARE_VERSION.toString())); + result.setSoftwareStatement(requestObject.optString(SOFTWARE_STATEMENT.toString())); + result.setBackchannelTokenDeliveryMode(requestObject.has(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()) ? + BackchannelTokenDeliveryMode.fromString(requestObject.getString(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())) : null); + result.setBackchannelClientNotificationEndpoint(requestObject.optString(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + result.setBackchannelAuthenticationRequestSigningAlg(requestObject.has(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()) ? + AsymmetricSignatureAlgorithm.fromString(requestObject.getString(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())) : null); + result.setBackchannelUserCodeParameter(requestObject.has(BACKCHANNEL_USER_CODE_PARAMETER.toString()) ? + requestObject.getBoolean(BACKCHANNEL_USER_CODE_PARAMETER.toString()) : null); + + return result; + } + + private static List extractList(JSONObject requestObject, String key) { + final List result = new ArrayList<>(); + if (requestObject.has(key)) { + try { + JSONArray jsonArray = requestObject.getJSONArray(key); + for (int i = 0; i < jsonArray.length(); i++) { + result.add(jsonArray.getString(i)); + } + } catch (JSONException e) { + result.add(requestObject.optString(key)); + } + } + return result; + } + + @Override + public JSONObject getJSONParameters() throws JSONException { + JSONObject parameters = new JSONObject(); + + JsonApplier.getInstance().apply(this, parameters); + + if (redirectUris != null && !redirectUris.isEmpty()) { + parameters.put(REDIRECT_URIS.toString(), toJSONArray(redirectUris)); + } + if (claimsRedirectUris != null && !claimsRedirectUris.isEmpty()) { + parameters.put(CLAIMS_REDIRECT_URIS.toString(), toJSONArray(claimsRedirectUris)); + } + if (responseTypes != null && !responseTypes.isEmpty()) { + parameters.put(RESPONSE_TYPES.toString(), toJSONArray(responseTypes)); + } + if (grantTypes != null && !grantTypes.isEmpty()) { + parameters.put(GRANT_TYPES.toString(), toJSONArray(grantTypes)); + } + if (applicationType != null) { + parameters.put(APPLICATION_TYPE.toString(), applicationType.toString()); + } + if (contacts != null && !contacts.isEmpty()) { + parameters.put(CONTACTS.toString(), toJSONArray(contacts)); + } + if (StringUtils.isNotBlank(clientName)) { + parameters.put(CLIENT_NAME.toString(), clientName); + } + if (StringUtils.isNotBlank(idTokenTokenBindingCnf)) { + parameters.put(ID_TOKEN_TOKEN_BINDING_CNF.toString(), idTokenTokenBindingCnf); + } + if (StringUtils.isNotBlank(tlsClientAuthSubjectDn)) { + parameters.put(TLS_CLIENT_AUTH_SUBJECT_DN.toString(), tlsClientAuthSubjectDn); + } + if (allowSpontaneousScopes != null) { + parameters.put(ALLOW_SPONTANEOUS_SCOPES.toString(), allowSpontaneousScopes); + } + if (spontaneousScopes != null && !spontaneousScopes.isEmpty()) { + parameters.put(SPONTANEOUS_SCOPES.toString(), toJSONArray(spontaneousScopes)); + } + if (runIntrospectionScriptBeforeAccessTokenAsJwtCreationAndIncludeClaims != null) { + parameters.put(RUN_INTROSPECTION_SCRIPT_BEFORE_ACCESS_TOKEN_CREATION_AS_JWT_AND_INCLUDE_CLAIMS.toString(), runIntrospectionScriptBeforeAccessTokenAsJwtCreationAndIncludeClaims); + } + if (keepClientAuthorizationAfterExpiration != null) { + parameters.put(KEEP_CLIENT_AUTHORIZATION_AFTER_EXPIRATION.toString(), keepClientAuthorizationAfterExpiration); + } + if (StringUtils.isNotBlank(logoUri)) { + parameters.put(LOGO_URI.toString(), logoUri); + } + if (StringUtils.isNotBlank(clientUri)) { + parameters.put(CLIENT_URI.toString(), clientUri); + } + if (StringUtils.isNotBlank(policyUri)) { + parameters.put(POLICY_URI.toString(), policyUri); + } + if (StringUtils.isNotBlank(tosUri)) { + parameters.put(TOS_URI.toString(), tosUri); + } + if (StringUtils.isNotBlank(jwksUri)) { + parameters.put(JWKS_URI.toString(), jwksUri); + } + if (StringUtils.isNotBlank(jwks)) { + parameters.put(JWKS_URI.toString(), jwks); + } + if (StringUtils.isNotBlank(sectorIdentifierUri)) { + parameters.put(SECTOR_IDENTIFIER_URI.toString(), sectorIdentifierUri); + } + if (subjectType != null) { + parameters.put(SUBJECT_TYPE.toString(), subjectType.toString()); + } + if (rptAsJwt != null) { + parameters.put(RPT_AS_JWT.toString(), rptAsJwt.toString()); + } + if (accessTokenAsJwt != null) { + parameters.put(ACCESS_TOKEN_AS_JWT.toString(), accessTokenAsJwt.toString()); + } + if (accessTokenSigningAlg != null) { + parameters.put(ACCESS_TOKEN_SIGNING_ALG.toString(), accessTokenSigningAlg.toString()); + } + if (idTokenSignedResponseAlg != null) { + parameters.put(ID_TOKEN_SIGNED_RESPONSE_ALG.toString(), idTokenSignedResponseAlg.getName()); + } + if (idTokenEncryptedResponseAlg != null) { + parameters.put(ID_TOKEN_ENCRYPTED_RESPONSE_ALG.toString(), idTokenEncryptedResponseAlg.getName()); + } + if (idTokenEncryptedResponseEnc != null) { + parameters.put(ID_TOKEN_ENCRYPTED_RESPONSE_ENC.toString(), idTokenEncryptedResponseEnc.getName()); + } + if (userInfoSignedResponseAlg != null) { + parameters.put(USERINFO_SIGNED_RESPONSE_ALG.toString(), userInfoSignedResponseAlg.getName()); + } + if (userInfoEncryptedResponseAlg != null) { + parameters.put(USERINFO_ENCRYPTED_RESPONSE_ALG.toString(), userInfoEncryptedResponseAlg.getName()); + } + if (userInfoEncryptedResponseEnc != null) { + parameters.put(USERINFO_ENCRYPTED_RESPONSE_ENC.toString(), userInfoEncryptedResponseEnc.getName()); + } + if (requestObjectSigningAlg != null) { + parameters.put(REQUEST_OBJECT_SIGNING_ALG.toString(), requestObjectSigningAlg.getName()); + } + if (requestObjectEncryptionAlg != null) { + parameters.put(REQUEST_OBJECT_ENCRYPTION_ALG.toString(), requestObjectEncryptionAlg.getName()); + } + if (requestObjectEncryptionEnc != null) { + parameters.put(REQUEST_OBJECT_ENCRYPTION_ENC.toString(), requestObjectEncryptionEnc.getName()); + } + if (tokenEndpointAuthMethod != null) { + parameters.put(TOKEN_ENDPOINT_AUTH_METHOD.toString(), tokenEndpointAuthMethod.toString()); + } + if (tokenEndpointAuthSigningAlg != null) { + parameters.put(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString(), tokenEndpointAuthSigningAlg.toString()); + } + if (defaultMaxAge != null) { + parameters.put(DEFAULT_MAX_AGE.toString(), defaultMaxAge.toString()); + } + if (requireAuthTime != null) { + parameters.put(REQUIRE_AUTH_TIME.toString(), requireAuthTime.toString()); + } + if (defaultAcrValues != null && !defaultAcrValues.isEmpty()) { + parameters.put(DEFAULT_ACR_VALUES.toString(), toJSONArray(defaultAcrValues)); + } + if (StringUtils.isNotBlank(initiateLoginUri)) { + parameters.put(INITIATE_LOGIN_URI.toString(), initiateLoginUri); + } + if (postLogoutRedirectUris != null && !postLogoutRedirectUris.isEmpty()) { + parameters.put(POST_LOGOUT_REDIRECT_URIS.toString(), toJSONArray(postLogoutRedirectUris)); + } + if (frontChannelLogoutUris != null && !frontChannelLogoutUris.isEmpty()) { + parameters.put(FRONT_CHANNEL_LOGOUT_URI.toString(), toJSONArray(frontChannelLogoutUris)); + } + if (frontChannelLogoutSessionRequired != null) { + parameters.put(FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED.toString(), frontChannelLogoutSessionRequired.toString()); + } + if (backchannelLogoutUris != null && !backchannelLogoutUris.isEmpty()) { + parameters.put(BACKCHANNEL_LOGOUT_URI.toString(), toJSONArray(backchannelLogoutUris)); + } + if (backchannelLogoutSessionRequired != null) { + parameters.put(BACKCHANNEL_LOGOUT_SESSION_REQUIRED.toString(), backchannelLogoutSessionRequired.toString()); + } + if (requestUris != null && !requestUris.isEmpty()) { + parameters.put(REQUEST_URIS.toString(), toJSONArray(requestUris)); + } + if (authorizedOrigins != null && !authorizedOrigins.isEmpty()) { + parameters.put(AUTHORIZED_ORIGINS.toString(), toJSONArray(authorizedOrigins)); + } + if (scopes != null && !scopes.isEmpty()) { + parameters.put(SCOPES.toString(), toJSONArray(scopes)); + } + if (scope != null && !scope.isEmpty()) { + parameters.put(SCOPE.toString(), implode(scope, " ")); + } + if (claims != null && !claims.isEmpty()) { + parameters.put(CLAIMS.toString(), implode(claims, " ")); + } + if (accessTokenLifetime != null) { + parameters.put(ACCESS_TOKEN_LIFETIME.toString(), accessTokenLifetime); + } + if (StringUtils.isNotBlank(softwareId)) { + parameters.put(SOFTWARE_ID.toString(), softwareId); + } + if (StringUtils.isNotBlank(softwareVersion)) { + parameters.put(SOFTWARE_VERSION.toString(), softwareVersion); + } + if (StringUtils.isNotBlank(softwareStatement)) { + parameters.put(SOFTWARE_STATEMENT.toString(), softwareStatement); + } + if (backchannelTokenDeliveryMode != null) { + parameters.put(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString(), backchannelTokenDeliveryMode); + } + if (StringUtils.isNotBlank(backchannelClientNotificationEndpoint)) { + parameters.put(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString(), backchannelClientNotificationEndpoint); + } + if (backchannelAuthenticationRequestSigningAlg != null) { + parameters.put(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString(), backchannelAuthenticationRequestSigningAlg.toString()); + } + if (backchannelUserCodeParameter != null) { + parameters.put(BACKCHANNEL_USER_CODE_PARAMETER.toString(), backchannelUserCodeParameter); + } + // Custom params + if (customAttributes != null && !customAttributes.isEmpty()) { + for (Map.Entry entry : customAttributes.entrySet()) { + final String name = entry.getKey(); + final String value = entry.getValue(); + if (RegisterRequestParam.isCustomParameterValid(name) && StringUtils.isNotBlank(value)) { + parameters.put(name, value); + } + } + } + return parameters; + } + + public JSONObject getJsonObject() { + return jsonObject; + } + + public void setJsonObject(JSONObject p_jsonObject) { + jsonObject = p_jsonObject; + } + + @Override + public String getQueryString() { + try { + return ClientUtil.toPrettyJson(getJSONParameters()).replace("\\/", "/"); + } catch (JSONException | JsonProcessingException e) { + log.error(e.getMessage(), e); + return null; + } + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/RegisterResponse.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/RegisterResponse.java new file mode 100644 index 00000000..671999b7 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/RegisterResponse.java @@ -0,0 +1,249 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import static org.gluu.oxauth.model.register.RegisterRequestParam.GRANT_TYPES; +import static org.gluu.oxauth.model.register.RegisterRequestParam.RESPONSE_TYPES; +import static org.gluu.oxauth.model.register.RegisterResponseParam.CLIENT_ID_ISSUED_AT; +import static org.gluu.oxauth.model.register.RegisterResponseParam.CLIENT_SECRET; +import static org.gluu.oxauth.model.register.RegisterResponseParam.CLIENT_SECRET_EXPIRES_AT; +import static org.gluu.oxauth.model.register.RegisterResponseParam.REGISTRATION_CLIENT_URI; + +import java.lang.reflect.InvocationTargetException; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.ws.rs.core.Response; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.RegisterErrorResponseType; +import org.gluu.oxauth.model.register.RegisterResponseParam; +import org.gluu.oxauth.model.util.Util; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Represents a register response received from the authorization server. + * + * @author Javier Rojas Blum + * @version July 18, 2017 + */ +public class RegisterResponse extends BaseResponseWithErrors { + + private static final Logger LOG = Logger.getLogger(RegisterResponse.class); + + private String clientId; + private String clientSecret; + private String registrationAccessToken; + private String registrationClientUri; + private Date clientIdIssuedAt; + private Date clientSecretExpiresAt; + private List responseTypes; + private List grantTypes; + private Map claims = new HashMap(); + + public RegisterResponse() { + } + + /** + * Constructs a register response. + */ + public RegisterResponse(Response clientResponse) { + super(clientResponse); + + injectDataFromJson(entity); + } + + @Override + public RegisterErrorResponseType fromString(String p_string) { + return RegisterErrorResponseType.fromString(p_string); + } + + public void injectDataFromJson() { + injectDataFromJson(getEntity()); + } + + public static RegisterResponse valueOf(String p_json) { + final RegisterResponse r = new RegisterResponse(); + r.injectDataFromJson(p_json); + return r; + } + + public void injectDataFromJson(String p_json) { + if (StringUtils.isNotBlank(p_json)) { + try { + JSONObject jsonObj = new JSONObject(p_json); + if (jsonObj.has(RegisterResponseParam.CLIENT_ID.toString())) { + setClientId(jsonObj.getString(RegisterResponseParam.CLIENT_ID.toString())); + jsonObj.remove(RegisterResponseParam.CLIENT_ID.toString()); + } + if (jsonObj.has(CLIENT_SECRET.toString())) { + setClientSecret(jsonObj.getString(CLIENT_SECRET.toString())); + jsonObj.remove(CLIENT_SECRET.toString()); + } + if (jsonObj.has(RegisterResponseParam.REGISTRATION_ACCESS_TOKEN.toString())) { + setRegistrationAccessToken(jsonObj.getString(RegisterResponseParam.REGISTRATION_ACCESS_TOKEN.toString())); + jsonObj.remove(RegisterResponseParam.REGISTRATION_ACCESS_TOKEN.toString()); + } + if (jsonObj.has(REGISTRATION_CLIENT_URI.toString())) { + setRegistrationClientUri(jsonObj.getString(REGISTRATION_CLIENT_URI.toString())); + jsonObj.remove(REGISTRATION_CLIENT_URI.toString()); + } + if (jsonObj.has(CLIENT_ID_ISSUED_AT.toString())) { + long clientIdIssuedAt = jsonObj.getLong(CLIENT_ID_ISSUED_AT.toString()); + if (clientIdIssuedAt > 0) { + setClientIdIssuedAt(new Date(clientIdIssuedAt * 1000L)); + } + jsonObj.remove(CLIENT_ID_ISSUED_AT.toString()); + } + if (jsonObj.has(CLIENT_SECRET_EXPIRES_AT.toString())) { + long clientSecretExpiresAt = jsonObj.getLong(CLIENT_SECRET_EXPIRES_AT.toString()); + if (clientSecretExpiresAt > 0) { + setClientSecretExpiresAt(new Date(clientSecretExpiresAt * 1000L)); + } + jsonObj.remove(CLIENT_SECRET_EXPIRES_AT.toString()); + } + if (jsonObj.has(RESPONSE_TYPES.toString())) { + JSONArray responseTypesJsonArray = jsonObj.getJSONArray(RESPONSE_TYPES.toString()); + responseTypes = Util.asEnumList(responseTypesJsonArray, ResponseType.class); + } + if (jsonObj.has(GRANT_TYPES.toString())) { + JSONArray grantTypesJsonArray = jsonObj.getJSONArray(GRANT_TYPES.toString()); + grantTypes = Util.asEnumList(grantTypesJsonArray, GrantType.class); + } + + for (Iterator it = jsonObj.keys(); it.hasNext(); ) { + String key = it.next(); + getClaims().put(key, String.valueOf(jsonObj.get(key))); + } + } catch (JSONException e) { + LOG.error(e.getMessage(), e); + } catch (NoSuchMethodException e) { + LOG.error(e.getMessage(), e); + } catch (IllegalAccessException e) { + LOG.error(e.getMessage(), e); + } catch (InvocationTargetException e) { + LOG.error(e.getMessage(), e); + } + } + } + + /** + * Returns the client's identifier. + * + * @return The client's identifier. + */ + public String getClientId() { + return clientId; + } + + /** + * Sets the client's identifier. + * + * @param clientId The client's identifier. + */ + public void setClientId(String clientId) { + this.clientId = clientId; + } + + /** + * Returns the client's password. + * + * @return The client's password. + */ + public String getClientSecret() { + return clientSecret; + } + + /** + * Sets the client's password. + * + * @param clientSecret The client's password. + */ + public void setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + } + + public String getRegistrationAccessToken() { + return registrationAccessToken; + } + + public void setRegistrationAccessToken(String registrationAccessToken) { + this.registrationAccessToken = registrationAccessToken; + } + + public String getRegistrationClientUri() { + return registrationClientUri; + } + + public void setRegistrationClientUri(String registrationClientUri) { + this.registrationClientUri = registrationClientUri; + } + + public Date getClientIdIssuedAt() { + // findbugs : return copy instead of original object + return clientIdIssuedAt != null ? new Date(clientIdIssuedAt.getTime()) : null; + } + + public void setClientIdIssuedAt(Date clientIdIssuedAt) { + // findbugs : save copy instead of original object + this.clientIdIssuedAt = clientIdIssuedAt != null ? new Date(clientIdIssuedAt.getTime()) : null; + } + + /** + * Return the expiration date after which the client's account will expire. + * null if the client's account never expires. + * + * @return The expiration date. + */ + public Date getClientSecretExpiresAt() { + // findbugs : return copy instead of original object + return clientSecretExpiresAt != null ? new Date(clientSecretExpiresAt.getTime()) : null; + } + + /** + * Sets the expiration date after which the client's account will expire. + * null if the client's account never expires. + * + * @param clientSecretExpiresAt The expiration date. + */ + public void setClientSecretExpiresAt(Date clientSecretExpiresAt) { + // findbugs : save copy instead of original object + this.clientSecretExpiresAt = clientSecretExpiresAt != null ? new Date(clientSecretExpiresAt.getTime()) : null; + } + + public List getResponseTypes() { + return responseTypes; + } + + public void setResponseTypes(List responseTypes) { + this.responseTypes = responseTypes; + } + + public List getGrantTypes() { + return grantTypes; + } + + public void setGrantTypes(List grantTypes) { + this.grantTypes = grantTypes; + } + + public Map getClaims() { + return claims; + } + + public void setClaims(Map claims) { + this.claims = claims; + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/RevokeSessionClient.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/RevokeSessionClient.java new file mode 100644 index 00000000..c89a39de --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/RevokeSessionClient.java @@ -0,0 +1,71 @@ +package org.gluu.oxauth.client; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation.Builder; + +import org.apache.log4j.Logger; + +/** + * @author Yuriy Zabrovarnyy + */ +public class RevokeSessionClient extends BaseClient{ + + private static final Logger LOG = Logger.getLogger(RevokeSessionClient.class); + + /** + * Constructs a token client by providing a REST url where the token service + * is located. + * + * @param url The REST Service location. + */ + public RevokeSessionClient(String url) { + super(url); + } + + @Override + public String getHttpMethod() { + return HttpMethod.POST; + } + + public RevokeSessionResponse exec(RevokeSessionRequest request) { + setRequest(request); + return exec(); + } + + public RevokeSessionResponse exec() { + initClientRequest(); + + Builder clientRequest = webTarget.request(); + applyCookies(clientRequest); + + new ClientAuthnEnabler(clientRequest, requestForm).exec(request); + + clientRequest.header("Content-Type", request.getContentType()); +// clientRequest.setHttpMethod(getHttpMethod()); + + if (getRequest().getUserCriterionKey() != null) { + requestForm.param("user_criterion_key", getRequest().getUserCriterionKey()); + } + if (getRequest().getUserCriterionValue() != null) { + requestForm.param("user_criterion_value", getRequest().getUserCriterionValue()); + } + + for (String key : getRequest().getCustomParameters().keySet()) { + requestForm.param(key, getRequest().getCustomParameters().get(key)); + } + + try { + clientResponse = clientRequest.buildPost(Entity.form(requestForm)).invoke(); + + final RevokeSessionResponse response = new RevokeSessionResponse(clientResponse); + setResponse(response); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } finally { + closeConnection(); + } + + return getResponse(); + } +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/RevokeSessionRequest.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/RevokeSessionRequest.java new file mode 100644 index 00000000..09c40c5b --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/RevokeSessionRequest.java @@ -0,0 +1,54 @@ +package org.gluu.oxauth.client; + +import javax.ws.rs.core.MediaType; + +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.util.QueryBuilder; + +/** + * @author Yuriy Zabrovarnyy + */ +public class RevokeSessionRequest extends ClientAuthnRequest { + + private String userCriterionKey; + private String userCriterionValue; + + public RevokeSessionRequest() { + setContentType(MediaType.APPLICATION_FORM_URLENCODED); + setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + } + + public RevokeSessionRequest(String userCriterionKey, String userCriterionValue) { + this.userCriterionKey = userCriterionKey; + this.userCriterionValue = userCriterionValue; + setContentType(MediaType.APPLICATION_FORM_URLENCODED); + setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + } + + public String getUserCriterionKey() { + return userCriterionKey; + } + + public void setUserCriterionKey(String userCriterionKey) { + this.userCriterionKey = userCriterionKey; + } + + public String getUserCriterionValue() { + return userCriterionValue; + } + + public void setUserCriterionValue(String userCriterionValue) { + this.userCriterionValue = userCriterionValue; + } + + @Override + public String getQueryString() { + QueryBuilder builder = QueryBuilder.instance(); + + builder.append("user_criterion_key", userCriterionKey); + builder.append("user_criterion_value", userCriterionValue); + appendClientAuthnToQuery(builder); + + return builder.toString(); + } +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/RevokeSessionResponse.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/RevokeSessionResponse.java new file mode 100644 index 00000000..fdbabb23 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/RevokeSessionResponse.java @@ -0,0 +1,28 @@ +package org.gluu.oxauth.client; + +import javax.ws.rs.core.Response; + +import org.gluu.oxauth.model.session.EndSessionErrorResponseType; + +/** + * @author Yuriy Zabrovarnyy + */ +public class RevokeSessionResponse extends BaseResponseWithErrors{ + + public RevokeSessionResponse() { + } + + public RevokeSessionResponse(Response clientResponse) { + super(clientResponse); + injectDataFromJson(); + } + + @Override + public EndSessionErrorResponseType fromString(String params) { + return EndSessionErrorResponseType.fromString(params); + } + + public void injectDataFromJson() { + injectDataFromJson(entity); + } +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/TokenClient.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/TokenClient.java new file mode 100644 index 00000000..15578b5a --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/TokenClient.java @@ -0,0 +1,283 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation.Builder; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.gluu.oxauth.model.common.GrantType; + +/** + * Encapsulates functionality to make token request calls to an authorization + * server via REST Services. + * + * @author Javier Rojas Blum + * @version February 25, 2020 + */ +public class TokenClient extends BaseClient { + + private static final Logger LOG = Logger.getLogger(TokenClient.class); + + /** + * Constructs a token client by providing a REST url where the token service + * is located. + * + * @param url The REST Service location. + */ + public TokenClient(String url) { + super(url); + } + + @Override + public String getHttpMethod() { + return HttpMethod.POST; + } + + /** + *

+ * Executes the call to the REST Service requesting the authorization and + * processes the response. + *

+ *

+ * The authorization code is obtained by using an authorization server as an + * intermediary between the client and resource owner. Instead of requesting + * authorization directly from the resource owner, the client directs the + * resource owner to an authorization server (via its user- agent as defined in + * [RFC2616]), which in turn directs the resource owner back to the client with + * the authorization code. + *

+ *

+ * Before directing the resource owner back to the client with the authorization + * code, the authorization server authenticates the resource owner and obtains + * authorization. Because the resource owner only authenticates with the + * authorization server, the resource owner's credentials are never shared with + * the client. + *

+ *

+ * The authorization code provides a few important security benefits such as the + * ability to authenticate the client, and the transmission of the access token + * directly to the client without passing it through the resource owner's + * user-agent, potentially exposing it to others, including the resource owner. + *

+ * + * @param code he authorization code received from the authorization server. + * This parameter is required. + * @param redirectUri The redirection URI. This parameter is required. + * @param clientId The client identifier. + * @param clientSecret The client secret. + * @return The token response. + */ + public TokenResponse execAuthorizationCode(String code, String redirectUri, + String clientId, String clientSecret) { + setRequest(new TokenRequest(GrantType.AUTHORIZATION_CODE)); + getRequest().setCode(code); + getRequest().setRedirectUri(redirectUri); + getRequest().setAuthUsername(clientId); + getRequest().setAuthPassword(clientSecret); + + return exec(); + } + + /** + *

+ * Executes the call to the REST Service requesting the authorization and + * processes the response. + *

+ *

+ * The resource owner password credentials grant type is suitable in cases + * where the resource owner has a trust relationship with the client, such + * as its device operating system or a highly privileged application. The + * authorization server should take special care when enabling this grant + * type, and only allow it when other flows are not viable. + *

+ *

+ * The grant type is suitable for clients capable of obtaining the resource + * owner's credentials (username and password, typically using an + * interactive form). It is also used to migrate existing clients using + * direct authentication schemes such as HTTP Basic or Digest authentication + * to OAuth by converting the stored credentials to an access token. + *

+ * + * @param username The resource owner username. This parameter is required. + * @param password The resource owner password. This parameter is required. + * @param scope The scope of the access request. This parameter is optional. + * @param clientId The client identifier. + * @param clientSecret The client secret. + * @return The token response. + */ + public TokenResponse execResourceOwnerPasswordCredentialsGrant( + String username, String password, String scope, + String clientId, String clientSecret) { + setRequest(new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS)); + getRequest().setUsername(username); + getRequest().setPassword(password); + getRequest().setScope(scope); + getRequest().setAuthUsername(clientId); + getRequest().setAuthPassword(clientSecret); + + return exec(); + } + + /** + *

+ * Executes the call to the REST Service requesting the authorization and + * processes the response. + *

+ *

+ * The client can request an access token using only its client credentials + * when the client is requesting access to the protected resources under its + * control, or those of another resource owner which has been previously + * arranged with the authorization server. The client credentials grant type + * must only be used by confidential clients. + *

+ * + * @param scope The scope of the access request. This parameter is optional. + * @param clientId The client identifier. + * @param clientSecret The client secret. + * @return The token response. + */ + public TokenResponse execClientCredentialsGrant( + String scope, String clientId, String clientSecret) { + setRequest(new TokenRequest(GrantType.CLIENT_CREDENTIALS)); + getRequest().setScope(scope); + getRequest().setAuthUsername(clientId); + getRequest().setAuthPassword(clientSecret); + + return exec(); + } + + /** + *

+ * Executes the call to the REST Service requesting the authorization and + * processes the response. + *

+ *

+ * The client uses an extension grant type by specifying the grant type + * using an absolute URI (defined by the authorization server) as the value + * of the grant_type parameter of the token endpoint, and by adding any + * additional parameters necessary. + *

+ * + * @param grantTypeUri Absolute URI. + * @param assertion Assertion grant type. + * @param clientId The client identifier. + * @param clientSecret The client secret. + * @return The token response. + */ + public TokenResponse execExtensionGrant(String grantTypeUri, String assertion, + String clientId, String clientSecret) { + GrantType grantType = GrantType.fromString(grantTypeUri); + setRequest(new TokenRequest(grantType)); + getRequest().setAssertion(assertion); + getRequest().setAuthUsername(clientId); + getRequest().setAuthPassword(clientSecret); + + return exec(); + } + + /** + *

+ * Executes the call to the REST Service requesting the authorization and + * processes the response. + *

+ *

+ * If the authorization server issued a refresh token to the client, the + * client can make a request to the token endpoint for a new access token. + *

+ * + * @param scope The scope of the access request. This value is optional. + * @param refreshToken The refresh token issued to the client. This value is + * required. + * @param clientId The client identifier. + * @param clientSecret The client secret. + * @return The token response. + */ + public TokenResponse execRefreshToken(String scope, String refreshToken, + String clientId, String clientSecret) { + setRequest(new TokenRequest(GrantType.REFRESH_TOKEN)); + getRequest().setScope(scope); + getRequest().setRefreshToken(refreshToken); + getRequest().setAuthUsername(clientId); + getRequest().setAuthPassword(clientSecret); + + return exec(); + } + + /** + * Executes the call to the REST Service and processes the response. + * + * @return The token response. + */ + public TokenResponse exec() { + // Prepare request parameters + initClientRequest(); + + if (getRequest().getGrantType() != null) { + requestForm.param("grant_type", getRequest().getGrantType().toString()); + } + if (StringUtils.isNotBlank(getRequest().getCode())) { + requestForm.param("code", getRequest().getCode()); + } + if (StringUtils.isNotBlank(getRequest().getCodeVerifier())) { + requestForm.param("code_verifier", getRequest().getCodeVerifier()); + } + if (StringUtils.isNotBlank(getRequest().getRedirectUri())) { + requestForm.param("redirect_uri", getRequest().getRedirectUri()); + } + if (StringUtils.isNotBlank(getRequest().getUsername())) { + requestForm.param("username", getRequest().getUsername()); + } + if (StringUtils.isNotBlank(getRequest().getPassword())) { + requestForm.param("password", getRequest().getPassword()); + } + if (StringUtils.isNotBlank(getRequest().getScope())) { + requestForm.param("scope", getRequest().getScope()); + } + if (StringUtils.isNotBlank(getRequest().getAssertion())) { + requestForm.param("assertion", getRequest().getAssertion()); + } + if (StringUtils.isNotBlank(getRequest().getRefreshToken())) { + requestForm.param("refresh_token", getRequest().getRefreshToken()); + } + + for (String key : getRequest().getCustomParameters().keySet()) { + requestForm.param(key, getRequest().getCustomParameters().get(key)); + } + if (StringUtils.isNotBlank(getRequest().getAuthReqId())) { + requestForm.param("auth_req_id", getRequest().getAuthReqId()); + } + if (StringUtils.isNotBlank(getRequest().getDeviceCode())) { + requestForm.param("device_code", getRequest().getDeviceCode()); + } + + Builder clientRequest = webTarget.request(); + applyCookies(clientRequest); + + new ClientAuthnEnabler(clientRequest, requestForm).exec(request); + + clientRequest.header("Content-Type", request.getContentType()); +// clientRequest.setHttpMethod(getHttpMethod()); + + // Call REST Service and handle response + try { + clientResponse = clientRequest.buildPost(Entity.form(requestForm)).invoke(); + + final TokenResponse tokenResponse = new TokenResponse(clientResponse); + tokenResponse.injectDataFromJson(); + setResponse(tokenResponse); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } finally { + closeConnection(); + } + + return getResponse(); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/TokenRequest.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/TokenRequest.java new file mode 100644 index 00000000..dae5df45 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/TokenRequest.java @@ -0,0 +1,352 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import java.util.HashMap; +import java.util.Map; + +import javax.ws.rs.core.MediaType; + +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.token.ClientAssertionType; +import org.gluu.oxauth.model.uma.UmaScopeType; +import org.gluu.oxauth.model.util.QueryBuilder; + +/** + * Represents a token request to send to the authorization server. + * + * @author Javier Rojas Blum + * @version June 28, 2017 + */ +public class TokenRequest extends ClientAuthnRequest { + + public static class Builder { + + private GrantType grantType; + private String scope; + + public Builder grantType(GrantType grantType) { + this.grantType = grantType; + return this; + } + + public Builder scope(String scope) { + this.scope = scope; + return this; + } + + public Builder pat(String... scopeArray) { + String scope = UmaScopeType.PROTECTION.getValue(); + if (scopeArray != null && scopeArray.length > 0) { + for (String s : scopeArray) { + scope = scope + " " + s; + } + } + return scope(scope); + } + + public TokenRequest build() { + final TokenRequest request = new TokenRequest(grantType); + request.setScope(scope); + return request; + } + } + + private GrantType grantType; + private String code; + private String redirectUri; + private String username; + private String password; + private String scope; + private String assertion; + private String refreshToken; + private String codeVerifier; + private String authReqId; + private String deviceCode; + + /** + * Constructs a token request. + * + * @param grantType The grant type is mandatory and could be: + * authorization_code, password, + * client_credentials, refresh_token. + */ + public TokenRequest(GrantType grantType) { + super(); + this.grantType = grantType; + + setContentType(MediaType.APPLICATION_FORM_URLENCODED); + setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + } + + public static Builder builder() { + return new Builder(); + } + + public static Builder umaBuilder() { + return new Builder().grantType(GrantType.CLIENT_CREDENTIALS); + } + + /** + * Returns the grant type. + * + * @return The grant type. + */ + public GrantType getGrantType() { + return grantType; + } + + /** + * Sets the grant type. + * + * @param grantType The grant type. + */ + public void setGrantType(GrantType grantType) { + this.grantType = grantType; + } + + /** + * Returns the authorization code. + * + * @return The authorization code. + */ + public String getCode() { + return code; + } + + /** + * Sets the authorization code. + * + * @param code The authorization code. + */ + public void setCode(String code) { + this.code = code; + } + + /** + * Gets PKCE code verifier. + * + * @return code verifier + */ + public String getCodeVerifier() { + return codeVerifier; + } + + /** + * Sets PKCE code verifier. + * + * @param codeVerifier code verifier + */ + public void setCodeVerifier(String codeVerifier) { + this.codeVerifier = codeVerifier; + } + + /** + * Returns the redirect URI. + * + * @return The redirect URI. + */ + public String getRedirectUri() { + return redirectUri; + } + + /** + * Sets the redirect URI. + * + * @param redirectUri The redirect URI. + */ + public void setRedirectUri(String redirectUri) { + this.redirectUri = redirectUri; + } + + /** + * Returns the username. + * + * @return The username. + */ + public String getUsername() { + return username; + } + + /** + * Sets the username. + * + * @param username The username. + */ + public void setUsername(String username) { + this.username = username; + } + + /** + * Returns the password. + * + * @return The password. + */ + public String getPassword() { + return password; + } + + /** + * Sets the password. + * + * @param password The password. + */ + public void setPassword(String password) { + this.password = password; + } + + /** + * Returns the scope. + * + * @return The scope. + */ + public String getScope() { + return scope; + } + + /** + * Sets the scope. + * + * @param scope The scope. + */ + public void setScope(String scope) { + this.scope = scope; + } + + /** + * Returns the assertion. + * + * @return The assertion. + */ + public String getAssertion() { + return assertion; + } + + /** + * Sets the assertion. + * + * @param assertion The assertion. + */ + public void setAssertion(String assertion) { + this.assertion = assertion; + } + + /** + * Returns the refresh token. + * + * @return The refresh token. + */ + public String getRefreshToken() { + return refreshToken; + } + + /** + * Sets the refresh token. + * + * @param refreshToken The refresh token. + */ + public void setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } + + public String getAuthReqId() { + return authReqId; + } + + public void setAuthReqId(String authReqId) { + this.authReqId = authReqId; + } + + public String getDeviceCode() { + return deviceCode; + } + + public void setDeviceCode(String deviceCode) { + this.deviceCode = deviceCode; + } + + /** + * Returns a query string with the parameters of the authorization request. + * Any null or empty parameter will be omitted. + * + * @return A query string of parameters. + */ + @Override + public String getQueryString() { + QueryBuilder builder = QueryBuilder.instance(); + + builder.appendIfNotNull("grant_type", grantType); + builder.append("code", code); + builder.append("redirect_uri", redirectUri); + builder.append("scope", scope); + builder.append("username", username); + builder.append("password", password); + builder.append("assertion", assertion); + builder.append("refreshToken", refreshToken); + builder.append("authReqId", authReqId); + builder.append("device_code", deviceCode); + appendClientAuthnToQuery(builder); + for (String key : getCustomParameters().keySet()) { + builder.append(key, getCustomParameters().get(key)); + } + + return builder.toString(); + } + + /** + * Returns a collection of parameters of the token request. Any + * null or empty parameter will be omitted. + * + * @return A collection of parameters. + */ + public Map getParameters() { + Map parameters = new HashMap(); + + if (grantType != null) { + parameters.put("grant_type", grantType.toString()); + } + if (code != null && !code.isEmpty()) { + parameters.put("code", code); + } + if (redirectUri != null && !redirectUri.isEmpty()) { + parameters.put("redirect_uri", redirectUri); + } + if (username != null && !username.isEmpty()) { + parameters.put("username", username); + } + if (password != null && !password.isEmpty()) { + parameters.put("password", password); + } + if (scope != null && !scope.isEmpty()) { + parameters.put("scope", scope); + } + if (assertion != null && !assertion.isEmpty()) { + parameters.put("assertion", assertion); + } + if (refreshToken != null && !refreshToken.isEmpty()) { + parameters.put("refresh_token", refreshToken); + } + if (getAuthenticationMethod() == AuthenticationMethod.CLIENT_SECRET_POST) { + if (getAuthUsername() != null && !getAuthUsername().isEmpty()) { + parameters.put("client_id", getAuthUsername()); + } + if (getAuthPassword() != null && !getAuthPassword().isEmpty()) { + parameters.put("client_secret", getAuthPassword()); + } + } else if (getAuthenticationMethod() == AuthenticationMethod.CLIENT_SECRET_JWT || + getAuthenticationMethod() == AuthenticationMethod.PRIVATE_KEY_JWT) { + parameters.put("client_assertion_type", ClientAssertionType.JWT_BEARER.toString()); + parameters.put("client_assertion", getClientAssertion()); + } + for (String key : getCustomParameters().keySet()) { + parameters.put(key, getCustomParameters().get(key)); + } + + return parameters; + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/TokenResponse.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/TokenResponse.java new file mode 100644 index 00000000..580687ae --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/TokenResponse.java @@ -0,0 +1,194 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import javax.ws.rs.core.Response; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.gluu.oxauth.model.common.TokenType; +import org.gluu.oxauth.model.token.TokenErrorResponseType; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Represents a token response received from the authorization server. + * + * @author Javier Rojas Blum Date: 10.19.2011 + */ +public class TokenResponse extends BaseResponseWithErrors { + + private static final Logger LOG = Logger.getLogger(TokenResponse.class); + + private String accessToken; + private TokenType tokenType; + private Integer expiresIn; + private String refreshToken; + private String scope; + private String idToken; + + public TokenResponse() { + } + + /** + * Constructs a token response. + * + * @param clientResponse The response + */ + public TokenResponse(Response clientResponse) { + super(clientResponse); + } + + @Override + public TokenErrorResponseType fromString(String p_str) { + return TokenErrorResponseType.fromString(p_str); + } + + public void injectDataFromJson() { + injectDataFromJson(entity); + } + + @Override + public void injectDataFromJson(String p_json) { + if (StringUtils.isNotBlank(entity)) { + try { + JSONObject jsonObj = new JSONObject(entity); + if (jsonObj.has("access_token")) { + setAccessToken(jsonObj.getString("access_token")); + } + if (jsonObj.has("token_type")) { + setTokenType(TokenType.fromString(jsonObj.getString("token_type"))); + } + if (jsonObj.has("expires_in")) { + setExpiresIn(jsonObj.getInt("expires_in")); + } + if (jsonObj.has("refresh_token")) { + setRefreshToken(jsonObj.getString("refresh_token")); + } + if (jsonObj.has("scope")) { + setScope(jsonObj.getString("scope")); + } + if (jsonObj.has("id_token")) { + setIdToken(jsonObj.getString("id_token")); + } + } catch (JSONException e) { + LOG.error(e.getMessage(), e); + } + } + } + + + /** + * Returns the access token issued by the authorization server. + * + * @return The access token issued by the authorization server. + */ + public String getAccessToken() { + return accessToken; + } + + /** + * Sets the access token issued by the authorization server. + * + * @param accessToken The access token issued by the authorization server. + */ + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + /** + * Returns the type of the token issued. Value is case insensitive. + * + * @return The type of the token issued. + */ + public TokenType getTokenType() { + return tokenType; + } + + /** + * Sets the type of the token issued. Value is case insensitive. + * + * @param tokenType The type of the token issued. + */ + public void setTokenType(TokenType tokenType) { + this.tokenType = tokenType; + } + + /** + * Returns the lifetime in seconds of the access token. + * + * @return The lifetime in seconds of the access token. + */ + public Integer getExpiresIn() { + return expiresIn; + } + + /** + * Sets the lifetime in seconds of the access token. + * + * @param expiresIn The lifetime in seconds of the access token. + */ + public void setExpiresIn(Integer expiresIn) { + this.expiresIn = expiresIn; + } + + /** + * Returns the refresh token which can be used to obtain new access tokens + * using the same authorization grant. + * + * @return The refresh token. + */ + public String getRefreshToken() { + return refreshToken; + } + + /** + * Sets the refresh token which can be used to obtain new access tokens + * using the same authorization grant. + * + * @param refreshToken The refresh token. + */ + public void setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } + + /** + * Returns the scope of the access token. + * + * @return The scope of the access token. + */ + public String getScope() { + return scope; + } + + /** + * Sets the scope of the access token. + * + * @param scope The scope of the access token. + */ + public void setScope(String scope) { + this.scope = scope; + } + + /** + * Gets the value of the id token. + * + * @return The id token. + */ + public String getIdToken() { + return idToken; + } + + /** + * Sets the value of the id token. + * + * @param idToken The id token. + */ + public void setIdToken(String idToken) { + this.idToken = idToken; + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/TokenRevocationClient.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/TokenRevocationClient.java new file mode 100644 index 00000000..f6ab8f9b --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/TokenRevocationClient.java @@ -0,0 +1,114 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation.Builder; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.gluu.oxauth.model.common.TokenTypeHint; +import org.gluu.oxauth.model.token.TokenRevocationRequestParam; + +/** + * Encapsulates functionality to make token revocation request calls to an authorization server via REST Services. + * + * @author Javier Rojas Blum + * @version January 16, 2019 + */ +public class TokenRevocationClient extends BaseClient { + + private static final Logger LOG = Logger.getLogger(TokenRevocationClient.class); + + /** + * Constructs a token revocation client by providing a REST url where the token service is located. + * + * @param url The REST Service location. + */ + public TokenRevocationClient(String url) { + super(url); + } + + @Override + public String getHttpMethod() { + return HttpMethod.POST; + } + + /** + * Executes the call to the REST Service requesting the token revocation and processes the response. + * + * @param clientId The client identifier. + * @param clientSecret The client secret. + * @param token The token that the client wants to get revoked. + * @return The token revocation response. + */ + public TokenRevocationResponse execTokenRevocation(String clientId, String clientSecret, String token) { + return execTokenRevocation(clientId, clientSecret, token, null); + } + + /** + * Executes the call to the REST Service requesting the token revocation and processes the response. + * + * @param clientId The client identifier. + * @param clientSecret The client secret. + * @param token The token that the client wants to get revoked. + * @param tokenTypeHint A hint about the type of the token submitted for revocation. + * @return The token revocation response. + */ + public TokenRevocationResponse execTokenRevocation(String clientId, String clientSecret, String token, TokenTypeHint tokenTypeHint) { + setRequest(new TokenRevocationRequest()); + getRequest().setToken(token); + getRequest().setTokenTypeHint(tokenTypeHint); + getRequest().setAuthUsername(clientId); + getRequest().setAuthPassword(clientSecret); + + return exec(); + } + + /** + * Executes the call to the REST Service and processes the response. + * + * @return The token revocation response. + */ + public TokenRevocationResponse exec() { + // Prepare request parameters + initClientRequest(); + + if (StringUtils.isNotBlank(getRequest().getToken())) { + requestForm.param(TokenRevocationRequestParam.TOKEN, getRequest().getToken()); + } + if (getRequest().getTokenTypeHint() != null) { + requestForm.param(TokenRevocationRequestParam.TOKEN_TYPE_HINT, getRequest().getTokenTypeHint().toString()); + } + if (request.getAuthUsername() != null && !request.getAuthUsername().isEmpty()) { + requestForm.param("client_id", request.getAuthUsername()); + } + + Builder clientRequest = webTarget.request(); + applyCookies(clientRequest); + + new ClientAuthnEnabler(clientRequest, requestForm).exec(request); + + clientRequest.header("Content-Type", request.getContentType()); +// clientRequest.setHttpMethod(getHttpMethod()); + + // Call REST Service and handle response + try { + clientResponse = clientRequest.buildPost(Entity.form(requestForm)).invoke(); + + final TokenRevocationResponse tokenResponse = new TokenRevocationResponse(clientResponse); + setResponse(tokenResponse); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } finally { + closeConnection(); + } + + return getResponse(); + } +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/TokenRevocationRequest.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/TokenRevocationRequest.java new file mode 100644 index 00000000..f8aa2eb3 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/TokenRevocationRequest.java @@ -0,0 +1,68 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import javax.ws.rs.core.MediaType; + +import org.apache.log4j.Logger; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.TokenTypeHint; +import org.gluu.oxauth.model.token.TokenRevocationRequestParam; +import org.gluu.oxauth.model.util.QueryBuilder; + +/** + * @author Javier Rojas Blum + * @version January 16, 2019 + */ +public class TokenRevocationRequest extends ClientAuthnRequest { + + private static final Logger LOG = Logger.getLogger(TokenRevocationRequest.class); + + private String token; + private TokenTypeHint tokenTypeHint; + + /** + * Constructs a token revocation request. + */ + public TokenRevocationRequest() { + super(); + + setContentType(MediaType.APPLICATION_FORM_URLENCODED); + setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public TokenTypeHint getTokenTypeHint() { + return tokenTypeHint; + } + + public void setTokenTypeHint(TokenTypeHint tokenTypeHint) { + this.tokenTypeHint = tokenTypeHint; + } + + /** + * Returns a query string with the parameters of the toke revocation request. + * Any null or empty parameter will be omitted. + * + * @return A query string of parameters. + */ + @Override + public String getQueryString() { + QueryBuilder queryBuilder = new QueryBuilder(); + queryBuilder.append(TokenRevocationRequestParam.TOKEN, token); + queryBuilder.append(TokenRevocationRequestParam.TOKEN_TYPE_HINT, tokenTypeHint != null ? tokenTypeHint.toString() : null); + queryBuilder.append("client_id", getAuthUsername()); + return queryBuilder.toString(); + } +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/TokenRevocationResponse.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/TokenRevocationResponse.java new file mode 100644 index 00000000..77c6f897 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/TokenRevocationResponse.java @@ -0,0 +1,113 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import javax.ws.rs.core.Response; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.token.TokenRevocationErrorResponseType; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * @author Javier Rojas Blum + * @version January 16, 2019 + */ +public class TokenRevocationResponse extends BaseResponse { + + private TokenRevocationErrorResponseType errorType; + private String errorDescription; + private String errorUri; + + /** + * Constructs an token revocation response. + */ + public TokenRevocationResponse(Response clientResponse) { + super(clientResponse); + + if (StringUtils.isNotBlank(entity)) { + try { + JSONObject jsonObj = new JSONObject(entity); + if (jsonObj.has("error")) { + errorType = TokenRevocationErrorResponseType.getByValue(jsonObj.getString("error")); + } + if (jsonObj.has("error_description")) { + errorDescription = jsonObj.getString("error_description"); + } + if (jsonObj.has("error_uri")) { + errorUri = jsonObj.getString("error_uri"); + } + } catch (JSONException e) { + e.printStackTrace(); + } + } + } + + /** + * Returns the error code when the request fails, otherwise will return + * null. + * + * @return The error code when the request fails. + */ + public TokenRevocationErrorResponseType getErrorType() { + return errorType; + } + + /** + * Sets the error code when the request fails, otherwise will return + * null. + * + * @param errorType The error code when the request fails. + */ + public void setErrorType(TokenRevocationErrorResponseType errorType) { + this.errorType = errorType; + } + + /** + * Returns a human-readable UTF-8 encoded text providing additional + * information, used to assist the client developer in understanding the + * error that occurred. + * + * @return The error description. + */ + public String getErrorDescription() { + return errorDescription; + } + + /** + * Sets a human-readable UTF-8 encoded text providing additional + * information, used to assist the client developer in understanding the + * error that occurred. + * + * @param errorDescription The error description. + */ + public void setErrorDescription(String errorDescription) { + this.errorDescription = errorDescription; + } + + /** + * Returns a URI identifying a human-readable web page with information + * about the error, used to provide the client developer with additional + * information about the error. + * + * @return A URI with information about the error. + */ + public String getErrorUri() { + return errorUri; + } + + /** + * Sets a URI identifying a human-readable web page with information about + * the error, used to provide the client developer with additional + * information about the error. + * + * @param errorUri A URI with information about the error. + */ + public void setErrorUri(String errorUri) { + this.errorUri = errorUri; + } +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/UserInfoClient.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/UserInfoClient.java new file mode 100644 index 00000000..c4c43044 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/UserInfoClient.java @@ -0,0 +1,216 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import java.security.PrivateKey; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation.Builder; +import javax.ws.rs.core.MediaType; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.common.AuthorizationMethod; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.jwe.Jwe; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.userinfo.UserInfoErrorResponseType; +import org.gluu.oxauth.model.util.JwtUtil; +import org.gluu.oxauth.model.util.Util; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Encapsulates functionality to make user info request calls to an authorization server via REST Services. + * + * @author Javier Rojas Blum + * @version December 26, 2016 + */ +public class UserInfoClient extends BaseClient { + + private String sharedKey; + private PrivateKey privateKey; + private String jwksUri; + + /** + * Constructs an User Info client by providing a REST url where the service is located. + * + * @param url The REST Service location. + */ + public UserInfoClient(String url) { + super(url); + } + + @Override + public String getHttpMethod() { + if (request.getAuthorizationMethod() == null + || request.getAuthorizationMethod() == AuthorizationMethod.AUTHORIZATION_REQUEST_HEADER_FIELD + || request.getAuthorizationMethod() == AuthorizationMethod.URL_QUERY_PARAMETER) { + return HttpMethod.GET; + } else /*if (request.getAuthorizationMethod() == AuthorizationMethod.FORM_ENCODED_BODY_PARAMETER)*/ { + return HttpMethod.POST; + } + } + + /** + * Executes the call to the REST Service and processes the response. + * + * @param accessToken The access token obtained from the oxAuth authorization request. + * @return The service response. + */ + public UserInfoResponse execUserInfo(String accessToken) { + setRequest(new UserInfoRequest(accessToken)); + + return exec(); + } + + /** + * Executes the call to the REST Service and processes the response. + * + * @return The service response. + */ + public UserInfoResponse exec() { + // Prepare request parameters + initClientRequest(); + + Builder clientRequest = null; + if (getRequest().getAuthorizationMethod() == null + || getRequest().getAuthorizationMethod() == AuthorizationMethod.AUTHORIZATION_REQUEST_HEADER_FIELD) { + if (StringUtils.isNotBlank(getRequest().getAccessToken())) { + clientRequest = webTarget.request(); + clientRequest.header("Authorization", "Bearer " + getRequest().getAccessToken()); + } + } else if (getRequest().getAuthorizationMethod() == AuthorizationMethod.FORM_ENCODED_BODY_PARAMETER) { + if (StringUtils.isNotBlank(getRequest().getAccessToken())) { + requestForm.param("access_token", getRequest().getAccessToken()); + } + } else if (getRequest().getAuthorizationMethod() == AuthorizationMethod.URL_QUERY_PARAMETER) { + if (StringUtils.isNotBlank(getRequest().getAccessToken())) { + addReqParam("access_token", getRequest().getAccessToken().toString()); + } + } + + if (clientRequest == null) { + clientRequest = webTarget.request(); + } + + clientRequest.header("Content-Type", MediaType.APPLICATION_FORM_URLENCODED); +// clientRequest.setHttpMethod(getHttpMethod()); + + // Call REST Service and handle response + try { + if (getRequest().getAuthorizationMethod() == null + || getRequest().getAuthorizationMethod() == AuthorizationMethod.AUTHORIZATION_REQUEST_HEADER_FIELD + || getRequest().getAuthorizationMethod() == AuthorizationMethod.URL_QUERY_PARAMETER) { + clientResponse = clientRequest.buildGet().invoke(); + } else if (getRequest().getAuthorizationMethod() == AuthorizationMethod.FORM_ENCODED_BODY_PARAMETER) { + clientResponse = clientRequest.buildPost(Entity.form(requestForm)).invoke(); + } + + int status = clientResponse.getStatus(); + + setResponse(new UserInfoResponse(status)); + + String entity = clientResponse.readEntity(String.class); + getResponse().setEntity(entity); + getResponse().setHeaders(clientResponse.getMetadata()); + if (StringUtils.isNotBlank(entity)) { + List contentType = clientResponse.getHeaders().get("Content-Type"); + if (contentType != null && contentType.contains("application/jwt")) { + String[] jwtParts = entity.split("\\."); + if (jwtParts.length == 5) { + byte[] sharedSymmetricKey = sharedKey != null ? sharedKey.getBytes(Util.UTF8_STRING_ENCODING) : null; + Jwe jwe = Jwe.parse(entity, privateKey, sharedSymmetricKey); + getResponse().setClaims(jwe.getClaims().toMap()); + } else { + Jwt jwt = Jwt.parse(entity); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + boolean signatureVerified = cryptoProvider.verifySignature( + jwt.getSigningInput(), + jwt.getEncodedSignature(), + jwt.getHeader().getKeyId(), + JwtUtil.getJSONWebKeys(jwksUri), + sharedKey, + jwt.getHeader().getSignatureAlgorithm()); + + if (signatureVerified) { + getResponse().setClaims(jwt.getClaims().toMap()); + } + } + } else { + try { + JSONObject jsonObj = new JSONObject(entity); + + if (jsonObj.has("error")) { + getResponse().setErrorType(UserInfoErrorResponseType.fromString(jsonObj.getString("error"))); + jsonObj.remove("error"); + } + if (jsonObj.has("error_description")) { + getResponse().setErrorDescription(jsonObj.getString("error_description")); + jsonObj.remove("error_description"); + } + if (jsonObj.has("error_uri")) { + getResponse().setErrorUri(jsonObj.getString("error_uri")); + jsonObj.remove("error_uri"); + } + + for (Iterator iterator = jsonObj.keys(); iterator.hasNext(); ) { + String key = iterator.next(); + List values = new ArrayList(); + + JSONArray jsonArray = jsonObj.optJSONArray(key); + if (jsonArray != null) { + for (int i = 0; i < jsonArray.length(); i++) { + String value = jsonArray.optString(i); + if (value != null) { + values.add(value); + } + } + } else { + String value = jsonObj.optString(key); + if (value != null) { + values.add(value); + } + } + + getResponse().getClaims().put(key, values); + } + } catch (JSONException e) { + e.printStackTrace(); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + closeConnection(); + } + + return getResponse(); + } + + public void setSharedKey(String sharedKey) { + this.sharedKey = sharedKey; + } + + public void setPrivateKey(PrivateKey privateKey) { + this.privateKey = privateKey; + } + + public String getJwksUri() { + return jwksUri; + } + + public void setJwksUri(String jwksUri) { + this.jwksUri = jwksUri; + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/UserInfoRequest.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/UserInfoRequest.java new file mode 100644 index 00000000..985ccb7f --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/UserInfoRequest.java @@ -0,0 +1,90 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.common.AuthorizationMethod; + +/** + * Represents a User Info request to send to the authorization server. + * + * @author Javier Rojas Blum Date: 11.28.2011 + */ +public class UserInfoRequest extends BaseRequest { + + private String accessToken; + + /** + * Constructs a User Info Request. + * + * @param accessToken The access token obtained from the oxAuth authorization request. + */ + public UserInfoRequest(String accessToken) { + this.accessToken = accessToken; + setAuthorizationMethod(AuthorizationMethod.AUTHORIZATION_REQUEST_HEADER_FIELD); + } + + /** + * Returns the access token obtained from oxAuth authorization request. + * + * @return The access token obtained from oxAuth authorization request. + */ + public String getAccessToken() { + return accessToken; + } + + /** + * Sets the access token obtained from oxAuth authorization request. + * + * @param accessToken The access token obtained from oxAuth authorization request. + */ + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + /** + * Returns a query string with the parameters of the User Info request. + * Any null or empty parameter will be omitted. + * + * @return A query string of parameters. + */ + @Override + public String getQueryString() { + StringBuilder queryStringBuilder = new StringBuilder(); + + if (StringUtils.isNotBlank(accessToken)) { + if (getAuthorizationMethod() == AuthorizationMethod.FORM_ENCODED_BODY_PARAMETER + || getAuthorizationMethod() == AuthorizationMethod.URL_QUERY_PARAMETER) { + queryStringBuilder.append("access_token=").append(accessToken); + } + } + + return queryStringBuilder.toString(); + } + + /** + * Returns a collection of parameters of the user info request. Any + * null or empty parameter will be omitted. + * + * @return A collection of parameters. + */ + public Map getParameters() { + Map parameters = new HashMap(); + + if (accessToken != null && !accessToken.isEmpty()) { + if (getAuthorizationMethod() == AuthorizationMethod.FORM_ENCODED_BODY_PARAMETER + || getAuthorizationMethod() == AuthorizationMethod.URL_QUERY_PARAMETER) { + parameters.put("access_token", accessToken); + } + } + + return parameters; + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/UserInfoResponse.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/UserInfoResponse.java new file mode 100644 index 00000000..34ad89af --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/UserInfoResponse.java @@ -0,0 +1,129 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.gluu.oxauth.model.userinfo.UserInfoErrorResponseType; + +/** + * Represents an user info response received from the authorization server. + * + * @author Javier Rojas Blum Date: 11.30.2011 + */ +public class UserInfoResponse extends BaseResponse { + + private Map> claims; + + private UserInfoErrorResponseType errorType; + private String errorDescription; + private String errorUri; + + /** + * Constructs a User Info response. + * + * @param status The response status code. + */ + public UserInfoResponse(int status) { + super(status); + claims = new HashMap>(); + } + + public Map> getClaims() { + return claims; + } + + public void setClaims(Map> claims) { + this.claims = claims; + } + + /** + * Returns the error code when the request fails, otherwise will return null. + * + * @return The error code when the request fails. + */ + public UserInfoErrorResponseType getErrorType() { + return errorType; + } + + /** + * Sets the error code when the request fails, otherwise will return + * null. + * + * @param errorType The error code when the request fails. + */ + public void setErrorType(UserInfoErrorResponseType errorType) { + this.errorType = errorType; + } + + /** + * Returns a human-readable UTF-8 encoded text providing additional + * information, used to assist the client developer in understanding the + * error that occurred. + * + * @return The error description. + */ + public String getErrorDescription() { + return errorDescription; + } + + /** + * Sets a human-readable UTF-8 encoded text providing additional + * information, used to assist the client developer in understanding the + * error that occurred. + * + * @param errorDescription The error description. + */ + public void setErrorDescription(String errorDescription) { + this.errorDescription = errorDescription; + } + + /** + * Returns a URI identifying a human-readable web page with information + * about the error, used to provide the client developer with additional + * information about the error. + * + * @return A URI with information about the error. + */ + public String getErrorUri() { + return errorUri; + } + + /** + * Sets a URI identifying a human-readable web page with information about + * the error, used to provide the client developer with additional + * information about the error. + * + * @param errorUri A URI with information about the error. + */ + public void setErrorUri(String errorUri) { + this.errorUri = errorUri; + } + + public List getClaim(String claimName) { + if (claims.containsKey(claimName)) { + return claims.get(claimName); + } + + return null; + } + + @Override + public String toString() { + return "UserInfoResponse{" + + "status=" + status + + "entity=" + entity + + "headers=" + headers + + "claims=" + claims + + ", errorType=" + errorType + + ", errorDescription='" + errorDescription + '\'' + + ", errorUri='" + errorUri + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/fcm/FirebaseCloudMessagingClient.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/fcm/FirebaseCloudMessagingClient.java new file mode 100644 index 00000000..45cd7e6b --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/fcm/FirebaseCloudMessagingClient.java @@ -0,0 +1,73 @@ +/* + * oxAuth-CIBA is available under the Gluu Enterprise License (2019). + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.ciba.fcm; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation.Builder; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.gluu.oxauth.client.BaseClient; +import org.json.JSONObject; + +/** + * @author Javier Rojas Blum + * @version August 20, 2019 + */ +public class FirebaseCloudMessagingClient extends BaseClient { + + private static final Logger LOG = Logger.getLogger(FirebaseCloudMessagingClient.class); + + public FirebaseCloudMessagingClient(String url) { + super(url); + } + + @Override + public String getHttpMethod() { + return HttpMethod.POST; + } + + public FirebaseCloudMessagingResponse execFirebaseCloudMessaging(String key, String to, String title, String body, String clickAction) { + setRequest(new FirebaseCloudMessagingRequest(key, to, title, body, clickAction)); + + return exec(); + } + + public FirebaseCloudMessagingResponse exec() { + initClientRequest(); + return _exec(); + } + + private FirebaseCloudMessagingResponse _exec() { + try { + // Prepare request parameters + // clientRequest.setHttpMethod(getHttpMethod()); + Builder clientRequest = webTarget.request(); + applyCookies(clientRequest); + + clientRequest.header("Content-Type", getRequest().getContentType()); + clientRequest.accept(getRequest().getMediaType()); + + if (StringUtils.isNotBlank(getRequest().getKey())) { + clientRequest.header("Authorization", "key=" + getRequest().getKey()); + } + + JSONObject requestBody = getRequest().getJSONParameters(); + + // Call REST Service and handle response + clientResponse = clientRequest.buildPost(Entity.json(requestBody.toString(4))).invoke(); + setResponse(new FirebaseCloudMessagingResponse(clientResponse)); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } finally { + closeConnection(); + } + + return getResponse(); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/fcm/FirebaseCloudMessagingRequest.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/fcm/FirebaseCloudMessagingRequest.java new file mode 100644 index 00000000..f5de1ab2 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/fcm/FirebaseCloudMessagingRequest.java @@ -0,0 +1,100 @@ +/* + * oxAuth-CIBA is available under the Gluu Enterprise License (2019). + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.ciba.fcm; + +import static org.gluu.oxauth.model.ciba.FirebaseCloudMessagingRequestParam.BODY; +import static org.gluu.oxauth.model.ciba.FirebaseCloudMessagingRequestParam.CLICK_ACTION; +import static org.gluu.oxauth.model.ciba.FirebaseCloudMessagingRequestParam.NOTIFICATION; +import static org.gluu.oxauth.model.ciba.FirebaseCloudMessagingRequestParam.TITLE; +import static org.gluu.oxauth.model.ciba.FirebaseCloudMessagingRequestParam.TO; + +import javax.ws.rs.core.MediaType; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.client.BaseRequest; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * @author Javier Rojas Blum + * @version August 20, 2019 + */ +public class FirebaseCloudMessagingRequest extends BaseRequest { + + private String key; + private String to; + private Notification notification; + + public FirebaseCloudMessagingRequest(String key, String to, String title, String body, String clickAction) { + this.key = key; + this.to = to; + this.notification = new Notification(title, body, clickAction); + + setContentType(MediaType.APPLICATION_JSON); + setMediaType(MediaType.APPLICATION_JSON); + } + + public String getKey() { + return key; + } + + @Override + public JSONObject getJSONParameters() throws JSONException { + JSONObject parameters = new JSONObject(); + + if (StringUtils.isNotBlank(to)) { + parameters.put(TO, to); + } + + parameters.put(NOTIFICATION, notification.getJSONParameters()); + + return parameters; + } + + @Override + public String getQueryString() { + String jsonQueryString = null; + + try { + jsonQueryString = getJSONParameters().toString(4).replace("\\/", "/"); + } catch (JSONException e) { + e.printStackTrace(); + } + + return jsonQueryString; + } +} + +class Notification { + private String title; + private String body; + private String clickAction; + + public Notification(String title, String body, String clickAction) { + this.title = title; + this.body = body; + this.clickAction = clickAction; + } + + public JSONObject getJSONParameters() throws JSONException { + JSONObject parameters = new JSONObject(); + + if (StringUtils.isNotBlank(title)) { + parameters.put(TITLE, title); + } + + if (StringUtils.isNotBlank(body)) { + parameters.put(BODY, body); + } + + if (StringUtils.isNotBlank(clickAction)) { + parameters.put(CLICK_ACTION, clickAction); + } + + return parameters; + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/fcm/FirebaseCloudMessagingResponse.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/fcm/FirebaseCloudMessagingResponse.java new file mode 100644 index 00000000..7a586012 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/fcm/FirebaseCloudMessagingResponse.java @@ -0,0 +1,85 @@ +/* + * oxAuth-CIBA is available under the Gluu Enterprise License (2019). + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.ciba.fcm; + +import static org.gluu.oxauth.model.ciba.FirebaseCloudMessagingResponseParam.FAILURE; +import static org.gluu.oxauth.model.ciba.FirebaseCloudMessagingResponseParam.MESSAGE_ID; +import static org.gluu.oxauth.model.ciba.FirebaseCloudMessagingResponseParam.MULTICAST_ID; +import static org.gluu.oxauth.model.ciba.FirebaseCloudMessagingResponseParam.RESULTS; +import static org.gluu.oxauth.model.ciba.FirebaseCloudMessagingResponseParam.SUCCESS; + +import java.util.ArrayList; +import java.util.List; + +import javax.ws.rs.core.Response; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.gluu.oxauth.client.BaseResponse; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * @author Javier Rojas Blum + * @version September 4, 2019 + */ +public class FirebaseCloudMessagingResponse extends BaseResponse { + + private static final Logger LOG = Logger.getLogger(FirebaseCloudMessagingResponse.class); + + private Long multicastId; + private int success; + private int failure; + private List results; + + public FirebaseCloudMessagingResponse(Response clientResponse) { + super(clientResponse); + injectDataFromJson(entity); + } + + public void injectDataFromJson(String p_json) { + if (StringUtils.isNotBlank(p_json)) { + try { + JSONObject jsonObj = new JSONObject(p_json); + + if (jsonObj.has(MULTICAST_ID)) { + multicastId = jsonObj.getLong(MULTICAST_ID); + } + if (jsonObj.has(SUCCESS)) { + success = jsonObj.getInt(SUCCESS); + } + if (jsonObj.has(FAILURE)) { + failure = jsonObj.getInt(FAILURE); + } + if (jsonObj.has(RESULTS)) { + results = new ArrayList<>(); + JSONArray resultsJsonArray = jsonObj.getJSONArray(RESULTS); + + for (int i = 0; i < resultsJsonArray.length(); i++) { + JSONObject resultJsonObject = resultsJsonArray.getJSONObject(i); + + if (resultJsonObject.has(MESSAGE_ID)) { + Result result = new Result(resultJsonObject.getString(MESSAGE_ID)); + results.add(result); + } + } + } + } catch (JSONException e) { + LOG.error(e.getMessage(), e); + } + } + } + + class Result { + private String messageId; + + public Result(String messageId) { + this.messageId = messageId; + } + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/ping/PingCallbackClient.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/ping/PingCallbackClient.java new file mode 100644 index 00000000..c1091b64 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/ping/PingCallbackClient.java @@ -0,0 +1,84 @@ +/* + * oxAuth-CIBA is available under the Gluu Enterprise License (2019). + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.ciba.ping; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation.Builder; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.gluu.oxauth.client.BaseClient; +import org.gluu.oxauth.util.ClientUtil; +import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient43Engine; +import org.json.JSONObject; + +/** + * @author Javier Rojas Blum + * @version December 21, 2019 + */ +public class PingCallbackClient extends BaseClient { + + private static final Logger LOG = Logger.getLogger(PingCallbackClient.class); + + private final boolean fapiCompatibility; + + public PingCallbackClient(String url, boolean fapiCompatibility) { + super(url); + this.fapiCompatibility = fapiCompatibility; + } + + @Override + public String getHttpMethod() { + return HttpMethod.POST; + } + + public PingCallbackResponse exec() { + if (this.fapiCompatibility) { + setExecutor(getApacheHttpClient4ExecutorForMTLS()); + } + initClientRequest(); + return _exec(); + } + + private PingCallbackResponse _exec() { + try { + // Prepare request parameters + // clientRequest.setHttpMethod(getHttpMethod()); + Builder clientRequest = webTarget.request(); + applyCookies(clientRequest); + + clientRequest.header("Content-Type", getRequest().getContentType()); + + if (StringUtils.isNotBlank(getRequest().getClientNotificationToken())) { + clientRequest.header("Authorization", "Bearer " + getRequest().getClientNotificationToken()); + } + + JSONObject requestBody = getRequest().getJSONParameters(); + + // Call REST Service and handle response + clientResponse = clientRequest.buildPost(Entity.json(requestBody.toString(4))).invoke(); + setResponse(new PingCallbackResponse(clientResponse)); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } finally { + closeConnection(); + } + + return getResponse(); + } + + /** + * Creates an executor responsible to process rest calls using special SSL context defined in FAPI-CIBA specs. + */ + private ApacheHttpClient43Engine getApacheHttpClient4ExecutorForMTLS() { + // Ciphers accepted by FAPI-CIBA specs and OpenJDK. + String[] ciphers = new String[] { "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" }; + return new ApacheHttpClient43Engine(ClientUtil.createHttpClient("TLSv1.2", ciphers)); + } + +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/ping/PingCallbackRequest.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/ping/PingCallbackRequest.java new file mode 100644 index 00000000..9528f627 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/ping/PingCallbackRequest.java @@ -0,0 +1,69 @@ +/* + * oxAuth-CIBA is available under the Gluu Enterprise License (2019). + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.ciba.ping; + +import static org.gluu.oxauth.model.ciba.PushTokenDeliveryRequestParam.AUTHORIZATION_REQUEST_ID; + +import org.apache.commons.lang.StringUtils; +import org.apache.http.entity.ContentType; +import org.gluu.oxauth.client.BaseRequest; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * @author Javier Rojas Blum + * @version December 21, 2019 + */ +public class PingCallbackRequest extends BaseRequest { + + private String clientNotificationToken; + private String authReqId; + + public PingCallbackRequest() { + setContentType(ContentType.APPLICATION_JSON.toString()); + } + + public String getClientNotificationToken() { + return clientNotificationToken; + } + + public void setClientNotificationToken(String clientNotificationToken) { + this.clientNotificationToken = clientNotificationToken; + } + + public String getAuthReqId() { + return authReqId; + } + + public void setAuthReqId(String authReqId) { + this.authReqId = authReqId; + } + + @Override + public JSONObject getJSONParameters() throws JSONException { + JSONObject parameters = new JSONObject(); + + if (StringUtils.isNotBlank(authReqId)) { + parameters.put(AUTHORIZATION_REQUEST_ID, authReqId); + } + + return parameters; + } + + @Override + public String getQueryString() { + String jsonQueryString = null; + + try { + jsonQueryString = getJSONParameters().toString(4).replace("\\/", "/"); + } catch (JSONException e) { + e.printStackTrace(); + } + + return jsonQueryString; + } +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/ping/PingCallbackResponse.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/ping/PingCallbackResponse.java new file mode 100644 index 00000000..696e5b95 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/ping/PingCallbackResponse.java @@ -0,0 +1,25 @@ +/* + * oxAuth-CIBA is available under the Gluu Enterprise License (2019). + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.ciba.ping; + +import javax.ws.rs.core.Response; + +import org.apache.log4j.Logger; +import org.gluu.oxauth.client.BaseResponse; + +/** + * @author Javier Rojas Blum + * @version December 21, 2019 + */ +public class PingCallbackResponse extends BaseResponse { + + private static final Logger LOG = Logger.getLogger(PingCallbackResponse.class); + + public PingCallbackResponse(Response clientResponse) { + super(clientResponse); + } +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/push/PushErrorClient.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/push/PushErrorClient.java new file mode 100644 index 00000000..b72abc75 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/push/PushErrorClient.java @@ -0,0 +1,66 @@ +/* + * oxAuth-CIBA is available under the Gluu Enterprise License (2019). + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.ciba.push; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation.Builder; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.gluu.oxauth.client.BaseClient; +import org.json.JSONObject; + +/** + * @author Javier Rojas Blum + * @version May 9, 2020 + */ +public class PushErrorClient extends BaseClient { + + private static final Logger LOG = Logger.getLogger(PushErrorClient.class); + + public PushErrorClient(String url) { + super(url); + } + + @Override + public String getHttpMethod() { + return HttpMethod.POST; + } + + public PushErrorResponse exec() { + initClientRequest(); + return _exec(); + } + + private PushErrorResponse _exec() { + try { + // Prepare request parameters + // clientRequest.setHttpMethod(getHttpMethod()); + Builder clientRequest = webTarget.request(); + applyCookies(clientRequest); + + clientRequest.header("Content-Type", getRequest().getContentType()); + + if (StringUtils.isNotBlank(getRequest().getClientNotificationToken())) { + clientRequest.header("Authorization", "Bearer " + getRequest().getClientNotificationToken()); + } + + JSONObject requestBody = getRequest().getJSONParameters(); + + // Call REST Service and handle response + clientResponse = clientRequest.buildPost(Entity.json(requestBody.toString(4))).invoke(); + setResponse(new PushErrorResponse(clientResponse)); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } finally { + closeConnection(); + } + + return getResponse(); + } +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/push/PushErrorRequest.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/push/PushErrorRequest.java new file mode 100644 index 00000000..834e8c46 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/push/PushErrorRequest.java @@ -0,0 +1,112 @@ +/* + * oxAuth-CIBA is available under the Gluu Enterprise License (2019). + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.ciba.push; + +import static org.gluu.oxauth.model.ciba.PushErrorRequestParam.AUTHORIZATION_REQUEST_ID; +import static org.gluu.oxauth.model.ciba.PushErrorRequestParam.ERROR; +import static org.gluu.oxauth.model.ciba.PushErrorRequestParam.ERROR_DESCRIPTION; +import static org.gluu.oxauth.model.ciba.PushErrorRequestParam.ERROR_URI; + +import org.apache.commons.lang.StringUtils; +import org.apache.http.entity.ContentType; +import org.gluu.oxauth.client.BaseRequest; +import org.gluu.oxauth.model.ciba.PushErrorResponseType; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * @author Javier Rojas Blum + * @version May 9, 2020 + */ +public class PushErrorRequest extends BaseRequest { + + private String clientNotificationToken; + private String authReqId; + private PushErrorResponseType errorType; + private String errorDescription; + private String errorUri; + + public PushErrorRequest() { + setContentType(ContentType.APPLICATION_JSON.toString()); + } + + public String getClientNotificationToken() { + return clientNotificationToken; + } + + public void setClientNotificationToken(String clientNotificationToken) { + this.clientNotificationToken = clientNotificationToken; + } + + public String getAuthReqId() { + return authReqId; + } + + public void setAuthReqId(String authReqId) { + this.authReqId = authReqId; + } + + public PushErrorResponseType getErrorType() { + return errorType; + } + + public void setErrorType(PushErrorResponseType errorType) { + this.errorType = errorType; + } + + public String getErrorDescription() { + return errorDescription; + } + + public void setErrorDescription(String errorDescription) { + this.errorDescription = errorDescription; + } + + public String getErrorUri() { + return errorUri; + } + + public void setErrorUri(String errorUri) { + this.errorUri = errorUri; + } + + @Override + public JSONObject getJSONParameters() throws JSONException { + JSONObject parameters = new JSONObject(); + + if (StringUtils.isNotBlank(authReqId)) { + parameters.put(AUTHORIZATION_REQUEST_ID, authReqId); + } + + if (errorType != null) { + parameters.put(ERROR, errorType.toString()); + } + + if (StringUtils.isNotBlank(errorDescription)) { + parameters.put(ERROR_DESCRIPTION, errorDescription); + } + + if (StringUtils.isNotBlank(errorUri)) { + parameters.put(ERROR_URI, errorUri); + } + + return parameters; + } + + @Override + public String getQueryString() { + String jsonQueryString = null; + + try { + jsonQueryString = getJSONParameters().toString(4).replace("\\/", "/"); + } catch (JSONException e) { + e.printStackTrace(); + } + + return jsonQueryString; + } +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/push/PushErrorResponse.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/push/PushErrorResponse.java new file mode 100644 index 00000000..39c2de19 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/push/PushErrorResponse.java @@ -0,0 +1,25 @@ +/* + * oxAuth-CIBA is available under the Gluu Enterprise License (2019). + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.ciba.push; + +import javax.ws.rs.core.Response; + +import org.apache.log4j.Logger; +import org.gluu.oxauth.client.BaseResponse; + +/** + * @author Javier Rojas Blum + * @version May 9, 2020 + */ +public class PushErrorResponse extends BaseResponse { + + private static final Logger LOG = Logger.getLogger(PushErrorResponse.class); + + public PushErrorResponse(Response clientResponse) { + super(clientResponse); + } +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/push/PushTokenDeliveryClient.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/push/PushTokenDeliveryClient.java new file mode 100644 index 00000000..911a2ed4 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/push/PushTokenDeliveryClient.java @@ -0,0 +1,66 @@ +/* + * oxAuth-CIBA is available under the Gluu Enterprise License (2019). + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.ciba.push; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation.Builder; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.gluu.oxauth.client.BaseClient; +import org.json.JSONObject; + +/** + * @author Javier Rojas Blum + * @version September 4, 2019 + */ +public class PushTokenDeliveryClient extends BaseClient { + + private static final Logger LOG = Logger.getLogger(PushTokenDeliveryClient.class); + + public PushTokenDeliveryClient(String url) { + super(url); + } + + @Override + public String getHttpMethod() { + return HttpMethod.POST; + } + + public PushTokenDeliveryResponse exec() { + initClientRequest(); + return _exec(); + } + + private PushTokenDeliveryResponse _exec() { + try { + // Prepare request parameters + // clientRequest.setHttpMethod(getHttpMethod()); + Builder clientRequest = webTarget.request(); + applyCookies(clientRequest); + + clientRequest.header("Content-Type", getRequest().getContentType()); + + if (StringUtils.isNotBlank(getRequest().getClientNotificationToken())) { + clientRequest.header("Authorization", "Bearer " + getRequest().getClientNotificationToken()); + } + + JSONObject requestBody = getRequest().getJSONParameters(); + + // Call REST Service and handle response + clientResponse = clientRequest.buildPost(Entity.json(requestBody.toString(4))).invoke(); + setResponse(new PushTokenDeliveryResponse(clientResponse)); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } finally { + closeConnection(); + } + + return getResponse(); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/push/PushTokenDeliveryRequest.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/push/PushTokenDeliveryRequest.java new file mode 100644 index 00000000..0e8e6521 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/push/PushTokenDeliveryRequest.java @@ -0,0 +1,140 @@ +/* + * oxAuth-CIBA is available under the Gluu Enterprise License (2019). + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.ciba.push; + +import static org.gluu.oxauth.model.ciba.PushTokenDeliveryRequestParam.ACCESS_TOKEN; +import static org.gluu.oxauth.model.ciba.PushTokenDeliveryRequestParam.AUTHORIZATION_REQUEST_ID; +import static org.gluu.oxauth.model.ciba.PushTokenDeliveryRequestParam.EXPIRES_IN; +import static org.gluu.oxauth.model.ciba.PushTokenDeliveryRequestParam.ID_TOKEN; +import static org.gluu.oxauth.model.ciba.PushTokenDeliveryRequestParam.REFRESH_TOKEN; +import static org.gluu.oxauth.model.ciba.PushTokenDeliveryRequestParam.TOKEN_TYPE; + +import org.apache.commons.lang.StringUtils; +import org.apache.http.entity.ContentType; +import org.gluu.oxauth.client.BaseRequest; +import org.gluu.oxauth.model.common.TokenType; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * @author Javier Rojas Blum + * @version September 4, 2019 + */ +public class PushTokenDeliveryRequest extends BaseRequest { + + private String clientNotificationToken; + private String authReqId; + private String accessToken; + private TokenType tokenType; + private String refreshToken; + private Integer expiresIn; + private String idToken; + + public PushTokenDeliveryRequest() { + setContentType(ContentType.APPLICATION_JSON.toString()); + } + + public String getClientNotificationToken() { + return clientNotificationToken; + } + + public void setClientNotificationToken(String clientNotificationToken) { + this.clientNotificationToken = clientNotificationToken; + } + + public String getAuthReqId() { + return authReqId; + } + + public void setAuthReqId(String authReqId) { + this.authReqId = authReqId; + } + + public String getAccessToken() { + return accessToken; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public TokenType getTokenType() { + return tokenType; + } + + public void setTokenType(TokenType tokenType) { + this.tokenType = tokenType; + } + + public String getRefreshToken() { + return refreshToken; + } + + public void setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } + + public Integer getExpiresIn() { + return expiresIn; + } + + public void setExpiresIn(Integer expiresIn) { + this.expiresIn = expiresIn; + } + + public String getIdToken() { + return idToken; + } + + public void setIdToken(String idToken) { + this.idToken = idToken; + } + + @Override + public JSONObject getJSONParameters() throws JSONException { + JSONObject parameters = new JSONObject(); + + if (StringUtils.isNotBlank(authReqId)) { + parameters.put(AUTHORIZATION_REQUEST_ID, authReqId); + } + + if (StringUtils.isNotBlank(accessToken)) { + parameters.put(ACCESS_TOKEN, accessToken); + } + + if (tokenType != null) { + parameters.put(TOKEN_TYPE, tokenType.getName()); + } + + if (StringUtils.isNotBlank(refreshToken)) { + parameters.put(REFRESH_TOKEN, refreshToken); + } + + if (expiresIn != null) { + parameters.put(EXPIRES_IN, expiresIn); + } + + if (StringUtils.isNotBlank(idToken)) { + parameters.put(ID_TOKEN, idToken); + } + + return parameters; + } + + @Override + public String getQueryString() { + String jsonQueryString = null; + + try { + jsonQueryString = getJSONParameters().toString(4).replace("\\/", "/"); + } catch (JSONException e) { + e.printStackTrace(); + } + + return jsonQueryString; + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/push/PushTokenDeliveryResponse.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/push/PushTokenDeliveryResponse.java new file mode 100644 index 00000000..498ada87 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/ciba/push/PushTokenDeliveryResponse.java @@ -0,0 +1,25 @@ +/* + * oxAuth-CIBA is available under the Gluu Enterprise License (2019). + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.ciba.push; + +import javax.ws.rs.core.Response; + +import org.apache.log4j.Logger; +import org.gluu.oxauth.client.BaseResponse; + +/** + * @author Javier Rojas Blum + * @version August 20, 2019 + */ +public class PushTokenDeliveryResponse extends BaseResponse { + + private static final Logger LOG = Logger.getLogger(PushTokenDeliveryResponse.class); + + public PushTokenDeliveryResponse(Response clientResponse) { + super(clientResponse); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/fido/u2f/AuthenticationRequestService.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/fido/u2f/AuthenticationRequestService.java new file mode 100644 index 00000000..f149590f --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/fido/u2f/AuthenticationRequestService.java @@ -0,0 +1,34 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.fido.u2f; + +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; + +import org.gluu.oxauth.model.fido.u2f.protocol.AuthenticateRequestMessage; +import org.gluu.oxauth.model.fido.u2f.protocol.AuthenticateStatus; + +/** + * The endpoint allows to start and finish U2F authentication process + * + * @author Yuriy Movchan + * @version August 9, 2017 + */ +public interface AuthenticationRequestService { + + @GET + @Produces({"application/json"}) + public AuthenticateRequestMessage startAuthentication(@QueryParam("username") String userName, @QueryParam("keyhandle") String keyHandle, @QueryParam("application") String appId, @QueryParam("session_id") String sessionId); + + @POST + @Produces({"application/json"}) + public AuthenticateStatus finishAuthentication(@FormParam("username") String userName, @FormParam("tokenResponse") String authenticateResponseString); + +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/fido/u2f/FidoU2fClientFactory.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/fido/u2f/FidoU2fClientFactory.java new file mode 100644 index 00000000..6042293a --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/fido/u2f/FidoU2fClientFactory.java @@ -0,0 +1,60 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.fido.u2f; + +import javax.ws.rs.core.UriBuilder; + +import org.gluu.oxauth.client.service.ClientFactory; +import org.gluu.oxauth.model.fido.u2f.U2fConfiguration; +import org.jboss.resteasy.client.jaxrs.ResteasyClient; +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; +import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget; +import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient43Engine; + +/** + * Helper class which creates proxy FIDO U2F services + * + * @author Yuriy Movchan Date: 05/27/2015 + */ +public class FidoU2fClientFactory { + + private final static FidoU2fClientFactory instance = new FidoU2fClientFactory(); + + private ApacheHttpClient43Engine engine; + + private FidoU2fClientFactory() { + this.engine = ClientFactory.instance().createEngine(); + } + + public static FidoU2fClientFactory instance() { + return instance; + } + + public U2fConfigurationService createMetaDataConfigurationService(String u2fMetaDataUri) { + ResteasyClient client = ((ResteasyClientBuilder) ResteasyClientBuilder.newBuilder()).httpEngine(engine).build(); + ResteasyWebTarget target = client.target(UriBuilder.fromPath(u2fMetaDataUri)); + U2fConfigurationService proxy = target.proxy(U2fConfigurationService.class); + + return proxy; + } + + public AuthenticationRequestService createAuthenticationRequestService(U2fConfiguration metadataConfiguration) { + ResteasyClient client = ((ResteasyClientBuilder) ResteasyClientBuilder.newBuilder()).httpEngine(engine).build(); + ResteasyWebTarget target = client.target(UriBuilder.fromPath(metadataConfiguration.getAuthenticationEndpoint())); + AuthenticationRequestService proxy = target.proxy(AuthenticationRequestService.class); + + return proxy; + } + + public RegistrationRequestService createRegistrationRequestService(U2fConfiguration metadataConfiguration) { + ResteasyClient client = ((ResteasyClientBuilder) ResteasyClientBuilder.newBuilder()).httpEngine(engine).build(); + ResteasyWebTarget target = client.target(UriBuilder.fromPath(metadataConfiguration.getRegistrationEndpoint())); + RegistrationRequestService proxy = target.proxy(RegistrationRequestService.class); + + return proxy; + } +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/fido/u2f/RegistrationRequestService.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/fido/u2f/RegistrationRequestService.java new file mode 100644 index 00000000..0867c571 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/fido/u2f/RegistrationRequestService.java @@ -0,0 +1,38 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.fido.u2f; + +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; + +import org.gluu.oxauth.model.fido.u2f.protocol.RegisterRequestMessage; +import org.gluu.oxauth.model.fido.u2f.protocol.RegisterStatus; + +/** + * Еhe endpoint allows to start and finish U2F registration process + * + * @author Yuriy Movchan + * @version August 9, 2017 + */ +public interface RegistrationRequestService { + + @GET + @Produces({"application/json"}) + public RegisterRequestMessage startRegistration(@QueryParam("username") String userName, @QueryParam("application") String appId, @QueryParam("session_id") String sessionId); + + @GET + @Produces({"application/json"}) + public RegisterRequestMessage startRegistration(@QueryParam("username") String userName, @QueryParam("application") String appId, @QueryParam("session_id") String sessionId, @QueryParam("enrollment_code") String enrollmentCode); + + @POST + @Produces({"application/json"}) + public RegisterStatus finishRegistration(@FormParam("username") String userName, @FormParam("tokenResponse") String registerResponseString); + +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/fido/u2f/U2fConfigurationService.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/fido/u2f/U2fConfigurationService.java new file mode 100644 index 00000000..70534f5a --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/fido/u2f/U2fConfigurationService.java @@ -0,0 +1,27 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.fido.u2f; + +import javax.ws.rs.GET; +import javax.ws.rs.Produces; + +import org.gluu.oxauth.model.fido.u2f.U2fConfiguration; +import org.gluu.oxauth.model.uma.UmaConstants; + +/** + * The endpoint at which the requester can obtain FIDO U2F metadata configuration + * + * @author Yuriy Movchan Date: 05/27/2015 + * + */ +public interface U2fConfigurationService { + + @GET + @Produces({ UmaConstants.JSON_MEDIA_TYPE }) + public U2fConfiguration getMetadataConfiguration(); + +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/model/JwtState.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/model/JwtState.java new file mode 100644 index 00000000..c874f346 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/model/JwtState.java @@ -0,0 +1,518 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.model; + +import static org.gluu.oxauth.model.jwt.JwtStateClaimName.ADDITIONAL_CLAIMS; +import static org.gluu.oxauth.model.jwt.JwtStateClaimName.AS; +import static org.gluu.oxauth.model.jwt.JwtStateClaimName.AT_HASH; +import static org.gluu.oxauth.model.jwt.JwtStateClaimName.AUD; +import static org.gluu.oxauth.model.jwt.JwtStateClaimName.C_HASH; +import static org.gluu.oxauth.model.jwt.JwtStateClaimName.EXP; +import static org.gluu.oxauth.model.jwt.JwtStateClaimName.IAT; +import static org.gluu.oxauth.model.jwt.JwtStateClaimName.ISS; +import static org.gluu.oxauth.model.jwt.JwtStateClaimName.JTI; +import static org.gluu.oxauth.model.jwt.JwtStateClaimName.KID; +import static org.gluu.oxauth.model.jwt.JwtStateClaimName.RFP; +import static org.gluu.oxauth.model.jwt.JwtStateClaimName.TARGET_LINK_URI; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.security.PublicKey; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.gluu.oxauth.model.crypto.AbstractCryptoProvider; +import org.gluu.oxauth.model.crypto.encryption.BlockEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.encryption.KeyEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.exception.InvalidJwtException; +import org.gluu.oxauth.model.jwe.Jwe; +import org.gluu.oxauth.model.jwe.JweEncrypterImpl; +import org.gluu.oxauth.model.jwt.JwtClaims; +import org.gluu.oxauth.model.jwt.JwtHeader; +import org.gluu.oxauth.model.jwt.JwtType; +import org.gluu.oxauth.model.util.Base64Util; +import org.gluu.oxauth.model.util.Util; +import org.gluu.oxauth.util.ClientUtil; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * @author Javier Rojas Blum + * @version November 20, 2018 + */ +public class JwtState { + + private static final Logger LOG = Logger.getLogger(JwtState.class); + + // Header + private JwtType type; + private SignatureAlgorithm signatureAlgorithm; + private KeyEncryptionAlgorithm keyEncryptionAlgorithm; + private BlockEncryptionAlgorithm blockEncryptionAlgorithm; + private String keyId; + + // Payload + private String rfp; + private String iat; + private String exp; + private String iss; + private String aud; + private String targetLinkUri; + private String as; + private String jti; + private String atHash; + private String cHash; + private JSONObject additionalClaims; + + // Signature/Encryption Keys + private String sharedKey; + private AbstractCryptoProvider cryptoProvider; + + public JwtState(SignatureAlgorithm signatureAlgorithm, AbstractCryptoProvider cryptoProvider) { + this(signatureAlgorithm, cryptoProvider, null, null, null); + } + + public JwtState(SignatureAlgorithm signatureAlgorithm, + String sharedKey, AbstractCryptoProvider cryptoProvider) { + this(signatureAlgorithm, cryptoProvider, null, null, sharedKey); + } + + public JwtState(KeyEncryptionAlgorithm keyEncryptionAlgorithm, + BlockEncryptionAlgorithm blockEncryptionAlgorithm, AbstractCryptoProvider cryptoProvider) { + this(null, cryptoProvider, keyEncryptionAlgorithm, blockEncryptionAlgorithm, null); + } + + public JwtState(KeyEncryptionAlgorithm keyEncryptionAlgorithm, + BlockEncryptionAlgorithm blockEncryptionAlgorithm, String sharedKey) { + this(null, null, keyEncryptionAlgorithm, blockEncryptionAlgorithm, sharedKey); + } + + private JwtState(SignatureAlgorithm signatureAlgorithm, + AbstractCryptoProvider cryptoProvider, KeyEncryptionAlgorithm keyEncryptionAlgorithm, + BlockEncryptionAlgorithm blockEncryptionAlgorithm, String sharedKey) { + this.type = JwtType.JWT; + this.signatureAlgorithm = signatureAlgorithm; + this.cryptoProvider = cryptoProvider; + this.keyEncryptionAlgorithm = keyEncryptionAlgorithm; + this.blockEncryptionAlgorithm = blockEncryptionAlgorithm; + this.sharedKey = sharedKey; + } + + public JwtType getType() { + return type; + } + + public void setType(JwtType type) { + this.type = type; + } + + public SignatureAlgorithm getSignatureAlgorithm() { + return signatureAlgorithm; + } + + public void setSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + } + + public KeyEncryptionAlgorithm getKeyEncryptionAlgorithm() { + return keyEncryptionAlgorithm; + } + + public void setKeyEncryptionAlgorithm(KeyEncryptionAlgorithm keyEncryptionAlgorithm) { + this.keyEncryptionAlgorithm = keyEncryptionAlgorithm; + } + + public BlockEncryptionAlgorithm getBlockEncryptionAlgorithm() { + return blockEncryptionAlgorithm; + } + + public void setBlockEncryptionAlgorithm(BlockEncryptionAlgorithm blockEncryptionAlgorithm) { + this.blockEncryptionAlgorithm = blockEncryptionAlgorithm; + } + + /** + * Identifier of the key used to sign this state token at the issuer. + * Identifier of the key used to encrypt this JWT state token at the issuer. + * + * @return The key identifier + */ + public String getKeyId() { + return keyId; + } + + /** + * Identifier of the key used to sign this state token at the issuer. + * Identifier of the key used to encrypt this JWT state token at the issuer. + * + * @param keyId The key identifier + */ + public void setKeyId(String keyId) { + this.keyId = keyId; + } + + /** + * String containing a verifiable identifier for the browser session, + * that cannot be guessed by a third party. + * The verification of this element by the client protects it from + * accepting authorization responses generated in response to forged + * requests generated by third parties. + * + * @return The Request Forgery Protection value + */ + public String getRfp() { + return rfp; + } + + /** + * String containing a verifiable identifier for the browser session, + * that cannot be guessed by a third party. + * The verification of this element by the client protects it from + * accepting authorization responses generated in response to forged + * requests generated by third parties. + * + * @param rfp The Request Forgery Protection value + */ + public void setRfp(String rfp) { + this.rfp = rfp; + } + + /** + * Timestamp of when this Authorization Request was issued. + * + * @return The Issued at value + */ + public String getIat() { + return iat; + } + + /** + * Timestamp of when this Authorization Request was issued. + * + * @param iat The Issued at value + */ + public void setIat(String iat) { + this.iat = iat; + } + + /** + * The expiration time claim identifies the expiration time on or after which + * the JWT MUST NOT be accepted for processing. + * The processing of the "exp" claim requires that the current date/time MUST + * be before the expiration date/time listed in the "exp" claim. + * Implementers MAY provide for some small leeway, usually no more than a + * few minutes, to account for clock skew. + * Its value MUST be a number containing an IntDate value. + * + * @return The expiration time value + */ + public String getExp() { + return exp; + } + + /** + * The expiration time claim identifies the expiration time on or after which + * the JWT MUST NOT be accepted for processing. + * The processing of the "exp" claim requires that the current date/time MUST + * be before the expiration date/time listed in the "exp" claim. + * Implementers MAY provide for some small leeway, usually no more than a + * few minutes, to account for clock skew. + * Its value MUST be a number containing an IntDate value. + * + * @param exp The expiration time value + */ + public void setExp(String exp) { + this.exp = exp; + } + + /** + * String identifying the party that issued this state value. + * + * @return The issuer value + */ + public String getIss() { + return iss; + } + + /** + * String identifying the party that issued this state value. + * + * @param iss The issuer value + */ + public void setIss(String iss) { + this.iss = iss; + } + + /** + * String identifying the client that this state value is intended for. + * + * @return The audience + */ + public String getAud() { + return aud; + } + + /** + * String identifying the client that this state value is intended for. + * + * @param aud The audience + */ + public void setAud(String aud) { + this.aud = aud; + } + + /** + * URI containing the location the user agent is to be redirected to after authorization. + * + * @return The target link URI + */ + public String getTargetLinkUri() { + return targetLinkUri; + } + + /** + * URI containing the location the user agent is to be redirected to after authorization. + * + * @param targetLinkUri The target link URI + */ + public void setTargetLinkUri(String targetLinkUri) { + this.targetLinkUri = targetLinkUri; + } + + /** + * String identifying the authorization server that this request was sent to. + * + * @return The authorization server + */ + public String getAs() { + return as; + } + + /** + * String identifying the authorization server that this request was sent to. + * + * @param as The authorization server + */ + public void setAs(String as) { + this.as = as; + } + + /** + * The "jti" (JWT ID) claim provides a unique identifier for the JWT. + * The identifier value MUST be assigned in a manner that ensures that + * there is a negligible probability that the same value will be + * accidentally assigned to a different data object. + * The "jti" claim can be used to prevent the JWT from being replayed. + * The "jti" value is a case-sensitive string. + * + * @return The JWT ID + */ + public String getJti() { + return jti; + } + + /** + * The "jti" (JWT ID) claim provides a unique identifier for the JWT. + * The identifier value MUST be assigned in a manner that ensures that + * there is a negligible probability that the same value will be + * accidentally assigned to a different data object. + * The "jti" claim can be used to prevent the JWT from being replayed. + * The "jti" value is a case-sensitive string. + * + * @param jti The JWT ID + */ + public void setJti(String jti) { + this.jti = jti; + } + + /** + * Access Token hash value. Its value is the base64url encoding of the left-most half + * of the hash of the octets of the ASCII representation of the "access_token" value, + * where the hash algorithm used is the hash algorithm used in the "alg" parameter of + * the State Token's JWS header. + * For instance, if the "alg" is "RS256", hash the "access_token" value with SHA-256, + * then take the left-most 128 bits and base64url encode them. + * The "at_hash" value is a case sensitive string. + * This is REQUIRED if the JWT [RFC7519] state token is being produced by the AS and + * issued with a "access_token" in the authorization response. + * + * @return The access token hash value + */ + public String getAtHash() { + return atHash; + } + + /** + * Access Token hash value. Its value is the base64url encoding of the left-most half + * of the hash of the octets of the ASCII representation of the "access_token" value, + * where the hash algorithm used is the hash algorithm used in the "alg" parameter of + * the State Token's JWS header. + * For instance, if the "alg" is "RS256", hash the "access_token" value with SHA-256, + * then take the left-most 128 bits and base64url encode them. + * The "at_hash" value is a case sensitive string. + * This is REQUIRED if the JWT [RFC7519] state token is being produced by the AS and + * issued with a "access_token" in the authorization response. + * + * @param atHash The access token hash value + */ + public void setAtHash(String atHash) { + this.atHash = atHash; + } + + /** + * Code hash value. Its value is the base64url encoding of the left-most half of the + * hash of the octets of the ASCII representation of the "code" value, where the hash + * algorithm used is the hash algorithm used in the "alg" header parameter of the + * State Token's JWS [RFC7515] header. + * For instance, if the "alg" is "HS512", hash the "code" value with SHA-512, then + * take the left-most 256 bits and base64url encode them. + * The "c_hash" value is a case sensitive string. + * This is REQUIRED if the JWT [RFC7519] state token is being produced by the AS and + * issued with a "code" in the authorization response. + * + * @return The code hash value + */ + public String getcHash() { + return cHash; + } + + /** + * Code hash value. Its value is the base64url encoding of the left-most half of the + * hash of the octets of the ASCII representation of the "code" value, where the hash + * algorithm used is the hash algorithm used in the "alg" header parameter of the + * State Token's JWS [RFC7515] header. + * For instance, if the "alg" is "HS512", hash the "code" value with SHA-512, then + * take the left-most 256 bits and base64url encode them. + * The "c_hash" value is a case sensitive string. + * This is REQUIRED if the JWT [RFC7519] state token is being produced by the AS and + * issued with a "code" in the authorization response. + * + * @param cHash The code hash value + */ + public void setcHash(String cHash) { + this.cHash = cHash; + } + + public JSONObject getAdditionalClaims() { + return additionalClaims; + } + + public void setAdditionalClaims(JSONObject additionalClaims) { + this.additionalClaims = additionalClaims; + } + + public String getEncodedJwt(JSONObject jwks) throws Exception { + String encodedJwt = null; + + if (keyEncryptionAlgorithm != null && blockEncryptionAlgorithm != null) { + JweEncrypterImpl jweEncrypter; + if (cryptoProvider != null && jwks != null) { + PublicKey publicKey = cryptoProvider.getPublicKey(keyId, jwks, null); + jweEncrypter = new JweEncrypterImpl(keyEncryptionAlgorithm, blockEncryptionAlgorithm, publicKey); + } else { + jweEncrypter = new JweEncrypterImpl(keyEncryptionAlgorithm, blockEncryptionAlgorithm, sharedKey.getBytes(Util.UTF8_STRING_ENCODING)); + } + + String header = ClientUtil.toPrettyJson(headerToJSONObject()); + String encodedHeader = Base64Util.base64urlencode(header.getBytes(Util.UTF8_STRING_ENCODING)); + + String claims = ClientUtil.toPrettyJson(payloadToJSONObject()); + String encodedClaims = Base64Util.base64urlencode(claims.getBytes(Util.UTF8_STRING_ENCODING)); + + Jwe jwe = new Jwe(); + jwe.setHeader(new JwtHeader(encodedHeader)); + jwe.setClaims(new JwtClaims(encodedClaims)); + jweEncrypter.encrypt(jwe); + + encodedJwt = jwe.toString(); + } else { + if (cryptoProvider == null) { + throw new Exception("The Crypto Provider cannot be null."); + } + + JSONObject headerJsonObject = headerToJSONObject(); + JSONObject payloadJsonObject = payloadToJSONObject(); + String headerString = ClientUtil.toPrettyJson(headerJsonObject); + String payloadString = ClientUtil.toPrettyJson(payloadJsonObject); + String encodedHeader = Base64Util.base64urlencode(headerString.getBytes(Util.UTF8_STRING_ENCODING)); + String encodedPayload = Base64Util.base64urlencode(payloadString.getBytes(Util.UTF8_STRING_ENCODING)); + String signingInput = encodedHeader + "." + encodedPayload; + String encodedSignature = cryptoProvider.sign(signingInput, keyId, sharedKey, signatureAlgorithm); + + encodedJwt = encodedHeader + "." + encodedPayload + "." + encodedSignature; + } + + return encodedJwt; + } + + public String getEncodedJwt() throws Exception { + return getEncodedJwt(null); + } + + protected JSONObject headerToJSONObject() throws InvalidJwtException { + JwtHeader jwtHeader = new JwtHeader(); + + jwtHeader.setType(type); + if (keyEncryptionAlgorithm != null && blockEncryptionAlgorithm != null) { + jwtHeader.setAlgorithm(keyEncryptionAlgorithm); + jwtHeader.setEncryptionMethod(blockEncryptionAlgorithm); + } else { + jwtHeader.setAlgorithm(signatureAlgorithm); + } + jwtHeader.setKeyId(keyId); + + return jwtHeader.toJsonObject(); + } + + protected JSONObject payloadToJSONObject() throws JSONException { + JSONObject obj = new JSONObject(); + + try { + if (StringUtils.isNotBlank(rfp)) { + obj.put(RFP, rfp); + } + if (StringUtils.isNotBlank(keyId)) { + obj.put(KID, keyId); + } + if (StringUtils.isNotBlank(iat)) { + obj.put(IAT, iat); + } + if (StringUtils.isNotBlank(exp)) { + obj.put(EXP, exp); + } + if (StringUtils.isNotBlank(iss)) { + obj.put(ISS, iss); + } + if (StringUtils.isNotBlank(aud)) { + obj.put(AUD, aud); + } + if (StringUtils.isNotBlank(targetLinkUri)) { + obj.put(TARGET_LINK_URI, URLEncoder.encode(targetLinkUri, "UTF-8")); + } + if (StringUtils.isNotBlank(as)) { + obj.put(AS, as); + } + if (StringUtils.isNotBlank(jti)) { + obj.put(JTI, jti); + } + if (StringUtils.isNotBlank(atHash)) { + obj.put(AT_HASH, atHash); + } + if (StringUtils.isNotBlank(cHash)) { + obj.put(C_HASH, cHash); + } + if (additionalClaims != null) { + obj.put(ADDITIONAL_CLAIMS, additionalClaims); + } + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + + return obj; + } + +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/model/SoftwareStatement.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/model/SoftwareStatement.java new file mode 100644 index 00000000..5093c5f2 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/model/SoftwareStatement.java @@ -0,0 +1,124 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.model; + +import org.apache.log4j.Logger; +import org.gluu.oxauth.model.crypto.AbstractCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.exception.InvalidJwtException; +import org.gluu.oxauth.model.jwt.JwtHeader; +import org.gluu.oxauth.model.util.Base64Util; +import org.gluu.oxauth.model.util.Util; +import org.gluu.oxauth.util.ClientUtil; +import org.json.JSONObject; + +/** + * @author Javier Rojas Blum + * @version December 4, 2018 + */ +public class SoftwareStatement { + + private static final Logger LOG = Logger.getLogger(JwtState.class); + + // Header + private SignatureAlgorithm signatureAlgorithm; + private String keyId; + + // Payload + private JSONObject claims; + + // Signature/Encryption Keys + private String sharedKey; + private AbstractCryptoProvider cryptoProvider; + + public SoftwareStatement(SignatureAlgorithm signatureAlgorithm, AbstractCryptoProvider cryptoProvider) { + this(signatureAlgorithm, cryptoProvider, null); + } + + public SoftwareStatement(SignatureAlgorithm signatureAlgorithm, + String sharedKey, AbstractCryptoProvider cryptoProvider) { + this(signatureAlgorithm, cryptoProvider, sharedKey); + } + + private SoftwareStatement(SignatureAlgorithm signatureAlgorithm, AbstractCryptoProvider cryptoProvider, String sharedKey) { + this.signatureAlgorithm = signatureAlgorithm; + this.cryptoProvider = cryptoProvider; + this.sharedKey = sharedKey; + this.claims = new JSONObject(); + } + + public SignatureAlgorithm getSignatureAlgorithm() { + return signatureAlgorithm; + } + + public void setSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + } + + /** + * Identifier of the key used to sign this state token at the issuer. + * Identifier of the key used to encrypt this JWT state token at the issuer. + * + * @return The key identifier + */ + public String getKeyId() { + return keyId; + } + + /** + * Identifier of the key used to sign this state token at the issuer. + * Identifier of the key used to encrypt this JWT state token at the issuer. + * + * @param keyId The key identifier + */ + public void setKeyId(String keyId) { + this.keyId = keyId; + } + + public JSONObject getClaims() { + return claims; + } + + public void setClaims(JSONObject claims) { + this.claims = claims; + } + + public String getEncodedJwt(JSONObject jwks) throws Exception { + String encodedJwt = null; + + if (cryptoProvider == null) { + throw new Exception("The Crypto Provider cannot be null."); + } + + JSONObject headerJsonObject = headerToJSONObject(); + JSONObject payloadJsonObject = getClaims(); + String headerString = ClientUtil.toPrettyJson(headerJsonObject); + String payloadString = ClientUtil.toPrettyJson(payloadJsonObject); + String encodedHeader = Base64Util.base64urlencode(headerString.getBytes(Util.UTF8_STRING_ENCODING)); + String encodedPayload = Base64Util.base64urlencode(payloadString.getBytes(Util.UTF8_STRING_ENCODING)); + String signingInput = encodedHeader + "." + encodedPayload; + String encodedSignature = cryptoProvider.sign(signingInput, keyId, sharedKey, signatureAlgorithm); + + encodedJwt = encodedHeader + "." + encodedPayload + "." + encodedSignature; + + return encodedJwt; + } + + public String getEncodedJwt() throws Exception { + return getEncodedJwt(null); + } + + protected JSONObject headerToJSONObject() throws InvalidJwtException { + JwtHeader jwtHeader = new JwtHeader(); + + jwtHeader.setAlgorithm(signatureAlgorithm); + jwtHeader.setKeyId(keyId); + + return jwtHeader.toJsonObject(); + } + +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/model/authorize/Claim.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/model/authorize/Claim.java new file mode 100644 index 00000000..8d548f9c --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/model/authorize/Claim.java @@ -0,0 +1,37 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.model.authorize; + +/** + * @author Javier Rojas Blum Date: 03.07.2012 + */ +public class Claim { + + private String name; + private ClaimValue claimValue; + + public Claim(String name, ClaimValue claimValue) { + this.name = name; + this.claimValue = claimValue; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public ClaimValue getClaimValue() { + return claimValue; + } + + public void setClaimValue(ClaimValue claimValue) { + this.claimValue = claimValue; + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/model/authorize/ClaimValue.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/model/authorize/ClaimValue.java new file mode 100644 index 00000000..9a686c1f --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/model/authorize/ClaimValue.java @@ -0,0 +1,103 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.model.authorize; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * @author Javier Rojas Blum Date: 03.07.2012 + */ +public class ClaimValue { + + private ClaimValueType claimValueType; + private List values; + private String value; + + private ClaimValue() { + } + + public static ClaimValue createNull() { + ClaimValue claimValue = new ClaimValue(); + claimValue.claimValueType = ClaimValueType.NULL; + + return claimValue; + } + + public static ClaimValue createEssential(boolean essentialValue) { + ClaimValue claimValue = new ClaimValue(); + if (essentialValue) { + claimValue.claimValueType = ClaimValueType.ESSENTIAL_TRUE; + } else { + claimValue.claimValueType = ClaimValueType.ESSENTIAL_FALSE; + } + return claimValue; + } + + public static ClaimValue createValueList(String[] values) { + ClaimValue claimValue = new ClaimValue(); + claimValue.claimValueType = ClaimValueType.VALUE_LIST; + + claimValue.values = new ArrayList(); + Collections.addAll(claimValue.values, values); + + return claimValue; + } + + public static ClaimValue createSingleValue(String value) { + ClaimValue claimValue = new ClaimValue(); + claimValue.claimValueType = ClaimValueType.SINGLE_VALUE; + + claimValue.value = value; + + return claimValue; + } + + public JSONObject toJSONObject() throws JSONException { + JSONObject obj = null; + + switch (claimValueType) { + case NULL: + break; + case ESSENTIAL_TRUE: + obj = new JSONObject(); + obj.put("essential", true); + break; + case ESSENTIAL_FALSE: + obj = new JSONObject(); + obj.put("essential", false); + break; + case VALUE_LIST: + JSONArray arr = new JSONArray(); + for (String value : values) { + arr.put(value); + } + obj = new JSONObject(); + obj.put("values", arr); + break; + case SINGLE_VALUE: + obj = new JSONObject(); + obj.put("value", value); + break; + } + + return obj; + } + + enum ClaimValueType { + NULL, + ESSENTIAL_TRUE, + ESSENTIAL_FALSE, + VALUE_LIST, + SINGLE_VALUE; + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/model/authorize/IdTokenMember.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/model/authorize/IdTokenMember.java new file mode 100644 index 00000000..bdba4406 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/model/authorize/IdTokenMember.java @@ -0,0 +1,63 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.model.authorize; + +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * @author Javier Rojas Blum Date: 03.07.2012 + */ +public class IdTokenMember { + + private List claims; + private Integer maxAge; + + public IdTokenMember() { + claims = new ArrayList(); + maxAge = null; + } + + public List getClaims() { + return claims; + } + + public void setClaims(List claims) { + this.claims = claims; + } + + public Integer getMaxAge() { + return maxAge; + } + + public void setMaxAge(Integer maxAge) { + this.maxAge = maxAge; + } + + public JSONObject toJSONObject() throws JSONException { + JSONObject obj = new JSONObject(); + + if (claims != null && !claims.isEmpty()) { + for (Claim claim : claims) { + JSONObject claimValue = claim.getClaimValue().toJSONObject(); + if (claimValue == null) { + obj.put(claim.getName(), JSONObject.NULL); + } else { + obj.put(claim.getName(), claimValue); + } + } + } + if (maxAge != null) { + obj.put("max_age", maxAge); + } + + return obj; + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/model/authorize/JwtAuthorizationRequest.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/model/authorize/JwtAuthorizationRequest.java new file mode 100644 index 00000000..3989e757 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/model/authorize/JwtAuthorizationRequest.java @@ -0,0 +1,633 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.model.authorize; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.security.PublicKey; +import java.util.List; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.model.common.Display; +import org.gluu.oxauth.model.common.Prompt; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.AbstractCryptoProvider; +import org.gluu.oxauth.model.crypto.encryption.BlockEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.encryption.KeyEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.exception.InvalidJwtException; +import org.gluu.oxauth.model.jwe.Jwe; +import org.gluu.oxauth.model.jwe.JweEncrypterImpl; +import org.gluu.oxauth.model.jwt.JwtClaims; +import org.gluu.oxauth.model.jwt.JwtHeader; +import org.gluu.oxauth.model.jwt.JwtType; +import org.gluu.oxauth.model.util.Base64Util; +import org.gluu.oxauth.model.util.Util; +import org.gluu.oxauth.util.ClientUtil; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import com.fasterxml.jackson.core.JsonProcessingException; + +/** + * @author Javier Rojas Blum + * @version November 20, 2018 + */ +public class JwtAuthorizationRequest { + + private static final Logger LOG = Logger.getLogger(JwtAuthorizationRequest.class); + + // Header + private JwtType type; + private SignatureAlgorithm signatureAlgorithm; + private KeyEncryptionAlgorithm keyEncryptionAlgorithm; + private BlockEncryptionAlgorithm blockEncryptionAlgorithm; + private String keyId; + + // Payload + private List responseTypes; + private String clientId; + private List scopes; + private String redirectUri; + private String state; + private String nonce; + private Display display; + private List prompts; + private Integer maxAge; + private List uiLocales; + private List claimsLocales; + private String idTokenHint; + private String loginHint; + private List acrValues; + private String registration; + private boolean requestUniqueId; + private String aud; + private Integer exp; + private String iss; + private Integer iat; + private Integer nbf; + private String jti; + private String clientNotificationToken; + private String loginHintToken; + private String bindingMessage; + private String userCode; + private Integer requestedExpiry; + + private UserInfoMember userInfoMember; + private IdTokenMember idTokenMember; + + // Signature/Encryption Keys + private String sharedKey; + private AbstractCryptoProvider cryptoProvider; + + public JwtAuthorizationRequest(AuthorizationRequest authorizationRequest, SignatureAlgorithm signatureAlgorithm, + AbstractCryptoProvider cryptoProvider) { + this(authorizationRequest, signatureAlgorithm, cryptoProvider, null, null, null); + } + + public JwtAuthorizationRequest(AuthorizationRequest authorizationRequest, SignatureAlgorithm signatureAlgorithm, + String sharedKey, AbstractCryptoProvider cryptoProvider) { + this(authorizationRequest, signatureAlgorithm, cryptoProvider, null, null, sharedKey); + } + + public JwtAuthorizationRequest( + AuthorizationRequest authorizationRequest, KeyEncryptionAlgorithm keyEncryptionAlgorithm, + BlockEncryptionAlgorithm blockEncryptionAlgorithm, AbstractCryptoProvider cryptoProvider) { + this(authorizationRequest, null, cryptoProvider, keyEncryptionAlgorithm, blockEncryptionAlgorithm, null); + } + + public JwtAuthorizationRequest( + AuthorizationRequest authorizationRequest, KeyEncryptionAlgorithm keyEncryptionAlgorithm, + BlockEncryptionAlgorithm blockEncryptionAlgorithm, String sharedKey) { + this(authorizationRequest, null, null, keyEncryptionAlgorithm, blockEncryptionAlgorithm, sharedKey); + } + + private JwtAuthorizationRequest( + AuthorizationRequest authorizationRequest, SignatureAlgorithm signatureAlgorithm, + AbstractCryptoProvider cryptoProvider, KeyEncryptionAlgorithm keyEncryptionAlgorithm, + BlockEncryptionAlgorithm blockEncryptionAlgorithm, String sharedKey) { + setAuthorizationRequestParams(authorizationRequest); + + this.type = JwtType.JWT; + this.signatureAlgorithm = signatureAlgorithm; + this.cryptoProvider = cryptoProvider; + this.keyEncryptionAlgorithm = keyEncryptionAlgorithm; + this.blockEncryptionAlgorithm = blockEncryptionAlgorithm; + this.sharedKey = sharedKey; + + this.userInfoMember = new UserInfoMember(); + this.idTokenMember = new IdTokenMember(); + } + + private void setAuthorizationRequestParams(AuthorizationRequest authorizationRequest) { + if (authorizationRequest != null) { + this.responseTypes = authorizationRequest.getResponseTypes(); + this.clientId = authorizationRequest.getClientId(); + this.scopes = authorizationRequest.getScopes(); + this.redirectUri = authorizationRequest.getRedirectUri(); + this.state = authorizationRequest.getState(); + this.nonce = authorizationRequest.getNonce(); + this.display = authorizationRequest.getDisplay(); + this.prompts = authorizationRequest.getPrompts(); + this.maxAge = authorizationRequest.getMaxAge(); + this.uiLocales = authorizationRequest.getUiLocales(); + this.claimsLocales = authorizationRequest.getClaimsLocales(); + this.idTokenHint = authorizationRequest.getIdTokenHint(); + this.loginHint = authorizationRequest.getLoginHint(); + this.acrValues = authorizationRequest.getAcrValues(); + this.registration = authorizationRequest.getRegistration(); + this.requestUniqueId = authorizationRequest.isRequestSessionId(); + } + } + + public JwtType getType() { + return type; + } + + public void setType(JwtType type) { + this.type = type; + } + + public SignatureAlgorithm getSignatureAlgorithm() { + return signatureAlgorithm; + } + + public void setAlgorithm(SignatureAlgorithm signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + } + + public KeyEncryptionAlgorithm getKeyEncryptionAlgorithm() { + return keyEncryptionAlgorithm; + } + + public void setKeyEncryptionAlgorithm(KeyEncryptionAlgorithm keyEncryptionAlgorithm) { + this.keyEncryptionAlgorithm = keyEncryptionAlgorithm; + } + + public BlockEncryptionAlgorithm getBlockEncryptionAlgorithm() { + return blockEncryptionAlgorithm; + } + + public void setBlockEncryptionAlgorithm(BlockEncryptionAlgorithm blockEncryptionAlgorithm) { + this.blockEncryptionAlgorithm = blockEncryptionAlgorithm; + } + + public String getKeyId() { + return keyId; + } + + public void setKeyId(String keyId) { + this.keyId = keyId; + } + + public boolean isRequestUniqueId() { + return requestUniqueId; + } + + public void setRequestUniqueId(boolean p_requestUniqueId) { + requestUniqueId = p_requestUniqueId; + } + + public List getResponseTypes() { + return responseTypes; + } + + public void setResponseTypes(List responseTypes) { + this.responseTypes = responseTypes; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public List getScopes() { + return scopes; + } + + public void setScopes(List scopes) { + this.scopes = scopes; + } + + public String getRedirectUri() { + return redirectUri; + } + + public void setRedirectUri(String redirectUri) { + this.redirectUri = redirectUri; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public String getNonce() { + return nonce; + } + + public void setNonce(String nonce) { + this.nonce = nonce; + } + + public Display getDisplay() { + return display; + } + + public void setDisplay(Display display) { + this.display = display; + } + + public List getPrompts() { + return prompts; + } + + public void setPrompts(List prompts) { + this.prompts = prompts; + } + + public Integer getMaxAge() { + return maxAge; + } + + public void setMaxAge(Integer maxAge) { + this.maxAge = maxAge; + } + + public List getUiLocales() { + return uiLocales; + } + + public void setUiLocales(List uiLocales) { + this.uiLocales = uiLocales; + } + + public List getClaimsLocales() { + return claimsLocales; + } + + public void setClaimsLocales(List claimsLocales) { + this.claimsLocales = claimsLocales; + } + + public String getIdTokenHint() { + return idTokenHint; + } + + public void setIdTokenHint(String idTokenHint) { + this.idTokenHint = idTokenHint; + } + + public String getLoginHint() { + return loginHint; + } + + public void setLoginHint(String loginHint) { + this.loginHint = loginHint; + } + + public List getAcrValues() { + return acrValues; + } + + public void setAcrValues(List acrValues) { + this.acrValues = acrValues; + } + + public String getRegistration() { + return registration; + } + + public void setRegistration(String registration) { + this.registration = registration; + } + + public UserInfoMember getUserInfoMember() { + return userInfoMember; + } + + public void setUserInfoMember(UserInfoMember userInfoMember) { + this.userInfoMember = userInfoMember; + } + + public IdTokenMember getIdTokenMember() { + return idTokenMember; + } + + public void setIdTokenMember(IdTokenMember idTokenMember) { + this.idTokenMember = idTokenMember; + } + + public void addUserInfoClaim(Claim claim) { + userInfoMember.getClaims().add(claim); + } + + public void addIdTokenClaim(Claim claim) { + idTokenMember.getClaims().add(claim); + } + + public String getAud() { + return aud; + } + + public void setAud(String aud) { + this.aud = aud; + } + + public Integer getExp() { + return exp; + } + + public void setExp(Integer exp) { + this.exp = exp; + } + + public String getIss() { + return iss; + } + + public void setIss(String iss) { + this.iss = iss; + } + + public Integer getIat() { + return iat; + } + + public void setIat(Integer iat) { + this.iat = iat; + } + + public Integer getNbf() { + return nbf; + } + + public void setNbf(Integer nbf) { + this.nbf = nbf; + } + + public String getJti() { + return jti; + } + + public void setJti(String jti) { + this.jti = jti; + } + + public String getClientNotificationToken() { + return clientNotificationToken; + } + + public void setClientNotificationToken(String clientNotificationToken) { + this.clientNotificationToken = clientNotificationToken; + } + + public String getLoginHintToken() { + return loginHintToken; + } + + public void setLoginHintToken(String loginHintToken) { + this.loginHintToken = loginHintToken; + } + + public String getBindingMessage() { + return bindingMessage; + } + + public void setBindingMessage(String bindingMessage) { + this.bindingMessage = bindingMessage; + } + + public String getUserCode() { + return userCode; + } + + public void setUserCode(String userCode) { + this.userCode = userCode; + } + + public Integer getRequestedExpiry() { + return requestedExpiry; + } + + public void setRequestedExpiry(Integer requestedExpiry) { + this.requestedExpiry = requestedExpiry; + } + + public String getEncodedJwt(JSONObject jwks) throws Exception { + String encodedJwt = null; + + if (keyEncryptionAlgorithm != null && blockEncryptionAlgorithm != null) { + JweEncrypterImpl jweEncrypter; + if (cryptoProvider != null && jwks != null) { + PublicKey publicKey = cryptoProvider.getPublicKey(keyId, jwks, null); + jweEncrypter = new JweEncrypterImpl(keyEncryptionAlgorithm, blockEncryptionAlgorithm, publicKey); + } else { + jweEncrypter = new JweEncrypterImpl(keyEncryptionAlgorithm, blockEncryptionAlgorithm, sharedKey.getBytes(Util.UTF8_STRING_ENCODING)); + } + + String header = ClientUtil.toPrettyJson(headerToJSONObject()); + String encodedHeader = Base64Util.base64urlencode(header.getBytes(Util.UTF8_STRING_ENCODING)); + + String claims = ClientUtil.toPrettyJson(payloadToJSONObject()); + String encodedClaims = Base64Util.base64urlencode(claims.getBytes(Util.UTF8_STRING_ENCODING)); + + Jwe jwe = new Jwe(); + jwe.setHeader(new JwtHeader(encodedHeader)); + jwe.setClaims(new JwtClaims(encodedClaims)); + jweEncrypter.encrypt(jwe); + + encodedJwt = jwe.toString(); + } else { + if (cryptoProvider == null) { + throw new Exception("The Crypto Provider cannot be null."); + } + + JSONObject headerJsonObject = headerToJSONObject(); + JSONObject payloadJsonObject = payloadToJSONObject(); + String headerString = ClientUtil.toPrettyJson(headerJsonObject); + String payloadString = ClientUtil.toPrettyJson(payloadJsonObject); + String encodedHeader = Base64Util.base64urlencode(headerString.getBytes(Util.UTF8_STRING_ENCODING)); + String encodedPayload = Base64Util.base64urlencode(payloadString.getBytes(Util.UTF8_STRING_ENCODING)); + String signingInput = encodedHeader + "." + encodedPayload; + String encodedSignature = cryptoProvider.sign(signingInput, keyId, sharedKey, signatureAlgorithm); + + encodedJwt = encodedHeader + "." + encodedPayload + "." + encodedSignature; + } + + return encodedJwt; + } + + public String getEncodedJwt() throws Exception { + return getEncodedJwt(null); + } + + public String getDecodedJwt() { + String decodedJwt = null; + try { + decodedJwt = ClientUtil.toPrettyJson(payloadToJSONObject()); + } catch (JSONException e) { + e.printStackTrace(); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + + return decodedJwt; + } + + protected JSONObject headerToJSONObject() throws InvalidJwtException { + JwtHeader jwtHeader = new JwtHeader(); + + jwtHeader.setType(type); + if (keyEncryptionAlgorithm != null && blockEncryptionAlgorithm != null) { + jwtHeader.setAlgorithm(keyEncryptionAlgorithm); + jwtHeader.setEncryptionMethod(blockEncryptionAlgorithm); + } else { + jwtHeader.setAlgorithm(signatureAlgorithm); + } + jwtHeader.setKeyId(keyId); + + return jwtHeader.toJsonObject(); + } + + protected JSONObject payloadToJSONObject() throws JSONException { + JSONObject obj = new JSONObject(); + + try { + if (responseTypes != null && !responseTypes.isEmpty()) { + if (responseTypes.size() == 1) { + ResponseType responseType = responseTypes.get(0); + obj.put("response_type", responseType); + } else { + JSONArray responseTypeJsonArray = new JSONArray(); + for (ResponseType responseType : responseTypes) { + responseTypeJsonArray.put(responseType); + } + obj.put("response_type", responseTypeJsonArray); + } + } + if (StringUtils.isNotBlank(clientId)) { + obj.put("client_id", clientId); + } + if (scopes != null && !scopes.isEmpty()) { + if (scopes.size() == 1) { + String scope = scopes.get(0); + obj.put("scope", scope); + } else { + JSONArray scopeJsonArray = new JSONArray(); + for (String scope : scopes) { + scopeJsonArray.put(scope); + } + obj.put("scope", scopeJsonArray); + } + } + if (StringUtils.isNotBlank(redirectUri)) { + obj.put("redirect_uri", URLEncoder.encode(redirectUri, "UTF-8")); + } + if (StringUtils.isNotBlank(state)) { + obj.put("state", state); + } + if (StringUtils.isNotBlank(nonce)) { + obj.put("nonce", nonce); + } + if (display != null) { + obj.put("display", display); + } + if (prompts != null && !prompts.isEmpty()) { + JSONArray promptJsonArray = new JSONArray(); + for (Prompt prompt : prompts) { + promptJsonArray.put(prompt); + } + obj.put("prompt", promptJsonArray); + } + if (maxAge != null) { + obj.put("max_age", maxAge); + } + if (uiLocales != null && !uiLocales.isEmpty()) { + JSONArray uiLocalesJsonArray = new JSONArray(uiLocales); + obj.put("ui_locales", uiLocalesJsonArray); + } + if (claimsLocales != null && !claimsLocales.isEmpty()) { + JSONArray claimsLocalesJsonArray = new JSONArray(claimsLocales); + obj.put("claims_locales", claimsLocalesJsonArray); + } + if (StringUtils.isNotBlank(idTokenHint)) { + obj.put("id_token_hint", idTokenHint); + } + if (StringUtils.isNotBlank(loginHint)) { + obj.put("login_hint", loginHint); + } + if (acrValues != null && !acrValues.isEmpty()) { + JSONArray acrValuesJsonArray = new JSONArray(acrValues); + obj.put("acr_values", acrValues); + } + if (StringUtils.isNotBlank(registration)) { + obj.put("registration", registration); + } + + if (userInfoMember != null || idTokenMember != null) { + JSONObject claimsObj = new JSONObject(); + + if (userInfoMember != null) { + claimsObj.put("userinfo", userInfoMember.toJSONObject()); + } + if (idTokenMember != null) { + claimsObj.put("id_token", idTokenMember.toJSONObject()); + } + + obj.put("claims", claimsObj); + } + if (StringUtils.isNotBlank(aud)) { + obj.put("aud", aud); + } + if (exp != null && exp > 0) { + obj.put("exp", exp); + } + if (StringUtils.isNotBlank(iss)) { + obj.put("iss", iss); + } + if (iat != null && iat > 0) { + obj.put("iat", iat); + } + if (nbf != null && nbf > 0) { + obj.put("nbf", nbf); + } + if (StringUtils.isNotBlank(jti)) { + obj.put("jti", jti); + } + if (StringUtils.isNotBlank(clientNotificationToken)) { + obj.put("client_notification_token", clientNotificationToken); + } + if (StringUtils.isNotBlank(loginHintToken)) { + obj.put("login_hint_token", loginHintToken); + } + if (StringUtils.isNotBlank(bindingMessage)) { + obj.put("binding_message", bindingMessage); + } + if (StringUtils.isNotBlank(userCode)) { + obj.put("user_code", userCode); + } + if (requestedExpiry != null && requestedExpiry > 0) { + obj.put("requested_expirity", requestedExpiry); + } + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + + return obj; + } + +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/model/authorize/UserInfoMember.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/model/authorize/UserInfoMember.java new file mode 100644 index 00000000..6846b3ff --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/model/authorize/UserInfoMember.java @@ -0,0 +1,69 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.model.authorize; + +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * @author Javier Rojas Blum Date: 03.07.2012 + */ +public class UserInfoMember { + + private List claims; + private List preferredLocales; + + public UserInfoMember() { + claims = new ArrayList(); + preferredLocales = new ArrayList(); + } + + public List getClaims() { + return claims; + } + + public void setClaims(List claims) { + this.claims = claims; + } + + public List getPreferredLocales() { + return preferredLocales; + } + + public void setPreferredLocales(List preferredLocales) { + this.preferredLocales = preferredLocales; + } + + public JSONObject toJSONObject() throws JSONException { + JSONObject obj = new JSONObject(); + + if (claims != null && !claims.isEmpty()) { + for (Claim claim : claims) { + JSONObject claimValue = claim.getClaimValue().toJSONObject(); + if (claimValue == null) { + obj.put(claim.getName(), JSONObject.NULL); + } else { + obj.put(claim.getName(), claimValue); + } + } + } + if (preferredLocales != null && !preferredLocales.isEmpty()) { + JSONArray arr = new JSONArray(); + for (String locale : preferredLocales) { + arr.put(locale); + } + + obj.put("preferred_locales", arr); + } + + return obj; + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/service/ClientFactory.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/service/ClientFactory.java new file mode 100644 index 00000000..34a0f0d6 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/service/ClientFactory.java @@ -0,0 +1,82 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.service; + +import javax.ws.rs.core.UriBuilder; + +import org.apache.http.client.config.CookieSpecs; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.jboss.resteasy.client.jaxrs.ClientHttpEngine; +import org.jboss.resteasy.client.jaxrs.ResteasyClient; +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; +import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget; +import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient43Engine; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 26/06/2013 + */ + +public class ClientFactory { + + private final static ClientFactory INSTANCE = new ClientFactory(); + + private ApacheHttpClient43Engine engine; + + private ClientFactory() { + this.engine = createEngine(); + } + + public static ClientFactory instance() { + return INSTANCE; + } + + public IntrospectionService createIntrospectionService(String p_url) { + return createIntrospectionService(p_url, engine); + } + + public IntrospectionService createIntrospectionService(String p_url, ClientHttpEngine engine) { + ResteasyClient client = ((ResteasyClientBuilder) ResteasyClientBuilder.newBuilder()).httpEngine(engine).build(); + ResteasyWebTarget target = client.target(UriBuilder.fromPath(p_url)); + IntrospectionService proxy = target.proxy(IntrospectionService.class); + + return proxy; + } + + public StatService createStatService(String url) { + return createStatService(url, engine); + } + + public StatService createStatService(String url, ClientHttpEngine engine) { + ResteasyClient client = ((ResteasyClientBuilder) ResteasyClientBuilder.newBuilder()).httpEngine(engine).build(); + ResteasyWebTarget target = client.target(UriBuilder.fromPath(url)); + return target.proxy(StatService.class); + } + + public ApacheHttpClient43Engine createEngine() { + return createEngine(false); + } + + public ApacheHttpClient43Engine createEngine(boolean followRedirects) { + return createEngine(200, 20, CookieSpecs.STANDARD, followRedirects); + } + + public ApacheHttpClient43Engine createEngine(int maxTotal, int defaultMaxPerRoute, String cookieSpec, boolean followRedirects) { + PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(); + CloseableHttpClient httpClient = HttpClients.custom() + .setDefaultRequestConfig(RequestConfig.custom().setCookieSpec(cookieSpec).build()) + .setConnectionManager(cm).build(); + cm.setMaxTotal(maxTotal); + cm.setDefaultMaxPerRoute(defaultMaxPerRoute); + final ApacheHttpClient43Engine engine = new ApacheHttpClient43Engine(httpClient); + engine.setFollowRedirects(followRedirects); + return engine; + } +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/service/IntrospectionService.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/service/IntrospectionService.java new file mode 100644 index 00000000..cc159dcb --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/service/IntrospectionService.java @@ -0,0 +1,51 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.service; + +import javax.ws.rs.Consumes; +import javax.ws.rs.FormParam; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.gluu.oxauth.model.common.IntrospectionResponse; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * Introspection service. + * + * @author Yuriy Zabrovarnyy + * @version 0.9, 17/09/2013 + */ + +public interface IntrospectionService { + + /** + * Returns introspection response for specified token. + * + * @param p_authorization authorization token + * @param p_token token to introspect + * @return introspection response + */ + @POST + @Consumes({MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_JSON}) + IntrospectionResponse introspectToken(@HeaderParam("Authorization") String p_authorization, @FormParam("token") String p_token); + + @POST + @Consumes({MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_JSON}) + String introspectTokenWithResponseAsJwt(@HeaderParam("Authorization") String p_authorization, @FormParam("token") String p_token, @FormParam("response_as_jwt") boolean responseAsJwt); + + @POST + @Consumes({MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_JSON}) + JsonNode introspect(@HeaderParam("Authorization") String p_authorization, @FormParam("token") String p_token); + +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/service/StatService.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/service/StatService.java new file mode 100644 index 00000000..5d9f8a8f --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/service/StatService.java @@ -0,0 +1,24 @@ +package org.gluu.oxauth.client.service; + +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * @author Yuriy Zabrovarnyy + */ +public interface StatService { + @GET + @Produces({MediaType.APPLICATION_JSON}) + JsonNode stat(@HeaderParam("Authorization") String authorization, @QueryParam("month") String month, @QueryParam("format") String format); + + @POST + @Produces({MediaType.APPLICATION_JSON}) + JsonNode statPost(@HeaderParam("Authorization") String authorization, @FormParam("month") String month, @FormParam("format") String format); +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/UmaClientFactory.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/UmaClientFactory.java new file mode 100644 index 00000000..bf91f8f4 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/UmaClientFactory.java @@ -0,0 +1,114 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.uma; + +import javax.ws.rs.core.UriBuilder; + +import org.gluu.oxauth.client.service.ClientFactory; +import org.gluu.oxauth.model.uma.UmaMetadata; +import org.jboss.resteasy.client.jaxrs.ClientHttpEngine; +import org.jboss.resteasy.client.jaxrs.ResteasyClient; +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; +import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget; +import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient43Engine; + +/** + * Helper class which creates proxied UMA services + * + * @author Yuriy Movchan + * @author Yuriy Zabrovarnyy + */ +public class UmaClientFactory { + + private final static UmaClientFactory instance = new UmaClientFactory(); + + private ApacheHttpClient43Engine engine; + + private UmaClientFactory() { + this.engine = ClientFactory.instance().createEngine(true); + } + + public static UmaClientFactory instance() { + return instance; + } + + public UmaResourceService createResourceService(UmaMetadata metadata) { + return createResourceService(metadata, engine); + } + + public UmaResourceService createResourceService(UmaMetadata metadata, ClientHttpEngine engine) { + ResteasyClient client = ((ResteasyClientBuilder) ResteasyClientBuilder.newBuilder()).httpEngine(engine).build(); + ResteasyWebTarget target = client.target(UriBuilder.fromPath(metadata.getResourceRegistrationEndpoint())); + UmaResourceService proxy = target.proxy(UmaResourceService.class); + + return proxy; + } + + public UmaPermissionService createPermissionService(UmaMetadata metadata) { + return createPermissionService(metadata, engine); + } + + public UmaPermissionService createPermissionService(UmaMetadata metadata, ClientHttpEngine engine) { + ResteasyClient client = ((ResteasyClientBuilder) ResteasyClientBuilder.newBuilder()).httpEngine(engine).build(); + ResteasyWebTarget target = client.target(UriBuilder.fromPath(metadata.getPermissionEndpoint())); + UmaPermissionService proxy = target.proxy(UmaPermissionService.class); + + return proxy; + } + + public UmaRptIntrospectionService createRptStatusService(UmaMetadata metadata) { + return createRptStatusService(metadata, engine); + } + + public UmaRptIntrospectionService createRptStatusService(UmaMetadata metadata, ClientHttpEngine engine) { + ResteasyClient client = ((ResteasyClientBuilder) ResteasyClientBuilder.newBuilder()).httpEngine(engine).build(); + ResteasyWebTarget target = client.target(UriBuilder.fromPath(metadata.getIntrospectionEndpoint())); + UmaRptIntrospectionService proxy = target.proxy(UmaRptIntrospectionService.class); + + return proxy; + } + + public UmaMetadataService createMetadataService(String umaMetadataUri) { + return createMetadataService(umaMetadataUri, engine); + } + + public UmaMetadataService createMetadataService(String umaMetadataUri, ClientHttpEngine engine) { + ResteasyClient client = ((ResteasyClientBuilder) ResteasyClientBuilder.newBuilder()).httpEngine(engine).build(); + ResteasyWebTarget target = client.target(UriBuilder.fromPath(umaMetadataUri)); + UmaMetadataService proxy = target.proxy(UmaMetadataService.class); + + return proxy; + } + + public UmaScopeService createScopeService(String scopeEndpointUri) { + return createScopeService(scopeEndpointUri, engine); + } + + public UmaScopeService createScopeService(String scopeEndpointUri, ClientHttpEngine engine) { + ResteasyClient client = ((ResteasyClientBuilder) ResteasyClientBuilder.newBuilder()).httpEngine(engine).build(); + ResteasyWebTarget target = client.target(UriBuilder.fromPath(scopeEndpointUri)); + UmaScopeService proxy = target.proxy(UmaScopeService.class); + + return proxy; + } + + public UmaTokenService createTokenService(UmaMetadata metadata) { + return createTokenService(metadata, engine); + } + + public UmaTokenService createTokenService(UmaMetadata metadata, ClientHttpEngine engine) { + ResteasyClient client = ((ResteasyClientBuilder) ResteasyClientBuilder.newBuilder()).httpEngine(engine).build(); + ResteasyWebTarget target = client.target(UriBuilder.fromPath(metadata.getTokenEndpoint())); + UmaTokenService proxy = target.proxy(UmaTokenService.class); + + return proxy; + } + + public ResteasyClient newClient(ClientHttpEngine engine) { + return ((ResteasyClientBuilder) ResteasyClientBuilder.newBuilder()).httpEngine(engine).build(); + } +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/UmaMetadataService.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/UmaMetadataService.java new file mode 100644 index 00000000..347ee7c5 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/UmaMetadataService.java @@ -0,0 +1,26 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.uma; + +import javax.ws.rs.GET; +import javax.ws.rs.Produces; + +import org.gluu.oxauth.model.uma.UmaConstants; +import org.gluu.oxauth.model.uma.UmaMetadata; + +/** + * The endpoint at which the requester can obtain UMA metadata. + * + * @author Yuriy Zabrovarnyy + */ +public interface UmaMetadataService { + + @GET + @Produces({ UmaConstants.JSON_MEDIA_TYPE }) + UmaMetadata getMetadata(); + +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/UmaPermissionService.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/UmaPermissionService.java new file mode 100644 index 00000000..b7c56e07 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/UmaPermissionService.java @@ -0,0 +1,38 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.uma; + +import javax.ws.rs.Consumes; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Produces; + +import org.gluu.oxauth.model.uma.PermissionTicket; +import org.gluu.oxauth.model.uma.UmaConstants; +import org.gluu.oxauth.model.uma.UmaPermissionList; + +/** + * The endpoint at which the host registers permissions that it anticipates a + * requester will shortly be asking for from the AM. This AM's endpoint is part + * of resource registration API. + *

+ * In response to receiving an access request accompanied by an RPT that is + * invalid or has insufficient authorization data, the host SHOULD register a + * permission with the AS that would be sufficient for the type of access + * sought. The AS returns a permission ticket for the host to give to the + * requester in its response. + * + */ +public interface UmaPermissionService { + + @POST + @Consumes({ UmaConstants.JSON_MEDIA_TYPE }) + @Produces({ UmaConstants.JSON_MEDIA_TYPE }) + PermissionTicket registerPermission( + @HeaderParam("Authorization") String authorization, + UmaPermissionList permissions); +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/UmaResourceService.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/UmaResourceService.java new file mode 100644 index 00000000..905e22a1 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/UmaResourceService.java @@ -0,0 +1,66 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.uma; + +import java.util.List; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; + +import org.gluu.oxauth.model.uma.UmaConstants; +import org.gluu.oxauth.model.uma.UmaResource; +import org.gluu.oxauth.model.uma.UmaResourceResponse; +import org.gluu.oxauth.model.uma.UmaResourceWithId; + +/** + * REST WS UMA resource set description API + * + * @author Yuriy Zabrovarnyy + */ +public interface UmaResourceService { + + @POST + @Consumes({ UmaConstants.JSON_MEDIA_TYPE}) + @Produces({ UmaConstants.JSON_MEDIA_TYPE }) + UmaResourceResponse addResource(@HeaderParam("Authorization") String authorization, UmaResource resource); + + @PUT + @Path("{rsid}") + @Consumes({ UmaConstants.JSON_MEDIA_TYPE}) + @Produces({ UmaConstants.JSON_MEDIA_TYPE }) + UmaResourceResponse updateResource(@HeaderParam("Authorization") String authorization, @PathParam("rsid") String rsid, UmaResource resource); + + @GET + @Path("{rsid}") + @Produces({ UmaConstants.JSON_MEDIA_TYPE }) + UmaResourceWithId getResource(@HeaderParam("Authorization") String authorization, @PathParam("rsid") String rsid); + + /** + * Gets resources. + * ATTENTION: "scope" is parameter added by gluu to have additional filtering. + * There is no such parameter in UMA specification. + * + * @param authorization authorization + * @param scope scope of resource set for additional filtering, can blank string. + * @return resource set ids. + */ + @GET + @Produces({ UmaConstants.JSON_MEDIA_TYPE }) + List getResourceList(@HeaderParam("Authorization") String authorization, @QueryParam("scope") String scope); + + @DELETE + @Path("{rsid}") + void deleteResource(@HeaderParam("Authorization") String authorization, @PathParam("rsid") String rsid); +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/UmaRptIntrospectionService.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/UmaRptIntrospectionService.java new file mode 100644 index 00000000..84ce496a --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/UmaRptIntrospectionService.java @@ -0,0 +1,30 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.uma; + +import javax.ws.rs.FormParam; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Produces; + +import org.gluu.oxauth.model.uma.RptIntrospectionResponse; +import org.gluu.oxauth.model.uma.UmaConstants; + +/** + * The endpoint at which the host requests the status of an RPT presented to it by a requester. + * The endpoint is RPT introspection profile implementation defined here: + * http://docs.kantarainitiative.org/uma/draft-uma-core.html#uma-bearer-token-profile + */ +public interface UmaRptIntrospectionService { + + @POST + @Produces({UmaConstants.JSON_MEDIA_TYPE}) + RptIntrospectionResponse requestRptStatus(@HeaderParam("Authorization") String authorization, + @FormParam("token") String rptAsString, + @FormParam("token_type_hint") String tokenTypeHint); + +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/UmaScopeService.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/UmaScopeService.java new file mode 100644 index 00000000..450481d3 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/UmaScopeService.java @@ -0,0 +1,27 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.uma; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; + +import org.gluu.oxauth.model.uma.UmaConstants; +import org.gluu.oxauth.model.uma.UmaScopeDescription; + +/** + * @author Yuriy Zabrovarnyy + */ + +public interface UmaScopeService { + + @GET + @Path("{id}") + @Produces({UmaConstants.JSON_MEDIA_TYPE}) + UmaScopeDescription getScope(@PathParam("id") String id); +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/UmaTokenService.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/UmaTokenService.java new file mode 100644 index 00000000..fe53539d --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/UmaTokenService.java @@ -0,0 +1,43 @@ +package org.gluu.oxauth.client.uma; + +import javax.ws.rs.Consumes; +import javax.ws.rs.FormParam; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Produces; + +import org.gluu.oxauth.model.uma.UmaConstants; +import org.gluu.oxauth.model.uma.UmaTokenResponse; + +/** + * @author yuriyz on 06/21/2017. + */ +public interface UmaTokenService { + + @POST + @Consumes({UmaConstants.JSON_MEDIA_TYPE}) + @Produces({UmaConstants.JSON_MEDIA_TYPE}) + UmaTokenResponse requestRpt( + @HeaderParam("Authorization") String authorization, + @FormParam("grant_type") String grantType, + @FormParam("ticket") String ticket, + @FormParam("claim_token") String claimToken, + @FormParam("claim_token_format") String claimTokenFormat, + @FormParam("pct") String pctCode, + @FormParam("rpt") String rptCode, + @FormParam("scope") String scope); + + @POST + @Consumes({UmaConstants.JSON_MEDIA_TYPE}) + @Produces({UmaConstants.JSON_MEDIA_TYPE}) + UmaTokenResponse requestJwtAuthorizationRpt( + @FormParam("client_assertion_type") String clientAssertionType, + @FormParam("client_assertion") String clientAssertion, + @FormParam("grant_type") String grantType, + @FormParam("ticket") String ticket, + @FormParam("claim_token") String claimToken, + @FormParam("claim_token_format") String claimTokenFormat, + @FormParam("pct") String pctCode, + @FormParam("rpt") String rptCode, + @FormParam("scope") String scope); +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/exception/UmaException.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/exception/UmaException.java new file mode 100644 index 00000000..77b44d17 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/exception/UmaException.java @@ -0,0 +1,30 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.uma.exception; + +/** + * UMA Exception + * + * @author Yuriy Movchan Date: 12/08/2012 + */ +public class UmaException extends Exception { + + private static final long serialVersionUID = 2136659058534678566L; + + public UmaException() {} + + public UmaException(String message) {} + + public UmaException(Throwable cause) { + super(cause); + } + + public UmaException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/wrapper/UmaClient.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/wrapper/UmaClient.java new file mode 100644 index 00000000..5f1fa2b8 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/client/uma/wrapper/UmaClient.java @@ -0,0 +1,237 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.client.uma.wrapper; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.client.uma.exception.UmaException; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.Prompt; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.uma.UmaScopeType; +import org.gluu.oxauth.model.uma.wrapper.Token; +import org.gluu.oxauth.model.util.Util; +import org.gluu.util.StringHelper; +import org.jboss.resteasy.client.jaxrs.ClientHttpEngine; + +/** + * @author Yuriy Zabrovarnyy + */ + +public class UmaClient { + + public static Token requestPat(final String tokenUrl, final String clientKeyStoreFile, final String clientKeyStorePassword, final String clientId, final String keyId) throws UmaException { + TokenRequest tokenRequest = TokenRequest.builder().pat().grantType(GrantType.CLIENT_CREDENTIALS).build(); + + return request(tokenUrl, clientKeyStoreFile, clientKeyStorePassword, clientId, keyId, tokenRequest); + } + + @Deprecated + public static Token requestPat(final String authorizeUrl, final String tokenUrl, + final String umaUserId, final String umaUserSecret, + final String umaClientId, final String umaClientSecret, + final String umaRedirectUri, String... scopeArray) throws Exception { + return request(authorizeUrl, tokenUrl, umaUserId, umaUserSecret, umaClientId, umaClientSecret, umaRedirectUri, UmaScopeType.PROTECTION, scopeArray); + } + + public static Token requestPat(final String tokenUrl, final String umaClientId, final String umaClientSecret, String... scopeArray) throws Exception { + return requestPat(tokenUrl, umaClientId, umaClientSecret, null, scopeArray); + } + + public static Token requestPat(final String tokenUrl, final String umaClientId, final String umaClientSecret, ClientHttpEngine engine, String... scopeArray) throws Exception { + return request(tokenUrl, umaClientId, umaClientSecret, UmaScopeType.PROTECTION, engine, scopeArray); + } + + @Deprecated + public static Token request(final String authorizeUrl, final String tokenUrl, + final String umaUserId, final String umaUserSecret, + final String umaClientId, final String umaClientSecret, + final String umaRedirectUri, UmaScopeType p_type, String... scopeArray) throws Exception { + // 1. Request authorization and receive the authorization code. + List responseTypes = new ArrayList(); + responseTypes.add(ResponseType.CODE); + responseTypes.add(ResponseType.ID_TOKEN); + + List scopes = new ArrayList(); + scopes.add(p_type.getValue()); + if (scopeArray != null && scopeArray.length > 0) { + scopes.addAll(Arrays.asList(scopeArray)); + } + + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, umaClientId, scopes, umaRedirectUri, null); + request.setState(state); + request.setAuthUsername(umaUserId); + request.setAuthPassword(umaUserSecret); + request.getPrompts().add(Prompt.NONE); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizeUrl); + authorizeClient.setRequest(request); + AuthorizationResponse response1 = authorizeClient.exec(); + + String scope = response1.getScope(); + String authorizationCode = response1.getCode(); + + if (Util.allNotBlank(authorizationCode)) { + + // 2. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(umaRedirectUri); + tokenRequest.setAuthUsername(umaClientId); + tokenRequest.setAuthPassword(umaClientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + tokenRequest.setScope(scope); + + TokenClient tokenClient1 = new TokenClient(tokenUrl); + tokenClient1.setRequest(tokenRequest); + TokenResponse response2 = tokenClient1.exec(); + + if (response2.getStatus() == 200) { + final String patToken = response2.getAccessToken(); + final String patRefreshToken = response2.getRefreshToken(); + final Integer expiresIn = response2.getExpiresIn(); + if (Util.allNotBlank(patToken, patRefreshToken)) { + return new Token(authorizationCode, patRefreshToken, patToken, scope, expiresIn); + } + } + } + + return null; + } + + public static Token request(final String tokenUrl, final String umaClientId, final String umaClientSecret, UmaScopeType scopeType, + ClientHttpEngine engine, String... scopeArray) throws Exception { + + String scope = scopeType.getValue(); + if (scopeArray != null && scopeArray.length > 0) { + for (String s : scopeArray) { + scope = scope + " " + s; + } + } + + TokenClient tokenClient = new TokenClient(tokenUrl); + if (engine != null) { + tokenClient.setExecutor(engine); + } + TokenResponse response = tokenClient.execClientCredentialsGrant(scope, umaClientId, umaClientSecret); + + if (response.getStatus() == 200) { + final String patToken = response.getAccessToken(); + final Integer expiresIn = response.getExpiresIn(); + if (Util.allNotBlank(patToken)) { + return new Token(null, null, patToken, scopeType.getValue(), expiresIn); + } + } + + return null; + } + + public static Token requestWithClientSecretJwt(final String tokenUrl, + final String umaClientId, + final String umaClientSecret, + AuthenticationMethod authenticationMethod, + SignatureAlgorithm signatureAlgorithm, + String audience, + UmaScopeType scopeType, + String... scopeArray) throws Exception { + + String scope = scopeType.getValue(); + if (scopeArray != null && scopeArray.length > 0) { + for (String s : scopeArray) { + scope = scope + " " + s; + } + } + + TokenRequest request = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + request.setAuthUsername(umaClientId); + request.setAuthPassword(umaClientSecret); + request.setScope(scope); + request.setAuthenticationMethod(authenticationMethod); + request.setAlgorithm(signatureAlgorithm); + request.setAudience(audience); + + return request(tokenUrl, request); + } + + public static Token request(final String tokenUrl, final TokenRequest tokenRequest) throws Exception { + if (tokenRequest.getGrantType() != GrantType.CLIENT_CREDENTIALS) { + return null; + } + + TokenClient tokenClient = new TokenClient(tokenUrl); + + tokenClient.setRequest(tokenRequest); + + TokenResponse response = tokenClient.exec(); + + if (response.getStatus() == 200) { + final String patToken = response.getAccessToken(); + final Integer expiresIn = response.getExpiresIn(); + if (Util.allNotBlank(patToken)) { + return new Token(null, null, patToken, response.getScope(), expiresIn); + } + } + + return null; + } + + private static Token request(final String tokenUrl, final String clientKeyStoreFile, + final String clientKeyStorePassword, final String clientId, final String keyId, TokenRequest tokenRequest) + throws UmaException { + OxAuthCryptoProvider cryptoProvider; + try { + cryptoProvider = new OxAuthCryptoProvider(clientKeyStoreFile, clientKeyStorePassword, null); + } catch (Exception ex) { + throw new UmaException("Failed to initialize crypto provider"); + } + + try { + String tmpKeyId = keyId; + if (StringHelper.isEmpty(keyId)) { + // Get first key + tmpKeyId = cryptoProvider.getKeys().stream().filter(k -> k.contains("_sig_")).findFirst().orElse(null); + + if (tmpKeyId == null) { + throw new UmaException("Unable to find a key in the keystore with use = sig"); + } + } else if (keyId.contains("_enc_")) { + throw new UmaException("Encryption keys not allowed. Supply a key having use = sig"); + } + + SignatureAlgorithm algorithm = cryptoProvider.getSignatureAlgorithm(tmpKeyId); + + + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setAlgorithm(algorithm); + tokenRequest.setKeyId(tmpKeyId); + tokenRequest.setAudience(tokenUrl); + + Token umaPat = UmaClient.request(tokenUrl, tokenRequest); + + return umaPat; + } catch (Exception ex) { + throw new UmaException("Failed to obtain valid UMA PAT token", ex); + } + } + +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/util/ClientUtil.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/util/ClientUtil.java new file mode 100644 index 00000000..5e909616 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/util/ClientUtil.java @@ -0,0 +1,103 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2019, Gluu + */ +package org.gluu.oxauth.util; + +import java.util.ArrayList; +import java.util.List; + +import javax.net.ssl.SSLContext; + +import org.apache.commons.lang.StringUtils; +import org.apache.http.client.config.CookieSpecs; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.ssl.SSLContexts; +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsonorg.JsonOrgModule; + +/** + * @author Yuriy Zabrovarnyy + * @author Yuriy Movchan + * @version 0.9, 26/12/2012 + */ + +public class ClientUtil { + + private final static Logger log = LoggerFactory.getLogger(ClientUtil.class); + + public static String toPrettyJson(JSONObject jsonObject) throws JsonProcessingException { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JsonOrgModule()); + return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonObject); + } + + public static List extractListByKey(JSONObject jsonObject, String key) { + final List result = new ArrayList(); + if (jsonObject.has(key)) { + JSONArray arrayOfValues = jsonObject.optJSONArray(key); + if (arrayOfValues != null) { + for (int i = 0; i < arrayOfValues.length(); i++) { + result.add(arrayOfValues.getString(i)); + } + return result; + } + String listString = jsonObject.optString(key); + if (StringUtils.isNotBlank(listString)) { + String[] arrayOfStringValues = listString.split(" "); + for (String c : arrayOfStringValues) { + if (StringUtils.isNotBlank(c)) { + result.add(c); + } + } + } + } + return result; + } + + /** + * Creates a special SSLContext using a custom TLS version and a set of ciphers enabled to process SSL connections. + * @param tlsVersion TLS version, for example TLSv1.2 + * @param ciphers Set of ciphers used to create connections. + */ + public static CloseableHttpClient createHttpClient(String tlsVersion, String[] ciphers) { + try { + SSLContext sslContext = SSLContexts.createDefault(); + SSLConnectionSocketFactory sslConnectionFactory = new SSLConnectionSocketFactory(sslContext, + new String[] { tlsVersion }, ciphers, NoopHostnameVerifier.INSTANCE); + + Registry registry = RegistryBuilder. create() + .register("https", sslConnectionFactory) + .register("http", new PlainConnectionSocketFactory()) + .build(); + + PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(registry); + + return HttpClients.custom() + .setSSLContext(sslContext) + .setDefaultRequestConfig(RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build()) + .setConnectionManager(cm) + .build(); + } catch (Exception e) { + log.error("Error creating HttpClient with a custom TLS version and custom ciphers", e); + return null; + } + } + +} diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/util/KeyExporter.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/util/KeyExporter.java new file mode 100644 index 00000000..46bea906 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/util/KeyExporter.java @@ -0,0 +1,137 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.util; + +import java.io.File; +import java.security.PrivateKey; + +import org.apache.commons.cli.BasicParser; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.WordUtils; +import org.apache.log4j.Logger; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.status.StatusLogger; +import org.bouncycastle.util.encoders.Base64; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.AlgorithmFamily; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.util.security.SecurityProviderUtility; + +/** + * Export private key from JKS Command example: java -cp + * org.gluu.oxauth.util.KeyExporter -h + *

+ * KeyExporter -keystore /Users/yuriy/tmp/mykeystore.jks -keypasswd secret + * -alias "2d4817e7-5fe8-4b6b-8f64-fe3723625122" + * -exportfile=/Users/yuriy/tmp/mykey.pem + *

+ * + * @author Yuriy Movchan + * @version February 12, 2019 + */ +public class KeyExporter { + + private static final String KEY_STORE_FILE = "keystore"; + private static final String KEY_STORE_PASSWORD = "keypasswd"; + private static final String KEY_ALIAS = "alias"; + private static final String EXPORT_FILE = "exportfile"; + private static final String HELP = "h"; + private static final Logger log; + + static { + StatusLogger.getLogger().setLevel(Level.OFF); + log = Logger.getLogger(KeyExporter.class); + } + + public static void main(String[] args) throws Exception { + new Cli(args).parse(); + } + + public static class Cli { + private String[] args = null; + private Options options = new Options(); + + public Cli(String[] args) { + this.args = args; + + options.addOption(KEY_STORE_FILE, true, "Key Store file."); + options.addOption(KEY_STORE_PASSWORD, true, "Key Store password."); + options.addOption(KEY_ALIAS, true, "Key alias."); + options.addOption(EXPORT_FILE, true, "Export file."); + options.addOption(HELP, false, "Show help."); + } + + public void parse() { + CommandLineParser parser = new BasicParser(); + + CommandLine cmd = null; + try { + cmd = parser.parse(options, args); + + if (cmd.hasOption(HELP)) + help(); + + if (cmd.hasOption(KEY_STORE_FILE) && cmd.hasOption(KEY_STORE_PASSWORD) && cmd.hasOption(KEY_ALIAS) + && cmd.hasOption(EXPORT_FILE)) { + + String keyStore = cmd.getOptionValue(KEY_STORE_FILE); + String keyStorePasswd = cmd.getOptionValue(KEY_STORE_PASSWORD); + String keyAlias = cmd.getOptionValue(KEY_ALIAS); + String exportFile = cmd.getOptionValue(EXPORT_FILE); + + try { + SecurityProviderUtility.installBCProvider(true); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStore, keyStorePasswd, + "CN=oxAuth CA Certificates"); + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyAlias); + String base64EncodedKey = WordUtils.wrap(new String(Base64.encode(privateKey.getEncoded())), 64, + "\n", true); + + StringBuilder sb = new StringBuilder(); + SignatureAlgorithm signatureAlgorithm = cryptoProvider.getSignatureAlgorithm(keyAlias); + if (AlgorithmFamily.RSA.equals(signatureAlgorithm.getFamily())) { + sb.append("-----BEGIN RSA PRIVATE KEY-----\n"); + sb.append(base64EncodedKey); + sb.append("\n"); + sb.append("-----END RSA PRIVATE KEY-----\n"); + } else { + sb.append("-----BEGIN PRIVATE KEY-----\n"); + sb.append(base64EncodedKey); + sb.append("\n"); + sb.append("-----END PRIVATE KEY-----\n"); + } + + FileUtils.writeStringToFile(new File(exportFile), sb.toString()); + } catch (Exception e) { + log.error("Failed to export key", e); + help(); + } + } else { + help(); + } + } catch (ParseException e) { + log.error("Failed to export key", e); + help(); + } + } + + private void help() { + HelpFormatter formatter = new HelpFormatter(); + + formatter.printHelp( + "KeyExporter -keystore path -keypasswd secret -alias 2d4817e7-5fe8-4b6b-8f64-fe3723625122 -exportfile=export-path", + options); + System.exit(0); + } + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/main/java/org/gluu/oxauth/util/KeyGenerator.java b/oxAuth/Client/src/main/java/org/gluu/oxauth/util/KeyGenerator.java new file mode 100644 index 00000000..497fa233 --- /dev/null +++ b/oxAuth/Client/src/main/java/org/gluu/oxauth/util/KeyGenerator.java @@ -0,0 +1,254 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.util; + +import static org.gluu.oxauth.model.jwk.JWKParameter.CERTIFICATE_CHAIN; +import static org.gluu.oxauth.model.jwk.JWKParameter.EXPIRATION_TIME; +import static org.gluu.oxauth.model.jwk.JWKParameter.EXPONENT; +import static org.gluu.oxauth.model.jwk.JWKParameter.KEY_ID; +import static org.gluu.oxauth.model.jwk.JWKParameter.MODULUS; +import static org.gluu.oxauth.model.jwk.JWKParameter.X; +import static org.gluu.oxauth.model.jwk.JWKParameter.Y; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.List; + +import org.apache.commons.cli.BasicParser; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.apache.log4j.Logger; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.status.StatusLogger; +import org.gluu.oxauth.model.crypto.AbstractCryptoProvider; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.OxElevenCryptoProvider; +import org.gluu.oxauth.model.crypto.encryption.KeyEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jwk.Algorithm; +import org.gluu.oxauth.model.jwk.JSONWebKey; +import org.gluu.oxauth.model.jwk.JSONWebKeySet; +import org.gluu.oxauth.model.jwk.KeyType; +import org.gluu.oxauth.model.jwk.Use; +import org.gluu.util.security.SecurityProviderUtility; +import org.gluu.oxauth.model.util.StringUtils; +import org.gluu.util.StringHelper; +import org.json.JSONArray; +import org.json.JSONObject; + +/** + * Command example: + * java -cp bcprov-jdk18on-1.54.jar:.jar:bcpkix-jdk18on-1.54.jar:commons-cli-1.2.jar:commons-codec-1.5.jar:commons-lang-2.6.jar:jettison-1.3.jar:log4j-1.2.14.jar:oxauth-model.jar:oxauth.jar org.gluu.oxauth.util.KeyGenerator -h + *

+ * KeyGenerator -sig_keys RS256 RS384 RS512 ES256 ES384 ES512 PS256 PS384 PS512 -enc_keys RSA_OAEP RSA1_5 -keystore /Users/JAVIER/tmp/mykeystore.jks -keypasswd secret -dnname "CN=oxAuth CA Certificates" -expiration 365 + *

+ * KeyGenerator -sig_keys RS256 RS384 RS512 ES256 ES384 ES512 -ox11 https://ce.gluu.info:8443/oxeleven/rest/generateKey -expiration 365 -at xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + * + * @author Javier Rojas Blum + * @author Yuriy Movchan + * @version February 12, 2019 + */ +public class KeyGenerator { + + private static final String SIGNING_KEYS = "sig_keys"; + private static final String ENCRYPTION_KEYS = "enc_keys"; + private static final String KEY_STORE_FILE = "keystore"; + private static final String KEY_STORE_PASSWORD = "keypasswd"; + private static final String DN_NAME = "dnname"; + private static final String OXELEVEN_ACCESS_TOKEN = "at"; + private static final String OXELEVEN_GENERATE_KEY_ENDPOINT = "ox11"; + private static final String EXPIRATION = "expiration"; + private static final String EXPIRATION_HOURS = "expiration_hours"; + private static final String KEY_LENGTH = "key_length"; + private static final String KEY_STORE_FILE_TYPE = "keystore_type"; + private static final String HELP = "h"; + private static final Logger log; + + static { + StatusLogger.getLogger().setLevel(Level.OFF); + log = Logger.getLogger(KeyGenerator.class); + } + + public static void main(String[] args) throws Exception { + new Cli(args).parse(); + } + + public static class Cli { + private String[] args = null; + private Options options = new Options(); + + public Cli(String[] args) { + this.args = args; + + Option signingKeysOption = new Option(SIGNING_KEYS, true, + "Signature keys to generate. (RS256 RS384 RS512 ES256 ES384 ES512 PS256 PS384 PS512)."); + signingKeysOption.setArgs(Option.UNLIMITED_VALUES); + + Option encryptionKeysOption = new Option(ENCRYPTION_KEYS, true, + "Encryption keys to generate. (RSA_OAEP RSA1_5)."); + encryptionKeysOption.setArgs(Option.UNLIMITED_VALUES); + + options.addOption(signingKeysOption); + options.addOption(encryptionKeysOption); + options.addOption(KEY_STORE_FILE, true, "Key Store file."); + options.addOption(KEY_STORE_PASSWORD, true, "Key Store password."); + options.addOption(DN_NAME, true, "DN of certificate issuer."); + options.addOption(OXELEVEN_ACCESS_TOKEN, true, "oxEleven Access Token"); + options.addOption(OXELEVEN_GENERATE_KEY_ENDPOINT, true, "oxEleven Generate Key Endpoint."); + options.addOption(EXPIRATION, true, "Expiration in days."); + options.addOption(EXPIRATION_HOURS, true, "Expiration in hours."); + options.addOption(KEY_LENGTH, true, "Key length"); + options.addOption(KEY_STORE_FILE_TYPE, true, "Key Store type"); + options.addOption(HELP, false, "Show help."); + } + + public void parse() { + CommandLineParser parser = new BasicParser(); + + CommandLine cmd = null; + try { + cmd = parser.parse(options, args); + + if (cmd.hasOption(HELP)) { + help(); + } + + if (!((cmd.hasOption(SIGNING_KEYS) || cmd.hasOption(ENCRYPTION_KEYS)) + && (cmd.hasOption(EXPIRATION) || cmd.hasOption(EXPIRATION_HOURS)))) { + help(); + } + + String[] sigAlgorithms = cmd.getOptionValues(SIGNING_KEYS); + String[] encAlgorithms = cmd.getOptionValues(ENCRYPTION_KEYS); + List signatureAlgorithms = cmd.hasOption(SIGNING_KEYS) ? Algorithm.fromString(sigAlgorithms, Use.SIGNATURE) : new ArrayList(); + List encryptionAlgorithms = cmd.hasOption(ENCRYPTION_KEYS) ? Algorithm.fromString(encAlgorithms, Use.ENCRYPTION) : new ArrayList(); + if (signatureAlgorithms.isEmpty() && encryptionAlgorithms.isEmpty()) { + help(); + } + + int keyLength = StringHelper.toInt(cmd.getOptionValue(KEY_LENGTH), 2048); + int expiration = StringHelper.toInt(cmd.getOptionValue(EXPIRATION), 0); + int expiration_hours = StringHelper.toInt(cmd.getOptionValue(EXPIRATION_HOURS), 0); + + if(cmd.hasOption(KEY_STORE_FILE_TYPE)) { + String keyStoreFileType = cmd.getOptionValue(KEY_STORE_FILE_TYPE); + SecurityProviderUtility.KeyStorageType keyStorageType = SecurityProviderUtility.KeyStorageType.fromString(keyStoreFileType); + if (keyStorageType == null) { + throw new ParseException(String.format("Wrong option = %s value = %s", KEY_STORE_FILE_TYPE, keyStoreFileType)); + } + SecurityProviderUtility.SecurityModeType securityMode = keyStorageType.getSecurityMode(); + SecurityProviderUtility.setSecurityMode(securityMode); + } + + if (cmd.hasOption(OXELEVEN_ACCESS_TOKEN) && cmd.hasOption(OXELEVEN_GENERATE_KEY_ENDPOINT)) { + String accessToken = cmd.getOptionValue(OXELEVEN_ACCESS_TOKEN); + String generateKeyEndpoint = cmd.getOptionValue(OXELEVEN_GENERATE_KEY_ENDPOINT); + + try { + OxElevenCryptoProvider cryptoProvider = new OxElevenCryptoProvider(generateKeyEndpoint, + null, null, null, accessToken); + + generateKeys(cryptoProvider, signatureAlgorithms, encryptionAlgorithms, expiration, expiration_hours, keyLength); + } catch (Exception e) { + log.error("Failed to generate keys", e); + help(); + } + } else if (cmd.hasOption(KEY_STORE_FILE) + && cmd.hasOption(KEY_STORE_PASSWORD) + && cmd.hasOption(DN_NAME)) { + String keystore = cmd.getOptionValue(KEY_STORE_FILE); + String keypasswd = cmd.getOptionValue(KEY_STORE_PASSWORD); + String dnName = cmd.getOptionValue(DN_NAME); + + try { + SecurityProviderUtility.installBCProvider(true); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keystore, keypasswd, dnName); + generateKeys(cryptoProvider, signatureAlgorithms, encryptionAlgorithms, expiration, expiration_hours, keyLength); + } catch (Exception e) { + e.printStackTrace(); + log.error("Failed to generate keys", e); + help(); + } + } else { + help(); + } + } catch (ParseException e) { + log.error("Failed to generate keys", e); + help(); + } + } + + private void generateKeys(AbstractCryptoProvider cryptoProvider, List signatureAlgorithms, + List encryptionAlgorithms, int expiration, int expiration_hours, int keyLength) throws Exception { + JSONWebKeySet jwks = new JSONWebKeySet(); + + Calendar calendar = new GregorianCalendar(); + calendar.add(Calendar.DATE, expiration); + calendar.add(Calendar.HOUR, expiration_hours); + + for (Algorithm algorithm : signatureAlgorithms) { + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.fromString(algorithm.name()); + JSONObject result = cryptoProvider.generateKey(algorithm, calendar.getTimeInMillis(), Use.SIGNATURE, keyLength); + + JSONWebKey key = new JSONWebKey(); + key.setKid(result.getString(KEY_ID)); + key.setUse(Use.SIGNATURE); + key.setAlg(algorithm); + key.setKty(KeyType.fromString(signatureAlgorithm.getFamily().toString())); + key.setExp(result.optLong(EXPIRATION_TIME)); + key.setCrv(signatureAlgorithm.getCurve()); + key.setN(result.optString(MODULUS)); + key.setE(result.optString(EXPONENT)); + key.setX(result.optString(X)); + key.setY(result.optString(Y)); + + JSONArray x5c = result.optJSONArray(CERTIFICATE_CHAIN); + key.setX5c(StringUtils.toList(x5c)); + + jwks.getKeys().add(key); + } + + for (Algorithm algorithm : encryptionAlgorithms) { + KeyEncryptionAlgorithm encryptionAlgorithm = KeyEncryptionAlgorithm.fromName(algorithm.getParamName()); + JSONObject result = cryptoProvider.generateKey(algorithm, calendar.getTimeInMillis(), Use.ENCRYPTION, keyLength); + + JSONWebKey key = new JSONWebKey(); + key.setKid(result.getString(KEY_ID)); + key.setUse(Use.ENCRYPTION); + key.setAlg(algorithm); + key.setKty(KeyType.fromString(encryptionAlgorithm.getFamily())); + key.setExp(result.optLong(EXPIRATION_TIME)); + key.setN(result.optString(MODULUS)); + key.setE(result.optString(EXPONENT)); + key.setX(result.optString(X)); + key.setY(result.optString(Y)); + + JSONArray x5c = result.optJSONArray(CERTIFICATE_CHAIN); + key.setX5c(StringUtils.toList(x5c)); + + jwks.getKeys().add(key); + } + + System.out.println(jwks); + } + + private void help() { + HelpFormatter formatter = new HelpFormatter(); + + formatter.printHelp( + "KeyGenerator -sig_keys alg ... -enc_keys alg ... -expiration n_days [-expiration_hours n_hours] [-ox11 url] [-keystore path -keystore_type ks_type -keypasswd secret -dnname dn_name]", + options); + System.exit(0); + } + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/BaseTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/BaseTest.java new file mode 100644 index 00000000..cf0250a2 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/BaseTest.java @@ -0,0 +1,1132 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.UUID; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; + +import org.apache.commons.lang.StringUtils; +import org.apache.http.client.CookieStore; +import org.apache.http.client.HttpClient; +import org.apache.http.client.config.CookieSpecs; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.conn.ssl.TrustSelfSignedStrategy; +import org.apache.http.conn.ssl.TrustStrategy; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.client.LaxRedirectStrategy; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.ssl.SSLContextBuilder; +import org.apache.http.ssl.SSLContexts; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.BaseClient; +import org.gluu.oxauth.client.BaseResponseWithErrors; +import org.gluu.oxauth.client.ClientUtils; +import org.gluu.oxauth.client.OpenIdConfigurationClient; +import org.gluu.oxauth.client.OpenIdConfigurationResponse; +import org.gluu.oxauth.client.OpenIdConnectDiscoveryClient; +import org.gluu.oxauth.client.OpenIdConnectDiscoveryResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RevokeSessionClient; +import org.gluu.oxauth.client.RevokeSessionRequest; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoRequest; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.dev.HostnameVerifierType; +import org.gluu.oxauth.model.common.ResponseMode; +import org.gluu.oxauth.model.crypto.AbstractCryptoProvider; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.error.IErrorType; +import org.gluu.oxauth.model.util.Util; +import org.gluu.oxauth.page.AbstractPage; +import org.gluu.oxauth.page.PageConfig; +import org.gluu.util.StringHelper; +import org.gluu.util.security.SecurityProviderUtility; +import org.jboss.resteasy.client.jaxrs.ClientHttpEngine; +import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient43Engine; +import org.jetbrains.annotations.Nullable; +import org.openqa.selenium.By; +import org.openqa.selenium.Cookie; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.htmlunit.HtmlUnitDriver; +import org.openqa.selenium.interactions.Actions; +import org.openqa.selenium.support.ui.FluentWait; +import org.openqa.selenium.support.ui.Wait; +import org.openqa.selenium.support.ui.WebDriverWait; +import org.testng.ITestContext; +import org.testng.Reporter; +import org.testng.annotations.BeforeSuite; +import org.testng.annotations.BeforeTest; + +import com.google.common.collect.Maps; + +/** + * @author Javier Rojas Blum + * @version August 20, 2019 + */ +public abstract class BaseTest { + + public static final boolean ENABLE_REDIRECT_TO_LOGIN_PAGE = StringHelper.toBoolean(System.getProperty("gluu.enable-redirect", "false"), false); + + protected HtmlUnitDriver driver; + + protected String authorizationEndpoint; + protected String authorizationPageEndpoint; + protected String gluuConfigurationEndpoint; + protected String tokenEndpoint; + protected String tokenRevocationEndpoint; + protected String userInfoEndpoint; + protected String clientInfoEndpoint; + protected String checkSessionIFrame; + protected String endSessionEndpoint; + protected String jwksUri; + protected String registrationEndpoint; + protected String configurationEndpoint; + protected String idGenEndpoint; + protected String introspectionEndpoint; + protected String deviceAuthzEndpoint; + protected String backchannelAuthenticationEndpoint; + protected String revokeSessionEndpoint; + protected Map> scopeToClaimsMapping; + protected String issuer; + + protected Map allTestKeys = Maps.newHashMap(); + + // Form Interaction + protected String loginFormUsername; + protected String loginFormPassword; + protected String loginFormLoginButton; + private String authorizeFormAllowButton; + protected String authorizeFormDoNotAllowButton; + + @BeforeSuite + public void initTestSuite(ITestContext context) throws IOException { + SecurityProviderUtility.installBCProvider(); + + Reporter.log("Invoked init test suite method \n", true); + + String propertiesFile = context.getCurrentXmlTest().getParameter("propertiesFile"); + if (StringHelper.isEmpty(propertiesFile)) { + propertiesFile = "target/test-classes/testng.properties"; + } + + FileInputStream conf = new FileInputStream(propertiesFile); + Properties prop = new Properties(); + prop.load(conf); + + Map parameters = new HashMap(); + for (Entry entry : prop.entrySet()) { + Object key = entry.getKey(); + Object value = entry.getValue(); + + if (StringHelper.isEmptyString(key) || StringHelper.isEmptyString(value)) { + continue; + } + parameters.put(key.toString(), value.toString()); + } + + // Overrided test paramters + context.getSuite().getXmlSuite().setParameters(parameters); + } + + public WebDriver getDriver() { + return driver; + } + + public String getAuthorizationEndpoint() { + return authorizationEndpoint; + } + + public void setAuthorizationEndpoint(String authorizationEndpoint) { + this.authorizationEndpoint = authorizationEndpoint; + } + + public String getTokenEndpoint() { + return tokenEndpoint; + } + + public void setTokenEndpoint(String tokenEndpoint) { + this.tokenEndpoint = tokenEndpoint; + } + + public String getTokenRevocationEndpoint() { + return tokenRevocationEndpoint; + } + + public void setTokenRevocationEndpoint(String tokenRevocationEndpoint) { + this.tokenRevocationEndpoint = tokenRevocationEndpoint; + } + + public String getUserInfoEndpoint() { + return userInfoEndpoint; + } + + public void setUserInfoEndpoint(String userInfoEndpoint) { + this.userInfoEndpoint = userInfoEndpoint; + } + + public String getClientInfoEndpoint() { + return clientInfoEndpoint; + } + + public void setClientInfoEndpoint(String clientInfoEndpoint) { + this.clientInfoEndpoint = clientInfoEndpoint; + } + + public String getCheckSessionIFrame() { + return checkSessionIFrame; + } + + public void setCheckSessionIFrame(String checkSessionIFrame) { + this.checkSessionIFrame = checkSessionIFrame; + } + + public String getEndSessionEndpoint() { + return endSessionEndpoint; + } + + public void setEndSessionEndpoint(String endSessionEndpoint) { + this.endSessionEndpoint = endSessionEndpoint; + } + + public String getJwksUri() { + return jwksUri; + } + + public void setJwksUri(String jwksUri) { + this.jwksUri = jwksUri; + } + + public String getRegistrationEndpoint() { + return registrationEndpoint; + } + + public void setRegistrationEndpoint(String registrationEndpoint) { + this.registrationEndpoint = registrationEndpoint; + } + + public String getIntrospectionEndpoint() { + return introspectionEndpoint; + } + + public void setIntrospectionEndpoint(String p_introspectionEndpoint) { + introspectionEndpoint = p_introspectionEndpoint; + } + + public String getBackchannelAuthenticationEndpoint() { + return backchannelAuthenticationEndpoint; + } + + public void setBackchannelAuthenticationEndpoint(String backchannelAuthenticationEndpoint) { + this.backchannelAuthenticationEndpoint = backchannelAuthenticationEndpoint; + } + + public String getRevokeSessionEndpoint() { + return revokeSessionEndpoint; + } + + public void setRevokeSessionEndpoint(String revokeSessionEndpoint) { + this.revokeSessionEndpoint = revokeSessionEndpoint; + } + + public Map> getScopeToClaimsMapping() { + return scopeToClaimsMapping; + } + + public void setScopeToClaimsMapping(Map> p_scopeToClaimsMapping) { + scopeToClaimsMapping = p_scopeToClaimsMapping; + } + + public String getIdGenEndpoint() { + return idGenEndpoint; + } + + public void setIdGenEndpoint(String p_idGenEndpoint) { + idGenEndpoint = p_idGenEndpoint; + } + + public String getConfigurationEndpoint() { + return configurationEndpoint; + } + + public void setConfigurationEndpoint(String configurationEndpoint) { + this.configurationEndpoint = configurationEndpoint; + } + + public void startSelenium() { + //System.setProperty("webdriver.chrome.driver", "/Users/JAVIER/tmp/chromedriver"); + //driver = new ChromeDriver(); + + //driver = new SafariDriver(); + + //driver = new FirefoxDriver(); + + //driver = new InternetExplorerDriver(); + + driver = new HtmlUnitDriver(true); + driver.getWebClient().getOptions().setThrowExceptionOnScriptError(false); + } + + public void stopSelenium() { +// driver.close(); + driver.quit(); + driver = null; + } + + /** + * The authorization server authenticates the resource owner (via the user-agent) + * and establishes whether the resource owner grants or denies the client's access request. + */ + public AuthorizationResponse authenticateResourceOwnerAndGrantAccess( + String authorizeUrl, AuthorizationRequest authorizationRequest, String userId, String userSecret) { + return authenticateResourceOwnerAndGrantAccess(authorizeUrl, authorizationRequest, userId, userSecret, true); + } + + /** + * The authorization server authenticates the resource owner (via the user-agent) + * and establishes whether the resource owner grants or denies the client's access request. + */ + public AuthorizationResponse authenticateResourceOwnerAndGrantAccess( + String authorizeUrl, AuthorizationRequest authorizationRequest, String userId, String userSecret, boolean cleanupCookies) { + return authenticateResourceOwnerAndGrantAccess(authorizeUrl, authorizationRequest, userId, userSecret, cleanupCookies, false); + } + + /** + * The authorization server authenticates the resource owner (via the user-agent) + * and establishes whether the resource owner grants or denies the client's access request. + */ + public AuthorizationResponse authenticateResourceOwnerAndGrantAccess( + String authorizeUrl, AuthorizationRequest authorizationRequest, String userId, String userSecret, + boolean cleanupCookies, boolean useNewDriver) { + return authenticateResourceOwnerAndGrantAccess(authorizeUrl, authorizationRequest, userId, userSecret, cleanupCookies, useNewDriver, 1); + } + + /** + * The authorization server authenticates the resource owner (via the user-agent) + * and establishes whether the resource owner grants or denies the client's access request. + */ + public AuthorizationResponse authenticateResourceOwnerAndGrantAccess( + String authorizeUrl, AuthorizationRequest authorizationRequest, String userId, String userSecret, + boolean cleanupCookies, boolean useNewDriver, int authzSteps) { + WebDriver currentDriver = initWebDriver(useNewDriver, cleanupCookies); + + AuthorizeClient authorizeClient = processAuthentication(currentDriver, authorizeUrl, authorizationRequest, + userId, userSecret); + + int remainAuthzSteps = authzSteps; + + String authorizationResponseStr = null; + do { + authorizationResponseStr = acceptAuthorization(currentDriver, authorizationRequest.getRedirectUri()); + remainAuthzSteps--; + } while (remainAuthzSteps >= 1); + + AuthorizationResponse authorizationResponse = buildAuthorizationResponse(authorizationRequest, + currentDriver, authorizeClient, authorizationResponseStr); + + stopWebDriver(useNewDriver, currentDriver); + + return authorizationResponse; + } + + protected WebDriver initWebDriver(boolean useNewDriver, boolean cleanupCookies) { + // Allow to run test in multi thread mode + HtmlUnitDriver currentDriver; + if (useNewDriver) { + currentDriver = new HtmlUnitDriver(true); + } else { + startSelenium(); + currentDriver = driver; + if (cleanupCookies) { + System.out.println("authenticateResourceOwnerAndGrantAccess: Cleaning cookies"); + deleteAllCookies(); + } + } + + return currentDriver; + } + + protected void stopWebDriver(boolean useNewDriver, WebDriver currentDriver) { + if (useNewDriver) { + currentDriver.close(); + currentDriver.quit(); + } else { + stopSelenium(); + } + } + + protected AuthorizeClient processAuthentication(WebDriver currentDriver, String authorizeUrl, + AuthorizationRequest authorizationRequest, String userId, String userSecret) { + String authorizationRequestUrl = authorizeUrl + "?" + authorizationRequest.getQueryString(); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizeUrl); + authorizeClient.setRequest(authorizationRequest); + + System.out.println("authenticateResourceOwnerAndGrantAccess: authorizationRequestUrl:" + authorizationRequestUrl); + + navigateToAuhorizationUrl(currentDriver, authorizationRequestUrl); + if (userSecret != null) { + final String previousUrl = currentDriver.getCurrentUrl(); + + WebElement loginButton = waitForRequredElementLoad(currentDriver, loginFormLoginButton); + + if (userId != null) { + setWebElementValue(currentDriver, loginFormUsername, userId); + } + + setWebElementValue(currentDriver, loginFormPassword, userSecret); + + loginButton.click(); + + if (ENABLE_REDIRECT_TO_LOGIN_PAGE) { + waitForPageSwitch(currentDriver, previousUrl); + } + + if (currentDriver.getPageSource().contains("Failed to authenticate.")) { + fail("Failed to authenticate user"); + } + } + + return authorizeClient; + } + + private WebElement waitForRequredElementLoad(WebDriver currentDriver, String id) { + Wait wait = new FluentWait<>(currentDriver) + .withTimeout(Duration.ofSeconds(PageConfig.WAIT_OPERATION_TIMEOUT)) + .pollingEvery(Duration.ofMillis(1000)) + .ignoring(NoSuchElementException.class); + + WebElement loginButton = wait.until(d -> { + return d.findElement(By.id(id)); + }); + return loginButton; + } + + private void setWebElementValue(WebDriver currentDriver, String elemnetId, String value) { + WebElement webElement = currentDriver.findElement(By.id(elemnetId)); + webElement.sendKeys(value); + + int remainAttempts = 10; + do { + if (value.equals(webElement.getAttribute("value"))) { + break; + } + + ((JavascriptExecutor) currentDriver).executeScript("arguments[0].value='" + value + "';", webElement); + + try { + Thread.sleep(500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + remainAttempts--; + } while (remainAttempts >= 1); + } + + protected String acceptAuthorization(WebDriver currentDriver, String redirectUri) { + String authorizationResponseStr = currentDriver.getCurrentUrl(); + + // Check for authorization form if client has no persistent authorization + if (!authorizationResponseStr.contains("#")) { + Wait wait = new FluentWait(driver) + .withTimeout(Duration.ofSeconds(PageConfig.WAIT_OPERATION_TIMEOUT)) + .pollingEvery(Duration.ofMillis(500)) + .ignoring(NoSuchElementException.class); + + WebElement allowButton = wait.until(d -> currentDriver.findElement(By.id(authorizeFormAllowButton))); + + // We have to use JavaScript because target is link with onclick + JavascriptExecutor jse = (JavascriptExecutor) currentDriver; + jse.executeScript("scroll(0, 1000)"); + + String previousURL = currentDriver.getCurrentUrl(); + + Actions actions = new Actions(currentDriver); + actions.click(allowButton).perform(); + + waitForPageSwitch(currentDriver, previousURL); + + authorizationResponseStr = currentDriver.getCurrentUrl(); + + if (redirectUri != null && !authorizationResponseStr.startsWith(redirectUri)) { + navigateToAuhorizationUrl(currentDriver, authorizationResponseStr); + authorizationResponseStr = waitForPageSwitch(authorizationResponseStr); + } + + if (redirectUri == null && !authorizationResponseStr.contains("code=")) { // corner case for redirect_uri = null + navigateToAuhorizationUrl(currentDriver, authorizationResponseStr); + authorizationResponseStr = waitForPageSwitch(authorizationResponseStr); + } + } else { + fail("The authorization form was expected to be shown."); + } + + return authorizationResponseStr; + } + + public String waitForPageSwitch(String previousUrl) { + return waitForPageSwitch(driver, previousUrl); + } + + public static String waitForPageSwitch(WebDriver driver, String previousUrl) { + return AbstractPage.waitForPageSwitch(driver, previousUrl); + } + + protected AuthorizationResponse buildAuthorizationResponse(AuthorizationRequest authorizationRequest, + WebDriver currentDriver, + String authorizationResponseStr) { + return buildAuthorizationResponse(authorizationRequest, currentDriver, null, authorizationResponseStr); + } + + protected AuthorizationResponse buildAuthorizationResponse(AuthorizationRequest authorizationRequest, + WebDriver currentDriver, @Nullable AuthorizeClient authorizeClient, + String authorizationResponseStr) { + final WebDriver.Options options = currentDriver.manage(); + Cookie sessionStateCookie = options.getCookieNamed("session_state"); + Cookie sessionIdCookie = options.getCookieNamed("session_id"); + + if (sessionStateCookie != null) { + System.out.println("authenticateResourceOwnerAndGrantAccess: sessionState:" + sessionStateCookie.getValue()); ; + } + if (sessionIdCookie != null) { + System.out.println("authenticateResourceOwnerAndGrantAccess: sessionId:" + sessionIdCookie.getValue()); ; + } + + AuthorizationResponse authorizationResponse = new AuthorizationResponse(authorizationResponseStr); + if (authorizationRequest.getRedirectUri() != null && authorizationRequest.getRedirectUri().equals(authorizationResponseStr)) { + authorizationResponse.setResponseMode(ResponseMode.FORM_POST); + } + if (authorizeClient != null) { + authorizeClient.setResponse(authorizationResponse); + showClientUserAgent(authorizeClient); + } + + return authorizationResponse; + } + + public AuthorizationResponse authenticateResourceOwnerAndDenyAccess( + String authorizeUrl, AuthorizationRequest authorizationRequest, String userId, String userSecret) { + String authorizationRequestUrl = authorizeUrl + "?" + authorizationRequest.getQueryString(); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizeUrl); + authorizeClient.setRequest(authorizationRequest); + + System.out.println("authenticateResourceOwnerAndDenyAccess: authorizationRequestUrl:" + authorizationRequestUrl); + startSelenium(); + navigateToAuhorizationUrl(driver, authorizationRequestUrl); + + WebElement usernameElement = driver.findElement(By.id(loginFormUsername)); + WebElement passwordElement = driver.findElement(By.id(loginFormPassword)); + WebElement loginButton = driver.findElement(By.id(loginFormLoginButton)); + + if (userId != null) { + usernameElement.sendKeys(userId); + } + passwordElement.sendKeys(userSecret); + + String previousUrl = driver.getCurrentUrl(); + loginButton.click(); + + if (ENABLE_REDIRECT_TO_LOGIN_PAGE) { + waitForPageSwitch(driver, previousUrl); + } + + String authorizationResponseStr = driver.getCurrentUrl(); + + WebElement doNotAllowButton = driver.findElement(By.id(authorizeFormDoNotAllowButton)); + + final String previousUrl2 = driver.getCurrentUrl(); + doNotAllowButton.click(); + waitForPageSwitch(driver, previousUrl2); + + authorizationResponseStr = driver.getCurrentUrl(); + + Cookie sessionIdCookie = driver.manage().getCookieNamed("session_id"); + String sessionId = null; + if (sessionIdCookie != null) { + sessionId = sessionIdCookie.getValue(); + } + System.out.println("authenticateResourceOwnerAndDenyAccess: sessionId:" + sessionId); + + stopSelenium(); + + AuthorizationResponse authorizationResponse = new AuthorizationResponse(authorizationResponseStr); + if (authorizationRequest.getRedirectUri() != null && authorizationRequest.getRedirectUri().equals(authorizationResponseStr)) { + authorizationResponse.setResponseMode(ResponseMode.FORM_POST); + } + authorizationResponse.setSessionId(sessionId); + authorizeClient.setResponse(authorizationResponse); + showClientUserAgent(authorizeClient); + + return authorizationResponse; + } + + public AuthorizationResponse authorizationRequestAndGrantAccess( + String authorizeUrl, AuthorizationRequest authorizationRequest) { + String authorizationRequestUrl = authorizeUrl + "?" + authorizationRequest.getQueryString(); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizeUrl); + authorizeClient.setRequest(authorizationRequest); + + System.out.println("authorizationRequestAndGrantAccess: authorizationRequestUrl:" + authorizationRequestUrl); + startSelenium(); + navigateToAuhorizationUrl(driver, authorizationRequestUrl); + + String authorizationResponseStr = driver.getCurrentUrl(); + + WebElement allowButton = driver.findElement(By.id(authorizeFormAllowButton)); + + final String previousURL = driver.getCurrentUrl(); + allowButton.click(); + + waitForPageSwitch(previousURL); + + authorizationResponseStr = driver.getCurrentUrl(); + + if (!authorizationResponseStr.startsWith(authorizationRequest.getRedirectUri())) { + navigateToAuhorizationUrl(driver, authorizationResponseStr); + authorizationResponseStr = waitForPageSwitch(authorizationResponseStr); + } + + Cookie sessionStateCookie = driver.manage().getCookieNamed("session_state"); + String sessionState = null; + if (sessionStateCookie != null) { + sessionState = sessionStateCookie.getValue(); + } + System.out.println("authorizationRequestAndGrantAccess: sessionState:" + sessionState); + + stopSelenium(); + + AuthorizationResponse authorizationResponse = new AuthorizationResponse(authorizationResponseStr); + if (authorizationRequest.getRedirectUri() != null && authorizationRequest.getRedirectUri().equals(authorizationResponseStr)) { + authorizationResponse.setResponseMode(ResponseMode.FORM_POST); + } + authorizeClient.setResponse(authorizationResponse); + showClientUserAgent(authorizeClient); + + return authorizationResponse; + } + + public AuthorizationResponse authorizationRequestAndDenyAccess( + String authorizeUrl, AuthorizationRequest authorizationRequest) { + String authorizationRequestUrl = authorizeUrl + "?" + authorizationRequest.getQueryString(); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizeUrl); + authorizeClient.setRequest(authorizationRequest); + + System.out.println("authorizationRequestAndDenyAccess: authorizationRequestUrl:" + authorizationRequestUrl); + startSelenium(); + navigateToAuhorizationUrl(driver, authorizationRequestUrl); + + WebElement doNotAllowButton = driver.findElement(By.id(authorizeFormDoNotAllowButton)); + + final String previousURL = driver.getCurrentUrl(); + doNotAllowButton.click(); + WebDriverWait wait = new WebDriverWait(driver, 1); + wait.until((WebDriver d) -> (d.getCurrentUrl() != previousURL)); + + String authorizationResponseStr = driver.getCurrentUrl(); + + Cookie sessionStateCookie = driver.manage().getCookieNamed("session_state"); + String sessionState = null; + if (sessionStateCookie != null) { + sessionState = sessionStateCookie.getValue(); + } + System.out.println("authorizationRequestAndDenyAccess: sessionState:" + sessionState); + + stopSelenium(); + + AuthorizationResponse authorizationResponse = new AuthorizationResponse(authorizationResponseStr); + if (authorizationRequest.getRedirectUri() != null && authorizationRequest.getRedirectUri().equals(authorizationResponseStr)) { + authorizationResponse.setResponseMode(ResponseMode.FORM_POST); + } + authorizeClient.setResponse(authorizationResponse); + showClientUserAgent(authorizeClient); + + return authorizationResponse; + } + + /** + * The authorization server authenticates the resource owner (via the user-agent) + * No authorization page. + */ + public AuthorizationResponse authenticateResourceOwner( + String authorizeUrl, AuthorizationRequest authorizationRequest, String userId, String userSecret, boolean cleanupCookies) { + String authorizationRequestUrl = authorizeUrl + "?" + authorizationRequest.getQueryString(); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizeUrl); + authorizeClient.setRequest(authorizationRequest); + + System.out.println("authenticateResourceOwner: authorizationRequestUrl:" + authorizationRequestUrl); + startSelenium(); + if (cleanupCookies) { + System.out.println("authenticateResourceOwner: Cleaning cookies"); + deleteAllCookies(); + } + + navigateToAuhorizationUrl(driver, authorizationRequestUrl); + + if (userSecret != null) { + if (userId != null) { + WebElement usernameElement = driver.findElement(By.id(loginFormUsername)); + usernameElement.sendKeys(userId); + } + + WebElement passwordElement = driver.findElement(By.id(loginFormPassword)); + passwordElement.sendKeys(userSecret); + + WebElement loginButton = driver.findElement(By.id(loginFormLoginButton)); + + loginButton.click(); + + navigateToAuhorizationUrl(driver, driver.getCurrentUrl()); + + new WebDriverWait(driver, PageConfig.WAIT_OPERATION_TIMEOUT) + .until(webDriver ->!webDriver.getCurrentUrl().contains("/authorize")); + } + + String authorizationResponseStr = driver.getCurrentUrl(); + + Cookie sessionStateCookie = driver.manage().getCookieNamed("session_state"); + String sessionState = null; + if (sessionStateCookie != null) { + sessionState = sessionStateCookie.getValue(); + } + System.out.println("authenticateResourceOwner: sessionState:" + sessionState + ", url:" + authorizationResponseStr); + + stopSelenium(); + + AuthorizationResponse authorizationResponse = new AuthorizationResponse(authorizationResponseStr); + if (authorizationRequest.getRedirectUri() != null && authorizationRequest.getRedirectUri().equals(authorizationResponseStr)) { + authorizationResponse.setResponseMode(ResponseMode.FORM_POST); + } + authorizeClient.setResponse(authorizationResponse); + showClientUserAgent(authorizeClient); + + return authorizationResponse; + } + + /** + * Try to open login form (via the user-agent) + */ + public String waitForResourceOwnerAndGrantLoginForm( + String authorizeUrl, AuthorizationRequest authorizationRequest, boolean cleanupCookies) { + String authorizationRequestUrl = authorizeUrl + "?" + authorizationRequest.getQueryString(); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizeUrl); + authorizeClient.setRequest(authorizationRequest); + + System.out.println("waitForResourceOwnerAndGrantLoginForm: authorizationRequestUrl:" + authorizationRequestUrl); + startSelenium(); + if (cleanupCookies) { + System.out.println("waitForResourceOwnerAndGrantLoginForm: Cleaning cookies"); + deleteAllCookies(); + } + navigateToAuhorizationUrl(driver, authorizationRequestUrl); + + WebElement usernameElement = driver.findElement(By.id(loginFormUsername)); + WebElement passwordElement = driver.findElement(By.id(loginFormPassword)); + WebElement loginButton = driver.findElement(By.id(loginFormLoginButton)); + + if ((usernameElement == null) || (passwordElement == null) || (loginButton == null)) { + return null; + } + + Cookie sessionStateCookie = driver.manage().getCookieNamed("session_state"); + String sessionState = null; + if (sessionStateCookie != null) { + sessionState = sessionStateCookie.getValue(); + } + System.out.println("waitForResourceOwnerAndGrantLoginForm: sessionState:" + sessionState); + + stopSelenium(); + + showClientUserAgent(authorizeClient); + + return sessionState; + } + + /** + * Try to open login form (via the user-agent) + */ + public String waitForResourceOwnerAndGrantLoginForm( + String authorizeUrl, AuthorizationRequest authorizationRequest) { + return waitForResourceOwnerAndGrantLoginForm(authorizeUrl, authorizationRequest, true); + } + + private void deleteAllCookies() { + try { + driver.manage().deleteAllCookies(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @BeforeTest + public void discovery(ITestContext context) throws Exception { + // Load Form Interaction + loginFormUsername = context.getCurrentXmlTest().getParameter("loginFormUsername"); + loginFormPassword = context.getCurrentXmlTest().getParameter("loginFormPassword"); + loginFormLoginButton = context.getCurrentXmlTest().getParameter("loginFormLoginButton"); + authorizeFormAllowButton = context.getCurrentXmlTest().getParameter("authorizeFormAllowButton"); + authorizeFormDoNotAllowButton = context.getCurrentXmlTest().getParameter("authorizeFormDoNotAllowButton"); + allTestKeys = Maps.newHashMap(context.getCurrentXmlTest().getAllParameters()); + + String resource = context.getCurrentXmlTest().getParameter("swdResource"); + + if (StringUtils.isNotBlank(resource)) { + + showTitle("OpenID Connect Discovery"); + + OpenIdConnectDiscoveryClient openIdConnectDiscoveryClient = new OpenIdConnectDiscoveryClient(resource); + OpenIdConnectDiscoveryResponse openIdConnectDiscoveryResponse = openIdConnectDiscoveryClient.exec(clientEngine(true)); + + showClient(openIdConnectDiscoveryClient); + assertEquals(openIdConnectDiscoveryResponse.getStatus(), 200, "Unexpected response code"); + assertNotNull(openIdConnectDiscoveryResponse.getSubject()); + assertTrue(openIdConnectDiscoveryResponse.getLinks().size() > 0); + + configurationEndpoint = openIdConnectDiscoveryResponse.getLinks().get(0).getHref() + + "/.well-known/openid-configuration"; + + System.out.println("OpenID Connect Configuration"); + + OpenIdConfigurationClient client = new OpenIdConfigurationClient(configurationEndpoint); + client.setExecutor(clientEngine(true)); + OpenIdConfigurationResponse response = client.execOpenIdConfiguration(); + + showClient(client); + assertEquals(response.getStatus(), 200, "Unexpected response code"); + assertNotNull(response.getIssuer(), "The issuer is null"); + assertNotNull(response.getAuthorizationEndpoint(), "The authorizationEndpoint is null"); + assertNotNull(response.getTokenEndpoint(), "The tokenEndpoint is null"); + assertNotNull(response.getRevocationEndpoint(), "The revocationEndpoint is null"); + assertNotNull(response.getUserInfoEndpoint(), "The userInfoEndPoint is null"); + assertNotNull(response.getJwksUri(), "The jwksUri is null"); + assertNotNull(response.getRegistrationEndpoint(), "The registrationEndpoint is null"); + + assertTrue(response.getScopesSupported().size() > 0, "The scopesSupported is empty"); + assertTrue(response.getScopeToClaimsMapping().size() > 0, "The scope to claims mapping is empty"); + assertTrue(response.getResponseTypesSupported().size() > 0, "The responseTypesSupported is empty"); + assertTrue(response.getGrantTypesSupported().size() > 0, "The grantTypesSupported is empty"); + assertTrue(response.getAcrValuesSupported().size() >= 0, "The acrValuesSupported is empty"); + assertTrue(response.getSubjectTypesSupported().size() > 0, "The subjectTypesSupported is empty"); + assertTrue(response.getIdTokenSigningAlgValuesSupported().size() > 0, "The idTokenSigningAlgValuesSupported is empty"); + assertTrue(response.getRequestObjectSigningAlgValuesSupported().size() > 0, "The requestObjectSigningAlgValuesSupported is empty"); + assertTrue(response.getTokenEndpointAuthMethodsSupported().size() > 0, "The tokenEndpointAuthMethodsSupported is empty"); + assertTrue(response.getClaimsSupported().size() > 0, "The claimsSupported is empty"); + + authorizationEndpoint = response.getAuthorizationEndpoint(); + tokenEndpoint = response.getTokenEndpoint(); + tokenRevocationEndpoint = response.getRevocationEndpoint(); + userInfoEndpoint = response.getUserInfoEndpoint(); + clientInfoEndpoint = response.getClientInfoEndpoint(); + checkSessionIFrame = response.getCheckSessionIFrame(); + endSessionEndpoint = response.getEndSessionEndpoint(); + jwksUri = response.getJwksUri(); + registrationEndpoint = response.getRegistrationEndpoint(); + idGenEndpoint = response.getIdGenerationEndpoint(); + introspectionEndpoint = response.getIntrospectionEndpoint(); + deviceAuthzEndpoint = response.getDeviceAuthzEndpoint(); + backchannelAuthenticationEndpoint = response.getBackchannelAuthenticationEndpoint(); + revokeSessionEndpoint = response.getSessionRevocationEndpoint(); + scopeToClaimsMapping = response.getScopeToClaimsMapping(); + gluuConfigurationEndpoint = determineGluuConfigurationEndpoint(openIdConnectDiscoveryResponse.getLinks().get(0).getHref()); + issuer = response.getIssuer(); + } else { + showTitle("Loading configuration endpoints from properties file"); + + authorizationEndpoint = context.getCurrentXmlTest().getParameter("authorizationEndpoint"); + tokenEndpoint = context.getCurrentXmlTest().getParameter("tokenEndpoint"); + tokenRevocationEndpoint = context.getCurrentXmlTest().getParameter("tokenRevocationEndpoint"); + userInfoEndpoint = context.getCurrentXmlTest().getParameter("userInfoEndpoint"); + clientInfoEndpoint = context.getCurrentXmlTest().getParameter("clientInfoEndpoint"); + checkSessionIFrame = context.getCurrentXmlTest().getParameter("checkSessionIFrame"); + endSessionEndpoint = context.getCurrentXmlTest().getParameter("endSessionEndpoint"); + jwksUri = context.getCurrentXmlTest().getParameter("jwksUri"); + registrationEndpoint = context.getCurrentXmlTest().getParameter("registrationEndpoint"); + configurationEndpoint = context.getCurrentXmlTest().getParameter("configurationEndpoint"); + idGenEndpoint = context.getCurrentXmlTest().getParameter("idGenEndpoint"); + introspectionEndpoint = context.getCurrentXmlTest().getParameter("introspectionEndpoint"); + backchannelAuthenticationEndpoint = context.getCurrentXmlTest().getParameter("backchannelAuthenticationEndpoint"); + revokeSessionEndpoint = context.getCurrentXmlTest().getParameter("revokeSessionEndpoint"); + scopeToClaimsMapping = new HashMap>(); + issuer = context.getCurrentXmlTest().getParameter("issuer"); + } + + authorizationPageEndpoint = determineAuthorizationPageEndpoint(authorizationEndpoint); + } + + private String determineAuthorizationPageEndpoint(String authorizationEndpoint) { + return authorizationEndpoint.replace("/restv1/authorize.htm", "/authorize"); + } + + private String determineGluuConfigurationEndpoint(String host) { + return host + "/oxauth/restv1/gluu-configuration"; + } + + public void showTitle(String title) { + title = "TEST: " + title; + + System.out.println("#######################################################"); + System.out.println(title); + System.out.println("#######################################################"); + } + + public static void showClient(BaseClient client) { + ClientUtils.showClient(client); + } + + public static void showClient(BaseClient client, CookieStore cookieStore) { + ClientUtils.showClient(client, cookieStore); + } + + public static void showClientUserAgent(BaseClient client) { + ClientUtils.showClientUserAgent(client); + } + + public static void assertErrorResponse(BaseResponseWithErrors p_response, IErrorType p_errorType) { + assertEquals(p_response.getStatus(), 400, "Unexpected response code. Entity: " + p_response.getEntity()); + assertNotNull(p_response.getEntity(), "The entity is null"); + assertEquals(p_response.getErrorType(), p_errorType); + assertTrue(StringUtils.isNotBlank(p_response.getErrorDescription())); + } + + public static CloseableHttpClient createHttpClient() { + return createHttpClient(HostnameVerifierType.DEFAULT); + } + + public static CloseableHttpClient createHttpClient(HostnameVerifierType p_verifierType) { + if (p_verifierType != null && p_verifierType != HostnameVerifierType.DEFAULT) { + switch (p_verifierType) { + case ALLOW_ALL: + return HttpClients.custom() + .setDefaultRequestConfig(RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build()) + .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE).build(); + + } + } + + return HttpClients.custom() + .setDefaultRequestConfig(RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build()) + .build(); + } + + public static ClientHttpEngine engine() throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + return clientEngine(false); + } + + public static ClientHttpEngine engine(boolean trustAll) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + if (trustAll) { + return new ApacheHttpClient43Engine(createHttpClientTrustAll()); + } + return new ApacheHttpClient43Engine(); + } + + public static ClientHttpEngine clientEngine() throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + return clientEngine(false); + } + + public static ClientHttpEngine clientEngine(boolean trustAll) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + if (trustAll) { + return new ApacheHttpClient43Engine(createAcceptSelfSignedCertificateClient()); + } + return new ApacheHttpClient43Engine(createClient()); + } + + public static HttpClient createClient() { + return createClient(null); + } + + public static HttpClient createAcceptSelfSignedCertificateClient() + throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException { + SSLConnectionSocketFactory connectionFactory = createAcceptSelfSignedSocketFactory(); + + return createClient(connectionFactory); + } + + private static HttpClient createClient(SSLConnectionSocketFactory connectionFactory) { + PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(); + HttpClientBuilder httClientBuilder = HttpClients.custom(); + if (connectionFactory != null) { + httClientBuilder = httClientBuilder.setSSLSocketFactory(connectionFactory); + } + + HttpClient httpClient = httClientBuilder + .setDefaultRequestConfig(RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build()) + .setConnectionManager(cm).build(); + cm.setMaxTotal(200); // Increase max total connection to 200 + cm.setDefaultMaxPerRoute(20); // Increase default max connection per route to 20 + + return httpClient; + } + + private static SSLConnectionSocketFactory createAcceptSelfSignedSocketFactory() + throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException { + // Use the TrustSelfSignedStrategy to allow Self Signed Certificates + SSLContext sslContext = SSLContextBuilder.create().loadTrustMaterial(new TrustSelfSignedStrategy()).build(); + + // We can optionally disable hostname verification. + // If you don't want to further weaken the security, you don't have to include this. + HostnameVerifier allowAllHosts = new NoopHostnameVerifier(); + + // Create an SSL Socket Factory to use the SSLContext with the trust self signed certificate strategy + // and allow all hosts verifier. + SSLConnectionSocketFactory connectionFactory = new SSLConnectionSocketFactory(sslContext, allowAllHosts); + + return connectionFactory; + } + + public static CloseableHttpClient createHttpClientTrustAll() + throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(new TrustStrategy() { + @Override + public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException { + return true; + } + }).build(); + SSLConnectionSocketFactory sslContextFactory = new SSLConnectionSocketFactory(sslContext); + CloseableHttpClient httpclient = HttpClients.custom() + .setDefaultRequestConfig(RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build()) + .setSSLSocketFactory(sslContextFactory) + .setRedirectStrategy(new LaxRedirectStrategy()).build(); + + return httpclient; + } + + protected void navigateToAuhorizationUrl(WebDriver driver, String authorizationRequestUrl) { + try { + driver.navigate().to(URLDecoder.decode(authorizationRequestUrl, Util.UTF8_STRING_ENCODING)); + } catch (UnsupportedEncodingException ex) { + fail("Failed to decode the authorization URL."); + } + } + + private ClientHttpEngine getClientExecutor() throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + return clientEngine(true); + } + + protected RegisterClient newRegisterClient(RegisterRequest request) { + try { + final RegisterClient client = new RegisterClient(registrationEndpoint); + client.setRequest(request); + client.setExecutor(getClientExecutor()); + return client; + } catch (Exception e) { + throw new AssertionError("Failed to create register client"); + } + } + + protected RevokeSessionClient newRevokeSessionClient(RevokeSessionRequest request) { + try { + final RevokeSessionClient client = new RevokeSessionClient(revokeSessionEndpoint); + client.setRequest(request); + client.setExecutor(getClientExecutor()); + return client; + } catch (Exception e) { + throw new AssertionError("Failed to create register client"); + } + } + + protected UserInfoClient newUserInfoClient(UserInfoRequest request) { + try { + final UserInfoClient client = new UserInfoClient(userInfoEndpoint); + client.setRequest(request); + client.setExecutor(getClientExecutor()); + return client; + } catch (Exception e) { + throw new AssertionError("Failed to create userinfo client"); + } + } + + protected UserInfoResponse requestUserInfo(String accessToken) { + try { + final UserInfoClient client = new UserInfoClient(userInfoEndpoint); + client.setExecutor(getClientExecutor()); + final UserInfoResponse userInfoResponse = client.execUserInfo(accessToken); + showClient(client); + return userInfoResponse; + } catch (Exception e) { + throw new AssertionError("Failed to request userinfo"); + } + } + + protected AuthorizeClient newAuthorizeClient(AuthorizationRequest request) { + try { + final AuthorizeClient client = new AuthorizeClient(authorizationEndpoint); + client.setRequest(request); + client.setExecutor(getClientExecutor()); + return client; + } catch (Exception e) { + throw new AssertionError("Failed to create authorize client"); + } + } + + protected TokenClient newTokenClient(TokenRequest request) { + try { + final TokenClient client = new TokenClient(tokenEndpoint); + client.setRequest(request); + client.setExecutor(getClientExecutor()); + return client; + } catch (Exception e) { + throw new AssertionError("Failed to create token client"); + } + } + + protected PageConfig newPageConfig(WebDriver driver) { + PageConfig config = new PageConfig(driver); + config.getTestKeys().putAll(allTestKeys); + return config; + } + + public static String randomUUID() { + return UUID.randomUUID().toString(); + } + + public static void output(String str) { + System.out.println(str); // switch to logger? + } + + public static AbstractCryptoProvider createCryptoProviderWithAllowedNone() throws Exception { + return new OxAuthCryptoProvider(null, null, null, false); + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/BackchannelAuthenticationExpiredRequestsTests.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/BackchannelAuthenticationExpiredRequestsTests.java new file mode 100644 index 00000000..109e77bd --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/BackchannelAuthenticationExpiredRequestsTests.java @@ -0,0 +1,301 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ciba; + +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_TOKEN_DELIVERY_MODE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_USER_CODE_PARAMETER; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.UUID; + +import org.apache.commons.lang.RandomStringUtils; +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.BackchannelAuthenticationClient; +import org.gluu.oxauth.client.BackchannelAuthenticationRequest; +import org.gluu.oxauth.client.BackchannelAuthenticationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.model.ciba.BackchannelAuthenticationErrorResponseType; +import org.gluu.oxauth.model.common.BackchannelTokenDeliveryMode; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.crypto.signature.AsymmetricSignatureAlgorithm; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.token.TokenErrorResponseType; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Milton BO + * @version May 25, 2020 + */ +public class BackchannelAuthenticationExpiredRequestsTests extends BaseTest { + + /** + * Test poll flow when a request expires, response from the server should be expired_token and 400 status. + */ + @Parameters({"clientJwksUri", "backchannelUserCode", "userId"}) + @Test + public void backchannelTokenDeliveryModePollExpiredRequest( + final String clientJwksUri, final String backchannelUserCode, final String userId) throws InterruptedException { + showTitle("backchannelTokenDeliveryModePollExpiredRequest"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String bindingMessage = RandomStringUtils.randomAlphanumeric(6); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid", "profile", "email", "address", "phone")); + backchannelAuthenticationRequest.setLoginHint(userId); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1); + backchannelAuthenticationRequest.setAcrValues(Arrays.asList("auth_ldap_server", "basic")); + backchannelAuthenticationRequest.setBindingMessage(bindingMessage); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + + // 3. Request token - expected expiration error + + TokenResponse tokenResponse; + int pollCount = 0; + do { + Thread.sleep(3500); + + TokenRequest tokenRequest = new TokenRequest(GrantType.CIBA); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthReqId(backchannelAuthenticationResponse.getAuthReqId()); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + pollCount++; + } while (pollCount < 5 && tokenResponse.getStatus() == 400 + && tokenResponse.getErrorType() == TokenErrorResponseType.AUTHORIZATION_PENDING); + + assertEquals(tokenResponse.getStatus(), 400, "Unexpected HTTP status resposne: " + tokenResponse.getEntity()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertEquals(tokenResponse.getErrorType(), TokenErrorResponseType.EXPIRED_TOKEN, "Unexpected error type, should be expired_token."); + assertNotNull(tokenResponse.getErrorDescription()); + } + + /** + * Test ping flow when a request expires, response from the server should be expired_token and 400 status. + */ + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode", "userId"}) + @Test + public void backchannelTokenDeliveryModePingExpiredRequest( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode, + final String userId) throws InterruptedException { + showTitle("backchannelTokenDeliveryModePingExpiredRequest"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String bindingMessage = RandomStringUtils.randomAlphanumeric(6); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid", "profile", "email", "address", "phone")); + backchannelAuthenticationRequest.setLoginHint(userId); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1); + backchannelAuthenticationRequest.setAcrValues(Arrays.asList("auth_ldap_server", "basic")); + backchannelAuthenticationRequest.setBindingMessage(bindingMessage); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + + // 3. Request token - expected expiration error + + TokenResponse tokenResponse; + int pollCount = 0; + do { + Thread.sleep(3500); + + TokenRequest tokenRequest = new TokenRequest(GrantType.CIBA); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthReqId(backchannelAuthenticationResponse.getAuthReqId()); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + pollCount++; + } while (pollCount < 5 && tokenResponse.getStatus() == 400 + && tokenResponse.getErrorType() == TokenErrorResponseType.AUTHORIZATION_PENDING); + + assertEquals(tokenResponse.getStatus(), 400, "Unexpected HTTP status resposne: " + tokenResponse.getEntity()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertEquals(tokenResponse.getErrorType(), TokenErrorResponseType.EXPIRED_TOKEN, "Unexpected error type, should be expired_token."); + assertNotNull(tokenResponse.getErrorDescription()); + } + + /** + * Test big expiration times are not allowed. + */ + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode", "userId"}) + @Test + public void backchannelBigExpirationTimeAreNotAlloed( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode, + final String userId) throws InterruptedException { + showTitle("backchannelBigExpirationTimeAreNotAlloed"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String bindingMessage = RandomStringUtils.randomAlphanumeric(6); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid", "profile", "email", "address", "phone")); + backchannelAuthenticationRequest.setLoginHint(userId); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(10000000); + backchannelAuthenticationRequest.setAcrValues(Arrays.asList("auth_ldap_server", "basic")); + backchannelAuthenticationRequest.setBindingMessage(bindingMessage); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 400, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNull(backchannelAuthenticationResponse.getExpiresIn()); + assertEquals(backchannelAuthenticationResponse.getErrorType(), BackchannelAuthenticationErrorResponseType.INVALID_REQUEST); + assertNotNull(backchannelAuthenticationResponse.getErrorDescription()); + } + +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/BackchannelAuthenticationPingMode.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/BackchannelAuthenticationPingMode.java new file mode 100644 index 00000000..67b68849 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/BackchannelAuthenticationPingMode.java @@ -0,0 +1,3614 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ciba; + +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_TOKEN_DELIVERY_MODE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_USER_CODE_PARAMETER; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.security.PrivateKey; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.apache.commons.lang.RandomStringUtils; +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.BackchannelAuthenticationClient; +import org.gluu.oxauth.client.BackchannelAuthenticationRequest; +import org.gluu.oxauth.client.BackchannelAuthenticationResponse; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.ciba.BackchannelAuthenticationErrorResponseType; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.BackchannelTokenDeliveryMode; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.encryption.BlockEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.encryption.KeyEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.signature.AsymmetricSignatureAlgorithm; +import org.gluu.oxauth.model.crypto.signature.ECDSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jwe.Jwe; +import org.gluu.oxauth.model.jws.ECDSASigner; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.gluu.oxauth.model.util.Util; +import org.json.JSONObject; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Javier Rojas Blum + * @version May 28, 2020 + */ +public class BackchannelAuthenticationPingMode extends BaseTest { + + String idTokenHintRS256; + String idTokenHintRS384; + String idTokenHintRS512; + String idTokenHintES256; + String idTokenHintES384; + String idTokenHintES512; + String idTokenHintPS256; + String idTokenHintPS384; + String idTokenHintPS512; + String idTokenHintAlgA128KWEncA128GCM; + String idTokenHintAlgA256KWEncA256GCM; + String idTokenHintAlgRSA15EncA128CBCPLUSHS256; + String idTokenHintAlgRSA15EncA256CBCPLUSHS512; + String idTokenHintAlgRSAOAEPEncA256GCM; + + String loginHintTokenRS256; + String loginHintTokenRS384; + String loginHintTokenRS512; + String loginHintTokenES256; + String loginHintTokenES384; + String loginHintTokenES512; + String loginHintTokenPS256; + String loginHintTokenPS384; + String loginHintTokenPS512; + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode", "userId"}) + @Test + public void backchannelTokenDeliveryModePingLoginHint1( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode, + final String userId) throws InterruptedException { + showTitle("backchannelTokenDeliveryModePingLoginHint1"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String bindingMessage = RandomStringUtils.randomAlphanumeric(6); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid", "profile", "email", "address", "phone")); + backchannelAuthenticationRequest.setLoginHint(userId); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAcrValues(Arrays.asList("auth_ldap_server", "basic")); + backchannelAuthenticationRequest.setBindingMessage(bindingMessage); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + + String authReqId = backchannelAuthenticationResponse.getAuthReqId(); + + // 3. Token Request Using CIBA Grant Type + // Uncomment for manual testing + /* + TokenResponse tokenResponse = null; + int pollCount = 0; + do { + Thread.sleep(5000); + + TokenRequest tokenRequest = new TokenRequest(GrantType.CIBA); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthReqId(authReqId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + pollCount++; + } while (tokenResponse.getStatus() == 400 && pollCount < 5); + + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + + String accessToken = tokenResponse.getAccessToken(); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.WEBSITE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.BIRTHDATE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GENDER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PROFILE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PHONE_NUMBER_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PREFERRED_USERNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.MIDDLE_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.UPDATED_AT)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NICKNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PHONE_NUMBER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + */ + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode", "userEmail"}) + @Test + public void backchannelTokenDeliveryModePingLoginHint2( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode, + final String userEmail) { + showTitle("backchannelTokenDeliveryModePingLoginHint2"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHint(userEmail); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode", "userInum"}) + @Test + public void backchannelTokenDeliveryModePingLoginHint3( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode, + final String userInum) { + showTitle("backchannelTokenDeliveryModePingLoginHint3"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHint(userInum); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode", "userInum"}) + @Test(enabled = false) // Enable it for manual testing + public void backchannelTokenDeliveryModePingLoginHint4( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode, + final String userInum) throws Exception { + showTitle("backchannelTokenDeliveryModePingLoginHint4"); + + RegisterResponse registerResponse1 = requestClientRegistration(clientJwksUri, backchannelClientNotificationEndpoint); + RegisterResponse registerResponse2 = requestClientRegistration(clientJwksUri, backchannelClientNotificationEndpoint); + + String sub1 = requestBackchannelAuthentication(userInum, registerResponse1.getClientId(), registerResponse1.getClientSecret(), backchannelUserCode); + String sub2 = requestBackchannelAuthentication(userInum, registerResponse2.getClientId(), registerResponse2.getClientSecret(), backchannelUserCode); + + assertEquals(sub1, sub2, "Each client must share the same sub value"); + + String sub3 = requestBackchannelAuthentication(userInum, registerResponse1.getClientId(), registerResponse1.getClientSecret(), backchannelUserCode); + String sub4 = requestBackchannelAuthentication(userInum, registerResponse2.getClientId(), registerResponse2.getClientSecret(), backchannelUserCode); + + assertEquals(sub1, sub3, "Same client must receive the same sub value"); + assertEquals(sub2, sub4, "Same client must receive the same sub value"); + } + + public RegisterResponse requestClientRegistration( + final String clientJwksUri, final String backchannelClientNotificationEndpoint) { + // Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + return registerResponse; + } + + public String requestBackchannelAuthentication( + final String userInum, final String clientId, final String clientSecret, final String backchannelUserCode) throws Exception { + // Authentication Request + String bindingMessage = RandomStringUtils.randomAlphanumeric(6); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid", "profile", "email", "address", "phone")); + backchannelAuthenticationRequest.setLoginHint(userInum); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAcrValues(Arrays.asList("auth_ldap_server", "basic")); + backchannelAuthenticationRequest.setBindingMessage(bindingMessage); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + + String authReqId = backchannelAuthenticationResponse.getAuthReqId(); + + // 3. Token Request Using CIBA Grant Type + TokenResponse tokenResponse = null; + int pollCount = 0; + do { + Thread.sleep(5000); + + TokenRequest tokenRequest = new TokenRequest(GrantType.CIBA); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthReqId(authReqId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + pollCount++; + } while (tokenResponse.getStatus() == 400 && pollCount < 5); + + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + + String accessToken = tokenResponse.getAccessToken(); + String idToken = tokenResponse.getIdToken(); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.WEBSITE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.BIRTHDATE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GENDER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PROFILE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PHONE_NUMBER_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PREFERRED_USERNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.MIDDLE_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.UPDATED_AT)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NICKNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PHONE_NUMBER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + + // Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + String sub = jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER); + + return sub; + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode", + "RS256_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test(dependsOnMethods = "idTokenHintRS256") + public void backchannelTokenDeliveryModePingIdTokenHintRS256( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("backchannelTokenDeliveryModePingIdTokenHintRS256"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.RS256); + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + + // 2. Authentication Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintRS256); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + backchannelAuthenticationRequest.setAlgorithm(SignatureAlgorithm.RS256); + backchannelAuthenticationRequest.setCryptoProvider(cryptoProvider); + backchannelAuthenticationRequest.setKeyId(keyId); + backchannelAuthenticationRequest.setAudience(tokenEndpoint); + backchannelAuthenticationRequest.setAuthUsername(clientId); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode", + "RS384_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test(dependsOnMethods = "idTokenHintRS384") + public void backchannelTokenDeliveryModePingIdTokenHintRS384( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("backchannelTokenDeliveryModePingIdTokenHintRS384"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.RS384); + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS384); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS384.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + + // 2. Authentication Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintRS384); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + backchannelAuthenticationRequest.setAlgorithm(SignatureAlgorithm.RS384); + backchannelAuthenticationRequest.setCryptoProvider(cryptoProvider); + backchannelAuthenticationRequest.setKeyId(keyId); + backchannelAuthenticationRequest.setAudience(tokenEndpoint); + backchannelAuthenticationRequest.setAuthUsername(clientId); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode", + "RS512_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test(dependsOnMethods = "idTokenHintRS512") + public void backchannelTokenDeliveryModePingIdTokenHintRS512( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("backchannelTokenDeliveryModePingIdTokenHintRS512"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.RS512); + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS512); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS512.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + + // 2. Authentication Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintRS512); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + backchannelAuthenticationRequest.setAlgorithm(SignatureAlgorithm.RS512); + backchannelAuthenticationRequest.setCryptoProvider(cryptoProvider); + backchannelAuthenticationRequest.setKeyId(keyId); + backchannelAuthenticationRequest.setAudience(tokenEndpoint); + backchannelAuthenticationRequest.setAuthUsername(clientId); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode", + "ES256_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test(dependsOnMethods = "idTokenHintES256") + public void backchannelTokenDeliveryModePingIdTokenHintES256( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("backchannelTokenDeliveryModePingIdTokenHintES256"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.ES256); + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.ES256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.ES256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + + // 2. Authentication Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintES256); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + backchannelAuthenticationRequest.setAlgorithm(SignatureAlgorithm.ES256); + backchannelAuthenticationRequest.setCryptoProvider(cryptoProvider); + backchannelAuthenticationRequest.setKeyId(keyId); + backchannelAuthenticationRequest.setAudience(tokenEndpoint); + backchannelAuthenticationRequest.setAuthUsername(clientId); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode", + "ES384_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test(dependsOnMethods = "idTokenHintES384") + public void backchannelTokenDeliveryModePingIdTokenHintES384( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("backchannelTokenDeliveryModePingIdTokenHintES384"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.ES384); + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.ES384); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.ES384.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + + // 2. Authentication Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintES384); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + backchannelAuthenticationRequest.setAlgorithm(SignatureAlgorithm.ES384); + backchannelAuthenticationRequest.setCryptoProvider(cryptoProvider); + backchannelAuthenticationRequest.setKeyId(keyId); + backchannelAuthenticationRequest.setAudience(tokenEndpoint); + backchannelAuthenticationRequest.setAuthUsername(clientId); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode", + "ES512_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test(dependsOnMethods = "idTokenHintES512") + public void backchannelTokenDeliveryModePingIdTokenHintES512( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("backchannelTokenDeliveryModePingIdTokenHintES512"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.ES512); + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.ES512); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.ES512.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + + // 2. Authentication Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintES512); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + backchannelAuthenticationRequest.setAlgorithm(SignatureAlgorithm.ES512); + backchannelAuthenticationRequest.setCryptoProvider(cryptoProvider); + backchannelAuthenticationRequest.setKeyId(keyId); + backchannelAuthenticationRequest.setAudience(tokenEndpoint); + backchannelAuthenticationRequest.setAuthUsername(clientId); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode", + "PS256_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test(dependsOnMethods = "idTokenHintPS256") + public void backchannelTokenDeliveryModePingIdTokenHintPS256( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("backchannelTokenDeliveryModePingIdTokenHintPS256"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.PS256); + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.PS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.PS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + + // 2. Authentication Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintPS256); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + backchannelAuthenticationRequest.setAlgorithm(SignatureAlgorithm.PS256); + backchannelAuthenticationRequest.setCryptoProvider(cryptoProvider); + backchannelAuthenticationRequest.setKeyId(keyId); + backchannelAuthenticationRequest.setAudience(tokenEndpoint); + backchannelAuthenticationRequest.setAuthUsername(clientId); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode", + "PS384_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test(dependsOnMethods = "idTokenHintPS384") + public void backchannelTokenDeliveryModePingIdTokenHintPS384( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("backchannelTokenDeliveryModePingIdTokenHintPS384"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.PS384); + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.PS384); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.PS384.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + + // 2. Authentication Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintPS384); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + backchannelAuthenticationRequest.setAlgorithm(SignatureAlgorithm.PS384); + backchannelAuthenticationRequest.setCryptoProvider(cryptoProvider); + backchannelAuthenticationRequest.setKeyId(keyId); + backchannelAuthenticationRequest.setAudience(tokenEndpoint); + backchannelAuthenticationRequest.setAuthUsername(clientId); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode", + "PS512_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test(dependsOnMethods = "idTokenHintPS512") + public void backchannelTokenDeliveryModePingIdTokenHintPS512( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("backchannelTokenDeliveryModePingIdTokenHintPS512"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.PS512); + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.PS512); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.PS512.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + + // 2. Authentication Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintPS512); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + backchannelAuthenticationRequest.setAlgorithm(SignatureAlgorithm.PS512); + backchannelAuthenticationRequest.setCryptoProvider(cryptoProvider); + backchannelAuthenticationRequest.setKeyId(keyId); + backchannelAuthenticationRequest.setAudience(tokenEndpoint); + backchannelAuthenticationRequest.setAuthUsername(clientId); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode"}) + @Test(dependsOnMethods = "idTokenHintAlgA128KWEncA128GCM") + public void backchannelTokenDeliveryModePingIdTokenHintAlgA128KWEncA128GCM( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePingIdTokenHintAlgA128KWEncA128GCM"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintAlgA128KWEncA128GCM); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode"}) + @Test(dependsOnMethods = "idTokenHintAlgA256KWEncA256GCM") + public void backchannelTokenDeliveryModePingIdTokenHintAlgA256KWEncA256GCM( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePingIdTokenHintAlgA256KWEncA256GCM"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintAlgA256KWEncA256GCM); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode"}) + @Test(dependsOnMethods = "idTokenHintAlgRSA15EncA128CBCPLUSHS256") + public void backchannelTokenDeliveryModePingIdTokenHintAlgRSA15EncA128CBCPLUSHS256( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePingIdTokenHintAlgRSA15EncA128CBCPLUSHS256"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintAlgRSA15EncA128CBCPLUSHS256); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode"}) + @Test(dependsOnMethods = "idTokenHintAlgRSA15EncA256CBCPLUSHS512") + public void backchannelTokenDeliveryModePingIdTokenHintAlgRSA15EncA256CBCPLUSHS512( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePingIdTokenHintAlgRSA15EncA256CBCPLUSHS512"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintAlgRSA15EncA256CBCPLUSHS512); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode"}) + @Test(dependsOnMethods = "idTokenHintAlgRSAOAEPEncA256GCM") + public void backchannelTokenDeliveryModePingIdTokenHintAlgRSAOAEPEncA256GCM( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePingIdTokenHintAlgRSAOAEPEncA256GCM"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintAlgRSAOAEPEncA256GCM); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode"}) + @Test(dependsOnMethods = "loginHintTokenRS256") + public void backchannelTokenDeliveryModePingLoginHintTokenRS256( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePingLoginHintTokenRS256"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHintToken(loginHintTokenRS256); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode"}) + @Test(dependsOnMethods = "loginHintTokenRS384") + public void backchannelTokenDeliveryModePingLoginHintTokenRS384( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePingLoginHintTokenRS384"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS384); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS384.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHintToken(loginHintTokenRS384); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode"}) + @Test(dependsOnMethods = "loginHintTokenRS512") + public void backchannelTokenDeliveryModePingLoginHintTokenRS512( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePingLoginHintTokenRS512"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS512); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS512.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHintToken(loginHintTokenRS512); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode"}) + @Test(dependsOnMethods = "loginHintTokenES256") + public void backchannelTokenDeliveryModePingLoginHintTokenES256( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePingLoginHintTokenES256"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.ES256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.ES256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHintToken(loginHintTokenES256); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode"}) + @Test(dependsOnMethods = "loginHintTokenES384") + public void backchannelTokenDeliveryModePingLoginHintTokenES384( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePingLoginHintTokenES384"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.ES384); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.ES384.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHintToken(loginHintTokenES384); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode"}) + @Test(dependsOnMethods = "loginHintTokenES512") + public void backchannelTokenDeliveryModePingLoginHintTokenES512( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePingLoginHintTokenES512"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.ES512); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.ES512.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHintToken(loginHintTokenES512); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode"}) + @Test(dependsOnMethods = "loginHintTokenPS256") + public void backchannelTokenDeliveryModePingLoginHintTokenPS256( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePingLoginHintTokenPS256"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.PS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.PS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHintToken(loginHintTokenPS256); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode"}) + @Test(dependsOnMethods = "loginHintTokenPS384") + public void backchannelTokenDeliveryModePingLoginHintTokenPS384( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePingLoginHintTokenPS384"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.PS384); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.PS384.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHintToken(loginHintTokenPS384); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode"}) + @Test(dependsOnMethods = "loginHintTokenPS512") + public void backchannelTokenDeliveryModePingLoginHintTokenPS512( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePingLoginHintTokenPS512"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.PS512); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.PS512.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHintToken(loginHintTokenPS512); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint"}) + @Test + public void backchannelTokenDeliveryModePingFail1(final String clientJwksUri, + final String backchannelClientNotificationEndpoint) { + showTitle("backchannelTokenDeliveryModePingFail1"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + // 2. Authentication Request + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 400, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getEntity(), "The entity is null"); + assertNotNull(backchannelAuthenticationResponse.getErrorType(), "The error type is null"); + assertEquals(BackchannelAuthenticationErrorResponseType.INVALID_REQUEST, backchannelAuthenticationResponse.getErrorType()); + assertNotNull(backchannelAuthenticationResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint"}) + @Test + public void backchannelTokenDeliveryModePingFail2(final String clientJwksUri, + final String backchannelClientNotificationEndpoint) { + showTitle("backchannelTokenDeliveryModePingFail2"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + + // 2. Authentication Request + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword("INVALID_CLIENT_SECRET"); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 401, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getEntity(), "The entity is null"); + assertNotNull(backchannelAuthenticationResponse.getErrorType(), "The error type is null"); + assertEquals(BackchannelAuthenticationErrorResponseType.INVALID_CLIENT, backchannelAuthenticationResponse.getErrorType()); + assertNotNull(backchannelAuthenticationResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint"}) + @Test + public void backchannelTokenDeliveryModePingFail3(final String clientJwksUri, + final String backchannelClientNotificationEndpoint) { + showTitle("backchannelTokenDeliveryModePingFail3"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(null); // Invalid Scope + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 400, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getEntity(), "The entity is null"); + assertNotNull(backchannelAuthenticationResponse.getErrorType(), "The error type is null"); + assertEquals(BackchannelAuthenticationErrorResponseType.UNKNOWN_USER_ID, backchannelAuthenticationResponse.getErrorType()); + assertNotNull(backchannelAuthenticationResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint"}) + @Test + public void backchannelTokenDeliveryModePingFail4(final String clientJwksUri, + final String backchannelClientNotificationEndpoint) { + showTitle("backchannelTokenDeliveryModePingFail4"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHint(null); // Invalid login hint. + backchannelAuthenticationRequest.setLoginHintToken(null); // Invalid login hint token. + backchannelAuthenticationRequest.setIdTokenHint(null); // Invalid id token hint + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 400, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getEntity(), "The entity is null"); + assertNotNull(backchannelAuthenticationResponse.getErrorType(), "The error type is null"); + assertEquals(BackchannelAuthenticationErrorResponseType.UNKNOWN_USER_ID, backchannelAuthenticationResponse.getErrorType()); + assertNotNull(backchannelAuthenticationResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint"}) + @Test + public void backchannelTokenDeliveryModePingFail5(final String clientJwksUri, + final String backchannelClientNotificationEndpoint) { + showTitle("backchannelTokenDeliveryModePingFail5"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHint("admin"); + backchannelAuthenticationRequest.setClientNotificationToken(null); // Invalid client notification token. + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 400, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getEntity(), "The entity is null"); + assertNotNull(backchannelAuthenticationResponse.getErrorType(), "The error type is null"); + assertEquals(BackchannelAuthenticationErrorResponseType.INVALID_REQUEST, backchannelAuthenticationResponse.getErrorType()); + assertNotNull(backchannelAuthenticationResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint"}) + @Test + public void backchannelTokenDeliveryModePingFail6(final String clientJwksUri, + final String backchannelClientNotificationEndpoint) { + showTitle("backchannelTokenDeliveryModePingFail6"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHint("admin"); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(null); // Invalid user code. + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 400, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getEntity(), "The entity is null"); + assertNotNull(backchannelAuthenticationResponse.getErrorType(), "The error type is null"); + assertEquals(BackchannelAuthenticationErrorResponseType.INVALID_USER_CODE, backchannelAuthenticationResponse.getErrorType()); + assertNotNull(backchannelAuthenticationResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode", "userId"}) + @Test + public void backchannelTokenDeliveryModePingFail7( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, + final String backchannelUserCode, final String userId) throws InterruptedException { + showTitle("backchannelTokenDeliveryModePingFail7"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid", "profile", "email", "address", "phone")); + backchannelAuthenticationRequest.setLoginHint(userId); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAcrValues(Arrays.asList("auth_ldap_server", "basic")); + backchannelAuthenticationRequest.setBindingMessage("####"); // Invalid binding message + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 400, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getEntity(), "The entity is null"); + assertNotNull(backchannelAuthenticationResponse.getErrorType(), "The error type is null"); + assertEquals(BackchannelAuthenticationErrorResponseType.INVALID_BINDING_MESSAGE, backchannelAuthenticationResponse.getErrorType()); + assertNotNull(backchannelAuthenticationResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintRS256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintRS256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS256); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + idTokenHintRS256 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintRS384( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintRS384"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS384); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS384, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + idTokenHintRS384 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintRS512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintRS512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS512); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS512, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + idTokenHintRS512 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintES256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintES256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.ES256); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + ECDSAPublicKey publicKey = JwkClient.getECDSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + ECDSASigner ecdsaSigner = new ECDSASigner(SignatureAlgorithm.ES256, publicKey); + + assertTrue(ecdsaSigner.validate(jwt)); + + idTokenHintES256 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintES384( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintES384"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.ES384); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + ECDSAPublicKey publicKey = JwkClient.getECDSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + ECDSASigner ecdsaSigner = new ECDSASigner(SignatureAlgorithm.ES384, publicKey); + + assertTrue(ecdsaSigner.validate(jwt)); + + idTokenHintES384 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintES512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintES512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.ES512); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + ECDSAPublicKey publicKey = JwkClient.getECDSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + ECDSASigner ecdsaSigner = new ECDSASigner(SignatureAlgorithm.ES512, publicKey); + + assertTrue(ecdsaSigner.validate(jwt)); + + idTokenHintES512 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintPS256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintPS256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.PS256); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.PS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + idTokenHintPS256 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintPS384( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintPS384"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.PS384); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.PS384, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + idTokenHintPS384 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintPS512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintPS512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.PS512); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.PS512, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + idTokenHintPS512 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintAlgA128KWEncA128GCM( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintAlgA128KWEncA128GCM"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.A128KW); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A128GCM); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwe jwe = Jwe.parse(idToken, null, clientSecret.getBytes(Util.UTF8_STRING_ENCODING)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + idTokenHintAlgA128KWEncA128GCM = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintAlgA256KWEncA256GCM( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintAlgA256KWEncA256GCM"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.A256KW); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A256GCM); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwe jwe = Jwe.parse(idToken, null, clientSecret.getBytes(Util.UTF8_STRING_ENCODING)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + idTokenHintAlgA256KWEncA256GCM = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri", + "clientJwksUri", "RSA1_5_keyId", "keyStoreFile", "keyStoreSecret"}) + @Test + public void idTokenHintAlgRSA15EncA128CBCPLUSHS256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri, final String clientJwksUri, final String keyId, final String keyStoreFile, + final String keyStoreSecret) throws Exception { + showTitle("idTokenHintAlgRSA15EncA128CBCPLUSHS256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A128CBC_PLUS_HS256); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, null); + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + + Jwe jwe = Jwe.parse(idToken, privateKey, null); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + idTokenHintAlgRSA15EncA128CBCPLUSHS256 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri", + "clientJwksUri", "RSA1_5_keyId", "keyStoreFile", "keyStoreSecret"}) + @Test + public void idTokenHintAlgRSA15EncA256CBCPLUSHS512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri, final String clientJwksUri, final String keyId, final String keyStoreFile, + final String keyStoreSecret) throws Exception { + showTitle("idTokenHintAlgRSA15EncA256CBCPLUSHS512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A256CBC_PLUS_HS512); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, null); + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + + Jwe jwe = Jwe.parse(idToken, privateKey, null); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + idTokenHintAlgRSA15EncA256CBCPLUSHS512 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri", + "clientJwksUri", "RSA_OAEP_keyId", "keyStoreFile", "keyStoreSecret"}) + @Test + public void idTokenHintAlgRSAOAEPEncA256GCM( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri, final String clientJwksUri, final String keyId, final String keyStoreFile, + final String keyStoreSecret) throws Exception { + showTitle("idTokenHintAlgRSAOAEPEncA256GCM"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA_OAEP); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A256GCM); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, null); + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + + Jwe jwe = Jwe.parse(idToken, privateKey, null); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + idTokenHintAlgRSAOAEPEncA256GCM = idToken; + } + + @Parameters({"RS256_keyId", "userEmail", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void loginHintTokenRS256( + final String keyId, final String userEmail, + final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("loginHintTokenRS256"); + + JSONObject subjectValue = new JSONObject(); + subjectValue.put("subject_type", "email"); + subjectValue.put("email", userEmail); + + Jwt jwt = new Jwt(); + jwt.getHeader().setAlgorithm(SignatureAlgorithm.RS256); + jwt.getHeader().setKeyId(keyId); + jwt.getClaims().setClaim("subject", subjectValue); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(jwt.getSigningInput(), keyId, null, SignatureAlgorithm.RS256); + jwt.setEncodedSignature(encodedSignature); + + loginHintTokenRS256 = jwt.toString(); + } + + @Parameters({"RS384_keyId", "userEmail", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void loginHintTokenRS384( + final String keyId, final String userEmail, + final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("loginHintTokenRS384"); + + JSONObject subjectValue = new JSONObject(); + subjectValue.put("subject_type", "email"); + subjectValue.put("email", userEmail); + + Jwt jwt = new Jwt(); + jwt.getHeader().setAlgorithm(SignatureAlgorithm.RS384); + jwt.getHeader().setKeyId(keyId); + jwt.getClaims().setClaim("subject", subjectValue); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(jwt.getSigningInput(), keyId, null, SignatureAlgorithm.RS384); + jwt.setEncodedSignature(encodedSignature); + + loginHintTokenRS384 = jwt.toString(); + } + + @Parameters({"RS512_keyId", "userEmail", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void loginHintTokenRS512( + final String keyId, final String userEmail, + final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("loginHintTokenRS512"); + + JSONObject subjectValue = new JSONObject(); + subjectValue.put("subject_type", "email"); + subjectValue.put("email", userEmail); + + Jwt jwt = new Jwt(); + jwt.getHeader().setAlgorithm(SignatureAlgorithm.RS512); + jwt.getHeader().setKeyId(keyId); + jwt.getClaims().setClaim("subject", subjectValue); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(jwt.getSigningInput(), keyId, null, SignatureAlgorithm.RS512); + jwt.setEncodedSignature(encodedSignature); + + loginHintTokenRS512 = jwt.toString(); + } + + @Parameters({"ES256_keyId", "userEmail", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void loginHintTokenES256( + final String keyId, final String userEmail, + final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("loginHintTokenES256"); + + JSONObject subjectValue = new JSONObject(); + subjectValue.put("subject_type", "email"); + subjectValue.put("email", userEmail); + + Jwt jwt = new Jwt(); + jwt.getHeader().setAlgorithm(SignatureAlgorithm.ES256); + jwt.getHeader().setKeyId(keyId); + jwt.getClaims().setClaim("subject", subjectValue); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(jwt.getSigningInput(), keyId, null, SignatureAlgorithm.ES256); + jwt.setEncodedSignature(encodedSignature); + + loginHintTokenES256 = jwt.toString(); + } + + @Parameters({"ES384_keyId", "userEmail", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void loginHintTokenES384( + final String keyId, final String userEmail, + final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("loginHintTokenES384"); + + JSONObject subjectValue = new JSONObject(); + subjectValue.put("subject_type", "email"); + subjectValue.put("email", userEmail); + + Jwt jwt = new Jwt(); + jwt.getHeader().setAlgorithm(SignatureAlgorithm.ES384); + jwt.getHeader().setKeyId(keyId); + jwt.getClaims().setClaim("subject", subjectValue); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(jwt.getSigningInput(), keyId, null, SignatureAlgorithm.ES384); + jwt.setEncodedSignature(encodedSignature); + + loginHintTokenES384 = jwt.toString(); + } + + @Parameters({"ES512_keyId", "userEmail", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void loginHintTokenES512( + final String keyId, final String userEmail, + final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("loginHintTokenES512"); + + JSONObject subjectValue = new JSONObject(); + subjectValue.put("subject_type", "email"); + subjectValue.put("email", userEmail); + + Jwt jwt = new Jwt(); + jwt.getHeader().setAlgorithm(SignatureAlgorithm.ES512); + jwt.getHeader().setKeyId(keyId); + jwt.getClaims().setClaim("subject", subjectValue); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(jwt.getSigningInput(), keyId, null, SignatureAlgorithm.ES512); + jwt.setEncodedSignature(encodedSignature); + + loginHintTokenES512 = jwt.toString(); + } + + @Parameters({"PS256_keyId", "userEmail", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void loginHintTokenPS256( + final String keyId, final String userEmail, + final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("loginHintTokenPS256"); + + JSONObject subjectValue = new JSONObject(); + subjectValue.put("subject_type", "email"); + subjectValue.put("email", userEmail); + + Jwt jwt = new Jwt(); + jwt.getHeader().setAlgorithm(SignatureAlgorithm.PS256); + jwt.getHeader().setKeyId(keyId); + jwt.getClaims().setClaim("subject", subjectValue); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(jwt.getSigningInput(), keyId, null, SignatureAlgorithm.PS256); + jwt.setEncodedSignature(encodedSignature); + + loginHintTokenPS256 = jwt.toString(); + } + + @Parameters({"PS384_keyId", "userEmail", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void loginHintTokenPS384( + final String keyId, final String userEmail, + final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("loginHintTokenPS384"); + + JSONObject subjectValue = new JSONObject(); + subjectValue.put("subject_type", "email"); + subjectValue.put("email", userEmail); + + Jwt jwt = new Jwt(); + jwt.getHeader().setAlgorithm(SignatureAlgorithm.PS384); + jwt.getHeader().setKeyId(keyId); + jwt.getClaims().setClaim("subject", subjectValue); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(jwt.getSigningInput(), keyId, null, SignatureAlgorithm.PS384); + jwt.setEncodedSignature(encodedSignature); + + loginHintTokenPS384 = jwt.toString(); + } + + @Parameters({"PS512_keyId", "userEmail", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void loginHintTokenPS512( + final String keyId, final String userEmail, + final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("loginHintTokenPS512"); + + JSONObject subjectValue = new JSONObject(); + subjectValue.put("subject_type", "email"); + subjectValue.put("email", userEmail); + + Jwt jwt = new Jwt(); + jwt.getHeader().setAlgorithm(SignatureAlgorithm.PS512); + jwt.getHeader().setKeyId(keyId); + jwt.getClaims().setClaim("subject", subjectValue); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(jwt.getSigningInput(), keyId, null, SignatureAlgorithm.PS512); + jwt.setEncodedSignature(encodedSignature); + + loginHintTokenPS512 = jwt.toString(); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/BackchannelAuthenticationPollMode.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/BackchannelAuthenticationPollMode.java new file mode 100644 index 00000000..68fbc296 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/BackchannelAuthenticationPollMode.java @@ -0,0 +1,3534 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ciba; + +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_TOKEN_DELIVERY_MODE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_USER_CODE_PARAMETER; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.security.PrivateKey; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.apache.commons.lang.RandomStringUtils; +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.BackchannelAuthenticationClient; +import org.gluu.oxauth.client.BackchannelAuthenticationRequest; +import org.gluu.oxauth.client.BackchannelAuthenticationResponse; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.ciba.BackchannelAuthenticationErrorResponseType; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.BackchannelTokenDeliveryMode; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.encryption.BlockEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.encryption.KeyEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.signature.AsymmetricSignatureAlgorithm; +import org.gluu.oxauth.model.crypto.signature.ECDSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jwe.Jwe; +import org.gluu.oxauth.model.jws.ECDSASigner; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.gluu.oxauth.model.util.Util; +import org.json.JSONObject; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Javier Rojas Blum + * @version May 28, 2020 + */ +public class BackchannelAuthenticationPollMode extends BaseTest { + + String idTokenHintRS256; + String idTokenHintRS384; + String idTokenHintRS512; + String idTokenHintES256; + String idTokenHintES384; + String idTokenHintES512; + String idTokenHintPS256; + String idTokenHintPS384; + String idTokenHintPS512; + String idTokenHintAlgA128KWEncA128GCM; + String idTokenHintAlgA256KWEncA256GCM; + String idTokenHintAlgRSA15EncA128CBCPLUSHS256; + String idTokenHintAlgRSA15EncA256CBCPLUSHS512; + String idTokenHintAlgRSAOAEPEncA256GCM; + + String loginHintTokenRS256; + String loginHintTokenRS384; + String loginHintTokenRS512; + String loginHintTokenES256; + String loginHintTokenES384; + String loginHintTokenES512; + String loginHintTokenPS256; + String loginHintTokenPS384; + String loginHintTokenPS512; + + @Parameters({"clientJwksUri", "userId", "backchannelUserCode"}) + @Test + public void backchannelTokenDeliveryModePollLoginHint1( + final String clientJwksUri, final String userId, final String backchannelUserCode) throws InterruptedException { + showTitle("backchannelTokenDeliveryModePollLoginHint1"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String bindingMessage = RandomStringUtils.randomAlphanumeric(6); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid", "profile", "email", "address", "phone")); + backchannelAuthenticationRequest.setLoginHint(userId); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAcrValues(Arrays.asList("auth_ldap_server", "basic")); + backchannelAuthenticationRequest.setBindingMessage(bindingMessage); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + + String authReqId = backchannelAuthenticationResponse.getAuthReqId(); + + // 3. Token Request Using CIBA Grant Type + // Uncomment for manual testing + /* + TokenResponse tokenResponse = null; + int pollCount = 0; + do { + Thread.sleep(5000); + + TokenRequest tokenRequest = new TokenRequest(GrantType.CIBA); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthReqId(authReqId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + pollCount++; + } while (tokenResponse.getStatus() == 400 && pollCount < 5); + + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + + String accessToken = tokenResponse.getAccessToken(); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.WEBSITE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.BIRTHDATE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GENDER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PROFILE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PHONE_NUMBER_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PREFERRED_USERNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.MIDDLE_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.UPDATED_AT)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NICKNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PHONE_NUMBER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + */ + } + + @Parameters({"clientJwksUri", "userEmail", "backchannelUserCode"}) + @Test + public void backchannelTokenDeliveryModePollLoginHint2( + final String clientJwksUri, final String userEmail, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePollLoginHint2"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHint(userEmail); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "userInum", "backchannelUserCode"}) + @Test + public void backchannelTokenDeliveryModePollLoginHint3( + final String clientJwksUri, final String userInum, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePollLoginHint3"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHint(userInum); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "userId", "backchannelUserCode"}) + @Test(enabled = false) // Enable it for manual testing + public void backchannelTokenDeliveryModePollLoginHint4( + final String clientJwksUri, final String userId, final String backchannelUserCode) throws Exception { + showTitle("backchannelTokenDeliveryModePollLoginHint4"); + + RegisterResponse registerResponse1 = requestClientRegistration(clientJwksUri); + RegisterResponse registerResponse2 = requestClientRegistration(clientJwksUri); + + String sub1 = requestBackchannelAuthentication(userId, registerResponse1.getClientId(), registerResponse1.getClientSecret(), backchannelUserCode); + String sub2 = requestBackchannelAuthentication(userId, registerResponse2.getClientId(), registerResponse2.getClientSecret(), backchannelUserCode); + + assertEquals(sub1, sub2, "Each client must share the same sub value"); + + String sub3 = requestBackchannelAuthentication(userId, registerResponse1.getClientId(), registerResponse1.getClientSecret(), backchannelUserCode); + String sub4 = requestBackchannelAuthentication(userId, registerResponse2.getClientId(), registerResponse2.getClientSecret(), backchannelUserCode); + + assertEquals(sub1, sub3, "Same client must receive the same sub value"); + assertEquals(sub2, sub4, "Same client must receive the same sub value"); + } + + public RegisterResponse requestClientRegistration( + final String clientJwksUri) { + // Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + return registerResponse; + } + + public String requestBackchannelAuthentication( + final String userId, final String clientId, final String clientSecret, final String backchannelUserCode) throws Exception { + // Authentication Request + String bindingMessage = RandomStringUtils.randomAlphanumeric(6); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid", "profile", "email", "address", "phone")); + backchannelAuthenticationRequest.setLoginHint(userId); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAcrValues(Arrays.asList("auth_ldap_server", "basic")); + backchannelAuthenticationRequest.setBindingMessage(bindingMessage); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + + String authReqId = backchannelAuthenticationResponse.getAuthReqId(); + + // Token Request Using CIBA Grant Type + TokenResponse tokenResponse = null; + int pollCount = 0; + do { + Thread.sleep(5000); + + TokenRequest tokenRequest = new TokenRequest(GrantType.CIBA); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthReqId(authReqId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + pollCount++; + } while (tokenResponse.getStatus() == 400 && pollCount < 5); + + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + + String accessToken = tokenResponse.getAccessToken(); + String idToken = tokenResponse.getIdToken(); + + // Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.WEBSITE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.BIRTHDATE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GENDER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PROFILE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PHONE_NUMBER_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PREFERRED_USERNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.MIDDLE_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.UPDATED_AT)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NICKNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PHONE_NUMBER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + + // Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + String sub = jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER); + + return sub; + } + + @Parameters({"clientJwksUri", "backchannelUserCode", + "RS256_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test(dependsOnMethods = "idTokenHintRS256") + public void backchannelTokenDeliveryModePollIdTokenHintRS256( + final String clientJwksUri, final String backchannelUserCode, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("backchannelTokenDeliveryModePollIdTokenHintRS256"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.RS256); + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + + // 2. Authentication Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintRS256); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + backchannelAuthenticationRequest.setAlgorithm(SignatureAlgorithm.RS256); + backchannelAuthenticationRequest.setCryptoProvider(cryptoProvider); + backchannelAuthenticationRequest.setKeyId(keyId); + backchannelAuthenticationRequest.setAudience(tokenEndpoint); + backchannelAuthenticationRequest.setAuthUsername(clientId); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelUserCode", + "RS384_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test(dependsOnMethods = "idTokenHintRS384") + public void backchannelTokenDeliveryModePollIdTokenHintRS384( + final String clientJwksUri, final String backchannelUserCode, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("backchannelTokenDeliveryModePollIdTokenHintRS384"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.RS384); + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS384); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS384.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + + // 2. Authentication Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintRS384); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + backchannelAuthenticationRequest.setAlgorithm(SignatureAlgorithm.RS384); + backchannelAuthenticationRequest.setCryptoProvider(cryptoProvider); + backchannelAuthenticationRequest.setKeyId(keyId); + backchannelAuthenticationRequest.setAudience(tokenEndpoint); + backchannelAuthenticationRequest.setAuthUsername(clientId); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelUserCode", + "RS512_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test(dependsOnMethods = "idTokenHintRS512") + public void backchannelTokenDeliveryModePollIdTokenHintRS512( + final String clientJwksUri, final String backchannelUserCode, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("backchannelTokenDeliveryModePollIdTokenHintRS512"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.RS512); + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS512); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS512.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + + // 2. Authentication Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintRS512); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + backchannelAuthenticationRequest.setAlgorithm(SignatureAlgorithm.RS512); + backchannelAuthenticationRequest.setCryptoProvider(cryptoProvider); + backchannelAuthenticationRequest.setKeyId(keyId); + backchannelAuthenticationRequest.setAudience(tokenEndpoint); + backchannelAuthenticationRequest.setAuthUsername(clientId); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelUserCode", + "ES256_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test(dependsOnMethods = "idTokenHintES256") + public void backchannelTokenDeliveryModePollIdTokenHintES256( + final String clientJwksUri, final String backchannelUserCode, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("backchannelTokenDeliveryModePollIdTokenHintES256"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.ES256); + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.ES256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.ES256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + + // 2. Authentication Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintES256); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + backchannelAuthenticationRequest.setAlgorithm(SignatureAlgorithm.ES256); + backchannelAuthenticationRequest.setCryptoProvider(cryptoProvider); + backchannelAuthenticationRequest.setKeyId(keyId); + backchannelAuthenticationRequest.setAudience(tokenEndpoint); + backchannelAuthenticationRequest.setAuthUsername(clientId); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelUserCode", + "ES384_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test(dependsOnMethods = "idTokenHintES384") + public void backchannelTokenDeliveryModePollIdTokenHintES384( + final String clientJwksUri, final String backchannelUserCode, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("backchannelTokenDeliveryModePollIdTokenHintES384"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.ES384); + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.ES384); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.ES384.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + + // 2. Authentication Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintES384); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + backchannelAuthenticationRequest.setAlgorithm(SignatureAlgorithm.ES384); + backchannelAuthenticationRequest.setCryptoProvider(cryptoProvider); + backchannelAuthenticationRequest.setKeyId(keyId); + backchannelAuthenticationRequest.setAudience(tokenEndpoint); + backchannelAuthenticationRequest.setAuthUsername(clientId); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelUserCode", + "ES512_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test(dependsOnMethods = "idTokenHintES512") + public void backchannelTokenDeliveryModePollIdTokenHintES512( + final String clientJwksUri, final String backchannelUserCode, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("backchannelTokenDeliveryModePollIdTokenHintES512"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.ES512); + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.ES512); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.ES512.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + + // 2. Authentication Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintES512); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + backchannelAuthenticationRequest.setAlgorithm(SignatureAlgorithm.ES512); + backchannelAuthenticationRequest.setCryptoProvider(cryptoProvider); + backchannelAuthenticationRequest.setKeyId(keyId); + backchannelAuthenticationRequest.setAudience(tokenEndpoint); + backchannelAuthenticationRequest.setAuthUsername(clientId); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelUserCode", + "PS256_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test(dependsOnMethods = "idTokenHintPS256") + public void backchannelTokenDeliveryModePollIdTokenHintPS256( + final String clientJwksUri, final String backchannelUserCode, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("backchannelTokenDeliveryModePollIdTokenHintPS256"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.PS256); + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.PS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.PS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + + // 2. Authentication Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintPS256); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + backchannelAuthenticationRequest.setAlgorithm(SignatureAlgorithm.PS256); + backchannelAuthenticationRequest.setCryptoProvider(cryptoProvider); + backchannelAuthenticationRequest.setKeyId(keyId); + backchannelAuthenticationRequest.setAudience(tokenEndpoint); + backchannelAuthenticationRequest.setAuthUsername(clientId); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelUserCode", + "PS384_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test(dependsOnMethods = "idTokenHintPS384") + public void backchannelTokenDeliveryModePollIdTokenHintPS384( + final String clientJwksUri, final String backchannelUserCode, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("backchannelTokenDeliveryModePollIdTokenHintPS384"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.PS384); + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.PS384); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.PS384.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + + // 2. Authentication Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintPS384); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + backchannelAuthenticationRequest.setAlgorithm(SignatureAlgorithm.PS384); + backchannelAuthenticationRequest.setCryptoProvider(cryptoProvider); + backchannelAuthenticationRequest.setKeyId(keyId); + backchannelAuthenticationRequest.setAudience(tokenEndpoint); + backchannelAuthenticationRequest.setAuthUsername(clientId); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelUserCode", + "PS512_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test(dependsOnMethods = "idTokenHintPS512") + public void backchannelTokenDeliveryModePollIdTokenHintPS512( + final String clientJwksUri, final String backchannelUserCode, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("backchannelTokenDeliveryModePollIdTokenHintPS512"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.PS512); + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.PS512); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.PS512.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + + // 2. Authentication Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintPS512); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + backchannelAuthenticationRequest.setAlgorithm(SignatureAlgorithm.PS512); + backchannelAuthenticationRequest.setCryptoProvider(cryptoProvider); + backchannelAuthenticationRequest.setKeyId(keyId); + backchannelAuthenticationRequest.setAudience(tokenEndpoint); + backchannelAuthenticationRequest.setAuthUsername(clientId); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelUserCode"}) + @Test(dependsOnMethods = "idTokenHintAlgA128KWEncA128GCM") + public void backchannelTokenDeliveryModePollIdTokenHintAlgA128KWEncA128GCM( + final String clientJwksUri, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePollIdTokenHintAlgA128KWEncA128GCM"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintAlgA128KWEncA128GCM); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelUserCode"}) + @Test(dependsOnMethods = "idTokenHintAlgA256KWEncA256GCM") + public void backchannelTokenDeliveryModePollIdTokenHintAlgA256KWEncA256GCM( + final String clientJwksUri, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePollIdTokenHintAlgA256KWEncA256GCM"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintAlgA256KWEncA256GCM); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelUserCode"}) + @Test(dependsOnMethods = "idTokenHintAlgRSA15EncA128CBCPLUSHS256") + public void backchannelTokenDeliveryModePollIdTokenHintAlgRSA15EncA128CBCPLUSHS256( + final String clientJwksUri, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePollIdTokenHintAlgRSA15EncA128CBCPLUSHS256"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintAlgRSA15EncA128CBCPLUSHS256); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelUserCode"}) + @Test(dependsOnMethods = "idTokenHintAlgRSA15EncA256CBCPLUSHS512") + public void backchannelTokenDeliveryModePollIdTokenHintAlgRSA15EncA256CBCPLUSHS512( + final String clientJwksUri, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePollIdTokenHintAlgRSA15EncA256CBCPLUSHS512"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintAlgRSA15EncA256CBCPLUSHS512); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelUserCode"}) + @Test(dependsOnMethods = "idTokenHintAlgRSAOAEPEncA256GCM") + public void backchannelTokenDeliveryModePollIdTokenHintAlgRSAOAEPEncA256GCM( + final String clientJwksUri, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePollIdTokenHintAlgRSAOAEPEncA256GCM"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintAlgRSAOAEPEncA256GCM); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelUserCode"}) + @Test(dependsOnMethods = "loginHintTokenRS256") + public void backchannelTokenDeliveryModePollLoginHintTokenRS256( + final String clientJwksUri, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePollLoginHintTokenRS256"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHintToken(loginHintTokenRS256); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelUserCode"}) + @Test(dependsOnMethods = "loginHintTokenRS384") + public void backchannelTokenDeliveryModePollLoginHintTokenRS384( + final String clientJwksUri, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePollLoginHintTokenRS384"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS384); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS384.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHintToken(loginHintTokenRS384); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelUserCode"}) + @Test(dependsOnMethods = "loginHintTokenRS512") + public void backchannelTokenDeliveryModePollLoginHintTokenRS512( + final String clientJwksUri, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePollLoginHintTokenRS512"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS512); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS512.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHintToken(loginHintTokenRS512); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelUserCode"}) + @Test(dependsOnMethods = "loginHintTokenES256") + public void backchannelTokenDeliveryModePollLoginHintTokenES256( + final String clientJwksUri, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePollLoginHintTokenES256"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.ES256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.ES256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHintToken(loginHintTokenES256); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelUserCode"}) + @Test(dependsOnMethods = "loginHintTokenES384") + public void backchannelTokenDeliveryModePollLoginHintTokenES384( + final String clientJwksUri, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePollLoginHintTokenES384"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.ES384); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.ES384.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHintToken(loginHintTokenES384); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelUserCode"}) + @Test(dependsOnMethods = "loginHintTokenES512") + public void backchannelTokenDeliveryModePollLoginHintTokenES512( + final String clientJwksUri, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePollLoginHintTokenES512"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.ES512); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.ES512.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHintToken(loginHintTokenES512); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelUserCode"}) + @Test(dependsOnMethods = "loginHintTokenPS256") + public void backchannelTokenDeliveryModePollLoginHintTokenPS256( + final String clientJwksUri, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePollLoginHintTokenPS256"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.PS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.PS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHintToken(loginHintTokenPS256); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelUserCode"}) + @Test(dependsOnMethods = "loginHintTokenPS384") + public void backchannelTokenDeliveryModePollLoginHintTokenPS384( + final String clientJwksUri, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePollLoginHintTokenPS384"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.PS384); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.PS384.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHintToken(loginHintTokenPS384); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelUserCode"}) + @Test(dependsOnMethods = "loginHintTokenPS512") + public void backchannelTokenDeliveryModePollLoginHintTokenPS512( + final String clientJwksUri, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePollLoginHintTokenPS512"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.PS512); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.PS512.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHintToken(loginHintTokenPS512); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri"}) + @Test + public void backchannelTokenDeliveryModePollFail1(final String clientJwksUri) { + showTitle("backchannelTokenDeliveryModePollFail1"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + // 2. Authentication Request + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 400, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getEntity(), "The entity is null"); + assertNotNull(backchannelAuthenticationResponse.getErrorType(), "The error type is null"); + assertEquals(BackchannelAuthenticationErrorResponseType.INVALID_REQUEST, backchannelAuthenticationResponse.getErrorType()); + assertNotNull(backchannelAuthenticationResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"clientJwksUri"}) + @Test + public void backchannelTokenDeliveryModePollFail2(final String clientJwksUri) { + showTitle("backchannelTokenDeliveryModePollFail2"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + + // 2. Authentication Request + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword("INVALID_CLIENT_SECRET"); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 401, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getEntity(), "The entity is null"); + assertNotNull(backchannelAuthenticationResponse.getErrorType(), "The error type is null"); + assertEquals(BackchannelAuthenticationErrorResponseType.INVALID_CLIENT, backchannelAuthenticationResponse.getErrorType()); + assertNotNull(backchannelAuthenticationResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"clientJwksUri"}) + @Test + public void backchannelTokenDeliveryModePollFail3(final String clientJwksUri) { + showTitle("backchannelTokenDeliveryModePollFail3"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(null); // Invalid Scope + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 400, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getEntity(), "The entity is null"); + assertNotNull(backchannelAuthenticationResponse.getErrorType(), "The error type is null"); + assertEquals(BackchannelAuthenticationErrorResponseType.UNKNOWN_USER_ID, backchannelAuthenticationResponse.getErrorType()); + assertNotNull(backchannelAuthenticationResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"clientJwksUri"}) + @Test + public void backchannelTokenDeliveryModePollFail4(final String clientJwksUri) { + showTitle("backchannelTokenDeliveryModePollFail4"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHint(null); // Invalid login hint. + backchannelAuthenticationRequest.setLoginHintToken(null); // Invalid login hint token. + backchannelAuthenticationRequest.setIdTokenHint(null); // Invalid id token hint + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 400, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getEntity(), "The entity is null"); + assertNotNull(backchannelAuthenticationResponse.getErrorType(), "The error type is null"); + assertEquals(BackchannelAuthenticationErrorResponseType.UNKNOWN_USER_ID, backchannelAuthenticationResponse.getErrorType()); + assertNotNull(backchannelAuthenticationResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"clientJwksUri"}) + @Test + public void backchannelTokenDeliveryModePollFail5(final String clientJwksUri) { + showTitle("backchannelTokenDeliveryModePollFail5"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHint("admin"); + backchannelAuthenticationRequest.setClientNotificationToken(null); // Invalid client notification token. + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 400, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getEntity(), "The entity is null"); + assertNotNull(backchannelAuthenticationResponse.getErrorType(), "The error type is null"); + assertEquals(BackchannelAuthenticationErrorResponseType.INVALID_USER_CODE, backchannelAuthenticationResponse.getErrorType()); + assertNotNull(backchannelAuthenticationResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"clientJwksUri"}) + @Test + public void backchannelTokenDeliveryModePollFail6(final String clientJwksUri) { + showTitle("backchannelTokenDeliveryModePollFail6"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHint("admin"); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(null); // Invalid user code. + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 400, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getEntity(), "The entity is null"); + assertNotNull(backchannelAuthenticationResponse.getErrorType(), "The error type is null"); + assertEquals(BackchannelAuthenticationErrorResponseType.INVALID_USER_CODE, backchannelAuthenticationResponse.getErrorType()); + assertNotNull(backchannelAuthenticationResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"clientJwksUri", "userId", "backchannelUserCode"}) + @Test + public void backchannelTokenDeliveryModePollFail7( + final String clientJwksUri, final String userId, final String backchannelUserCode) throws InterruptedException { + showTitle("backchannelTokenDeliveryModePollFail7"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid", "profile", "email", "address", "phone")); + backchannelAuthenticationRequest.setLoginHint(userId); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAcrValues(Arrays.asList("auth_ldap_server", "basic")); + backchannelAuthenticationRequest.setBindingMessage("####"); // Invalid binding message + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 400, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getEntity(), "The entity is null"); + assertNotNull(backchannelAuthenticationResponse.getErrorType(), "The error type is null"); + assertEquals(BackchannelAuthenticationErrorResponseType.INVALID_BINDING_MESSAGE, backchannelAuthenticationResponse.getErrorType()); + assertNotNull(backchannelAuthenticationResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintRS256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintRS256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS256); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + idTokenHintRS256 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintRS384( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintRS384"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS384); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS384, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + idTokenHintRS384 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintRS512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintRS512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS512); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS512, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + idTokenHintRS512 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintES256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintES256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.ES256); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + ECDSAPublicKey publicKey = JwkClient.getECDSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + ECDSASigner ecdsaSigner = new ECDSASigner(SignatureAlgorithm.ES256, publicKey); + + assertTrue(ecdsaSigner.validate(jwt)); + + idTokenHintES256 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintES384( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintES384"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.ES384); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + ECDSAPublicKey publicKey = JwkClient.getECDSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + ECDSASigner ecdsaSigner = new ECDSASigner(SignatureAlgorithm.ES384, publicKey); + + assertTrue(ecdsaSigner.validate(jwt)); + + idTokenHintES384 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintES512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintES512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.ES512); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + ECDSAPublicKey publicKey = JwkClient.getECDSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + ECDSASigner ecdsaSigner = new ECDSASigner(SignatureAlgorithm.ES512, publicKey); + + assertTrue(ecdsaSigner.validate(jwt)); + + idTokenHintES512 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintPS256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintPS256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.PS256); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.PS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + idTokenHintPS256 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintPS384( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintPS384"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.PS384); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.PS384, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + idTokenHintPS384 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintPS512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintPS512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.PS512); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.PS512, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + idTokenHintPS512 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintAlgA128KWEncA128GCM( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintAlgA128KWEncA128GCM"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.A128KW); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A128GCM); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwe jwe = Jwe.parse(idToken, null, clientSecret.getBytes(Util.UTF8_STRING_ENCODING)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + idTokenHintAlgA128KWEncA128GCM = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintAlgA256KWEncA256GCM( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintAlgA256KWEncA256GCM"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.A256KW); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A256GCM); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwe jwe = Jwe.parse(idToken, null, clientSecret.getBytes(Util.UTF8_STRING_ENCODING)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + idTokenHintAlgA256KWEncA256GCM = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri", + "clientJwksUri", "RSA1_5_keyId", "keyStoreFile", "keyStoreSecret"}) + @Test + public void idTokenHintAlgRSA15EncA128CBCPLUSHS256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri, final String clientJwksUri, final String keyId, final String keyStoreFile, + final String keyStoreSecret) throws Exception { + showTitle("idTokenHintAlgRSA15EncA128CBCPLUSHS256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A128CBC_PLUS_HS256); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, null); + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + + Jwe jwe = Jwe.parse(idToken, privateKey, null); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + idTokenHintAlgRSA15EncA128CBCPLUSHS256 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri", + "clientJwksUri", "RSA1_5_keyId", "keyStoreFile", "keyStoreSecret"}) + @Test + public void idTokenHintAlgRSA15EncA256CBCPLUSHS512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri, final String clientJwksUri, final String keyId, final String keyStoreFile, + final String keyStoreSecret) throws Exception { + showTitle("idTokenHintAlgRSA15EncA256CBCPLUSHS512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A256CBC_PLUS_HS512); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, null); + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + + Jwe jwe = Jwe.parse(idToken, privateKey, null); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + idTokenHintAlgRSA15EncA256CBCPLUSHS512 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri", + "clientJwksUri", "RSA_OAEP_keyId", "keyStoreFile", "keyStoreSecret"}) + @Test + public void idTokenHintAlgRSAOAEPEncA256GCM( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri, final String clientJwksUri, final String keyId, final String keyStoreFile, + final String keyStoreSecret) throws Exception { + showTitle("idTokenHintAlgRSAOAEPEncA256GCM"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA_OAEP); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A256GCM); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, null); + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + + Jwe jwe = Jwe.parse(idToken, privateKey, null); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + idTokenHintAlgRSAOAEPEncA256GCM = idToken; + } + + @Parameters({"RS256_keyId", "userEmail", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void loginHintTokenRS256( + final String keyId, final String userEmail, + final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("loginHintTokenRS256"); + + JSONObject subjectValue = new JSONObject(); + subjectValue.put("subject_type", "email"); + subjectValue.put("email", userEmail); + + Jwt jwt = new Jwt(); + jwt.getHeader().setAlgorithm(SignatureAlgorithm.RS256); + jwt.getHeader().setKeyId(keyId); + jwt.getClaims().setClaim("subject", subjectValue); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(jwt.getSigningInput(), keyId, null, SignatureAlgorithm.RS256); + jwt.setEncodedSignature(encodedSignature); + + loginHintTokenRS256 = jwt.toString(); + } + + @Parameters({"RS384_keyId", "userEmail", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void loginHintTokenRS384( + final String keyId, final String userEmail, + final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("loginHintTokenRS384"); + + JSONObject subjectValue = new JSONObject(); + subjectValue.put("subject_type", "email"); + subjectValue.put("email", userEmail); + + Jwt jwt = new Jwt(); + jwt.getHeader().setAlgorithm(SignatureAlgorithm.RS384); + jwt.getHeader().setKeyId(keyId); + jwt.getClaims().setClaim("subject", subjectValue); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(jwt.getSigningInput(), keyId, null, SignatureAlgorithm.RS384); + jwt.setEncodedSignature(encodedSignature); + + loginHintTokenRS384 = jwt.toString(); + } + + @Parameters({"RS512_keyId", "userEmail", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void loginHintTokenRS512( + final String keyId, final String userEmail, + final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("loginHintTokenRS512"); + + JSONObject subjectValue = new JSONObject(); + subjectValue.put("subject_type", "email"); + subjectValue.put("email", userEmail); + + Jwt jwt = new Jwt(); + jwt.getHeader().setAlgorithm(SignatureAlgorithm.RS512); + jwt.getHeader().setKeyId(keyId); + jwt.getClaims().setClaim("subject", subjectValue); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(jwt.getSigningInput(), keyId, null, SignatureAlgorithm.RS512); + jwt.setEncodedSignature(encodedSignature); + + loginHintTokenRS512 = jwt.toString(); + } + + @Parameters({"ES256_keyId", "userEmail", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void loginHintTokenES256( + final String keyId, final String userEmail, + final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("loginHintTokenES256"); + + JSONObject subjectValue = new JSONObject(); + subjectValue.put("subject_type", "email"); + subjectValue.put("email", userEmail); + + Jwt jwt = new Jwt(); + jwt.getHeader().setAlgorithm(SignatureAlgorithm.ES256); + jwt.getHeader().setKeyId(keyId); + jwt.getClaims().setClaim("subject", subjectValue); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(jwt.getSigningInput(), keyId, null, SignatureAlgorithm.ES256); + jwt.setEncodedSignature(encodedSignature); + + loginHintTokenES256 = jwt.toString(); + } + + @Parameters({"ES384_keyId", "userEmail", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void loginHintTokenES384( + final String keyId, final String userEmail, + final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("loginHintTokenES384"); + + JSONObject subjectValue = new JSONObject(); + subjectValue.put("subject_type", "email"); + subjectValue.put("email", userEmail); + + Jwt jwt = new Jwt(); + jwt.getHeader().setAlgorithm(SignatureAlgorithm.ES384); + jwt.getHeader().setKeyId(keyId); + jwt.getClaims().setClaim("subject", subjectValue); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(jwt.getSigningInput(), keyId, null, SignatureAlgorithm.ES384); + jwt.setEncodedSignature(encodedSignature); + + loginHintTokenES384 = jwt.toString(); + } + + @Parameters({"ES512_keyId", "userEmail", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void loginHintTokenES512( + final String keyId, final String userEmail, + final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("loginHintTokenES512"); + + JSONObject subjectValue = new JSONObject(); + subjectValue.put("subject_type", "email"); + subjectValue.put("email", userEmail); + + Jwt jwt = new Jwt(); + jwt.getHeader().setAlgorithm(SignatureAlgorithm.ES512); + jwt.getHeader().setKeyId(keyId); + jwt.getClaims().setClaim("subject", subjectValue); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(jwt.getSigningInput(), keyId, null, SignatureAlgorithm.ES512); + jwt.setEncodedSignature(encodedSignature); + + loginHintTokenES512 = jwt.toString(); + } + + @Parameters({"PS256_keyId", "userEmail", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void loginHintTokenPS256( + final String keyId, final String userEmail, + final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("loginHintTokenPS256"); + + JSONObject subjectValue = new JSONObject(); + subjectValue.put("subject_type", "email"); + subjectValue.put("email", userEmail); + + Jwt jwt = new Jwt(); + jwt.getHeader().setAlgorithm(SignatureAlgorithm.PS256); + jwt.getHeader().setKeyId(keyId); + jwt.getClaims().setClaim("subject", subjectValue); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(jwt.getSigningInput(), keyId, null, SignatureAlgorithm.PS256); + jwt.setEncodedSignature(encodedSignature); + + loginHintTokenPS256 = jwt.toString(); + } + + @Parameters({"PS384_keyId", "userEmail", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void loginHintTokenPS384( + final String keyId, final String userEmail, + final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("loginHintTokenPS384"); + + JSONObject subjectValue = new JSONObject(); + subjectValue.put("subject_type", "email"); + subjectValue.put("email", userEmail); + + Jwt jwt = new Jwt(); + jwt.getHeader().setAlgorithm(SignatureAlgorithm.PS384); + jwt.getHeader().setKeyId(keyId); + jwt.getClaims().setClaim("subject", subjectValue); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(jwt.getSigningInput(), keyId, null, SignatureAlgorithm.PS384); + jwt.setEncodedSignature(encodedSignature); + + loginHintTokenPS384 = jwt.toString(); + } + + @Parameters({"PS512_keyId", "userEmail", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void loginHintTokenPS512( + final String keyId, final String userEmail, + final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("loginHintTokenPS512"); + + JSONObject subjectValue = new JSONObject(); + subjectValue.put("subject_type", "email"); + subjectValue.put("email", userEmail); + + Jwt jwt = new Jwt(); + jwt.getHeader().setAlgorithm(SignatureAlgorithm.PS512); + jwt.getHeader().setKeyId(keyId); + jwt.getClaims().setClaim("subject", subjectValue); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(jwt.getSigningInput(), keyId, null, SignatureAlgorithm.PS512); + jwt.setEncodedSignature(encodedSignature); + + loginHintTokenPS512 = jwt.toString(); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/BackchannelAuthenticationPushMode.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/BackchannelAuthenticationPushMode.java new file mode 100644 index 00000000..debd0f4b --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/BackchannelAuthenticationPushMode.java @@ -0,0 +1,3544 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ciba; + +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_TOKEN_DELIVERY_MODE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_USER_CODE_PARAMETER; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +import java.security.PrivateKey; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.apache.commons.lang.RandomStringUtils; +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.BackchannelAuthenticationClient; +import org.gluu.oxauth.client.BackchannelAuthenticationRequest; +import org.gluu.oxauth.client.BackchannelAuthenticationResponse; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.ciba.BackchannelAuthenticationErrorResponseType; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.BackchannelTokenDeliveryMode; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.encryption.BlockEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.encryption.KeyEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.signature.AsymmetricSignatureAlgorithm; +import org.gluu.oxauth.model.crypto.signature.ECDSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jwe.Jwe; +import org.gluu.oxauth.model.jws.ECDSASigner; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.gluu.oxauth.model.util.Util; +import org.json.JSONObject; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Javier Rojas Blum + * @version May 28, 2020 + */ +public class BackchannelAuthenticationPushMode extends BaseTest { + + String idTokenHintRS256; + String idTokenHintRS384; + String idTokenHintRS512; + String idTokenHintES256; + String idTokenHintES384; + String idTokenHintES512; + String idTokenHintPS256; + String idTokenHintPS384; + String idTokenHintPS512; + String idTokenHintAlgA128KWEncA128GCM; + String idTokenHintAlgA256KWEncA256GCM; + String idTokenHintAlgRSA15EncA128CBCPLUSHS256; + String idTokenHintAlgRSA15EncA256CBCPLUSHS512; + String idTokenHintAlgRSAOAEPEncA256GCM; + + String loginHintTokenRS256; + String loginHintTokenRS384; + String loginHintTokenRS512; + String loginHintTokenES256; + String loginHintTokenES384; + String loginHintTokenES512; + String loginHintTokenPS256; + String loginHintTokenPS384; + String loginHintTokenPS512; + + @Parameters({"backchannelClientNotificationEndpoint", "backchannelUserCode", "userId"}) + @Test + public void backchannelTokenDeliveryModePushLoginHint1( + final String backchannelClientNotificationEndpoint, final String backchannelUserCode, final String userId) { + showTitle("backchannelTokenDeliveryModePushLoginHint1"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String bindingMessage = RandomStringUtils.randomAlphanumeric(6); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid", "profile", "address", "phone", "email")); + backchannelAuthenticationRequest.setLoginHint(userId); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAcrValues(Arrays.asList("auth_ldap_server", "basic")); + backchannelAuthenticationRequest.setBindingMessage(bindingMessage); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"backchannelClientNotificationEndpoint", "backchannelUserCode", "userEmail"}) + @Test + public void backchannelTokenDeliveryModePushLoginHint2( + final String backchannelClientNotificationEndpoint, final String backchannelUserCode, final String userEmail) { + showTitle("backchannelTokenDeliveryModePushLoginHint2"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHint(userEmail); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"backchannelClientNotificationEndpoint", "backchannelUserCode", "userInum"}) + @Test + public void backchannelTokenDeliveryModePushLoginHint3( + final String backchannelClientNotificationEndpoint, final String backchannelUserCode, final String userInum) { + showTitle("backchannelTokenDeliveryModePushLoginHint3"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHint(userInum); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"backchannelClientNotificationEndpoint", "userInum"}) + @Test + public void backchannelTokenDeliveryModePushLoginHint4( + final String backchannelClientNotificationEndpoint, final String userInum) { + showTitle("backchannelTokenDeliveryModePushLoginHint4"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(false); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(false).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHint(userInum); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"backchannelClientNotificationEndpoint", "userInum"}) + @Test + public void backchannelTokenDeliveryModePushLoginHint5( + final String backchannelClientNotificationEndpoint, final String userInum) { + showTitle("backchannelTokenDeliveryModePushLoginHint5"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHint(userInum); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode", + "RS256_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test(dependsOnMethods = "idTokenHintRS256") + public void backchannelTokenDeliveryModePushIdTokenHintRS256( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("backchannelTokenDeliveryModePushIdTokenHintRS256"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.RS256); + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + + // 2. Authentication Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintRS256); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + backchannelAuthenticationRequest.setAlgorithm(SignatureAlgorithm.RS256); + backchannelAuthenticationRequest.setCryptoProvider(cryptoProvider); + backchannelAuthenticationRequest.setKeyId(keyId); + backchannelAuthenticationRequest.setAudience(tokenEndpoint); + backchannelAuthenticationRequest.setAuthUsername(clientId); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode", + "RS384_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test(dependsOnMethods = "idTokenHintRS384") + public void backchannelTokenDeliveryModePushIdTokenHintRS384( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("backchannelTokenDeliveryModePushIdTokenHintRS384"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.RS384); + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS384); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS384.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + + // 2. Authentication Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintRS384); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + backchannelAuthenticationRequest.setAlgorithm(SignatureAlgorithm.RS384); + backchannelAuthenticationRequest.setCryptoProvider(cryptoProvider); + backchannelAuthenticationRequest.setKeyId(keyId); + backchannelAuthenticationRequest.setAudience(tokenEndpoint); + backchannelAuthenticationRequest.setAuthUsername(clientId); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode", + "RS512_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test(dependsOnMethods = "idTokenHintRS512") + public void backchannelTokenDeliveryModePushIdTokenHintRS512( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("backchannelTokenDeliveryModePushIdTokenHintRS512"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.RS512); + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS512); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS512.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + + // 2. Authentication Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintRS512); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + backchannelAuthenticationRequest.setAlgorithm(SignatureAlgorithm.RS512); + backchannelAuthenticationRequest.setCryptoProvider(cryptoProvider); + backchannelAuthenticationRequest.setKeyId(keyId); + backchannelAuthenticationRequest.setAudience(tokenEndpoint); + backchannelAuthenticationRequest.setAuthUsername(clientId); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode", + "ES256_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test(dependsOnMethods = "idTokenHintES256") + public void backchannelTokenDeliveryModePushIdTokenHintES256( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("backchannelTokenDeliveryModePushIdTokenHintES256"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.ES256); + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.ES256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.ES256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + + // 2. Authentication Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintES256); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + backchannelAuthenticationRequest.setAlgorithm(SignatureAlgorithm.ES256); + backchannelAuthenticationRequest.setCryptoProvider(cryptoProvider); + backchannelAuthenticationRequest.setKeyId(keyId); + backchannelAuthenticationRequest.setAudience(tokenEndpoint); + backchannelAuthenticationRequest.setAuthUsername(clientId); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode", + "ES384_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test(dependsOnMethods = "idTokenHintES384") + public void backchannelTokenDeliveryModePushIdTokenHintES384( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("backchannelTokenDeliveryModePushIdTokenHintES384"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.ES384); + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.ES384); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.ES384.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + + // 2. Authentication Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintES384); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + backchannelAuthenticationRequest.setAlgorithm(SignatureAlgorithm.ES384); + backchannelAuthenticationRequest.setCryptoProvider(cryptoProvider); + backchannelAuthenticationRequest.setKeyId(keyId); + backchannelAuthenticationRequest.setAudience(tokenEndpoint); + backchannelAuthenticationRequest.setAuthUsername(clientId); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode", + "ES512_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test(dependsOnMethods = "idTokenHintES512") + public void backchannelTokenDeliveryModePushIdTokenHintES512( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("backchannelTokenDeliveryModePushIdTokenHintES512"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.ES512); + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.ES512); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.ES512.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + + // 2. Authentication Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintES512); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + backchannelAuthenticationRequest.setAlgorithm(SignatureAlgorithm.ES512); + backchannelAuthenticationRequest.setCryptoProvider(cryptoProvider); + backchannelAuthenticationRequest.setKeyId(keyId); + backchannelAuthenticationRequest.setAudience(tokenEndpoint); + backchannelAuthenticationRequest.setAuthUsername(clientId); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode", + "PS256_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test(dependsOnMethods = "idTokenHintPS256") + public void backchannelTokenDeliveryModePushIdTokenHintPS256( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("backchannelTokenDeliveryModePushIdTokenHintPS256"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.PS256); + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.PS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.PS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + + // 2. Authentication Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintPS256); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + backchannelAuthenticationRequest.setAlgorithm(SignatureAlgorithm.PS256); + backchannelAuthenticationRequest.setCryptoProvider(cryptoProvider); + backchannelAuthenticationRequest.setKeyId(keyId); + backchannelAuthenticationRequest.setAudience(tokenEndpoint); + backchannelAuthenticationRequest.setAuthUsername(clientId); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode", + "PS384_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test(dependsOnMethods = "idTokenHintPS384") + public void backchannelTokenDeliveryModePushIdTokenHintPS384( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("backchannelTokenDeliveryModePushIdTokenHintPS384"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.PS384); + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.PS384); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.PS384.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + + // 2. Authentication Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintPS384); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + backchannelAuthenticationRequest.setAlgorithm(SignatureAlgorithm.PS384); + backchannelAuthenticationRequest.setCryptoProvider(cryptoProvider); + backchannelAuthenticationRequest.setKeyId(keyId); + backchannelAuthenticationRequest.setAudience(tokenEndpoint); + backchannelAuthenticationRequest.setAuthUsername(clientId); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode", + "PS512_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test(dependsOnMethods = "idTokenHintPS512") + public void backchannelTokenDeliveryModePushIdTokenHintPS512( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("backchannelTokenDeliveryModePushIdTokenHintPS512"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.PS512); + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.PS512); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.PS512.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + + // 2. Authentication Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintPS512); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + backchannelAuthenticationRequest.setAlgorithm(SignatureAlgorithm.PS512); + backchannelAuthenticationRequest.setCryptoProvider(cryptoProvider); + backchannelAuthenticationRequest.setKeyId(keyId); + backchannelAuthenticationRequest.setAudience(tokenEndpoint); + backchannelAuthenticationRequest.setAuthUsername(clientId); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode"}) + @Test(dependsOnMethods = "idTokenHintAlgA128KWEncA128GCM") + public void backchannelTokenDeliveryModePushIdTokenHintAlgA128KWEncA128GCM( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePushIdTokenHintAlgA128KWEncA128GCM"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintAlgA128KWEncA128GCM); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode"}) + @Test(dependsOnMethods = "idTokenHintAlgA256KWEncA256GCM") + public void backchannelTokenDeliveryModePushIdTokenHintAlgA256KWEncA256GCM( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePushIdTokenHintAlgA256KWEncA256GCM"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintAlgA256KWEncA256GCM); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode"}) + @Test(dependsOnMethods = "idTokenHintAlgRSA15EncA128CBCPLUSHS256") + public void backchannelTokenDeliveryModePushIdTokenHintAlgRSA15EncA128CBCPLUSHS256( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePushIdTokenHintAlgRSA15EncA128CBCPLUSHS256"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintAlgRSA15EncA128CBCPLUSHS256); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode"}) + @Test(dependsOnMethods = "idTokenHintAlgRSA15EncA256CBCPLUSHS512") + public void backchannelTokenDeliveryModePushIdTokenHintAlgRSA15EncA256CBCPLUSHS512( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePushIdTokenHintAlgRSA15EncA256CBCPLUSHS512"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintAlgRSA15EncA256CBCPLUSHS512); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode"}) + @Test(dependsOnMethods = "idTokenHintAlgRSAOAEPEncA256GCM") + public void backchannelTokenDeliveryModePushIdTokenHintAlgRSAOAEPEncA256GCM( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePushIdTokenHintAlgRSAOAEPEncA256GCM"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setIdTokenHint(idTokenHintAlgRSAOAEPEncA256GCM); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode"}) + @Test(dependsOnMethods = "loginHintTokenRS256") + public void backchannelTokenDeliveryModePushLoginHintTokenRS256( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePushLoginHintTokenRS256"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHintToken(loginHintTokenRS256); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode"}) + @Test(dependsOnMethods = "loginHintTokenRS384") + public void backchannelTokenDeliveryModePushLoginHintTokenRS384( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePushLoginHintTokenRS384"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS384); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS384.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHintToken(loginHintTokenRS384); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode"}) + @Test(dependsOnMethods = "loginHintTokenRS512") + public void backchannelTokenDeliveryModePushLoginHintTokenRS512( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePushLoginHintTokenRS512"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS512); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS512.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHintToken(loginHintTokenRS512); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode"}) + @Test(dependsOnMethods = "loginHintTokenES256") + public void backchannelTokenDeliveryModePushLoginHintTokenES256( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePushLoginHintTokenES256"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.ES256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.ES256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHintToken(loginHintTokenES256); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode"}) + @Test(dependsOnMethods = "loginHintTokenES384") + public void backchannelTokenDeliveryModePushLoginHintTokenES384( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePushLoginHintTokenES384"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.ES384); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.ES384.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHintToken(loginHintTokenES384); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode"}) + @Test(dependsOnMethods = "loginHintTokenES512") + public void backchannelTokenDeliveryModePushLoginHintTokenES512( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePushLoginHintTokenES512"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.ES512); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.ES512.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHintToken(loginHintTokenES512); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode"}) + @Test(dependsOnMethods = "loginHintTokenPS256") + public void backchannelTokenDeliveryModePushLoginHintTokenPS256( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePushLoginHintTokenPS256"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.PS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.PS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHintToken(loginHintTokenPS256); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode"}) + @Test(dependsOnMethods = "loginHintTokenPS384") + public void backchannelTokenDeliveryModePushLoginHintTokenPS384( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePushLoginHintTokenPS384"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.PS384); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.PS384.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHintToken(loginHintTokenPS384); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint", "backchannelUserCode"}) + @Test(dependsOnMethods = "loginHintTokenPS512") + public void backchannelTokenDeliveryModePushLoginHintTokenPS512( + final String clientJwksUri, final String backchannelClientNotificationEndpoint, final String backchannelUserCode) { + showTitle("backchannelTokenDeliveryModePushLoginHintTokenPS512"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.PS512); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.PS512.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHintToken(loginHintTokenPS512); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + @Parameters({"backchannelClientNotificationEndpoint"}) + @Test + public void backchannelTokenDeliveryModePushFail1(final String backchannelClientNotificationEndpoint) { + showTitle("backchannelTokenDeliveryModePushFail1"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + // 2. Authentication Request + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 400, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getEntity(), "The entity is null"); + assertNotNull(backchannelAuthenticationResponse.getErrorType(), "The error type is null"); + assertEquals(BackchannelAuthenticationErrorResponseType.INVALID_REQUEST, backchannelAuthenticationResponse.getErrorType()); + assertNotNull(backchannelAuthenticationResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"backchannelClientNotificationEndpoint"}) + @Test + public void backchannelTokenDeliveryModePushFail2(final String backchannelClientNotificationEndpoint) { + showTitle("backchannelTokenDeliveryModePushFail2"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + + // 2. Authentication Request + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword("INVALID_CLIENT_SECRET"); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 401, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getEntity(), "The entity is null"); + assertNotNull(backchannelAuthenticationResponse.getErrorType(), "The error type is null"); + assertEquals(BackchannelAuthenticationErrorResponseType.INVALID_CLIENT, backchannelAuthenticationResponse.getErrorType()); + assertNotNull(backchannelAuthenticationResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"backchannelClientNotificationEndpoint"}) + @Test + public void backchannelTokenDeliveryModePushFail3(final String backchannelClientNotificationEndpoint) { + showTitle("backchannelTokenDeliveryModePushFail3"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(null); // Invalid Scope + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 400, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getEntity(), "The entity is null"); + assertNotNull(backchannelAuthenticationResponse.getErrorType(), "The error type is null"); + assertEquals(BackchannelAuthenticationErrorResponseType.UNKNOWN_USER_ID, backchannelAuthenticationResponse.getErrorType()); + assertNotNull(backchannelAuthenticationResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"backchannelClientNotificationEndpoint"}) + @Test + public void backchannelTokenDeliveryModePushFail4(final String backchannelClientNotificationEndpoint) { + showTitle("backchannelTokenDeliveryModePushFail4"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHint(null); // Invalid login hint. + backchannelAuthenticationRequest.setLoginHintToken(null); // Invalid login hint token. + backchannelAuthenticationRequest.setIdTokenHint(null); // Invalid id token hint + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 400, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getEntity(), "The entity is null"); + assertNotNull(backchannelAuthenticationResponse.getErrorType(), "The error type is null"); + assertEquals(BackchannelAuthenticationErrorResponseType.UNKNOWN_USER_ID, backchannelAuthenticationResponse.getErrorType()); + assertNotNull(backchannelAuthenticationResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"backchannelClientNotificationEndpoint"}) + @Test + public void backchannelTokenDeliveryModePushFail5(final String backchannelClientNotificationEndpoint) { + showTitle("backchannelTokenDeliveryModePushFail5"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHint("admin"); + backchannelAuthenticationRequest.setClientNotificationToken(null); // Invalid client notification token. + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 400, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getEntity(), "The entity is null"); + assertNotNull(backchannelAuthenticationResponse.getErrorType(), "The error type is null"); + assertEquals(BackchannelAuthenticationErrorResponseType.INVALID_REQUEST, backchannelAuthenticationResponse.getErrorType()); + assertNotNull(backchannelAuthenticationResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"backchannelClientNotificationEndpoint"}) + @Test + public void backchannelTokenDeliveryModePushFail6(final String backchannelClientNotificationEndpoint) { + showTitle("backchannelTokenDeliveryModePushFail6"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHint("admin"); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(null); // Invalid user code. + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 400, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getEntity(), "The entity is null"); + assertNotNull(backchannelAuthenticationResponse.getErrorType(), "The error type is null"); + assertEquals(BackchannelAuthenticationErrorResponseType.INVALID_USER_CODE, backchannelAuthenticationResponse.getErrorType()); + assertNotNull(backchannelAuthenticationResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"backchannelClientNotificationEndpoint"}) + @Test + public void backchannelTokenDeliveryModePushFail7(final String backchannelClientNotificationEndpoint) { + showTitle("backchannelTokenDeliveryModePushFail7"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHint("admin"); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode("INVALID_USER_CODE"); // Invalid user code. + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 400, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getEntity(), "The entity is null"); + assertNotNull(backchannelAuthenticationResponse.getErrorType(), "The error type is null"); + assertEquals(BackchannelAuthenticationErrorResponseType.INVALID_USER_CODE, backchannelAuthenticationResponse.getErrorType()); + assertNotNull(backchannelAuthenticationResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"backchannelClientNotificationEndpoint", "backchannelUserCode", "userId"}) + @Test + public void backchannelTokenDeliveryModePushFail8( + final String backchannelClientNotificationEndpoint, final String backchannelUserCode, final String userId) { + showTitle("backchannelTokenDeliveryModePushFail8"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authentication Request + String clientNotificationToken = UUID.randomUUID().toString(); + + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setScope(Arrays.asList("openid")); + backchannelAuthenticationRequest.setLoginHint(userId); + backchannelAuthenticationRequest.setClientNotificationToken(clientNotificationToken); + backchannelAuthenticationRequest.setUserCode(backchannelUserCode); + backchannelAuthenticationRequest.setRequestedExpiry(1200); + backchannelAuthenticationRequest.setAcrValues(Arrays.asList("auth_ldap_server", "basic")); + backchannelAuthenticationRequest.setBindingMessage("####"); // Invalid binding message + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + assertEquals(backchannelAuthenticationResponse.getStatus(), 400, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getEntity(), "The entity is null"); + assertNotNull(backchannelAuthenticationResponse.getErrorType(), "The error type is null"); + assertEquals(BackchannelAuthenticationErrorResponseType.INVALID_BINDING_MESSAGE, backchannelAuthenticationResponse.getErrorType()); + assertNotNull(backchannelAuthenticationResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintRS256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintRS256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS256); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + idTokenHintRS256 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintRS384( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintRS384"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS384); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS384, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + idTokenHintRS384 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintRS512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintRS512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS512); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS512, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + idTokenHintRS512 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintES256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintES256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.ES256); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + ECDSAPublicKey publicKey = JwkClient.getECDSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + ECDSASigner ecdsaSigner = new ECDSASigner(SignatureAlgorithm.ES256, publicKey); + + assertTrue(ecdsaSigner.validate(jwt)); + + idTokenHintES256 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintES384( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintES384"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.ES384); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + ECDSAPublicKey publicKey = JwkClient.getECDSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + ECDSASigner ecdsaSigner = new ECDSASigner(SignatureAlgorithm.ES384, publicKey); + + assertTrue(ecdsaSigner.validate(jwt)); + + idTokenHintES384 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintES512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintES512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.ES512); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + ECDSAPublicKey publicKey = JwkClient.getECDSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + ECDSASigner ecdsaSigner = new ECDSASigner(SignatureAlgorithm.ES512, publicKey); + + assertTrue(ecdsaSigner.validate(jwt)); + + idTokenHintES512 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintPS256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintPS256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.PS256); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.PS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + idTokenHintPS256 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintPS384( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintPS384"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.PS384); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.PS384, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + idTokenHintPS384 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintPS512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintPS512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.PS512); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.PS512, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + idTokenHintPS512 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintAlgA128KWEncA128GCM( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintAlgA128KWEncA128GCM"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.A128KW); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A128GCM); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwe jwe = Jwe.parse(idToken, null, clientSecret.getBytes(Util.UTF8_STRING_ENCODING)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + idTokenHintAlgA128KWEncA128GCM = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintAlgA256KWEncA256GCM( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintAlgA256KWEncA256GCM"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.A256KW); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A256GCM); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwe jwe = Jwe.parse(idToken, null, clientSecret.getBytes(Util.UTF8_STRING_ENCODING)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + idTokenHintAlgA256KWEncA256GCM = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri", + "clientJwksUri", "RSA1_5_keyId", "keyStoreFile", "keyStoreSecret"}) + @Test + public void idTokenHintAlgRSA15EncA128CBCPLUSHS256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri, final String clientJwksUri, final String keyId, final String keyStoreFile, + final String keyStoreSecret) throws Exception { + showTitle("idTokenHintAlgRSA15EncA128CBCPLUSHS256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A128CBC_PLUS_HS256); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, null); + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + + Jwe jwe = Jwe.parse(idToken, privateKey, null); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + idTokenHintAlgRSA15EncA128CBCPLUSHS256 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri", + "clientJwksUri", "RSA1_5_keyId", "keyStoreFile", "keyStoreSecret"}) + @Test + public void idTokenHintAlgRSA15EncA256CBCPLUSHS512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri, final String clientJwksUri, final String keyId, final String keyStoreFile, + final String keyStoreSecret) throws Exception { + showTitle("idTokenHintAlgRSA15EncA256CBCPLUSHS512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A256CBC_PLUS_HS512); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, null); + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + + Jwe jwe = Jwe.parse(idToken, privateKey, null); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + idTokenHintAlgRSA15EncA256CBCPLUSHS512 = idToken; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri", + "clientJwksUri", "RSA_OAEP_keyId", "keyStoreFile", "keyStoreSecret"}) + @Test + public void idTokenHintAlgRSAOAEPEncA256GCM( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri, final String clientJwksUri, final String keyId, final String keyStoreFile, + final String keyStoreSecret) throws Exception { + showTitle("idTokenHintAlgRSAOAEPEncA256GCM"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA_OAEP); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A256GCM); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, null); + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + + Jwe jwe = Jwe.parse(idToken, privateKey, null); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + idTokenHintAlgRSAOAEPEncA256GCM = idToken; + } + + @Parameters({"RS256_keyId", "userEmail", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void loginHintTokenRS256( + final String keyId, final String userEmail, + final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("loginHintTokenRS256"); + + JSONObject subjectValue = new JSONObject(); + subjectValue.put("subject_type", "email"); + subjectValue.put("email", userEmail); + + Jwt jwt = new Jwt(); + jwt.getHeader().setAlgorithm(SignatureAlgorithm.RS256); + jwt.getHeader().setKeyId(keyId); + jwt.getClaims().setClaim("subject", subjectValue); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(jwt.getSigningInput(), keyId, null, SignatureAlgorithm.RS256); + jwt.setEncodedSignature(encodedSignature); + + loginHintTokenRS256 = jwt.toString(); + } + + @Parameters({"RS384_keyId", "userEmail", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void loginHintTokenRS384( + final String keyId, final String userEmail, + final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("loginHintTokenRS384"); + + JSONObject subjectValue = new JSONObject(); + subjectValue.put("subject_type", "email"); + subjectValue.put("email", userEmail); + + Jwt jwt = new Jwt(); + jwt.getHeader().setAlgorithm(SignatureAlgorithm.RS384); + jwt.getHeader().setKeyId(keyId); + jwt.getClaims().setClaim("subject", subjectValue); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(jwt.getSigningInput(), keyId, null, SignatureAlgorithm.RS384); + jwt.setEncodedSignature(encodedSignature); + + loginHintTokenRS384 = jwt.toString(); + } + + @Parameters({"RS512_keyId", "userEmail", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void loginHintTokenRS512( + final String keyId, final String userEmail, + final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("loginHintTokenRS512"); + + JSONObject subjectValue = new JSONObject(); + subjectValue.put("subject_type", "email"); + subjectValue.put("email", userEmail); + + Jwt jwt = new Jwt(); + jwt.getHeader().setAlgorithm(SignatureAlgorithm.RS512); + jwt.getHeader().setKeyId(keyId); + jwt.getClaims().setClaim("subject", subjectValue); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(jwt.getSigningInput(), keyId, null, SignatureAlgorithm.RS512); + jwt.setEncodedSignature(encodedSignature); + + loginHintTokenRS512 = jwt.toString(); + } + + @Parameters({"ES256_keyId", "userEmail", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void loginHintTokenES256( + final String keyId, final String userEmail, + final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("loginHintTokenES256"); + + JSONObject subjectValue = new JSONObject(); + subjectValue.put("subject_type", "email"); + subjectValue.put("email", userEmail); + + Jwt jwt = new Jwt(); + jwt.getHeader().setAlgorithm(SignatureAlgorithm.ES256); + jwt.getHeader().setKeyId(keyId); + jwt.getClaims().setClaim("subject", subjectValue); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(jwt.getSigningInput(), keyId, null, SignatureAlgorithm.ES256); + jwt.setEncodedSignature(encodedSignature); + + loginHintTokenES256 = jwt.toString(); + } + + @Parameters({"ES384_keyId", "userEmail", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void loginHintTokenES384( + final String keyId, final String userEmail, + final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("loginHintTokenES384"); + + JSONObject subjectValue = new JSONObject(); + subjectValue.put("subject_type", "email"); + subjectValue.put("email", userEmail); + + Jwt jwt = new Jwt(); + jwt.getHeader().setAlgorithm(SignatureAlgorithm.ES384); + jwt.getHeader().setKeyId(keyId); + jwt.getClaims().setClaim("subject", subjectValue); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(jwt.getSigningInput(), keyId, null, SignatureAlgorithm.ES384); + jwt.setEncodedSignature(encodedSignature); + + loginHintTokenES384 = jwt.toString(); + } + + @Parameters({"ES512_keyId", "userEmail", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void loginHintTokenES512( + final String keyId, final String userEmail, + final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("loginHintTokenES512"); + + JSONObject subjectValue = new JSONObject(); + subjectValue.put("subject_type", "email"); + subjectValue.put("email", userEmail); + + Jwt jwt = new Jwt(); + jwt.getHeader().setAlgorithm(SignatureAlgorithm.ES512); + jwt.getHeader().setKeyId(keyId); + jwt.getClaims().setClaim("subject", subjectValue); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(jwt.getSigningInput(), keyId, null, SignatureAlgorithm.ES512); + jwt.setEncodedSignature(encodedSignature); + + loginHintTokenES512 = jwt.toString(); + } + + @Parameters({"PS256_keyId", "userEmail", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void loginHintTokenPS256( + final String keyId, final String userEmail, + final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("loginHintTokenPS256"); + + JSONObject subjectValue = new JSONObject(); + subjectValue.put("subject_type", "email"); + subjectValue.put("email", userEmail); + + Jwt jwt = new Jwt(); + jwt.getHeader().setAlgorithm(SignatureAlgorithm.PS256); + jwt.getHeader().setKeyId(keyId); + jwt.getClaims().setClaim("subject", subjectValue); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(jwt.getSigningInput(), keyId, null, SignatureAlgorithm.PS256); + jwt.setEncodedSignature(encodedSignature); + + loginHintTokenPS256 = jwt.toString(); + } + + @Parameters({"PS384_keyId", "userEmail", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void loginHintTokenPS384( + final String keyId, final String userEmail, + final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("loginHintTokenPS384"); + + JSONObject subjectValue = new JSONObject(); + subjectValue.put("subject_type", "email"); + subjectValue.put("email", userEmail); + + Jwt jwt = new Jwt(); + jwt.getHeader().setAlgorithm(SignatureAlgorithm.PS384); + jwt.getHeader().setKeyId(keyId); + jwt.getClaims().setClaim("subject", subjectValue); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(jwt.getSigningInput(), keyId, null, SignatureAlgorithm.PS384); + jwt.setEncodedSignature(encodedSignature); + + loginHintTokenPS384 = jwt.toString(); + } + + @Parameters({"PS512_keyId", "userEmail", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void loginHintTokenPS512( + final String keyId, final String userEmail, + final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("loginHintTokenPS512"); + + JSONObject subjectValue = new JSONObject(); + subjectValue.put("subject_type", "email"); + subjectValue.put("email", userEmail); + + Jwt jwt = new Jwt(); + jwt.getHeader().setAlgorithm(SignatureAlgorithm.PS512); + jwt.getHeader().setKeyId(keyId); + jwt.getClaims().setClaim("subject", subjectValue); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(jwt.getSigningInput(), keyId, null, SignatureAlgorithm.PS512); + jwt.setEncodedSignature(encodedSignature); + + loginHintTokenPS512 = jwt.toString(); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/CibaPingModeJwtAuthRequestTests.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/CibaPingModeJwtAuthRequestTests.java new file mode 100644 index 00000000..f82c55ad --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/CibaPingModeJwtAuthRequestTests.java @@ -0,0 +1,434 @@ +package org.gluu.oxauth.ciba; + +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_TOKEN_DELIVERY_MODE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_USER_CODE_PARAMETER; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import org.apache.commons.lang.time.DateUtils; +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.BackchannelAuthenticationClient; +import org.gluu.oxauth.client.BackchannelAuthenticationRequest; +import org.gluu.oxauth.client.BackchannelAuthenticationResponse; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.model.authorize.JwtAuthorizationRequest; +import org.gluu.oxauth.model.ciba.BackchannelAuthenticationErrorResponseType; +import org.gluu.oxauth.model.common.BackchannelTokenDeliveryMode; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.AsymmetricSignatureAlgorithm; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * Responsible to validate many cases using JWT Requests for Ciba Ping flows. + */ +public class CibaPingModeJwtAuthRequestTests extends BaseTest { + + private RegisterResponse registerResponse; + private String idTokenHintRS384; + + @Parameters({"PS256_keyId", "userId", "dnName", "keyStoreFile", "keyStoreSecret", "clientJwksUri", + "backchannelClientNotificationEndpoint"}) + @Test + public void pingFlowPS256HappyFlow(final String keyId, final String userId, final String dnName, + final String keyStoreFile, final String keyStoreSecret, + final String clientJwksUri, final String backchannelClientNotificationEndpoint) throws Exception { + showTitle("pingFlowPS256HappyFlow"); + registerPingClient(clientJwksUri, BackchannelTokenDeliveryMode.PING, AsymmetricSignatureAlgorithm.PS256, + backchannelClientNotificationEndpoint); + + JwtAuthorizationRequest jwtAuthorizationRequest = createJwtRequest(keyStoreFile, keyStoreSecret, dnName, + userId, keyId, SignatureAlgorithm.PS256); + + processCibaAuthorizationEndpointSuccessfulCall(jwtAuthorizationRequest.getEncodedJwt(), + registerResponse.getClientId(), registerResponse.getClientSecret()); + } + + @Parameters({"PS384_keyId", "userId", "dnName", "keyStoreFile", "keyStoreSecret", "clientJwksUri", + "backchannelClientNotificationEndpoint"}) + @Test + public void pingFlowPS384HappyFlow(final String keyId, final String userId, final String dnName, + final String keyStoreFile, final String keyStoreSecret, + final String clientJwksUri, final String backchannelClientNotificationEndpoint) throws Exception { + showTitle("pingFlowPS384HappyFlow"); + registerPingClient(clientJwksUri, BackchannelTokenDeliveryMode.PING, AsymmetricSignatureAlgorithm.PS384, + backchannelClientNotificationEndpoint); + + JwtAuthorizationRequest jwtAuthorizationRequest = createJwtRequest(keyStoreFile, keyStoreSecret, dnName, + userId, keyId, SignatureAlgorithm.PS384); + + processCibaAuthorizationEndpointSuccessfulCall(jwtAuthorizationRequest.getEncodedJwt(), + registerResponse.getClientId(), registerResponse.getClientSecret()); + } + + @Parameters({"PS512_keyId", "userId", "dnName", "keyStoreFile", "keyStoreSecret", "clientJwksUri", + "backchannelClientNotificationEndpoint"}) + @Test + public void pingFlowPS512HappyFlow(final String keyId, final String userId, final String dnName, + final String keyStoreFile, final String keyStoreSecret, + final String clientJwksUri, final String backchannelClientNotificationEndpoint) throws Exception { + showTitle("pingFlowPS512HappyFlow"); + registerPingClient(clientJwksUri, BackchannelTokenDeliveryMode.PING, AsymmetricSignatureAlgorithm.PS512, + backchannelClientNotificationEndpoint); + + JwtAuthorizationRequest jwtAuthorizationRequest = createJwtRequest(keyStoreFile, keyStoreSecret, dnName, + userId, keyId, SignatureAlgorithm.PS512); + + processCibaAuthorizationEndpointSuccessfulCall(jwtAuthorizationRequest.getEncodedJwt(), + registerResponse.getClientId(), registerResponse.getClientSecret()); + } + + @Parameters({"ES256_keyId", "userId", "dnName", "keyStoreFile", "keyStoreSecret", "clientJwksUri", + "backchannelClientNotificationEndpoint"}) + @Test + public void pingFlowES256HappyFlow(final String keyId, final String userId, final String dnName, + final String keyStoreFile, final String keyStoreSecret, + final String clientJwksUri, final String backchannelClientNotificationEndpoint) throws Exception { + showTitle("pingFlowES256HappyFlow"); + registerPingClient(clientJwksUri, BackchannelTokenDeliveryMode.PING, AsymmetricSignatureAlgorithm.ES256, + backchannelClientNotificationEndpoint); + + JwtAuthorizationRequest jwtAuthorizationRequest = createJwtRequest(keyStoreFile, keyStoreSecret, dnName, + userId, keyId, SignatureAlgorithm.ES256); + + processCibaAuthorizationEndpointSuccessfulCall(jwtAuthorizationRequest.getEncodedJwt(), + registerResponse.getClientId(), registerResponse.getClientSecret()); + } + + @Parameters({"ES384_keyId", "userId", "dnName", "keyStoreFile", "keyStoreSecret", "clientJwksUri", + "backchannelClientNotificationEndpoint"}) + @Test + public void pingFlowES384HappyFlow(final String keyId, final String userId, final String dnName, + final String keyStoreFile, final String keyStoreSecret, + final String clientJwksUri, final String backchannelClientNotificationEndpoint) throws Exception { + showTitle("pingFlowES384HappyFlow"); + registerPingClient(clientJwksUri, BackchannelTokenDeliveryMode.PING, AsymmetricSignatureAlgorithm.ES384, + backchannelClientNotificationEndpoint); + + JwtAuthorizationRequest jwtAuthorizationRequest = createJwtRequest(keyStoreFile, keyStoreSecret, dnName, + userId, keyId, SignatureAlgorithm.ES384); + + processCibaAuthorizationEndpointSuccessfulCall(jwtAuthorizationRequest.getEncodedJwt(), + registerResponse.getClientId(), registerResponse.getClientSecret()); + } + + @Parameters({"ES512_keyId", "userId", "dnName", "keyStoreFile", "keyStoreSecret", "clientJwksUri", + "backchannelClientNotificationEndpoint"}) + @Test + public void pingFlowES512HappyFlow(final String keyId, final String userId, final String dnName, + final String keyStoreFile, final String keyStoreSecret, + final String clientJwksUri, final String backchannelClientNotificationEndpoint) throws Exception { + showTitle("pingFlowES512HappyFlow"); + registerPingClient(clientJwksUri, BackchannelTokenDeliveryMode.PING, AsymmetricSignatureAlgorithm.ES512, + backchannelClientNotificationEndpoint); + + JwtAuthorizationRequest jwtAuthorizationRequest = createJwtRequest(keyStoreFile, keyStoreSecret, dnName, + userId, keyId, SignatureAlgorithm.ES512); + + processCibaAuthorizationEndpointSuccessfulCall(jwtAuthorizationRequest.getEncodedJwt(), + registerResponse.getClientId(), registerResponse.getClientSecret()); + } + + @Parameters({"PS256_keyId", "userId", "dnName", "keyStoreFile", "keyStoreSecret", "clientJwksUri", + "backchannelClientNotificationEndpoint"}) + @Test + public void cibaPingJWTRequestDataValidations(final String keyId, final String userId, final String dnName, + final String keyStoreFile, final String keyStoreSecret, + final String clientJwksUri, final String backchannelClientNotificationEndpoint) throws Exception { + showTitle("cibaPingJWTRequestDataValidations"); + registerPingClient(clientJwksUri, BackchannelTokenDeliveryMode.PING, AsymmetricSignatureAlgorithm.PS256, + backchannelClientNotificationEndpoint); + + String clientId = registerResponse.getClientId(); + + // 1. Request doesn't include Aud + JwtAuthorizationRequest jwtAuthorizationRequest = createJwtRequest(keyStoreFile, keyStoreSecret, dnName, userId, keyId, SignatureAlgorithm.PS256); + jwtAuthorizationRequest.setAud(null); + + processCibaAuthorizationEndpointFailCall(jwtAuthorizationRequest.getEncodedJwt(), clientId, + registerResponse.getClientSecret(), 400, BackchannelAuthenticationErrorResponseType.INVALID_REQUEST.getParameter()); + + // 2. Request doesn't include any hint + jwtAuthorizationRequest = createJwtRequest(keyStoreFile, keyStoreSecret, dnName, userId, keyId, SignatureAlgorithm.PS256); + jwtAuthorizationRequest.setLoginHint(null); + + processCibaAuthorizationEndpointFailCall(jwtAuthorizationRequest.getEncodedJwt(), clientId, + registerResponse.getClientSecret(), 400, BackchannelAuthenticationErrorResponseType.UNKNOWN_USER_ID.getParameter()); + + // 3. Request has a wrong Binding Message + jwtAuthorizationRequest = createJwtRequest(keyStoreFile, keyStoreSecret, dnName, userId, keyId, SignatureAlgorithm.PS256); + jwtAuthorizationRequest.setBindingMessage("(/)=&/(%&/(%$/&($%/&)"); + + processCibaAuthorizationEndpointFailCall(jwtAuthorizationRequest.getEncodedJwt(), clientId, + registerResponse.getClientSecret(), 400, BackchannelAuthenticationErrorResponseType.INVALID_BINDING_MESSAGE.getParameter()); + + // 4. Request has wrong Client Id + jwtAuthorizationRequest = createJwtRequest(keyStoreFile, keyStoreSecret, dnName, userId, keyId, SignatureAlgorithm.PS256); + jwtAuthorizationRequest.setClientId("abcabcabcabcabcabcabcabcabcabc"); + + processCibaAuthorizationEndpointFailCall(jwtAuthorizationRequest.getEncodedJwt(), "abcabcabcabcabcabcabcabcabcabc", + registerResponse.getClientSecret(), 401, BackchannelAuthenticationErrorResponseType.INVALID_CLIENT.getParameter()); + + // 5. Request has wrong Client Id + jwtAuthorizationRequest = createJwtRequest(keyStoreFile, keyStoreSecret, dnName, userId, keyId, SignatureAlgorithm.PS256); + jwtAuthorizationRequest.setClientNotificationToken(null); + + processCibaAuthorizationEndpointFailCall(jwtAuthorizationRequest.getEncodedJwt(), clientId, + registerResponse.getClientSecret(), 400, BackchannelAuthenticationErrorResponseType.INVALID_REQUEST.getParameter()); + } + + @Parameters({"PS256_keyId", "userId", "dnName", "keyStoreFile", "keyStoreSecret", "clientJwksUri", + "backchannelClientNotificationEndpoint"}) + @Test(dependsOnMethods = "idTokenHintRS384") + public void cibaPingJWTRequestIdTokenHint(final String keyId, final String userId, final String dnName, + final String keyStoreFile, final String keyStoreSecret, + final String clientJwksUri, final String backchannelClientNotificationEndpoint) throws Exception { + showTitle("cibaPingJWTRequestIdTokenHint"); + registerPingClient(clientJwksUri, BackchannelTokenDeliveryMode.PING, AsymmetricSignatureAlgorithm.PS256, + backchannelClientNotificationEndpoint); + + // 1. Request doesn't include Aud + JwtAuthorizationRequest jwtAuthorizationRequest = createJwtRequest(keyStoreFile, keyStoreSecret, dnName, userId, keyId, SignatureAlgorithm.PS256); + jwtAuthorizationRequest.setLoginHint(null); + jwtAuthorizationRequest.setIdTokenHint(idTokenHintRS384); + + processCibaAuthorizationEndpointSuccessfulCall(jwtAuthorizationRequest.getEncodedJwt(), + registerResponse.getClientId(), registerResponse.getClientSecret()); + } + + @Parameters({"PS256_keyId", "userId", "dnName", "keyStoreFile", "keyStoreSecret", "clientJwksUri", + "backchannelClientNotificationEndpoint"}) + @Test + public void cibaPingJWTRequestWrongSigning(final String keyId, final String userId, final String dnName, + final String keyStoreFile, final String keyStoreSecret, + final String clientJwksUri, final String backchannelClientNotificationEndpoint) throws Exception { + showTitle("cibaPingJWTRequestWrongSigning"); + registerPingClient(clientJwksUri, BackchannelTokenDeliveryMode.PING, AsymmetricSignatureAlgorithm.PS256, + backchannelClientNotificationEndpoint); + + JwtAuthorizationRequest jwtAuthorizationRequest = createJwtRequest(keyStoreFile, keyStoreSecret, dnName, userId, keyId, SignatureAlgorithm.PS256); + + String jwt = jwtAuthorizationRequest.getEncodedJwt(); + String[] jwtParts = jwt.split("\\."); + String jwtWithWrongSigning = jwtParts[0] + "." + jwtParts[1] + ".WRONG-SIGNING"; + + processCibaAuthorizationEndpointFailCall(jwtWithWrongSigning, registerResponse.getClientId(), + registerResponse.getClientSecret(), 400, BackchannelAuthenticationErrorResponseType.INVALID_REQUEST.getParameter()); + } + + /** + * Registers a client using CIBA configuration for Ping flow and PS256 + * @param clientJwksUri + */ + private void registerPingClient(final String clientJwksUri, final BackchannelTokenDeliveryMode mode, + final AsymmetricSignatureAlgorithm algorithm, final String backchannelClientNotificationEndpoint) { + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Collections.singletonList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(mode); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(algorithm); + registerRequest.setBackchannelUserCodeParameter(false); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + registerResponse = registerClient.exec(); + + showClient(registerClient); + + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), mode.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), algorithm.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), "false"); + } + + /** + * Process a Ciba call to the OP using JWT Request object. + * @param jwtRequest JWT in plain String. + * @param clientId Client identifier. + * @param clientSecret Client secret. + */ + private void processCibaAuthorizationEndpointSuccessfulCall(String jwtRequest, String clientId, String clientSecret) { + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setRequest(jwtRequest); + backchannelAuthenticationRequest.setClientId(clientId); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Ping or Ping modes. + } + + /** + * Process a Ciba call to the OP using JWT Request object and validate HTTP status and error type. + * @param jwtRequest JWT in plain String. + * @param clientId Client identifier. + * @param clientSecret Client secret. + * @param httpStatus Param used to validate response from the server. + * @param error Error used to validate error response from the server. + */ + private void processCibaAuthorizationEndpointFailCall(String jwtRequest, String clientId, String clientSecret, int httpStatus, String error) { + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setRequest(jwtRequest); + backchannelAuthenticationRequest.setClientId(clientId); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + + assertEquals(backchannelAuthenticationResponse.getStatus(), httpStatus, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getErrorType()); + assertNotNull(backchannelAuthenticationResponse.getErrorDescription()); + assertEquals(error, backchannelAuthenticationResponse.getErrorType().getParameter()); + assertNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNull(backchannelAuthenticationResponse.getInterval()); + } + + /** + * Creates a new JwtAuthorizationRequest using default configuration and params. + */ + private JwtAuthorizationRequest createJwtRequest(String keyStoreFile, String keyStoreSecret, String dnName, + String userId, String keyId, SignatureAlgorithm signatureAlgorithm) throws Exception { + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String clientId = registerResponse.getClientId(); + + int now = (int)(System.currentTimeMillis() / 1000); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + null, signatureAlgorithm, cryptoProvider); + jwtAuthorizationRequest.setClientNotificationToken("notification-token-123"); + jwtAuthorizationRequest.setAud(issuer); + jwtAuthorizationRequest.setLoginHint(userId); + jwtAuthorizationRequest.setNbf(now); + jwtAuthorizationRequest.setScopes(Collections.singletonList("openid")); + jwtAuthorizationRequest.setIss(clientId); + jwtAuthorizationRequest.setBindingMessage("1234"); + jwtAuthorizationRequest.setExp((int)(DateUtils.addMinutes(new Date(), 5).getTime() / 1000)); + jwtAuthorizationRequest.setIat(now); + jwtAuthorizationRequest.setJti(UUID.randomUUID().toString()); + jwtAuthorizationRequest.setKeyId(keyId); + + return jwtAuthorizationRequest; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintRS384( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintRS384"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS384); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS384, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + idTokenHintRS384 = idToken; + } + +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/CibaPollModeJwtAuthRequestTests.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/CibaPollModeJwtAuthRequestTests.java new file mode 100644 index 00000000..10bd04aa --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/CibaPollModeJwtAuthRequestTests.java @@ -0,0 +1,406 @@ +package org.gluu.oxauth.ciba; + +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_TOKEN_DELIVERY_MODE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_USER_CODE_PARAMETER; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import org.apache.commons.lang.time.DateUtils; +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.BackchannelAuthenticationClient; +import org.gluu.oxauth.client.BackchannelAuthenticationRequest; +import org.gluu.oxauth.client.BackchannelAuthenticationResponse; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.model.authorize.JwtAuthorizationRequest; +import org.gluu.oxauth.model.ciba.BackchannelAuthenticationErrorResponseType; +import org.gluu.oxauth.model.common.BackchannelTokenDeliveryMode; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.AsymmetricSignatureAlgorithm; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * Responsible to validate many cases using JWT Requests for Ciba Poll flows. + */ +public class CibaPollModeJwtAuthRequestTests extends BaseTest { + + private RegisterResponse registerResponse; + private String idTokenHintRS384; + + @Parameters({"PS256_keyId", "userId", "dnName", "keyStoreFile", "keyStoreSecret", "clientJwksUri"}) + @Test + public void pollFlowPS256HappyFlow(final String keyId, final String userId, final String dnName, + final String keyStoreFile, final String keyStoreSecret, + final String clientJwksUri) throws Exception { + showTitle("pollFlowPS256HappyFlow"); + registerPollClient(clientJwksUri, BackchannelTokenDeliveryMode.POLL, AsymmetricSignatureAlgorithm.PS256); + + JwtAuthorizationRequest jwtAuthorizationRequest = createJwtRequest(keyStoreFile, keyStoreSecret, dnName, + userId, keyId, SignatureAlgorithm.PS256); + + processCibaAuthorizationEndpointSuccessfulCall(jwtAuthorizationRequest.getEncodedJwt(), + registerResponse.getClientId(), registerResponse.getClientSecret()); + } + + @Parameters({"PS384_keyId", "userId", "dnName", "keyStoreFile", "keyStoreSecret", "clientJwksUri"}) + @Test + public void pollFlowPS384HappyFlow(final String keyId, final String userId, final String dnName, + final String keyStoreFile, final String keyStoreSecret, + final String clientJwksUri) throws Exception { + showTitle("pollFlowPS384HappyFlow"); + registerPollClient(clientJwksUri, BackchannelTokenDeliveryMode.POLL, AsymmetricSignatureAlgorithm.PS384); + + JwtAuthorizationRequest jwtAuthorizationRequest = createJwtRequest(keyStoreFile, keyStoreSecret, dnName, + userId, keyId, SignatureAlgorithm.PS384); + + processCibaAuthorizationEndpointSuccessfulCall(jwtAuthorizationRequest.getEncodedJwt(), + registerResponse.getClientId(), registerResponse.getClientSecret()); + } + + @Parameters({"PS512_keyId", "userId", "dnName", "keyStoreFile", "keyStoreSecret", "clientJwksUri"}) + @Test + public void pollFlowPS512HappyFlow(final String keyId, final String userId, final String dnName, + final String keyStoreFile, final String keyStoreSecret, + final String clientJwksUri) throws Exception { + showTitle("pollFlowPS512HappyFlow"); + registerPollClient(clientJwksUri, BackchannelTokenDeliveryMode.POLL, AsymmetricSignatureAlgorithm.PS512); + + JwtAuthorizationRequest jwtAuthorizationRequest = createJwtRequest(keyStoreFile, keyStoreSecret, dnName, + userId, keyId, SignatureAlgorithm.PS512); + + processCibaAuthorizationEndpointSuccessfulCall(jwtAuthorizationRequest.getEncodedJwt(), + registerResponse.getClientId(), registerResponse.getClientSecret()); + } + + @Parameters({"ES256_keyId", "userId", "dnName", "keyStoreFile", "keyStoreSecret", "clientJwksUri"}) + @Test + public void pollFlowES256HappyFlow(final String keyId, final String userId, final String dnName, + final String keyStoreFile, final String keyStoreSecret, + final String clientJwksUri) throws Exception { + showTitle("pollFlowES256HappyFlow"); + registerPollClient(clientJwksUri, BackchannelTokenDeliveryMode.POLL, AsymmetricSignatureAlgorithm.ES256); + + JwtAuthorizationRequest jwtAuthorizationRequest = createJwtRequest(keyStoreFile, keyStoreSecret, dnName, + userId, keyId, SignatureAlgorithm.ES256); + + processCibaAuthorizationEndpointSuccessfulCall(jwtAuthorizationRequest.getEncodedJwt(), + registerResponse.getClientId(), registerResponse.getClientSecret()); + } + + @Parameters({"ES384_keyId", "userId", "dnName", "keyStoreFile", "keyStoreSecret", "clientJwksUri"}) + @Test + public void pollFlowES384HappyFlow(final String keyId, final String userId, final String dnName, + final String keyStoreFile, final String keyStoreSecret, + final String clientJwksUri) throws Exception { + showTitle("pollFlowES384HappyFlow"); + registerPollClient(clientJwksUri, BackchannelTokenDeliveryMode.POLL, AsymmetricSignatureAlgorithm.ES384); + + JwtAuthorizationRequest jwtAuthorizationRequest = createJwtRequest(keyStoreFile, keyStoreSecret, dnName, + userId, keyId, SignatureAlgorithm.ES384); + + processCibaAuthorizationEndpointSuccessfulCall(jwtAuthorizationRequest.getEncodedJwt(), + registerResponse.getClientId(), registerResponse.getClientSecret()); + } + + @Parameters({"ES512_keyId", "userId", "dnName", "keyStoreFile", "keyStoreSecret", "clientJwksUri"}) + @Test + public void pollFlowES512HappyFlow(final String keyId, final String userId, final String dnName, + final String keyStoreFile, final String keyStoreSecret, + final String clientJwksUri) throws Exception { + showTitle("pollFlowES512HappyFlow"); + registerPollClient(clientJwksUri, BackchannelTokenDeliveryMode.POLL, AsymmetricSignatureAlgorithm.ES512); + + JwtAuthorizationRequest jwtAuthorizationRequest = createJwtRequest(keyStoreFile, keyStoreSecret, dnName, + userId, keyId, SignatureAlgorithm.ES512); + + processCibaAuthorizationEndpointSuccessfulCall(jwtAuthorizationRequest.getEncodedJwt(), + registerResponse.getClientId(), registerResponse.getClientSecret()); + } + + @Parameters({"PS256_keyId", "userId", "dnName", "keyStoreFile", "keyStoreSecret", "clientJwksUri"}) + @Test + public void cibaPollJWTRequestDataValidations(final String keyId, final String userId, final String dnName, + final String keyStoreFile, final String keyStoreSecret, + final String clientJwksUri) throws Exception { + showTitle("cibaPollJWTRequestDataValidations"); + registerPollClient(clientJwksUri, BackchannelTokenDeliveryMode.POLL, AsymmetricSignatureAlgorithm.PS256); + + String clientId = registerResponse.getClientId(); + + // 1. Request doesn't include Aud + JwtAuthorizationRequest jwtAuthorizationRequest = createJwtRequest(keyStoreFile, keyStoreSecret, dnName, userId, keyId, SignatureAlgorithm.PS256); + jwtAuthorizationRequest.setAud(null); + + processCibaAuthorizationEndpointFailCall(jwtAuthorizationRequest.getEncodedJwt(), clientId, + registerResponse.getClientSecret(), 400, BackchannelAuthenticationErrorResponseType.INVALID_REQUEST.getParameter()); + + // 2. Request doesn't include any hint + jwtAuthorizationRequest = createJwtRequest(keyStoreFile, keyStoreSecret, dnName, userId, keyId, SignatureAlgorithm.PS256); + jwtAuthorizationRequest.setLoginHint(null); + + processCibaAuthorizationEndpointFailCall(jwtAuthorizationRequest.getEncodedJwt(), clientId, + registerResponse.getClientSecret(), 400, BackchannelAuthenticationErrorResponseType.UNKNOWN_USER_ID.getParameter()); + + // 3. Request has a wrong Binding Message + jwtAuthorizationRequest = createJwtRequest(keyStoreFile, keyStoreSecret, dnName, userId, keyId, SignatureAlgorithm.PS256); + jwtAuthorizationRequest.setBindingMessage("(/)=&/(%&/(%$/&($%/&)"); + + processCibaAuthorizationEndpointFailCall(jwtAuthorizationRequest.getEncodedJwt(), clientId, + registerResponse.getClientSecret(), 400, BackchannelAuthenticationErrorResponseType.INVALID_BINDING_MESSAGE.getParameter()); + + // 4. Request has wrong Client Id + jwtAuthorizationRequest = createJwtRequest(keyStoreFile, keyStoreSecret, dnName, userId, keyId, SignatureAlgorithm.PS256); + jwtAuthorizationRequest.setClientId("abcabcabcabcabcabcabcabcabcabc"); + + processCibaAuthorizationEndpointFailCall(jwtAuthorizationRequest.getEncodedJwt(), "abcabcabcabcabcabcabcabcabcabc", + registerResponse.getClientSecret(), 401, BackchannelAuthenticationErrorResponseType.INVALID_CLIENT.getParameter()); + } + + @Parameters({"PS256_keyId", "userId", "dnName", "keyStoreFile", "keyStoreSecret", "clientJwksUri"}) + @Test(dependsOnMethods = "idTokenHintRS384") + public void cibaPollJWTRequestIdTokenHint(final String keyId, final String userId, final String dnName, + final String keyStoreFile, final String keyStoreSecret, + final String clientJwksUri) throws Exception { + showTitle("cibaPollJWTRequestIdTokenHint"); + registerPollClient(clientJwksUri, BackchannelTokenDeliveryMode.POLL, AsymmetricSignatureAlgorithm.PS256); + + // 1. Request doesn't include Aud + JwtAuthorizationRequest jwtAuthorizationRequest = createJwtRequest(keyStoreFile, keyStoreSecret, dnName, userId, keyId, SignatureAlgorithm.PS256); + jwtAuthorizationRequest.setLoginHint(null); + jwtAuthorizationRequest.setIdTokenHint(idTokenHintRS384); + + processCibaAuthorizationEndpointSuccessfulCall(jwtAuthorizationRequest.getEncodedJwt(), + registerResponse.getClientId(), registerResponse.getClientSecret()); + } + + @Parameters({"PS256_keyId", "userId", "dnName", "keyStoreFile", "keyStoreSecret", "clientJwksUri"}) + @Test + public void cibaPollJWTRequestWrongSigning(final String keyId, final String userId, final String dnName, + final String keyStoreFile, final String keyStoreSecret, + final String clientJwksUri) throws Exception { + showTitle("cibaPollJWTRequestWrongSigning"); + registerPollClient(clientJwksUri, BackchannelTokenDeliveryMode.POLL, AsymmetricSignatureAlgorithm.PS256); + + JwtAuthorizationRequest jwtAuthorizationRequest = createJwtRequest(keyStoreFile, keyStoreSecret, dnName, userId, keyId, SignatureAlgorithm.PS256); + + String jwt = jwtAuthorizationRequest.getEncodedJwt(); + String[] jwtParts = jwt.split("\\."); + String jwtWithWrongSigning = jwtParts[0] + "." + jwtParts[1] + ".WRONG-SIGNING"; + + processCibaAuthorizationEndpointFailCall(jwtWithWrongSigning, registerResponse.getClientId(), + registerResponse.getClientSecret(), 400, BackchannelAuthenticationErrorResponseType.INVALID_REQUEST.getParameter()); + } + + /** + * Registers a client using CIBA configuration for Poll flow and PS256 + * @param clientJwksUri + */ + private void registerPollClient(final String clientJwksUri, BackchannelTokenDeliveryMode mode, AsymmetricSignatureAlgorithm algorithm) { + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Collections.singletonList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(mode); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(algorithm); + registerRequest.setBackchannelUserCodeParameter(false); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + registerResponse = registerClient.exec(); + + showClient(registerClient); + + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), mode.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), algorithm.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), "false"); + } + + /** + * Process a Ciba call to the OP using JWT Request object. + * @param jwtRequest JWT in plain String. + * @param clientId Client identifier. + * @param clientSecret Client secret. + */ + private void processCibaAuthorizationEndpointSuccessfulCall(String jwtRequest, String clientId, String clientSecret) { + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setRequest(jwtRequest); + backchannelAuthenticationRequest.setClientId(clientId); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + + assertEquals(backchannelAuthenticationResponse.getStatus(), 200, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNotNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNotNull(backchannelAuthenticationResponse.getInterval()); // This parameter will only be present if the Client is registered to use the Poll or Ping modes. + } + + /** + * Process a Ciba call to the OP using JWT Request object and validate HTTP status and error type. + * @param jwtRequest JWT in plain String. + * @param clientId Client identifier. + * @param clientSecret Client secret. + * @param httpStatus Param used to validate response from the server. + * @param error Error used to validate error response from the server. + */ + private void processCibaAuthorizationEndpointFailCall(String jwtRequest, String clientId, String clientSecret, int httpStatus, String error) { + BackchannelAuthenticationRequest backchannelAuthenticationRequest = new BackchannelAuthenticationRequest(); + backchannelAuthenticationRequest.setRequest(jwtRequest); + backchannelAuthenticationRequest.setClientId(clientId); + backchannelAuthenticationRequest.setAuthUsername(clientId); + backchannelAuthenticationRequest.setAuthPassword(clientSecret); + + BackchannelAuthenticationClient backchannelAuthenticationClient = new BackchannelAuthenticationClient(backchannelAuthenticationEndpoint); + backchannelAuthenticationClient.setRequest(backchannelAuthenticationRequest); + BackchannelAuthenticationResponse backchannelAuthenticationResponse = backchannelAuthenticationClient.exec(); + + showClient(backchannelAuthenticationClient); + + assertEquals(backchannelAuthenticationResponse.getStatus(), httpStatus, "Unexpected response code: " + backchannelAuthenticationResponse.getEntity()); + assertNotNull(backchannelAuthenticationResponse.getErrorType()); + assertNotNull(backchannelAuthenticationResponse.getErrorDescription()); + assertEquals(error, backchannelAuthenticationResponse.getErrorType().getParameter()); + assertNull(backchannelAuthenticationResponse.getAuthReqId()); + assertNull(backchannelAuthenticationResponse.getExpiresIn()); + assertNull(backchannelAuthenticationResponse.getInterval()); + } + + /** + * Creates a new JwtAuthorizationRequest using default configuration and params. + */ + private JwtAuthorizationRequest createJwtRequest(String keyStoreFile, String keyStoreSecret, String dnName, + String userId, String keyId, SignatureAlgorithm signatureAlgorithm) throws Exception { + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String clientId = registerResponse.getClientId(); + + int now = (int)(System.currentTimeMillis() / 1000); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + null, signatureAlgorithm, cryptoProvider); + jwtAuthorizationRequest.setAud(issuer); + jwtAuthorizationRequest.setLoginHint(userId); + jwtAuthorizationRequest.setNbf(now); + jwtAuthorizationRequest.setScopes(Collections.singletonList("openid")); + jwtAuthorizationRequest.setIss(clientId); + jwtAuthorizationRequest.setBindingMessage("1234"); + jwtAuthorizationRequest.setExp((int)(DateUtils.addMinutes(new Date(), 5).getTime() / 1000)); + jwtAuthorizationRequest.setIat(now); + jwtAuthorizationRequest.setJti(UUID.randomUUID().toString()); + jwtAuthorizationRequest.setKeyId(keyId); + + return jwtAuthorizationRequest; + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void idTokenHintRS384( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("idTokenHintRS384"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS384); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS384, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + idTokenHintRS384 = idToken; + } + +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/ConfigurationTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/ConfigurationTest.java new file mode 100644 index 00000000..39a636eb --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/ConfigurationTest.java @@ -0,0 +1,71 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ciba; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.OpenIdConfigurationClient; +import org.gluu.oxauth.client.OpenIdConfigurationResponse; +import org.gluu.oxauth.client.OpenIdConnectDiscoveryClient; +import org.gluu.oxauth.client.OpenIdConnectDiscoveryResponse; +import org.gluu.oxauth.dev.HostnameVerifierType; +import org.gluu.oxauth.model.common.GrantType; +import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient43Engine; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Javier Rojas Blum + * @version August 20, 2019 + */ +public class ConfigurationTest extends BaseTest { + + @Test + @Parameters({"swdResource"}) + public void requestOpenIdConfiguration(final String resource) throws Exception { + showTitle("OpenID Connect Discovery"); + + OpenIdConnectDiscoveryClient openIdConnectDiscoveryClient = new OpenIdConnectDiscoveryClient(resource); + OpenIdConnectDiscoveryResponse openIdConnectDiscoveryResponse = openIdConnectDiscoveryClient.exec( + new ApacheHttpClient43Engine(createHttpClient(HostnameVerifierType.ALLOW_ALL))); + + showClient(openIdConnectDiscoveryClient); + assertEquals(openIdConnectDiscoveryResponse.getStatus(), 200, "Unexpected response code"); + assertNotNull(openIdConnectDiscoveryResponse.getSubject()); + assertTrue(openIdConnectDiscoveryResponse.getLinks().size() > 0); + + String configurationEndpoint = openIdConnectDiscoveryResponse.getLinks().get(0).getHref() + + "/.well-known/openid-configuration"; + + showTitle("OpenID Connect Configuration"); + + OpenIdConfigurationClient client = new OpenIdConfigurationClient(configurationEndpoint); + OpenIdConfigurationResponse response = client.execOpenIdConfiguration(); + + showClient(client); + assertEquals(response.getStatus(), 200, "Unexpected response code"); + assertNotNull(response.getIssuer(), "The issuer is null"); + assertNotNull(response.getAuthorizationEndpoint(), "The authorizationEndpoint is null"); + assertNotNull(response.getTokenEndpoint(), "The tokenEndpoint is null"); + assertNotNull(response.getRevocationEndpoint(), "The tokenRevocationEndpoint is null"); + assertNotNull(response.getUserInfoEndpoint(), "The userInfoEndPoint is null"); + assertNotNull(response.getEndSessionEndpoint(), "The endSessionEndpoint is null"); + assertNotNull(response.getJwksUri(), "The jwksUri is null"); + assertNotNull(response.getRegistrationEndpoint(), "The registrationEndpoint is null"); + + assertTrue(response.getGrantTypesSupported().size() > 0, "The grantTypesSupported is empty"); + assertTrue(response.getGrantTypesSupported().contains(GrantType.CIBA.getParamName()), "The grantTypes urn:openid:params:grant-type:ciba is null"); + + assertNotNull(response.getBackchannelAuthenticationEndpoint(), "The backchannelAuthenticationEndpoint is null"); + assertTrue(response.getBackchannelTokenDeliveryModesSupported().size() > 0, "The backchannelTokenDeliveryModesSupported is empty"); + assertTrue(response.getBackchannelAuthenticationRequestSigningAlgValuesSupported().size() > 0, "The backchannelAuthenticationRequestSigningAlgValuesSupported is empty"); + assertNotNull(response.getBackchannelUserCodeParameterSupported(), "The backchannelUserCodeParameterSupported is null"); + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/RegistrationTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/RegistrationTest.java new file mode 100644 index 00000000..da59e9e1 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ciba/RegistrationTest.java @@ -0,0 +1,1260 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ciba; + +import static org.gluu.oxauth.model.register.RegisterRequestParam.APPLICATION_TYPE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_TOKEN_DELIVERY_MODE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_USER_CODE_PARAMETER; +import static org.gluu.oxauth.model.register.RegisterRequestParam.CLIENT_NAME; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ID_TOKEN_SIGNED_RESPONSE_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.JWKS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.JWKS_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SCOPE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SECTOR_IDENTIFIER_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SUBJECT_TYPE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.TOKEN_ENDPOINT_AUTH_METHOD; +import static org.gluu.oxauth.model.register.RegisterRequestParam.TOKEN_ENDPOINT_AUTH_SIGNING_ALG; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; + +import javax.ws.rs.HttpMethod; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.JwkResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.BackchannelTokenDeliveryMode; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.SubjectType; +import org.gluu.oxauth.model.crypto.signature.AsymmetricSignatureAlgorithm; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.register.ApplicationType; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Javier Rojas Blum + * @version May 20, 2020 + */ +public class RegistrationTest extends BaseTest { + + @Parameters({"clientJwksUri"}) + @Test + public void backchannelTokenDeliveryModePoll1(final String clientJwksUri) { + showTitle("backchannelTokenDeliveryModePoll1"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client Read + RegisterRequest clientReadRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient clientReadClient = new RegisterClient(registrationClientUri); + clientReadClient.setRequest(clientReadRequest); + RegisterResponse clientReadResponse = clientReadClient.exec(); + + showClient(clientReadClient); + assertEquals(clientReadResponse.getStatus(), 200, "Unexpected response code: " + clientReadResponse.getEntity()); + assertNotNull(clientReadResponse.getClientId()); + assertNotNull(clientReadResponse.getClientSecret()); + assertNotNull(clientReadResponse.getRegistrationAccessToken()); + assertNotNull(clientReadResponse.getRegistrationClientUri()); + assertNotNull(clientReadResponse.getClientSecretExpiresAt()); + assertNotNull(clientReadResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(clientReadResponse.getClaims().get(SUBJECT_TYPE.toString())); + assertNotNull(clientReadResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(clientReadResponse.getClaims().get(JWKS_URI.toString())); + assertNotNull(clientReadResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(clientReadResponse.getClaims().get(SCOPE.toString())); + + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + } + + @Parameters({"sectorIdentifierUri", "clientJwksUri"}) + @Test + public void backchannelTokenDeliveryModePoll2(final String sectorIdentifierUri, final String clientJwksUri) { + showTitle("backchannelTokenDeliveryModePoll2"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(JWKS_URI.toString())); + assertTrue(registerResponse.getClaims().containsKey(SECTOR_IDENTIFIER_URI.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client Read + RegisterRequest clientReadRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient clientReadClient = new RegisterClient(registrationClientUri); + clientReadClient.setRequest(clientReadRequest); + RegisterResponse clientReadResponse = clientReadClient.exec(); + + showClient(clientReadClient); + assertEquals(clientReadResponse.getStatus(), 200, "Unexpected response code: " + clientReadResponse.getEntity()); + assertNotNull(clientReadResponse.getClientId()); + assertNotNull(clientReadResponse.getClientSecret()); + assertNotNull(clientReadResponse.getRegistrationAccessToken()); + assertNotNull(clientReadResponse.getRegistrationClientUri()); + assertNotNull(clientReadResponse.getClientSecretExpiresAt()); + assertNotNull(clientReadResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(clientReadResponse.getClaims().get(SECTOR_IDENTIFIER_URI.toString())); + assertNotNull(clientReadResponse.getClaims().get(SUBJECT_TYPE.toString())); + assertNotNull(clientReadResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(clientReadResponse.getClaims().get(JWKS_URI.toString())); + assertNotNull(clientReadResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(clientReadResponse.getClaims().get(SCOPE.toString())); + + assertTrue(clientReadResponse.getClaims().containsKey(JWKS_URI.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(SECTOR_IDENTIFIER_URI.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + } + + @Test + public void backchannelTokenDeliveryModePoll3() { + showTitle("backchannelTokenDeliveryModePoll3"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setSubjectType(SubjectType.PUBLIC); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client Read + RegisterRequest clientReadRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient clientReadClient = new RegisterClient(registrationClientUri); + clientReadClient.setRequest(clientReadRequest); + RegisterResponse clientReadResponse = clientReadClient.exec(); + + showClient(clientReadClient); + assertEquals(clientReadResponse.getStatus(), 200, "Unexpected response code: " + clientReadResponse.getEntity()); + assertNotNull(clientReadResponse.getClientId()); + assertNotNull(clientReadResponse.getClientSecret()); + assertNotNull(clientReadResponse.getRegistrationAccessToken()); + assertNotNull(clientReadResponse.getRegistrationClientUri()); + assertNotNull(clientReadResponse.getClientSecretExpiresAt()); + assertNotNull(clientReadResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(clientReadResponse.getClaims().get(SUBJECT_TYPE.toString())); + assertNotNull(clientReadResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(clientReadResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(clientReadResponse.getClaims().get(SCOPE.toString())); + + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + } + + @Parameters({"sectorIdentifierUri", "clientJwksUri"}) + @Test + public void backchannelTokenDeliveryModePoll4(final String sectorIdentifierUri, final String clientJwksUri) { + showTitle("backchannelTokenDeliveryModePoll4"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client Read + RegisterRequest clientReadRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient clientReadClient = new RegisterClient(registrationClientUri); + clientReadClient.setRequest(clientReadRequest); + RegisterResponse clientReadResponse = clientReadClient.exec(); + + showClient(clientReadClient); + assertEquals(clientReadResponse.getStatus(), 200, "Unexpected response code: " + clientReadResponse.getEntity()); + assertNotNull(clientReadResponse.getClientId()); + assertNotNull(clientReadResponse.getClientSecret()); + assertNotNull(clientReadResponse.getRegistrationAccessToken()); + assertNotNull(clientReadResponse.getRegistrationClientUri()); + assertNotNull(clientReadResponse.getClientSecretExpiresAt()); + assertNotNull(clientReadResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(clientReadResponse.getClaims().get(SUBJECT_TYPE.toString())); + assertNotNull(clientReadResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(clientReadResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(clientReadResponse.getClaims().get(SCOPE.toString())); + + // 3. Client Update + RegisterRequest clientUpdateRequest = new RegisterRequest(registrationAccessToken); + clientUpdateRequest.setHttpMethod(HttpMethod.PUT); + + clientUpdateRequest.setSectorIdentifierUri(sectorIdentifierUri); + clientUpdateRequest.setJwksUri(clientJwksUri); + clientUpdateRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + clientUpdateRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + clientUpdateRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + clientUpdateRequest.setBackchannelUserCodeParameter(true); + + RegisterClient clientUpdateClient = new RegisterClient(registrationClientUri); + clientUpdateClient.setRequest(clientUpdateRequest); + + RegisterResponse clientUpdateResponse = clientUpdateClient.exec(); + + showClient(clientUpdateClient); + assertEquals(clientUpdateResponse.getStatus(), 200, "Unexpected response code: " + clientUpdateResponse.getEntity()); + assertNotNull(clientUpdateResponse.getClientId()); + assertNotNull(clientUpdateResponse.getClientSecret()); + assertNotNull(clientUpdateResponse.getRegistrationAccessToken()); + assertNotNull(clientUpdateResponse.getRegistrationClientUri()); + assertNotNull(clientUpdateResponse.getClientSecretExpiresAt()); + assertNotNull(clientUpdateResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(clientUpdateResponse.getClaims().get(SECTOR_IDENTIFIER_URI.toString())); + assertNotNull(clientUpdateResponse.getClaims().get(SUBJECT_TYPE.toString())); + assertNotNull(clientUpdateResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(clientUpdateResponse.getClaims().get(JWKS_URI.toString())); + assertNotNull(clientUpdateResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(clientUpdateResponse.getClaims().get(SCOPE.toString())); + + assertTrue(clientUpdateResponse.getClaims().containsKey(JWKS_URI.toString())); + assertTrue(clientUpdateResponse.getClaims().containsKey(SECTOR_IDENTIFIER_URI.toString())); + assertTrue(clientUpdateResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(clientUpdateResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(clientUpdateResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertEquals(clientUpdateResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(clientUpdateResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(clientUpdateResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + } + + @Parameters({"clientJwksUri"}) + public void backchannelTokenDeliveryModePoll5(final String clientJwksUri) { + showTitle("backchannelTokenDeliveryModePoll5"); + + // 1. Dynamic Client Registration + JwkClient jwkClient = new JwkClient(clientJwksUri); + JwkResponse jwkResponse = jwkClient.exec(); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwks(jwkResponse.getJwks().toString()); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.PS256); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.PS256); + registerRequest.setBackchannelUserCodeParameter(false); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.PS256); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + assertNotNull(registerResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(registerResponse.getClaims().get(SUBJECT_TYPE.toString())); + assertNotNull(registerResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(registerResponse.getClaims().get(JWKS.toString())); + assertNotNull(registerResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(registerResponse.getClaims().get(SCOPE.toString())); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.PS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(false).toString()); + assertEquals(registerResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString()), SignatureAlgorithm.PS256.getName()); + assertEquals(registerResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), SignatureAlgorithm.PS256.getName()); + assertEquals(registerResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client Read + RegisterRequest clientReadRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient clientReadClient = new RegisterClient(registrationClientUri); + clientReadClient.setRequest(clientReadRequest); + RegisterResponse clientReadResponse = clientReadClient.exec(); + + showClient(clientReadClient); + assertEquals(clientReadResponse.getStatus(), 200, "Unexpected response code: " + clientReadResponse.getEntity()); + assertNotNull(clientReadResponse.getClientId()); + assertNotNull(clientReadResponse.getClientSecret()); + assertNotNull(clientReadResponse.getRegistrationAccessToken()); + assertNotNull(clientReadResponse.getRegistrationClientUri()); + assertNotNull(clientReadResponse.getClientSecretExpiresAt()); + assertNotNull(clientReadResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(clientReadResponse.getClaims().get(SUBJECT_TYPE.toString())); + assertNotNull(clientReadResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(clientReadResponse.getClaims().get(JWKS.toString())); + assertNotNull(clientReadResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(clientReadResponse.getClaims().get(SCOPE.toString())); + + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.POLL.getValue()); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.PS256.getValue()); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(false).toString()); + assertEquals(clientReadResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString()), SignatureAlgorithm.PS256.getName()); + assertEquals(clientReadResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), SignatureAlgorithm.PS256.getName()); + assertEquals(clientReadResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint"}) + @Test + public void backchannelTokenDeliveryModePing1(final String clientJwksUri, + final String backchannelClientNotificationEndpoint) { + showTitle("backchannelTokenDeliveryModePing1"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client Read + RegisterRequest clientReadRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient clientReadClient = new RegisterClient(registrationClientUri); + clientReadClient.setRequest(clientReadRequest); + RegisterResponse clientReadResponse = clientReadClient.exec(); + + showClient(clientReadClient); + assertEquals(clientReadResponse.getStatus(), 200, "Unexpected response code: " + clientReadResponse.getEntity()); + assertNotNull(clientReadResponse.getClientId()); + assertNotNull(clientReadResponse.getClientSecret()); + assertNotNull(clientReadResponse.getRegistrationAccessToken()); + assertNotNull(clientReadResponse.getRegistrationClientUri()); + assertNotNull(clientReadResponse.getClientSecretExpiresAt()); + assertNotNull(clientReadResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(clientReadResponse.getClaims().get(SUBJECT_TYPE.toString())); + assertNotNull(clientReadResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(clientReadResponse.getClaims().get(JWKS_URI.toString())); + assertNotNull(clientReadResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(clientReadResponse.getClaims().get(SCOPE.toString())); + + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + } + + @Parameters({"sectorIdentifierUri", "clientJwksUri", "backchannelClientNotificationEndpoint"}) + @Test + public void backchannelTokenDeliveryModePing2(final String sectorIdentifierUri, final String clientJwksUri, + final String backchannelClientNotificationEndpoint) { + showTitle("backchannelTokenDeliveryModePing2"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(JWKS_URI.toString())); + assertTrue(registerResponse.getClaims().containsKey(SECTOR_IDENTIFIER_URI.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client Read + RegisterRequest clientReadRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient clientReadClient = new RegisterClient(registrationClientUri); + clientReadClient.setRequest(clientReadRequest); + RegisterResponse clientReadResponse = clientReadClient.exec(); + + showClient(clientReadClient); + assertEquals(clientReadResponse.getStatus(), 200, "Unexpected response code: " + clientReadResponse.getEntity()); + assertNotNull(clientReadResponse.getClientId()); + assertNotNull(clientReadResponse.getClientSecret()); + assertNotNull(clientReadResponse.getRegistrationAccessToken()); + assertNotNull(clientReadResponse.getRegistrationClientUri()); + assertNotNull(clientReadResponse.getClientSecretExpiresAt()); + assertNotNull(clientReadResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(clientReadResponse.getClaims().get(SECTOR_IDENTIFIER_URI.toString())); + assertNotNull(clientReadResponse.getClaims().get(SUBJECT_TYPE.toString())); + assertNotNull(clientReadResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(clientReadResponse.getClaims().get(JWKS_URI.toString())); + assertNotNull(clientReadResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(clientReadResponse.getClaims().get(SCOPE.toString())); + + assertTrue(clientReadResponse.getClaims().containsKey(JWKS_URI.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(SECTOR_IDENTIFIER_URI.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + } + + @Parameters({"backchannelClientNotificationEndpoint"}) + @Test + public void backchannelTokenDeliveryModePing3(final String backchannelClientNotificationEndpoint) { + showTitle("backchannelTokenDeliveryModePing3"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setSubjectType(SubjectType.PUBLIC); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client Read + RegisterRequest clientReadRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient clientReadClient = new RegisterClient(registrationClientUri); + clientReadClient.setRequest(clientReadRequest); + RegisterResponse clientReadResponse = clientReadClient.exec(); + + showClient(clientReadClient); + assertEquals(clientReadResponse.getStatus(), 200, "Unexpected response code: " + clientReadResponse.getEntity()); + assertNotNull(clientReadResponse.getClientId()); + assertNotNull(clientReadResponse.getClientSecret()); + assertNotNull(clientReadResponse.getRegistrationAccessToken()); + assertNotNull(clientReadResponse.getRegistrationClientUri()); + assertNotNull(clientReadResponse.getClientSecretExpiresAt()); + assertNotNull(clientReadResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(clientReadResponse.getClaims().get(SUBJECT_TYPE.toString())); + assertNotNull(clientReadResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(clientReadResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(clientReadResponse.getClaims().get(SCOPE.toString())); + + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + } + + @Parameters({"sectorIdentifierUri", "clientJwksUri", "backchannelClientNotificationEndpoint"}) + @Test + public void backchannelTokenDeliveryModePing4(final String sectorIdentifierUri, final String clientJwksUri, + final String backchannelClientNotificationEndpoint) { + showTitle("backchannelTokenDeliveryModePing4"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client Read + RegisterRequest clientReadRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient clientReadClient = new RegisterClient(registrationClientUri); + clientReadClient.setRequest(clientReadRequest); + RegisterResponse clientReadResponse = clientReadClient.exec(); + + showClient(clientReadClient); + assertEquals(clientReadResponse.getStatus(), 200, "Unexpected response code: " + clientReadResponse.getEntity()); + assertNotNull(clientReadResponse.getClientId()); + assertNotNull(clientReadResponse.getClientSecret()); + assertNotNull(clientReadResponse.getRegistrationAccessToken()); + assertNotNull(clientReadResponse.getRegistrationClientUri()); + assertNotNull(clientReadResponse.getClientSecretExpiresAt()); + assertNotNull(clientReadResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(clientReadResponse.getClaims().get(SUBJECT_TYPE.toString())); + assertNotNull(clientReadResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(clientReadResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(clientReadResponse.getClaims().get(SCOPE.toString())); + + // 3. Client Update + RegisterRequest clientUpdateRequest = new RegisterRequest(registrationAccessToken); + clientUpdateRequest.setHttpMethod(HttpMethod.PUT); + + clientUpdateRequest.setSectorIdentifierUri(sectorIdentifierUri); + clientUpdateRequest.setJwksUri(clientJwksUri); + clientUpdateRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + clientUpdateRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + clientUpdateRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + clientUpdateRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + clientUpdateRequest.setBackchannelUserCodeParameter(true); + + RegisterClient clientUpdateClient = new RegisterClient(registrationClientUri); + clientUpdateClient.setRequest(clientUpdateRequest); + + RegisterResponse clientUpdateResponse = clientUpdateClient.exec(); + + showClient(clientUpdateClient); + assertEquals(clientUpdateResponse.getStatus(), 200, "Unexpected response code: " + clientUpdateResponse.getEntity()); + assertNotNull(clientUpdateResponse.getClientId()); + assertNotNull(clientUpdateResponse.getClientSecret()); + assertNotNull(clientUpdateResponse.getRegistrationAccessToken()); + assertNotNull(clientUpdateResponse.getRegistrationClientUri()); + assertNotNull(clientUpdateResponse.getClientSecretExpiresAt()); + assertNotNull(clientUpdateResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(clientUpdateResponse.getClaims().get(SECTOR_IDENTIFIER_URI.toString())); + assertNotNull(clientUpdateResponse.getClaims().get(SUBJECT_TYPE.toString())); + assertNotNull(clientUpdateResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(clientUpdateResponse.getClaims().get(JWKS_URI.toString())); + assertNotNull(clientUpdateResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(clientUpdateResponse.getClaims().get(SCOPE.toString())); + + assertTrue(clientUpdateResponse.getClaims().containsKey(JWKS_URI.toString())); + assertTrue(clientUpdateResponse.getClaims().containsKey(SECTOR_IDENTIFIER_URI.toString())); + assertTrue(clientUpdateResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(clientUpdateResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(clientUpdateResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(clientUpdateResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(clientUpdateResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(clientUpdateResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(clientUpdateResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint"}) + public void backchannelTokenDeliveryModePing5(final String clientJwksUri, + final String backchannelClientNotificationEndpoint) { + showTitle("backchannelTokenDeliveryModePing5"); + + // 1. Dynamic Client Registration + JwkClient jwkClient = new JwkClient(clientJwksUri); + JwkResponse jwkResponse = jwkClient.exec(); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwks(jwkResponse.getJwks().toString()); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.PS256); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.PS256); + registerRequest.setBackchannelUserCodeParameter(false); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.PS256); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + assertNotNull(registerResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(registerResponse.getClaims().get(SUBJECT_TYPE.toString())); + assertNotNull(registerResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(registerResponse.getClaims().get(JWKS.toString())); + assertNotNull(registerResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(registerResponse.getClaims().get(SCOPE.toString())); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.PS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(false).toString()); + assertEquals(registerResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString()), SignatureAlgorithm.PS256.getName()); + assertEquals(registerResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), SignatureAlgorithm.PS256.getName()); + assertEquals(registerResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client Read + RegisterRequest clientReadRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient clientReadClient = new RegisterClient(registrationClientUri); + clientReadClient.setRequest(clientReadRequest); + RegisterResponse clientReadResponse = clientReadClient.exec(); + + showClient(clientReadClient); + assertEquals(clientReadResponse.getStatus(), 200, "Unexpected response code: " + clientReadResponse.getEntity()); + assertNotNull(clientReadResponse.getClientId()); + assertNotNull(clientReadResponse.getClientSecret()); + assertNotNull(clientReadResponse.getRegistrationAccessToken()); + assertNotNull(clientReadResponse.getRegistrationClientUri()); + assertNotNull(clientReadResponse.getClientSecretExpiresAt()); + assertNotNull(clientReadResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(clientReadResponse.getClaims().get(SUBJECT_TYPE.toString())); + assertNotNull(clientReadResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(clientReadResponse.getClaims().get(JWKS.toString())); + assertNotNull(clientReadResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(clientReadResponse.getClaims().get(SCOPE.toString())); + + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PING.getValue()); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.PS256.getValue()); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(false).toString()); + assertEquals(clientReadResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString()), SignatureAlgorithm.PS256.getName()); + assertEquals(clientReadResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), SignatureAlgorithm.PS256.getName()); + assertEquals(clientReadResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + } + + @Parameters({"backchannelClientNotificationEndpoint"}) + @Test + public void backchannelTokenDeliveryModePush1(final String backchannelClientNotificationEndpoint) { + showTitle("backchannelTokenDeliveryModePush1"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client Read + RegisterRequest clientReadRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient clientReadClient = new RegisterClient(registrationClientUri); + clientReadClient.setRequest(clientReadRequest); + RegisterResponse clientReadResponse = clientReadClient.exec(); + + showClient(clientReadClient); + assertEquals(clientReadResponse.getStatus(), 200, "Unexpected response code: " + clientReadResponse.getEntity()); + assertNotNull(clientReadResponse.getClientId()); + assertNotNull(clientReadResponse.getClientSecret()); + assertNotNull(clientReadResponse.getRegistrationAccessToken()); + assertNotNull(clientReadResponse.getRegistrationClientUri()); + assertNotNull(clientReadResponse.getClientSecretExpiresAt()); + assertNotNull(clientReadResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(clientReadResponse.getClaims().get(SUBJECT_TYPE.toString())); + assertNotNull(clientReadResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(clientReadResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(clientReadResponse.getClaims().get(SCOPE.toString())); + + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + } + + @Parameters({"sectorIdentifierUri", "backchannelClientNotificationEndpoint"}) + @Test + public void backchannelTokenDeliveryModePush2(final String sectorIdentifierUri, final String backchannelClientNotificationEndpoint) { + showTitle("backchannelTokenDeliveryModePush2"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(SECTOR_IDENTIFIER_URI.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client Read + RegisterRequest clientReadRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient clientReadClient = new RegisterClient(registrationClientUri); + clientReadClient.setRequest(clientReadRequest); + RegisterResponse clientReadResponse = clientReadClient.exec(); + + showClient(clientReadClient); + assertEquals(clientReadResponse.getStatus(), 200, "Unexpected response code: " + clientReadResponse.getEntity()); + assertNotNull(clientReadResponse.getClientId()); + assertNotNull(clientReadResponse.getClientSecret()); + assertNotNull(clientReadResponse.getRegistrationAccessToken()); + assertNotNull(clientReadResponse.getRegistrationClientUri()); + assertNotNull(clientReadResponse.getClientSecretExpiresAt()); + assertNotNull(clientReadResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(clientReadResponse.getClaims().get(SECTOR_IDENTIFIER_URI.toString())); + assertNotNull(clientReadResponse.getClaims().get(SUBJECT_TYPE.toString())); + assertNotNull(clientReadResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(clientReadResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(clientReadResponse.getClaims().get(SCOPE.toString())); + + assertTrue(clientReadResponse.getClaims().containsKey(SECTOR_IDENTIFIER_URI.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + } + + @Parameters({"backchannelClientNotificationEndpoint"}) + @Test + public void backchannelTokenDeliveryModePush3(final String backchannelClientNotificationEndpoint) { + showTitle("backchannelTokenDeliveryModePush3"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setSubjectType(SubjectType.PUBLIC); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(registerResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(registerResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client Read + RegisterRequest clientReadRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient clientReadClient = new RegisterClient(registrationClientUri); + clientReadClient.setRequest(clientReadRequest); + RegisterResponse clientReadResponse = clientReadClient.exec(); + + showClient(clientReadClient); + assertEquals(clientReadResponse.getStatus(), 200, "Unexpected response code: " + clientReadResponse.getEntity()); + assertNotNull(clientReadResponse.getClientId()); + assertNotNull(clientReadResponse.getClientSecret()); + assertNotNull(clientReadResponse.getRegistrationAccessToken()); + assertNotNull(clientReadResponse.getRegistrationClientUri()); + assertNotNull(clientReadResponse.getClientSecretExpiresAt()); + assertNotNull(clientReadResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(clientReadResponse.getClaims().get(SUBJECT_TYPE.toString())); + assertNotNull(clientReadResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(clientReadResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(clientReadResponse.getClaims().get(SCOPE.toString())); + + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(clientReadResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(clientReadResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + } + + @Parameters({"sectorIdentifierUri", "backchannelClientNotificationEndpoint"}) + @Test + public void backchannelTokenDeliveryModePush4(final String sectorIdentifierUri, final String backchannelClientNotificationEndpoint) { + showTitle("backchannelTokenDeliveryModePush4"); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client Read + RegisterRequest clientReadRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient clientReadClient = new RegisterClient(registrationClientUri); + clientReadClient.setRequest(clientReadRequest); + RegisterResponse clientReadResponse = clientReadClient.exec(); + + showClient(clientReadClient); + assertEquals(clientReadResponse.getStatus(), 200, "Unexpected response code: " + clientReadResponse.getEntity()); + assertNotNull(clientReadResponse.getClientId()); + assertNotNull(clientReadResponse.getClientSecret()); + assertNotNull(clientReadResponse.getRegistrationAccessToken()); + assertNotNull(clientReadResponse.getRegistrationClientUri()); + assertNotNull(clientReadResponse.getClientSecretExpiresAt()); + assertNotNull(clientReadResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(clientReadResponse.getClaims().get(SUBJECT_TYPE.toString())); + assertNotNull(clientReadResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(clientReadResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(clientReadResponse.getClaims().get(SCOPE.toString())); + + // 3. Client Update + RegisterRequest clientUpdateRequest = new RegisterRequest(registrationAccessToken); + clientUpdateRequest.setHttpMethod(HttpMethod.PUT); + + clientUpdateRequest.setSectorIdentifierUri(sectorIdentifierUri); + clientUpdateRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + clientUpdateRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + clientUpdateRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + clientUpdateRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + clientUpdateRequest.setBackchannelUserCodeParameter(true); + + RegisterClient clientUpdateClient = new RegisterClient(registrationClientUri); + clientUpdateClient.setRequest(clientUpdateRequest); + + RegisterResponse clientUpdateResponse = clientUpdateClient.exec(); + + showClient(clientUpdateClient); + assertEquals(clientUpdateResponse.getStatus(), 200, "Unexpected response code: " + clientUpdateResponse.getEntity()); + assertNotNull(clientUpdateResponse.getClientId()); + assertNotNull(clientUpdateResponse.getClientSecret()); + assertNotNull(clientUpdateResponse.getRegistrationAccessToken()); + assertNotNull(clientUpdateResponse.getRegistrationClientUri()); + assertNotNull(clientUpdateResponse.getClientSecretExpiresAt()); + assertNotNull(clientUpdateResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(clientUpdateResponse.getClaims().get(SECTOR_IDENTIFIER_URI.toString())); + assertNotNull(clientUpdateResponse.getClaims().get(SUBJECT_TYPE.toString())); + assertNotNull(clientUpdateResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(clientUpdateResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(clientUpdateResponse.getClaims().get(SCOPE.toString())); + + assertTrue(clientUpdateResponse.getClaims().containsKey(SECTOR_IDENTIFIER_URI.toString())); + assertTrue(clientUpdateResponse.getClaims().containsKey(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString())); + assertTrue(clientUpdateResponse.getClaims().containsKey(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString())); + assertTrue(clientUpdateResponse.getClaims().containsKey(BACKCHANNEL_USER_CODE_PARAMETER.toString())); + assertTrue(clientUpdateResponse.getClaims().containsKey(BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString())); + assertEquals(clientUpdateResponse.getClaims().get(BACKCHANNEL_TOKEN_DELIVERY_MODE.toString()), BackchannelTokenDeliveryMode.PUSH.getValue()); + assertEquals(clientUpdateResponse.getClaims().get(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString()), AsymmetricSignatureAlgorithm.RS256.getValue()); + assertEquals(clientUpdateResponse.getClaims().get(BACKCHANNEL_USER_CODE_PARAMETER.toString()), new Boolean(true).toString()); + } + + @Parameters({"clientJwksUri"}) + @Test + public void registrationFail1(final String clientJwksUri) { + showTitle("registrationFail1"); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(null); // Missing backchannel_token_delivery_mode + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 400, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getEntity(), "The entity is null"); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + } + + @Parameters({"clientJwksUri"}) + @Test + public void registrationFail2(final String clientJwksUri) { + showTitle("registrationFail2"); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(null); // Missing backchannel_client_notification_endpoint + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 400, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getEntity(), "The entity is null"); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + } + + @Parameters({"clientJwksUri"}) + @Test + public void registrationFail3(final String clientJwksUri) { + showTitle("registration3"); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PUSH); + registerRequest.setBackchannelClientNotificationEndpoint(null); // Missing backchannel_client_notification_endpoint + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 400, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getEntity(), "The entity is null"); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint"}) + @Test + public void registrationFail4(final String clientJwksUri, final String backchannelClientNotificationEndpoint) { + showTitle("registrationFail4"); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList()); // Missing grant type urn:openid:params:grant-type:ciba + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 400, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getEntity(), "The entity is null"); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + } + + @Parameters({"clientJwksUri", "backchannelClientNotificationEndpoint"}) + @Test + public void registrationFail5(final String clientJwksUri, final String backchannelClientNotificationEndpoint) { + showTitle("registrationFail5"); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setGrantTypes(Arrays.asList()); // Missing grant type urn:openid:params:grant-type:ciba + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 400, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getEntity(), "The entity is null"); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + } + + @Parameters({"backchannelClientNotificationEndpoint"}) + @Test + public void registrationFail6(final String backchannelClientNotificationEndpoint) { + showTitle("registrationFail6"); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(null); // Missing jwks_uri + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.PING); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 400, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getEntity(), "The entity is null"); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + } + + @Parameters({"backchannelClientNotificationEndpoint"}) + @Test + public void registrationFail7(final String backchannelClientNotificationEndpoint) { + showTitle("registrationFail7"); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", null); + registerRequest.setJwksUri(null); // Missing jwks_uri + registerRequest.setGrantTypes(Arrays.asList(GrantType.CIBA)); + + registerRequest.setBackchannelTokenDeliveryMode(BackchannelTokenDeliveryMode.POLL); + registerRequest.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + registerRequest.setBackchannelAuthenticationRequestSigningAlg(AsymmetricSignatureAlgorithm.RS256); + registerRequest.setBackchannelUserCodeParameter(true); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 400, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getEntity(), "The entity is null"); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/client/Asserter.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/client/Asserter.java new file mode 100644 index 00000000..8a6021b2 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/client/Asserter.java @@ -0,0 +1,50 @@ +package org.gluu.oxauth.client; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 25/03/2016 + */ + +public class Asserter { + + private Asserter() { + } + + public static void assertOk(RegisterResponse registerResponse) { + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + } + + public static void assertIdToken(Jwt idToken, String... claimsPresence) { + assertNotNull(idToken); + assertNotNull(idToken.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(idToken.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(idToken.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(idToken.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(idToken.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(idToken.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(idToken.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(idToken.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(idToken.getClaims().getClaimAsString(JwtClaimName.OX_OPENID_CONNECT_VERSION)); + assertNotNull(idToken.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE)); + assertNotNull(idToken.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_METHOD_REFERENCES)); + + if (claimsPresence == null) { + return; + } + + for (String claim : claimsPresence) { + assertNotNull(claim, "Claim " + claim + " is not found in id_token. "); + } + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/client/JSONObjectAsserter.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/client/JSONObjectAsserter.java new file mode 100644 index 00000000..50858203 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/client/JSONObjectAsserter.java @@ -0,0 +1,41 @@ +package org.gluu.oxauth.client; + +import static org.testng.Assert.assertTrue; +import static org.testng.AssertJUnit.assertNotNull; + +import org.apache.commons.lang.ArrayUtils; +import org.json.JSONObject; + +import com.google.common.base.Preconditions; + +/** + * @author yuriyz + */ +public class JSONObjectAsserter { + + private JSONObject json; + + private JSONObjectAsserter(JSONObject json) { + Preconditions.checkNotNull(json); + this.json = json; + } + + public static JSONObjectAsserter of(JSONObject json) { + assertNotNull(json); + return new JSONObjectAsserter(json); + } + + public JSONObjectAsserter hasKeys(String... keys) { + if (!ArrayUtils.isEmpty(keys)) { + for (String key : keys) { + assertTrue(json.has(key)); + } + } + + return this; + } + + public JSONObject getJson() { + return json; + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/client/RegisterRequestTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/client/RegisterRequestTest.java new file mode 100644 index 00000000..70e10f8b --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/client/RegisterRequestTest.java @@ -0,0 +1,41 @@ +package org.gluu.oxauth.client; + +import static org.testng.Assert.assertEquals; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.testng.annotations.Test; + +import com.google.common.collect.Lists; + +/** + * @author Yuriy Zabrovarnyy + */ +public class RegisterRequestTest { + + @Test + public void getParameters_forAdditionalAudience_shouldReturnCorrectValue() { + RegisterRequest request = new RegisterRequest(); + request.setAdditionalAudience(Lists.newArrayList("aud1", "aud2")); + + assertEquals(new JSONArray(Lists.newArrayList("aud1", "aud2")).toString(), request.getParameters().get("additional_audience")); + } + + @Test + public void fromJson_forAdditionalAudience_shouldReturnCorrectValue() { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("additional_audience", new JSONArray(Lists.newArrayList("aud1", "aud2"))); + + final RegisterRequest registerRequest = RegisterRequest.fromJson(jsonObject.toString(), true); + + assertEquals(Lists.newArrayList("aud1", "aud2"), registerRequest.getAdditionalAudience()); + } + + @Test + public void getJSONParameters_forAdditionalAudience_shouldReturnCorrectValue() { + RegisterRequest request = new RegisterRequest(); + request.setAdditionalAudience(Lists.newArrayList("aud1", "aud2")); + + assertEquals(new JSONArray(Lists.newArrayList("aud1", "aud2")), request.getJSONParameters().getJSONArray("additional_audience")); + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/client/ResponseAsserter.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/client/ResponseAsserter.java new file mode 100644 index 00000000..d976d9ef --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/client/ResponseAsserter.java @@ -0,0 +1,77 @@ +package org.gluu.oxauth.client; + +import static org.gluu.oxauth.model.register.RegisterResponseParam.CLIENT_ID_ISSUED_AT; +import static org.gluu.oxauth.model.register.RegisterResponseParam.CLIENT_SECRET; +import static org.gluu.oxauth.model.register.RegisterResponseParam.CLIENT_SECRET_EXPIRES_AT; +import static org.gluu.oxauth.model.register.RegisterResponseParam.REGISTRATION_ACCESS_TOKEN; +import static org.gluu.oxauth.model.register.RegisterResponseParam.REGISTRATION_CLIENT_URI; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.fail; + +import org.gluu.oxauth.model.register.RegisterResponseParam; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * @author yuriyz + */ +public class ResponseAsserter { + + private final int status; + private final String entity; + + private JSONObjectAsserter json; + + public ResponseAsserter(int status, String entity) { + this.status = status; + this.entity = entity; + } + + public static ResponseAsserter of(int status, String entity) { + return new ResponseAsserter(status, entity); + } + + public ResponseAsserter assertStatus(int expectedStatusCode) { + assertEquals(status, expectedStatusCode, "Unexpected status code: " + status); + return this; + } + + public ResponseAsserter assertStatusOk() { + assertStatus(200); + return this; + } + + public JSONObjectAsserter assertJsonObject() { + try { + return JSONObjectAsserter.of(new JSONObject(entity)); + } catch (JSONException e) { + fail(e.getMessage() + "\nResponse was: " + entity, e); + throw new RuntimeException(e); + } + } + + public ResponseAsserter assertRegisterResponse() { + assertStatusOk(); + json = assertJsonObject(); + json.hasKeys(RegisterResponseParam.CLIENT_ID.toString(), + CLIENT_SECRET.toString(), + REGISTRATION_ACCESS_TOKEN.toString(), + REGISTRATION_CLIENT_URI.toString(), + CLIENT_ID_ISSUED_AT.toString(), + CLIENT_SECRET_EXPIRES_AT.toString() + ); + return this; + } + + public JSONObjectAsserter getJson() { + return json; + } + + public int getStatus() { + return status; + } + + public String getEntity() { + return entity; + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/dev/.gitignore b/oxAuth/Client/src/test/java/org/gluu/oxauth/dev/.gitignore new file mode 100644 index 00000000..2c553cb4 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/dev/.gitignore @@ -0,0 +1 @@ +/local diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/dev/HostnameVerifierType.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/dev/HostnameVerifierType.java new file mode 100644 index 00000000..d527bdf3 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/dev/HostnameVerifierType.java @@ -0,0 +1,36 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.dev; + +import org.apache.commons.lang.StringUtils; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 27/09/2012 + */ + +public enum HostnameVerifierType { + DEFAULT("default"), + ALLOW_ALL("allow_all"); + + private final String value; + + HostnameVerifierType(String value) { + this.value = value; + } + + public static HostnameVerifierType fromString(String value) { + if (StringUtils.isNotBlank(value)) { + for (HostnameVerifierType v : values()) { + if (v.value.equals(value)) { + return v; + } + } + } + return DEFAULT; + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/dev/TestSessionWorkflow.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/dev/TestSessionWorkflow.java new file mode 100644 index 00000000..ceac29ba --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/dev/TestSessionWorkflow.java @@ -0,0 +1,165 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.dev; + +import java.util.Arrays; + +import org.apache.http.client.CookieStore; +import org.apache.http.impl.client.BasicCookieStore; +import org.apache.http.impl.client.DefaultHttpClient; +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.common.Prompt; +import org.gluu.oxauth.model.common.ResponseType; +import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient43Engine; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +import junit.framework.Assert; + +/** + * @version August 9, 2017 + */ +public class TestSessionWorkflow extends BaseTest { + + @Parameters({"userId", "userSecret", "clientId", "clientSecret", "redirectUri"}) + @Test + public void test(final String userId, final String userSecret, + final String clientId, final String clientSecret, + final String redirectUri) throws Exception { + DefaultHttpClient httpClient = new DefaultHttpClient(); + try { + CookieStore cookieStore = new BasicCookieStore(); + httpClient.setCookieStore(cookieStore); + ApacheHttpClient43Engine clientExecutor = new ApacheHttpClient43Engine(httpClient); + + //////////////////////////////////////////////// + // TV side. Code 1 // + //////////////////////////////////////////////// + + AuthorizationRequest authorizationRequest1 = new AuthorizationRequest( + Arrays.asList(ResponseType.CODE), + clientId, + Arrays.asList("openid", "profile", "email"), + redirectUri, + null); + + authorizationRequest1.setAuthUsername(userId); + authorizationRequest1.setAuthPassword(userSecret); + authorizationRequest1.getPrompts().add(Prompt.NONE); + authorizationRequest1.setState("af0ifjsldkj"); + authorizationRequest1.setRequestSessionId(true); + + AuthorizeClient authorizeClient1 = new AuthorizeClient(authorizationEndpoint); + authorizeClient1.setRequest(authorizationRequest1); + AuthorizationResponse authorizationResponse1 = authorizeClient1.exec(clientExecutor); + + // showClient(authorizeClient1, cookieStore); + + String code1 = authorizationResponse1.getCode(); + String sessionId = authorizationResponse1.getSessionId(); + Assert.assertNotNull("code1 is null", code1); + Assert.assertNotNull("sessionId is null", sessionId); + + // TV sends the code to the Backend + // We don't use httpClient and cookieStore during this call + + + //////////////////////////////////////////////// + // Backend 1 side. Code 1 // + //////////////////////////////////////////////// + + + // Get the access token + TokenClient tokenClient1 = new TokenClient(tokenEndpoint); + TokenResponse tokenResponse1 = tokenClient1.execAuthorizationCode(code1, redirectUri, clientId, clientSecret); + + String accessToken1 = tokenResponse1.getAccessToken(); + Assert.assertNotNull("accessToken1 is null", accessToken1); + + // Get the user's claims + UserInfoClient userInfoClient1 = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse1 = userInfoClient1.execUserInfo(accessToken1); + + Assert.assertTrue("userInfoResponse1.getStatus() is not 200", userInfoResponse1.getStatus() == 200); + // System.out.println(userInfoResponse1.getEntity()); + + + //////////////////////////////////////////////// + // TV side. Code 2 // + //////////////////////////////////////////////// + + AuthorizationRequest authorizationRequest2 = new AuthorizationRequest( + Arrays.asList(ResponseType.CODE), + clientId, + Arrays.asList("openid", "profile", "email"), + redirectUri, + null); + + authorizationRequest2.getPrompts().add(Prompt.NONE); + authorizationRequest2.setState("af0ifjsldkj"); + authorizationRequest2.setSessionId(sessionId); + + AuthorizeClient authorizeClient2 = new AuthorizeClient(authorizationEndpoint); + authorizeClient2.setRequest(authorizationRequest2); + AuthorizationResponse authorizationResponse2 = authorizeClient2.exec(clientExecutor); + + // showClient(authorizeClient2, cookieStore); + + String code2 = authorizationResponse2.getCode(); + Assert.assertNotNull("code2 is null", code2); + + + // TV sends the code to the Backend + // We don't use httpClient and cookieStore during this call + + + //////////////////////////////////////////////// + // Backend 2 side. Code 2 // + //////////////////////////////////////////////// + + + // Get the access token + TokenClient tokenClient2 = new TokenClient(tokenEndpoint); + TokenResponse tokenResponse2 = tokenClient2.execAuthorizationCode(code2, redirectUri, clientId, clientSecret); + + String accessToken2 = tokenResponse2.getAccessToken(); + Assert.assertNotNull("accessToken2 is null", accessToken2); + + // Get the user's claims + UserInfoClient userInfoClient2 = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse2 = userInfoClient2.execUserInfo(accessToken2); + + Assert.assertTrue("userInfoResponse1.getStatus() is not 200", userInfoResponse2.getStatus() == 200); + // System.out.println(userInfoResponse2.getEntity()); + } finally { + if (httpClient != null) { + httpClient.getConnectionManager().shutdown(); + } + } + } + + @Parameters({"userId", "userSecret", "clientId", "clientSecret", "redirectUri"}) + //@Test + public void stressTest(final String userId, final String userSecret, + final String clientId, final String clientSecret, + final String redirectUri) throws Exception { + long startTime = System.currentTimeMillis(); + for (int i = 0; i < 500; i++) { + System.out.println(i); + test(userId, userSecret, clientId, clientSecret, redirectUri); + } + long endTime = System.currentTimeMillis(); + System.out.println((endTime - startTime) / 1000); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/dev/manual/AccessTokenManualTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/dev/manual/AccessTokenManualTest.java new file mode 100644 index 00000000..4aab7c44 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/dev/manual/AccessTokenManualTest.java @@ -0,0 +1,65 @@ +package org.gluu.oxauth.dev.manual; + +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Yuriy Zabrovarnyy + */ +public class AccessTokenManualTest extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUri", "clientId"}) + @Test + public void accessTokenExpiration(final String userId, final String userSecret, final String redirectUri, String clientId) throws Exception { + showTitle("accessTokenExpiration"); + + // Request authorization and receive the authorization code. + String nonce = UUID.randomUUID().toString(); + List responseTypes = Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN, ResponseType.TOKEN); + List scopes = Arrays.asList("openid", "profile", "address", "email", "phone", "user_name"); + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, scopes, clientId, nonce); + + String accessToken = authorizationResponse.getAccessToken(); + + System.out.println("access_token: " + accessToken); + + for (int i = 0; i < 100; i++) { + requestUserInfo(accessToken); + + sleepSeconds(10); + + System.out.println("Obtained user info successfully, seconds: " + ((i + 1) * 10)); + } + } + + private static void sleepSeconds(int i) throws InterruptedException { + Thread.sleep(i * 1000); + } + + private AuthorizationResponse requestAuthorization(final String userId, final String userSecret, final String redirectUri, + List responseTypes, List scopes, String clientId, String nonce) { + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + return authorizationResponse; + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/dev/manual/BCFIPSTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/dev/manual/BCFIPSTest.java new file mode 100644 index 00000000..b70831cc --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/dev/manual/BCFIPSTest.java @@ -0,0 +1,43 @@ +package org.gluu.oxauth.dev.manual; + +import java.security.NoSuchAlgorithmException; +import java.security.Security; + +import javax.crypto.Cipher; + +import org.gluu.util.security.SecurityProviderUtility; + + +public class BCFIPSTest { + + public static void main(String a[]) throws NoSuchAlgorithmException + { + System.out.println("main"); + SecurityProviderUtility.installBCProvider(); + + // Security.setProperty("crypto.policy", "limited"); // uncomment to switch to limited crypto policies + System.out.println("Check for unlimited crypto policies"); + System.out.println("Java version: " + Runtime.version()); + //Security.setProperty("crypto.policy", "limited"); // must be set at the beginning ! + System.out.println("restricted cryptography: " + restrictedCryptography() + " Notice: 'false' means unlimited policies"); // false mean unlimited crypto + System.out.println("Security properties: " + Security.getProperty("crypto.policy")); + int maxKeyLen = Cipher.getMaxAllowedKeyLength("AES"); + System.out.println("Max AES key length = " + maxKeyLen); + } + + /** + * Determines if cryptography restrictions apply. + * Restrictions apply if the value of {@link Cipher#getMaxAllowedKeyLength(String)} returns a value smaller than {@link Integer#MAX_VALUE} if there are any restrictions according to the JavaDoc of the method. + * This method is used with the transform "AES/CBC/PKCS5Padding" as this is an often used algorithm that is an implementation requirement for Java SE. + * + * @return true if restrictions apply, false otherwise + * https://stackoverflow.com/posts/33849265/edit, author Maarten Bodewes + */ + public static boolean restrictedCryptography() { + try { + return Cipher.getMaxAllowedKeyLength("AES/CBC/PKCS5Padding") < Integer.MAX_VALUE; + } catch (final NoSuchAlgorithmException e) { + throw new IllegalStateException("The transform \"AES/CBC/PKCS5Padding\" is not available (the availability of this algorithm is mandatory for Java SE implementations)", e); + } + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/dev/manual/MTSLClientAuthenticationTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/dev/manual/MTSLClientAuthenticationTest.java new file mode 100644 index 00000000..be452e8b --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/dev/manual/MTSLClientAuthenticationTest.java @@ -0,0 +1,85 @@ +package org.gluu.oxauth.dev.manual; + +import static org.gluu.oxauth.BaseTest.showClient; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.security.KeyStore; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; + +import org.apache.http.client.HttpClient; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.impl.client.DefaultHttpClient; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient43Engine; + +/** + * @author Yuriy Zabrovarnyy + */ +public class MTSLClientAuthenticationTest { + + public static void main(String[] args) throws Exception { + + File jdkJks = new File("u:\\tmp\\ce-ob\\clientkeystore"); + if (!jdkJks.exists()) { + throw new RuntimeException("Failed to find jks trust store"); + } + + File certificate = new File("u:\\tmp\\ce-ob\\fullchain.p12"); + if (!certificate.exists()) { + throw new RuntimeException("Failed to find certificate"); + } + + HttpClient httpclient = new DefaultHttpClient(); +// truststore + KeyStore ts = KeyStore.getInstance("JKS", "SUN"); + ts.load(new FileInputStream(jdkJks), "secret".toCharArray()); +// if you remove me, you've got 'javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated' on missing truststore + if(0 == ts.size()) throw new IOException("Error loading truststore"); +// tmf + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ts); +// keystore + KeyStore ks = KeyStore.getInstance("PKCS12", "SunJSSE"); + ks.load(new FileInputStream(certificate), "".toCharArray()); +// if you remove me, you've got 'javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated' on missing keystore + if(0 == ks.size()) throw new IOException("Error loading keystore"); +// kmf + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(ks, "".toCharArray()); +// SSL + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); +// socket + SSLSocketFactory socketFactory = new SSLSocketFactory(ctx, SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); + Scheme sch = new Scheme("https", 443, socketFactory); + httpclient.getConnectionManager().getSchemeRegistry().register(sch); + + String clientId = "@!D445.22BF.5EF1.0D87!0001!03F2.297D!0008!F599.E2C7"; + String clientSecret = "testClientSecret"; + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode("testCode"); + tokenRequest.setRedirectUri("https://ce-ob.gluu.org/cas/login"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.TLS_CLIENT_AUTH); + + TokenClient tokenClient = new TokenClient("https://ce-ob.gluu.org/oxauth/restv1/token"); + tokenClient.setExecutor(new ApacheHttpClient43Engine(httpclient)); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + System.out.println(tokenResponse); + showClient(tokenClient); + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/AcceptRequestWithoutRedirectUriWhenOneRegistered.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/AcceptRequestWithoutRedirectUriWhenOneRegistered.java new file mode 100644 index 00000000..2ff781bf --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/AcceptRequestWithoutRedirectUriWhenOneRegistered.java @@ -0,0 +1,77 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Accept Request Without redirect uri when One Registered + * + * @author Javier Rojas Blum + * @version June 19, 2015 + */ +public class AcceptRequestWithoutRedirectUriWhenOneRegistered extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUri"}) + @Test + public void acceptRequestWithoutRedirectUriWhenOneRegistered(final String userId, final String userSecret, final String redirectUri) throws Exception { + showTitle("OC5:FeatureTest-Accept Request Without redirect uri when One Registered"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUri)); + registerRequest.setResponseTypes(responseTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization and receive the authorization code. + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, null, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/AcceptValidAsymmetricIdTokenSignature.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/AcceptValidAsymmetricIdTokenSignature.java new file mode 100644 index 00000000..2454ea81 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/AcceptValidAsymmetricIdTokenSignature.java @@ -0,0 +1,167 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.common.SubjectType; +import org.gluu.oxauth.model.crypto.signature.ECDSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.ECDSASigner; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Accept Valid Asymmetric ID Token Signature + * + * @author Javier Rojas Blum + * @version November 2, 2016 + */ +public class AcceptValidAsymmetricIdTokenSignature extends BaseTest { + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void acceptValidAsymmetricIdTokenSignatureRS256( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Accept Valid Asymmetric ID Token Signature RS256"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS256); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + assertEquals(authorizationResponse.getState(), state); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + assertTrue(rsaSigner.validate(jwt)); + } + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "postLogoutRedirectUri", "clientJwksUri"}) + @Test + public void acceptValidAsymmetricIdTokenSignatureES256( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String postLogoutRedirectUri, final String clientJwksUri) throws Exception { + showTitle("OC5:FeatureTest-Accept Valid Asymmetric ID Token Signature es256"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + + List grantTypes = Arrays.asList(GrantType.AUTHORIZATION_CODE); + + // 1. Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, null, + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.ES256); + registerRequest.setPostLogoutRedirectUris(StringUtils.spaceSeparatedToList(postLogoutRedirectUri)); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSubjectType(SubjectType.PUBLIC); + registerRequest.setRequireAuthTime(true); + registerRequest.setDefaultMaxAge(3600); + registerRequest.setGrantTypes(grantTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + assertEquals(authorizationResponse.getState(), state); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + ECDSAPublicKey publicKey = JwkClient.getECDSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + ECDSASigner ecdsaSigner = new ECDSASigner(SignatureAlgorithm.ES256, publicKey); + assertTrue(ecdsaSigner.validate(jwt)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/AcceptValidSymmetricIdTokenSignature.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/AcceptValidSymmetricIdTokenSignature.java new file mode 100644 index 00000000..c348a87d --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/AcceptValidSymmetricIdTokenSignature.java @@ -0,0 +1,92 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.HMACSigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Accept Valid Symmetric ID Token Signature + * + * @author Javier Rojas Blum + * @version November 2, 2016 + */ +public class AcceptValidSymmetricIdTokenSignature extends BaseTest { + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void acceptValidSymmetricIdTokenSignature( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Accept Valid Symmetric ID Token Signature"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.HS256); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request Authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + HMACSigner hmacSigner = new HMACSigner(SignatureAlgorithm.HS256, clientSecret); + assertTrue(hmacSigner.validate(jwt)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanDiscoverIdentifiersUsingEMailSyntax.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanDiscoverIdentifiersUsingEMailSyntax.java new file mode 100644 index 00000000..22f93b4d --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanDiscoverIdentifiersUsingEMailSyntax.java @@ -0,0 +1,92 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; + +import org.gluu.oxauth.client.OpenIdConnectDiscoveryRequest; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Can Discover Identifiers using E-Mail Syntax + * + * @author Javier Rojas Blum Date: 09.03.2013 + */ +public class CanDiscoverIdentifiersUsingEMailSyntax { + + @Test + public void emailNormalization1() throws Exception { + String resource = "acct:joe@example.com"; + String expectedHost = "example.com"; + String expectedPath = null; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void emailNormalization2() throws Exception { + String resource = "joe@example.com"; + String expectedHost = "example.com"; + String expectedPath = null; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void emailNormalization3() throws Exception { + String resource = "acct:joe@example.com:8080"; + String expectedHost = "example.com:8080"; + String expectedPath = null; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void emailNormalization4() throws Exception { + String resource = "joe@example.com:8080"; + String expectedHost = "example.com:8080"; + String expectedPath = null; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void emailNormalization5() throws Exception { + String resource = "joe@localhost"; + String expectedHost = "localhost"; + String expectedPath = null; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void emailNormalization6() throws Exception { + String resource = "joe@localhost:8080"; + String expectedHost = "localhost:8080"; + String expectedPath = null; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanDiscoverIdentifiersUsingUrlSyntax.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanDiscoverIdentifiersUsingUrlSyntax.java new file mode 100644 index 00000000..3374f124 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanDiscoverIdentifiersUsingUrlSyntax.java @@ -0,0 +1,152 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; + +import org.gluu.oxauth.client.OpenIdConnectDiscoveryRequest; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Can Discover Identifiers using URL Syntax + * + * @author Javier Rojas Blum Date: 09.03.2013 + */ +public class CanDiscoverIdentifiersUsingUrlSyntax { + + @Test + public void urlNormalization1() throws Exception { + String resource = "https://example.com"; + String expectedHost = "example.com"; + String expectedPath = null; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void urlNormalization2() throws Exception { + String resource = "https://example.com/joe"; + String expectedHost = "example.com"; + String expectedPath = "/joe"; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void urlNormalization3() throws Exception { + String resource = "https://example.com:8080/"; + String expectedHost = "example.com:8080"; + String expectedPath = null; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void urlNormalization4() throws Exception { + String resource = "https://example.com:8080/joe"; + String expectedHost = "example.com:8080"; + String expectedPath = "/joe"; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void urlNormalization5() throws Exception { + String resource = "https://example.com:8080/joe#fragment"; + String expectedHost = "example.com:8080"; + String expectedPath = "/joe"; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void urlNormalization6() throws Exception { + String resource = "https://example.com:8080/joe?param=value"; + String expectedHost = "example.com:8080"; + String expectedPath = "/joe"; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void urlNormalization7() throws Exception { + String resource = "https://example.com:8080/joe?param1=foo¶m2=bar#fragment"; + String expectedHost = "example.com:8080"; + String expectedPath = "/joe"; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void hostNormalization1() throws Exception { + String resource = "example.com"; + String expectedHost = "example.com"; + String expectedPath = null; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void hostNormalization2() throws Exception { + String resource = "example.com:8080"; + String expectedHost = "example.com:8080"; + String expectedPath = null; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void hostNormalization3() throws Exception { + String resource = "example.com/path"; + String expectedHost = "example.com"; + String expectedPath = "/path"; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void hostNormalization4() throws Exception { + String resource = "example.com:8080/path"; + String expectedHost = "example.com:8080"; + String expectedPath = "/path"; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanMakeAccessTokenRequestWithClientSecretBasicAuthentication.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanMakeAccessTokenRequestWithClientSecretBasicAuthentication.java new file mode 100644 index 00000000..a6dcd24d --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanMakeAccessTokenRequestWithClientSecretBasicAuthentication.java @@ -0,0 +1,107 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Can Make Access Token Request with client secret basic Authentication + * + * @author Javier Rojas Blum + * @version June 19, 2015 + */ +public class CanMakeAccessTokenRequestWithClientSecretBasicAuthentication extends BaseTest { + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void canMakeAccessTokenRequestWithClientSecretBasicAuthentication( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Can Make Access Token Request with client secret basic Authentication"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanMakeAccessTokenRequestWithClientSecretJwtAuthentication.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanMakeAccessTokenRequestWithClientSecretJwtAuthentication.java new file mode 100644 index 00000000..ad702085 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanMakeAccessTokenRequestWithClientSecretJwtAuthentication.java @@ -0,0 +1,111 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Can Make Access Token Request with client secret jwt Authentication + * + * @author Javier Rojas Blum + * @version June 19, 2015 + */ +public class CanMakeAccessTokenRequestWithClientSecretJwtAuthentication extends BaseTest { + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void canMakeAccessTokenRequestWithClientSecretJwtAuthentication( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Can Make Access Token Request with client secret jwt Authentication"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanMakeAccessTokenRequestWithClientSecretPostAuthentication.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanMakeAccessTokenRequestWithClientSecretPostAuthentication.java new file mode 100644 index 00000000..ae56fe67 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanMakeAccessTokenRequestWithClientSecretPostAuthentication.java @@ -0,0 +1,107 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Can Make Access Token Request with client secret post Authentication + * + * @author Javier Rojas Blum + * @version June 19, 2015 + */ +public class CanMakeAccessTokenRequestWithClientSecretPostAuthentication extends BaseTest { + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void canMakeAccessTokenRequestWithClientSecretPostAuthentication( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Can Make Access Token Request with client secret post Authentication"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_POST); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_POST); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanMakeAccessTokenRequestWithPrivateKeyJwtAuthentication.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanMakeAccessTokenRequestWithPrivateKeyJwtAuthentication.java new file mode 100644 index 00000000..5419c8e7 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanMakeAccessTokenRequestWithPrivateKeyJwtAuthentication.java @@ -0,0 +1,115 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Can Make Access Token Request with private key jwt Authentication + * + * @author Javier Rojas Blum + * @version June 15, 2016 + */ +public class CanMakeAccessTokenRequestWithPrivateKeyJwtAuthentication extends BaseTest { + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri", "clientJwksUri", + "RS256_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void canMakeAccessTokenRequestWithPrivateKeyJwtAuthentication( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String sectorIdentifierUri, final String clientJwksUri, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("OC5:FeatureTest-Can Make Access Token Request with private key jwt Authentication"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanProvideEncryptedIdTokenResponse.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanProvideEncryptedIdTokenResponse.java new file mode 100644 index 00000000..a68fe881 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanProvideEncryptedIdTokenResponse.java @@ -0,0 +1,408 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.fail; + +import java.security.PrivateKey; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.encryption.BlockEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.encryption.KeyEncryptionAlgorithm; +import org.gluu.oxauth.model.jwe.Jwe; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.gluu.oxauth.model.util.Util; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Can Provide Encrypted ID Token Response + * + * @author Javier Rojas Blum + * @version February 12, 2019 + */ +public class CanProvideEncryptedIdTokenResponse extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void canProvideEncryptedIdTokenResponseAlgA128KWEncA128GCM( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) { + try { + showTitle("OC5:FeatureTest-Can Provide Encrypted ID Token Response A128KW A128GCM"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.A128KW); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A128GCM); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Read Encrypted ID Token + Jwe jwe = Jwe.parse(idToken, null, clientSecret.getBytes(Util.UTF8_STRING_ENCODING)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + } catch (Exception ex) { + fail(ex.getMessage(), ex); + } + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void canProvideEncryptedIdTokenResponseAlgA256KWEncA256GCM( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) { + try { + showTitle("OC5:FeatureTest-Can Provide Encrypted ID Token Response A256KW A256GCM"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.A256KW); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A256GCM); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Read Encrypted ID Token + Jwe jwe = Jwe.parse(idToken, null, clientSecret.getBytes(Util.UTF8_STRING_ENCODING)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + } catch (Exception ex) { + fail(ex.getMessage(), ex); + } + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", + "clientJwksUri", "RSA1_5_keyId", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void canProvideEncryptedIdTokenResponseAlgRSA15EncA128CBCPLUSHS256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String jwksUri, final String keyId, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri) { + try { + showTitle("OC5:FeatureTest-Can Provide Encrypted ID Token Response RSA1_5 A128CBC_PLUS_HS256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setJwksUri(jwksUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A128CBC_PLUS_HS256); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Read Encrypted ID Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, null); + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + + Jwe jwe = Jwe.parse(idToken, privateKey, null); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + } catch (Exception ex) { + fail(ex.getMessage(), ex); + } + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", + "clientJwksUri", "RSA1_5_keyId", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void canProvideEncryptedIdTokenResponseAlgRSA15EncA256CBCPLUSHS512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String jwksUri, final String keyId, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri) { + try { + showTitle("OC5:FeatureTest-Can Provide Encrypted ID Token Response RSA1_5 A256CBC_PLUS_HS512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setJwksUri(jwksUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A256CBC_PLUS_HS512); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Read Encrypted ID Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, null); + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + + Jwe jwe = Jwe.parse(idToken, privateKey, null); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + } catch (Exception ex) { + fail(ex.getMessage(), ex); + } + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", + "clientJwksUri", "RSA_OAEP_keyId", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void canProvideEncryptedIdTokenResponseAlgRSAOAEPEncA256GCM( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String jwksUri, final String keyId, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri) { + try { + showTitle("OC5:FeatureTest-Can Provide Encrypted ID Token Response RSA_OAEP A256GCM"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setJwksUri(jwksUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA_OAEP); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A256GCM); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Read Encrypted ID Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, null); + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + + Jwe jwe = Jwe.parse(idToken, privateKey, null); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + } catch (Exception ex) { + fail(ex.getMessage(), ex); + } + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanProvideEncryptedUserInfoResponse.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanProvideEncryptedUserInfoResponse.java new file mode 100644 index 00000000..b7187cd2 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanProvideEncryptedUserInfoResponse.java @@ -0,0 +1,459 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.fail; + +import java.security.PrivateKey; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.encryption.BlockEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.encryption.KeyEncryptionAlgorithm; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Can Provide Encrypted UserInfo Response + * + * @author Javier Rojas Blum + * @version March 8, 2019 + */ +public class CanProvideEncryptedUserInfoResponse extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void canProvideEncryptedUserInfoResponseAlgA128KWEncA128GCM( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Can Provide Encrypted UserInfo Response A128KW A128GCM"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.A128KW); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A128GCM); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_COUNTRY))); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void canProvideEncryptedUserInfoResponseAlgA256KWEncA256GCM( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Can Provide Encrypted UserInfo Response A256KW A256GCM"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.A256KW); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A256GCM); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_COUNTRY))); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", + "clientJwksUri", "RSA1_5_keyId", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void canProvideEncryptedUserInfoResponseAlgRSA15EncA128CBCPLUSHS256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String jwksUri, final String keyId, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri) { + try { + showTitle("OC5:FeatureTest-Can Provide Encrypted UserInfo Response RSA1_5 A128CBC_PLUS_HS256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setJwksUri(jwksUri); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A128CBC_PLUS_HS256); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, null); + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setPrivateKey(privateKey); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_COUNTRY))); + } catch (Exception ex) { + fail(ex.getMessage(), ex); + } + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", + "clientJwksUri", "RSA1_5_keyId", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void canProvideEncryptedUserInfoResponseAlgRSA15EncA256CBCPLUSHS512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String jwksUri, final String keyId, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri) { + try { + showTitle("OC5:FeatureTest-Can Provide Encrypted UserInfo Response RSA1_5 A256CBC_PLUS_HS512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setJwksUri(jwksUri); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A256CBC_PLUS_HS512); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, null); + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setPrivateKey(privateKey); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_COUNTRY))); + } catch (Exception ex) { + fail(ex.getMessage(), ex); + } + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", + "clientJwksUri", "RSA_OAEP_keyId", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void canProvideEncryptedUserInfoResponseAlgRSAOAEPEncA256GCM( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String jwksUri, final String keyId, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri) { + try { + showTitle("OC5:FeatureTest-Can Provide Encrypted UserInfo Response RSA_OAEP A256GCM"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setJwksUri(jwksUri); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA_OAEP); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A256GCM); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, null); + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setPrivateKey(privateKey); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_COUNTRY))); + } catch (Exception ex) { + fail(ex.getMessage(), ex); + } + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanProvideSignedUserInfoResponse.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanProvideSignedUserInfoResponse.java new file mode 100644 index 00000000..ea0ba8a7 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanProvideSignedUserInfoResponse.java @@ -0,0 +1,718 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Can Provide Signed UserInfo Response + * + * @author Javier Rojas Blum + * @version March 8, 2019 + */ +public class CanProvideSignedUserInfoResponse extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void canProvideSignedUserInfoResponseHS256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Can Provide Signed UserInfo Response HS256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.HS256); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_COUNTRY))); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void canProvideSignedUserInfoResponseHS384( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Can Provide Signed UserInfo Response HS384"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.HS384); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_COUNTRY))); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void canProvideSignedUserInfoResponseHS512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Can Provide Signed UserInfo Response HS512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.HS512); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_COUNTRY))); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void canProvideSignedUserInfoResponseRS256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Can Provide Signed UserInfo Response RS256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.RS256); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_COUNTRY))); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void canProvideSignedUserInfoResponseRS384( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Can Provide Signed UserInfo Response RS384"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.RS384); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_COUNTRY))); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void canProvideSignedUserInfoResponseRS512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Can Provide Signed UserInfo Response RS512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.RS512); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_COUNTRY))); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void canProvideSignedUserInfoResponseES256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Can Provide Signed UserInfo Response ES256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.ES256); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_COUNTRY))); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void canProvideSignedUserInfoResponseES384( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Can Provide Signed UserInfo Response ES384"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.ES384); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_COUNTRY))); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void canProvideSignedUserInfoResponseES512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Can Provide Signed UserInfo Response ES512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.ES512); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_COUNTRY))); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanRequestAndUseClaimsInIdToken.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanRequestAndUseClaimsInIdToken.java new file mode 100644 index 00000000..8640cb77 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanRequestAndUseClaimsInIdToken.java @@ -0,0 +1,119 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.model.authorize.Claim; +import org.gluu.oxauth.client.model.authorize.ClaimValue; +import org.gluu.oxauth.client.model.authorize.JwtAuthorizationRequest; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.HMACSigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Can Request and Use Claims in id token + * + * @author Javier Rojas Blum + * @version November 3, 2016 + */ +public class CanRequestAndUseClaimsInIdToken extends BaseTest { + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void canRequestAndUseClaimsInIdToken( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Can Request and Use Claims in id token"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.HS256); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request Authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.HS256, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NAME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.NAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EMAIL)); + + HMACSigner hmacSigner = new HMACSigner(SignatureAlgorithm.HS256, clientSecret); + assertTrue(hmacSigner.validate(jwt)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanRequestAndUseEncryptedIdTokenResponse.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanRequestAndUseEncryptedIdTokenResponse.java new file mode 100644 index 00000000..f4493a30 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanRequestAndUseEncryptedIdTokenResponse.java @@ -0,0 +1,107 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.encryption.BlockEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.encryption.KeyEncryptionAlgorithm; +import org.gluu.oxauth.model.jwe.Jwe; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.gluu.oxauth.model.util.Util; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Can Request and Use Encrypted ID Token Response + * + * @author Javier Rojas Blum + * @version November 3, 2016 + */ +public class CanRequestAndUseEncryptedIdTokenResponse extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void canRequestAndUseEncryptedIdTokenResponse( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Can Request and Use Encrypted ID Token Response"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.A128KW); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A128GCM); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Read Encrypted ID Token + Jwe jwe = Jwe.parse(idToken, null, clientSecret.getBytes(Util.UTF8_STRING_ENCODING)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanRequestAndUseEncryptedUserInfoResponse.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanRequestAndUseEncryptedUserInfoResponse.java new file mode 100644 index 00000000..3698426c --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanRequestAndUseEncryptedUserInfoResponse.java @@ -0,0 +1,115 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.encryption.BlockEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.encryption.KeyEncryptionAlgorithm; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Can Request and Use Encrypted UserInfo Response + * + * @author Javier Rojas Blum + * @version November 3, 2016 + */ +public class CanRequestAndUseEncryptedUserInfoResponse extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void canRequestAndUseEncryptedUserInfoResponse( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Can Request and Use Encrypted UserInfo Response"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.A128KW); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A128GCM); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_REGION)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanRequestAndUseSignedUserInfoResponse.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanRequestAndUseSignedUserInfoResponse.java new file mode 100644 index 00000000..94873eec --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/CanRequestAndUseSignedUserInfoResponse.java @@ -0,0 +1,116 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Can Request and Use Signed UserInfo Response + * + * @author Javier Rojas Blum + * @version March 8, 2019 + */ +public class CanRequestAndUseSignedUserInfoResponse extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void canRequestAndUseSignedUserInfoResponse( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Can Request and Use Signed UserInfo Response"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.HS256); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_COUNTRY))); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/DisplaysLogoInLoginPage.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/DisplaysLogoInLoginPage.java new file mode 100644 index 00000000..f2b7b561 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/DisplaysLogoInLoginPage.java @@ -0,0 +1,94 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.fail; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.WebElement; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Displays Logo in Login Page + * + * @author Javier Rojas Blum + * @version November 3, 2016 + */ +public class DisplaysLogoInLoginPage extends BaseTest { + + @Parameters({ "redirectUris", "redirectUri", "sectorIdentifierUri" }) + @Test + public void displaysLogoInLoginPage(final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Displays Logo in Login Page"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + String logoUri = "http://www.gluu.org/wp-content/themes/gluursn/images/logo.png"; + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setLogoUri(logoUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization and receive the authorization code. + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, + redirectUri, null); + authorizationRequest.setState(state); + + String authorizationRequestUrl = getAuthorizationEndpoint() + "?" + authorizationRequest.getQueryString(); + + AuthorizeClient authorizeClient = new AuthorizeClient(getAuthorizationEndpoint()); + authorizeClient.setRequest(authorizationRequest); + + try { + startSelenium(); + navigateToAuhorizationUrl(driver, authorizationRequestUrl); + WebElement logo = driver.findElement(By.id("AppLogo")); + assertNotNull(logo); + } catch (NoSuchElementException ex) { + fail("Logo not found"); + } finally { + stopSelenium(); + } + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/DisplaysPolicyUriInLoginPage.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/DisplaysPolicyUriInLoginPage.java new file mode 100644 index 00000000..9b33e961 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/DisplaysPolicyUriInLoginPage.java @@ -0,0 +1,90 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.fail; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Displays Policy in Login Page + * + * @author Javier Rojas Blum + * @version November 3, 2016 + */ +public class DisplaysPolicyUriInLoginPage extends BaseTest { + + @Parameters({"redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void displaysPolicyUrlInLoginPage(final String redirectUris, final String redirectUri, final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Displays Policy in Login Page"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + String policyUri = "http://www.gluu.org/policy"; + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setPolicyUri(policyUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization and receive the authorization code. + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + String authorizationRequestUrl = getAuthorizationEndpoint() + "?" + authorizationRequest.getQueryString(); + + AuthorizeClient authorizeClient = new AuthorizeClient(getAuthorizationEndpoint()); + authorizeClient.setRequest(authorizationRequest); + + try { + startSelenium(); + navigateToAuhorizationUrl(driver, authorizationRequestUrl); + +// WebElement policy = driver.findElement(By.xpath("//a[@href='" + policyUri + "']")); +// assertNotNull(policy); + } catch (Exception ex) { + fail("Policy not found"); + } finally { + stopSelenium(); + } + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/EnablesDynamicRegistration.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/EnablesDynamicRegistration.java new file mode 100644 index 00000000..a57e212c --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/EnablesDynamicRegistration.java @@ -0,0 +1,63 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.SubjectType; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Enables Dynamic Registration + * + * @author Javier Rojas Blum Date: 07.27.2013 + */ +public class EnablesDynamicRegistration extends BaseTest { + + @Parameters({"redirectUris", "sectorIdentifierUri", "clientJwksUri"}) + @Test + public void enablesDynamicRegistration(final String redirectUris, final String sectorIdentifierUri, + final String clientJwksUri) throws Exception { + showTitle("OC5:FeatureTest-Enables Dynamic Registration"); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setContacts(Arrays.asList("javier@gluu.org", "javier.rojas.blum@gmail.com")); + registerRequest.setLogoUri("http://www.gluu.org/wp-content/themes/gluursn/images/logo.png"); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setPolicyUri("http://www.gluu.org/policy"); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setSubjectType(SubjectType.PUBLIC); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.RS256); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getRegistrationClientUri()); + assertNotNull(response.getClientIdIssuedAt()); + assertNotNull(response.getClientSecretExpiresAt()); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/IgnoresExtraQueryComponentInRequest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/IgnoresExtraQueryComponentInRequest.java new file mode 100644 index 00000000..bdc38b07 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/IgnoresExtraQueryComponentInRequest.java @@ -0,0 +1,85 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Ignores Extra Query Component in Request + * + * @author Javier Rojas Blum + * @version November 3, 2016 + */ +public class IgnoresExtraQueryComponentInRequest extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void ignoresExtraQueryComponentInRequest( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Ignores Extra Query Component in Request"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization and receive the authorization code. + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + String nonce = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.getCustomParameters().put("custom_param", "custom_value"); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/IncludesAtHashInIdTokenWhenImplicitFlowUsed.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/IncludesAtHashInIdTokenWhenImplicitFlowUsed.java new file mode 100644 index 00000000..201a57ff --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/IncludesAtHashInIdTokenWhenImplicitFlowUsed.java @@ -0,0 +1,115 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Includes at hash in ID Token when Implicit Flow Used + * + * @author Javier Rojas Blum + * @version November 3, 2016 + */ +public class IncludesAtHashInIdTokenWhenImplicitFlowUsed extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void includesAtHashInIdTokenWhenImplicitFlowUsed( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Includes at hash in ID Token when Implicit Flow Used"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getAccessToken()); + assertNotNull(authorizationResponse.getTokenType()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + + String accessToken = authorizationResponse.getAccessToken(); + String idToken = authorizationResponse.getIdToken(); + + // 4. Validate access_token and id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + assertTrue(rsaSigner.validateAccessToken(accessToken, jwt)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/IncludesCHashInIdTokenWhenCodeFlowUsed.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/IncludesCHashInIdTokenWhenCodeFlowUsed.java new file mode 100644 index 00000000..87338a94 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/IncludesCHashInIdTokenWhenCodeFlowUsed.java @@ -0,0 +1,143 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.gluu.oxauth.model.register.RegisterRequestParam.APPLICATION_TYPE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.CLIENT_NAME; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ID_TOKEN_SIGNED_RESPONSE_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REDIRECT_URIS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.RESPONSE_TYPES; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SCOPE; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Includes c hash in ID Token when Code Flow Used + * + * @author Javier Rojas Blum + * @version November 29, 2017 + */ +public class IncludesCHashInIdTokenWhenCodeFlowUsed extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void includesCHashInIdTokenWhenCodeFlowUsed( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Includes c hash in ID Token when Code Flow Used"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + String nonce = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The code is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String code = authorizationResponse.getCode(); + String idToken = authorizationResponse.getIdToken(); + + // 4. Validate code and id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.CODE_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + assertTrue(rsaSigner.validateAuthorizationCode(code, jwt)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/OPRegistrationJwks.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/OPRegistrationJwks.java new file mode 100644 index 00000000..18a0f8bf --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/OPRegistrationJwks.java @@ -0,0 +1,223 @@ +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.JwkResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.register.RegisterRequestParam; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Javier Rojas Blum + * @version October 4, 2017 + */ +public class OPRegistrationJwks extends BaseTest { + + @Parameters({"redirectUri", "postLogoutRedirectUri", "clientJwksUri", "userId", "userSecret", "RS256_keyId", + "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void opRegistrationJwks( + final String redirectUri, final String postLogoutRedirectUri, final String clientJwksUri, + final String userId, final String userSecret, final String keyId, final String dnName, + final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("opRegistrationJwks"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + List grantTypes = Arrays.asList(GrantType.AUTHORIZATION_CODE); + List contacts = Arrays.asList("javier@gluu.org", "javier.rojas.blum@gmail.com"); + + // 1. Register client + JwkClient jwkClient = new JwkClient(clientJwksUri); + JwkResponse jwkResponse = jwkClient.exec(); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUri)); + registerRequest.setPostLogoutRedirectUris(Arrays.asList(postLogoutRedirectUri)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setGrantTypes(grantTypes); + registerRequest.setContacts(contacts); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setJwks(jwkResponse.getJwks().toString()); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getRegistrationClientUri()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + assertNotNull(registerResponse.getResponseTypes()); + assertTrue(registerResponse.getResponseTypes().containsAll(responseTypes)); + assertNotNull(registerResponse.getGrantTypes()); + assertTrue(registerResponse.getGrantTypes().containsAll(grantTypes)); + assertNotNull(registerResponse.getClaims().get(RegisterRequestParam.JWKS.getName())); + assertNotNull(registerResponse.getClaims().get(RegisterRequestParam.TOKEN_ENDPOINT_AUTH_METHOD.getName())); + assertEquals(AuthenticationMethod.PRIVATE_KEY_JWT.toString(), registerResponse.getClaims().get(RegisterRequestParam.TOKEN_ENDPOINT_AUTH_METHOD.getName())); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getState()); + assertNotNull(authorizationResponse.getScope()); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Request access token using the authorization code. + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + assertNotNull(tokenResponse.getIdToken(), "The id token is null"); + } + + @Parameters({"redirectUri", "postLogoutRedirectUri", "clientJwksUri", "userId", "userSecret", "RS256_keyId", + "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void opRegistrationJwksUri( + final String redirectUri, final String postLogoutRedirectUri, final String clientJwksUri, + final String userId, final String userSecret, final String keyId, final String dnName, + final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("opRegistrationJwksUri"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + List grantTypes = Arrays.asList(GrantType.AUTHORIZATION_CODE); + List contacts = Arrays.asList("javier@gluu.org", "javier.rojas.blum@gmail.com"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUri)); + registerRequest.setPostLogoutRedirectUris(Arrays.asList(postLogoutRedirectUri)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setGrantTypes(grantTypes); + registerRequest.setContacts(contacts); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setJwksUri(clientJwksUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getRegistrationClientUri()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + assertNotNull(registerResponse.getResponseTypes()); + assertTrue(registerResponse.getResponseTypes().containsAll(responseTypes)); + assertNotNull(registerResponse.getGrantTypes()); + assertTrue(registerResponse.getGrantTypes().containsAll(grantTypes)); + assertNotNull(registerResponse.getClaims().get(RegisterRequestParam.JWKS_URI.getName())); + assertNotNull(registerResponse.getClaims().get(RegisterRequestParam.TOKEN_ENDPOINT_AUTH_METHOD.getName())); + assertEquals(AuthenticationMethod.PRIVATE_KEY_JWT.toString(), registerResponse.getClaims().get(RegisterRequestParam.TOKEN_ENDPOINT_AUTH_METHOD.getName())); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getState()); + assertNotNull(authorizationResponse.getScope()); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Request access token using the authorization code. + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + assertNotNull(tokenResponse.getIdToken(), "The id token is null"); + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/ProvidingAcrValues.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/ProvidingAcrValues.java new file mode 100644 index 00000000..82aff717 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/ProvidingAcrValues.java @@ -0,0 +1,83 @@ +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Javier Rojas Blum + * @version November 22, 2017 + */ +public class ProvidingAcrValues extends BaseTest { + + @Parameters({"redirectUri", "clientJwksUri", "userId", "userSecret"}) + @Test + public void providingAcrValues(final String redirectUri, final String jwksUri, final String userId, final String userSecret) throws Exception { + showTitle("providingAcrValues"); + + List responseTypes = Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN); + List grantTypes = Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.IMPLICIT); + List contacts = Arrays.asList("javier@gluu.org", "javier.rojas.blum@gmail.com"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUri)); + registerRequest.setJwksUri(jwksUri); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setGrantTypes(grantTypes); + registerRequest.setContacts(contacts); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getRegistrationClientUri()); + assertNotNull(response.getClientIdIssuedAt()); + assertNotNull(response.getClientSecretExpiresAt()); + assertNotNull(response.getResponseTypes()); + assertTrue(response.getResponseTypes().containsAll(responseTypes)); + assertNotNull(response.getGrantTypes()); + assertTrue(response.getGrantTypes().containsAll(grantTypes)); + + String clientId = response.getClientId(); + + // 3. Request authorization + List scopes = Arrays.asList("openid"); + List acrValues = Arrays.asList("basic"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.setAcrValues(acrValues); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getState()); + assertNotNull(authorizationResponse.getScope()); + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/ProvidingIdTokenWithEssentialAuthTimeClaim.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/ProvidingIdTokenWithEssentialAuthTimeClaim.java new file mode 100644 index 00000000..b5bbdfc9 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/ProvidingIdTokenWithEssentialAuthTimeClaim.java @@ -0,0 +1,129 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.model.authorize.Claim; +import org.gluu.oxauth.client.model.authorize.ClaimValue; +import org.gluu.oxauth.client.model.authorize.JwtAuthorizationRequest; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Providing ID Token with Essential auth time Claim + * + * @author Javier Rojas Blum + * @version November 3, 2016 + */ +public class ProvidingIdTokenWithEssentialAuthTimeClaim extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void providingIdTokenWithEssentialAuthTimeClaim( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Providing ID Token with Essential auth time Claim"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.HS256, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/ProvidingIdTokenWithMaxAgeRestriction.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/ProvidingIdTokenWithMaxAgeRestriction.java new file mode 100644 index 00000000..d6d72e88 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/ProvidingIdTokenWithMaxAgeRestriction.java @@ -0,0 +1,345 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.client.model.authorize.JwtAuthorizationRequest; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.common.SubjectType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Providing ID Token with max age Restriction + * + * @author Javier Rojas Blum + * @version August 9, 2017 + */ +public class ProvidingIdTokenWithMaxAgeRestriction extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "clientJwksUri"}) + @Test + public void providingIdTokenWithMaxAgeRestriction( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String clientJwksUri) throws Exception { + showTitle("OC5:FeatureTest-Providing ID Token with max age Restriction"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setSubjectType(SubjectType.PUBLIC); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setContacts(Arrays.asList("javier@gluu.org")); + registerRequest.setGrantTypes(Arrays.asList(GrantType.AUTHORIZATION_CODE)); + registerRequest.setPostLogoutRedirectUris(StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setRequireAuthTime(true); + registerRequest.setDefaultMaxAge(3600); + registerRequest.setResponseTypes(responseTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + String sessionId; + { + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + assertNotNull(authorizationResponse.getScope()); + assertEquals(authorizationResponse.getState(), state); + + String authorizationCode = authorizationResponse.getCode(); + sessionId = authorizationResponse.getSessionId(); + + // 3. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + String idToken = tokenResponse.getIdToken(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getIdToken(), "The ID Token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + + // 4. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + } + + Thread.sleep(60000); + + { + // 5. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + authorizationRequest.setMaxAge(30); + authorizationRequest.setSessionId(sessionId); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + assertNotNull(authorizationResponse.getScope()); + assertEquals(authorizationResponse.getState(), state); + + String authorizationCode = authorizationResponse.getCode(); + + // 6. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + String idToken = tokenResponse.getIdToken(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getIdToken(), "The ID Token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + + // 7. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + } + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void providingIdTokenWithMaxAgeRestrictionJwtAuthorizationRequest( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Providing ID Token with max age Restriction (JWT Authorization Request)"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + String sessionId; + { + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + assertNotNull(authorizationResponse.getScope()); + + String authorizationCode = authorizationResponse.getCode(); + sessionId = authorizationResponse.getSessionId(); + + // 3. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + Thread.sleep(60000); + + { + // 4. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.setSessionId(sessionId); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.HS256, clientSecret, cryptoProvider); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(30); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizationResponse authorizationResponse = authenticateResourceOwner( + authorizationEndpoint, authorizationRequest, userId, userSecret, false); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + assertNotNull(authorizationResponse.getScope()); + + String authorizationCode = authorizationResponse.getCode(); + + // 5. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/ProvidingIndividuallyRequestedEssentialAndVoluntaryClaims.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/ProvidingIndividuallyRequestedEssentialAndVoluntaryClaims.java new file mode 100644 index 00000000..f6889dd5 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/ProvidingIndividuallyRequestedEssentialAndVoluntaryClaims.java @@ -0,0 +1,154 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.client.model.authorize.Claim; +import org.gluu.oxauth.client.model.authorize.ClaimValue; +import org.gluu.oxauth.client.model.authorize.JwtAuthorizationRequest; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Providing Individually Requested Essential and Voluntary Claims + * + * @author Javier Rojas Blum + * @version May 30, 2018 + */ +public class ProvidingIndividuallyRequestedEssentialAndVoluntaryClaims extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void providingIndividuallyRequestedEssentialAndVoluntaryClaims( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Providing Individually Requested Essential and Voluntary Claims"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setClaims(Arrays.asList( + JwtClaimName.NAME, + JwtClaimName.EMAIL, + JwtClaimName.PICTURE)); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest, SignatureAlgorithm.HS256, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NAME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createNull())); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.NAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EMAIL)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.PICTURE)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/ProvidingIndividuallyRequestedEssentialClaims.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/ProvidingIndividuallyRequestedEssentialClaims.java new file mode 100644 index 00000000..b55b73a5 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/ProvidingIndividuallyRequestedEssentialClaims.java @@ -0,0 +1,144 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.client.model.authorize.Claim; +import org.gluu.oxauth.client.model.authorize.ClaimValue; +import org.gluu.oxauth.client.model.authorize.JwtAuthorizationRequest; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Providing Individually Requested Essential Claims + * + * @author Javier Rojas Blum + * @version May 30, 2018 + */ +public class ProvidingIndividuallyRequestedEssentialClaims extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void providingIndividuallyRequestedEssentialClaims( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Providing Individually Requested Essential Claims"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setClaims(Arrays.asList( + JwtClaimName.NAME)); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest, SignatureAlgorithm.HS256, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NAME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.NAME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/ProvidingIndividuallyRequestedVoluntaryClaims.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/ProvidingIndividuallyRequestedVoluntaryClaims.java new file mode 100644 index 00000000..aae06613 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/ProvidingIndividuallyRequestedVoluntaryClaims.java @@ -0,0 +1,148 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.client.model.authorize.Claim; +import org.gluu.oxauth.client.model.authorize.ClaimValue; +import org.gluu.oxauth.client.model.authorize.JwtAuthorizationRequest; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Providing Individually Requested Voluntary Claims + * + * @author Javier Rojas Blum + * @version May 30, 2018 + */ +public class ProvidingIndividuallyRequestedVoluntaryClaims extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void providingIndividuallyRequestedVoluntaryClaims( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Providing Individually Requested Voluntary Claims"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setClaims(Arrays.asList( + JwtClaimName.EMAIL, + JwtClaimName.PICTURE)); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.HS256, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createNull())); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EMAIL)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.PICTURE)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/PublishOpenIdConfigurationDiscoveryInformation.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/PublishOpenIdConfigurationDiscoveryInformation.java new file mode 100644 index 00000000..06310edd --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/PublishOpenIdConfigurationDiscoveryInformation.java @@ -0,0 +1,35 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertTrue; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.BaseTest; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Publish openid-configuration Discovery Information + * + * @author Javier Rojas Blum + * @version 0.9, 06/09/2014 + */ +public class PublishOpenIdConfigurationDiscoveryInformation extends BaseTest { + + @Test + public void publishOpenIdConfigurationDiscoveryInformation() { + showTitle("OC5:FeatureTest-Publish openid-configuration Discovery Information"); + + assertTrue(StringUtils.isNotBlank(authorizationEndpoint)); + assertTrue(StringUtils.isNotBlank(tokenEndpoint)); + assertTrue(StringUtils.isNotBlank(userInfoEndpoint)); + assertTrue(StringUtils.isNotBlank(checkSessionIFrame)); + assertTrue(StringUtils.isNotBlank(endSessionEndpoint)); + assertTrue(StringUtils.isNotBlank(jwksUri)); + assertTrue(StringUtils.isNotBlank(registrationEndpoint)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectInvalidAsymmetricIdTokenSignature.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectInvalidAsymmetricIdTokenSignature.java new file mode 100644 index 00000000..0d76ef25 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectInvalidAsymmetricIdTokenSignature.java @@ -0,0 +1,97 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Reject Invalid Asymmetric ID Token Signature + * + * @author Javier Rojas Blum + * @version November 3, 2016 + */ +public class RejectInvalidAsymmetricIdTokenSignature extends BaseTest { + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void rejectInvalidAsymmetricIdTokenSignature( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Reject Invalid Asymmetric ID Token Signature"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS512); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + assertFalse(rsaSigner.validate(jwt)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectInvalidSymmetricIdTokenSignature.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectInvalidSymmetricIdTokenSignature.java new file mode 100644 index 00000000..fd94757c --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectInvalidSymmetricIdTokenSignature.java @@ -0,0 +1,92 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.HMACSigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Reject Invalid Symmetric ID Token Signature + * + * @author Javier Rojas Blum + * @version November 3, 2016 + */ +public class RejectInvalidSymmetricIdTokenSignature extends BaseTest { + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void rejectInvalidSymmetricIdTokenSignature( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Reject Invalid Symmetric ID Token Signature"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.HS512); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request Authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + HMACSigner hmacSigner = new HMACSigner(SignatureAlgorithm.HS256, clientSecret); + assertFalse(hmacSigner.validate(jwt)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectRedirectUriNotMatchingARegisteredRedirectUri.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectRedirectUriNotMatchingARegisteredRedirectUri.java new file mode 100644 index 00000000..919b7dd3 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectRedirectUriNotMatchingARegisteredRedirectUri.java @@ -0,0 +1,80 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Reject redirect uri Not Matching a Registered redirect uri + * + * @author Javier Rojas Blum + * @version June 19, 2015 + */ +public class RejectRedirectUriNotMatchingARegisteredRedirectUri extends BaseTest { + + @Parameters({"redirectUri"}) + @Test + public void rejectRedirectUriNotMatchingARegisteredRedirectUri(final String redirectUri) throws Exception { + showTitle("OC5:FeatureTest-Reject redirect uri Not Matching a Registered redirect uri"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUri)); + registerRequest.setResponseTypes(responseTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization and receive the authorization code. + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, + "https://wrong_redirect_uri", null); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(authorizationResponse.getStatus(), 400, "Unexpected response code: " + authorizationResponse.getStatus()); + assertNotNull(authorizationResponse.getErrorType(), "The error type is null"); + assertNotNull(authorizationResponse.getErrorDescription(), "The error description is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectRegistrationOfRedirectUriWithFragment.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectRegistrationOfRedirectUriWithFragment.java new file mode 100644 index 00000000..84e28ed0 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectRegistrationOfRedirectUriWithFragment.java @@ -0,0 +1,53 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Reject Registration of redirect uri with Fragment + * + * @author Javier Rojas Blum Date: 08.13.2013 + */ +public class RejectRegistrationOfRedirectUriWithFragment extends BaseTest { + + @Parameters({"redirectUri"}) + @Test + public void rejectRegistrationOfRedirectUriWithFragment(final String redirectUri) throws Exception { + showTitle("OC5:FeatureTest-Reject Registration of redirect uri with Fragment"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUri + "#foo1=bar")); + registerRequest.setResponseTypes(responseTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 400, "Unexpected response code: " + registerResponse.getStatus()); + assertNotNull(registerResponse.getErrorType(), "The error type is null"); + assertNotNull(registerResponse.getErrorDescription(), "The error description is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectRequestWithoutRedirectUriWhenMultipleRegistered.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectRequestWithoutRedirectUriWhenMultipleRegistered.java new file mode 100644 index 00000000..d5f5009e --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectRequestWithoutRedirectUriWhenMultipleRegistered.java @@ -0,0 +1,83 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Reject Request Without redirect uri when Multiple Registered + * + * @author Javier Rojas Blum + * @version November 3, 2016 + */ +public class RejectRequestWithoutRedirectUriWhenMultipleRegistered extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "sectorIdentifierUri"}) + @Test + public void rejectRequestWithoutRedirectUriWhenMultipleRegistered( + final String userId, final String userSecret, final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Reject Request Without redirect uri when Multiple Registered"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization and receive the authorization code. + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, null, null); + authorizationRequest.setAuthUsername(userId); + authorizationRequest.setAuthPassword(userSecret); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(authorizationResponse.getStatus(), 400, "Unexpected response code: " + authorizationResponse.getStatus()); + assertNotNull(authorizationResponse.getErrorType(), "The error type is null"); + assertNotNull(authorizationResponse.getErrorDescription(), "The error description is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectRequestWithoutResponseType.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectRequestWithoutResponseType.java new file mode 100644 index 00000000..b10bc970 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectRequestWithoutResponseType.java @@ -0,0 +1,44 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Reject Request Without response type + * + * @author Javier Rojas Blum Date: 07.31.2013 + */ +public class RejectRequestWithoutResponseType extends BaseTest { + + @Parameters({"userId", "userSecret"}) + @Test + public void rejectRequestWithoutResponseType(final String userId, final String userSecret) throws Exception { + showTitle("OC5:FeatureTest-Reject Request Without response type"); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(null, null, null, null, null); + authorizationRequest.setAuthUsername(userId); + authorizationRequest.setAuthPassword(userSecret); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(authorizationResponse.getStatus(), 400, "Unexpected response code: " + authorizationResponse.getStatus()); + assertNotNull(authorizationResponse.getErrorType(), "The error type is null"); + assertNotNull(authorizationResponse.getErrorDescription(), "The error description is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectRequestsWithoutNonceUsingImplicitFlow.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectRequestsWithoutNonceUsingImplicitFlow.java new file mode 100644 index 00000000..2201a237 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectRequestsWithoutNonceUsingImplicitFlow.java @@ -0,0 +1,86 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Reject Requests Without nonce Using Implicit Flow + * + * @author Javier Rojas Blum + * @version November 3, 2016 + */ +public class RejectRequestsWithoutNonceUsingImplicitFlow extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void rejectRequestsWithoutNonceUsingImplicitFlow( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Reject Requests Without nonce Using Implicit Flow"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization and receive the authorization code. + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setAuthUsername(userId); + authorizationRequest.setAuthPassword(userSecret); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(authorizationResponse.getStatus(), 302, "Unexpected response code: " + authorizationResponse.getStatus()); + assertNotNull(authorizationResponse.getErrorType(), "The error type is null"); + assertNotNull(authorizationResponse.getErrorDescription(), "The error description is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectsIncorrectAtHashWhenImplicitFlowUsed.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectsIncorrectAtHashWhenImplicitFlowUsed.java new file mode 100644 index 00000000..3d023cb1 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectsIncorrectAtHashWhenImplicitFlowUsed.java @@ -0,0 +1,118 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Rejects Incorrect at hash when Implicit Flow Used + * + * @author Javier Rojas Blum + * @version November 3, 2016 + */ +public class RejectsIncorrectAtHashWhenImplicitFlowUsed extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void rejectsIncorrectAtHashWhenImplicitFlowUsed( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Rejects Incorrect at hash when Implicit Flow Used"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getAccessToken()); + assertNotNull(authorizationResponse.getTokenType()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + + String accessToken = authorizationResponse.getAccessToken(); + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate access_token and id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + jwt.getClaims().setClaim(JwtClaimName.ACCESS_TOKEN_HASH, "INCORRECT_AT_HASH"); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + assertFalse(rsaSigner.validateAccessToken(accessToken, jwt)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectsIncorrectCHashWhenCodeFlowUsed.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectsIncorrectCHashWhenCodeFlowUsed.java new file mode 100644 index 00000000..6c958ce6 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectsIncorrectCHashWhenCodeFlowUsed.java @@ -0,0 +1,117 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Rejects Incorrect c hash when Code Flow Used + * + * @author Javier Rojas Blum + * @version November 3, 2016 + */ +public class RejectsIncorrectCHashWhenCodeFlowUsed extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void rejectsIncorrectCHashWhenCodeFlowUsed( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Rejects Incorrect c hash when Code Flow Used"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + String nonce = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The code is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String code = authorizationResponse.getCode(); + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate code and id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.CODE_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + jwt.getClaims().setClaim(JwtClaimName.CODE_HASH, "INCORRECT_C_HASH"); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + assertFalse(rsaSigner.validateAuthorizationCode(code, jwt)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectsRedirectUriWhenQueryParameterDoesNotMatch.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectsRedirectUriWhenQueryParameterDoesNotMatch.java new file mode 100644 index 00000000..ae0a7830 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectsRedirectUriWhenQueryParameterDoesNotMatch.java @@ -0,0 +1,83 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Rejects redirect uri when Query Parameter Does Not Match + * + * @author Javier Rojas Blum + * @version June 19, 2015 + */ +public class RejectsRedirectUriWhenQueryParameterDoesNotMatch extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUri"}) + @Test + public void rejectsRedirectUriWhenQueryParameterDoesNotMatch(final String userId, final String userSecret, + final String redirectUri) throws Exception { + showTitle("OC5:FeatureTest-Rejects redirect uri when Query Parameter Does Not Match"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUri + "?foo1=bar")); + registerRequest.setResponseTypes(responseTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization and receive the authorization code. + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, + redirectUri + "?foo2=bar", null); + authorizationRequest.setAuthUsername(userId); + authorizationRequest.setAuthPassword(userSecret); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(authorizationResponse.getStatus(), 400, "Unexpected response code: " + authorizationResponse.getStatus()); + assertNotNull(authorizationResponse.getErrorType(), "The error type is null"); + assertNotNull(authorizationResponse.getErrorDescription(), "The error description is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectsSecondUseOfAccessCode.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectsSecondUseOfAccessCode.java new file mode 100644 index 00000000..d8dce8e8 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectsSecondUseOfAccessCode.java @@ -0,0 +1,140 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Rejects Second Use of Access Code + * + * @author Javier Rojas Blum + * @version November 3, 2016 + */ +public class RejectsSecondUseOfAccessCode extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void rejectsSecondUseOfAccessCode( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Rejects Second Use of Access Code"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization and receive the authorization code. + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + String nonce = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNotNull(authorizationResponse.getIdToken(), "The id token is null"); + + String scope = authorizationResponse.getScope(); + String authorizationCode = authorizationResponse.getCode(); + + // 3. Request access token using the authorization code. + String accessToken; + String refreshToken; + { + TokenClient tokenClient = new TokenClient(tokenEndpoint); + TokenResponse tokenResponse = tokenClient.execAuthorizationCode(authorizationCode, redirectUri, + clientId, clientSecret); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + + accessToken = tokenResponse.getAccessToken(); + refreshToken = tokenResponse.getRefreshToken(); + } + + // 4. Request user info + { + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + } + + // 5. Request access token using the same authorization code one more time. This call must fail. + { + TokenClient tokenClient = new TokenClient(tokenEndpoint); + TokenResponse tokenResponse = tokenClient.execAuthorizationCode(authorizationCode, redirectUri, clientId, clientSecret); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 400, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectsSectorIdentifierNotContainingRegisteredRedirectUriValues.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectsSectorIdentifierNotContainingRegisteredRedirectUriValues.java new file mode 100644 index 00000000..af23eb04 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RejectsSectorIdentifierNotContainingRegisteredRedirectUriValues.java @@ -0,0 +1,53 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Rejects Sector Identifier Not Containing Registered redirect uri Values + * + * @author Javier Rojas Blum Date: 08.22.2013 + */ +public class RejectsSectorIdentifierNotContainingRegisteredRedirectUriValues extends BaseTest { + + @Parameters({"sectorIdentifierUri"}) + @Test + public void rejectsSectorIdentifierNotContainingRegisteredRedirectUriValues(final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Rejects Sector Identifier Not Containing Registered redirect uri Values"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList("https://not_registered")); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setResponseTypes(responseTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 400, "Unexpected response code: " + registerResponse.getStatus()); + assertNotNull(registerResponse.getErrorType(), "The error type is null"); + assertNotNull(registerResponse.getErrorDescription(), "The error description is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RequestingUserInfoClaimsWithOpenIdRequestObject.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RequestingUserInfoClaimsWithOpenIdRequestObject.java new file mode 100644 index 00000000..c8b04979 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RequestingUserInfoClaimsWithOpenIdRequestObject.java @@ -0,0 +1,143 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.client.model.authorize.JwtAuthorizationRequest; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Requesting UserInfo Claims with OpenID Request Object + * + * @author Javier Rojas Blum + * @version January 11, 2017 + */ +public class RequestingUserInfoClaimsWithOpenIdRequestObject extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void requestingUserInfoClaimsWithOpenIdRequestObject( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Requesting UserInfo Claims with OpenID Request Object"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.HS256, clientSecret, cryptoProvider); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RequestingUserInfoClaimsWithScopeValues.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RequestingUserInfoClaimsWithScopeValues.java new file mode 100644 index 00000000..eed166b3 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/RequestingUserInfoClaimsWithScopeValues.java @@ -0,0 +1,102 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Requesting UserInfo Claims with scope Values + * + * @author Javier Rojas Blum + * @version June 19, 2015 + */ +public class RequestingUserInfoClaimsWithScopeValues extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestingUserInfoClaimsWithScopeValues( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Requesting UserInfo Claims with scope Values"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getAccessToken()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SecondUseOfAccessCodeRevokesPreviouslyIssuedAccessToken.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SecondUseOfAccessCodeRevokesPreviouslyIssuedAccessToken.java new file mode 100644 index 00000000..81d8d41f --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SecondUseOfAccessCodeRevokesPreviouslyIssuedAccessToken.java @@ -0,0 +1,163 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Second Use of Access Code Revokes Previously Issued Access Token + * + * @author Javier Rojas Blum + * @version May 14, 2019 + */ +public class SecondUseOfAccessCodeRevokesPreviouslyIssuedAccessToken extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void secondUseOfAccessCodeRevokesPreviouslyIssuedAccessToken( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Second Use of Access Code Revokes Previously Issued Access Token"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization and receive the authorization code. + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + String nonce = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNotNull(authorizationResponse.getIdToken(), "The id token is null"); + + String scope = authorizationResponse.getScope(); + String authorizationCode = authorizationResponse.getCode(); + + // 3. Request access token using the authorization code. + String accessToken; + String refreshToken; + { + TokenClient tokenClient = new TokenClient(tokenEndpoint); + TokenResponse tokenResponse = tokenClient.execAuthorizationCode(authorizationCode, redirectUri, + clientId, clientSecret); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + + accessToken = tokenResponse.getAccessToken(); + refreshToken = tokenResponse.getRefreshToken(); + } + + // 4. Request user info + { + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + } + + // 5. Request access token using the same authorization code one more time. This call must fail. + { + TokenClient tokenClient = new TokenClient(tokenEndpoint); + TokenResponse tokenResponse = tokenClient.execAuthorizationCode(authorizationCode, redirectUri, clientId, clientSecret); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 400, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + // 6. Request user info. This call must fail. + { + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 401, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getErrorType(), "Unexpected result: errorType not found"); + assertNotNull(userInfoResponse.getErrorDescription(), "Unexpected result: errorDescription not found"); + } + + // 7. Request new access token using the refresh token. This call must fail too. + { + TokenClient tokenClient = new TokenClient(tokenEndpoint); + TokenResponse tokenResponse = tokenClient.execRefreshToken(scope, refreshToken, clientId, clientSecret); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 400, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportAuthenticationToTokenEndpointUsingFormEncodedClientCredentialsInPostBody.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportAuthenticationToTokenEndpointUsingFormEncodedClientCredentialsInPostBody.java new file mode 100644 index 00000000..106a7379 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportAuthenticationToTokenEndpointUsingFormEncodedClientCredentialsInPostBody.java @@ -0,0 +1,106 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Support Authentication to Token Endpoint using Form-Encoded Client Credentials in POST Body + * + * @author Javier Rojas Blum + * @version June 19, 2015 + */ +public class SupportAuthenticationToTokenEndpointUsingFormEncodedClientCredentialsInPostBody extends BaseTest { + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void supportAuthenticationToTokenEndpointUsingFormEncodedClientCredentialsInPostBody( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Support Authentication to Token Endpoint using Form-Encoded Client Credentials in POST Body"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_POST); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_POST); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportAuthenticationToTokenEndpointUsingHttpBasicWithPost.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportAuthenticationToTokenEndpointUsingHttpBasicWithPost.java new file mode 100644 index 00000000..a91a5e11 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportAuthenticationToTokenEndpointUsingHttpBasicWithPost.java @@ -0,0 +1,106 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Support Authentication to Token Endpoint using HTTP Basic with POST + * + * @author Javier Rojas Blum + * @version June 19, 2015 + */ +public class SupportAuthenticationToTokenEndpointUsingHttpBasicWithPost extends BaseTest { + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void supportAuthenticationToTokenEndpointUsingHttpBasicWithPost( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Support Authentication to Token Endpoint using HTTP Basic with POST"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportAuthenticationToTokenEndpointWithAsymmetricallySignedJWTs.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportAuthenticationToTokenEndpointWithAsymmetricallySignedJWTs.java new file mode 100644 index 00000000..575b017a --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportAuthenticationToTokenEndpointWithAsymmetricallySignedJWTs.java @@ -0,0 +1,480 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Support Authentication to Token Endpoint with Asymmetrically Signed JWTs + * + * @author Javier Rojas Blum + * @version June 15, 2016 + */ +public class SupportAuthenticationToTokenEndpointWithAsymmetricallySignedJWTs extends BaseTest { + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri", "clientJwksUri", + "RS256_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void supportAuthenticationToTokenEndpointWithAsymmetricallySignedJWTsRS256( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String sectorIdentifierUri, final String clientJwksUri, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("OC5:FeatureTest-Support Authentication to Token Endpoint with Asymmetrically Signed JWTs (RS256)"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri", "clientJwksUri", + "RS384_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void supportAuthenticationToTokenEndpointWithAsymmetricallySignedJWTsRS384( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String sectorIdentifierUri, final String clientJwksUri, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("OC5:FeatureTest-Support Authentication to Token Endpoint with Asymmetrically Signed JWTs (RS384)"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri", "clientJwksUri", + "RS512_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void supportAuthenticationToTokenEndpointWithAsymmetricallySignedJWTsRS512( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String sectorIdentifierUri, final String clientJwksUri, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("OC5:FeatureTest-Support Authentication to Token Endpoint with Asymmetrically Signed JWTs (RS512)"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri", "clientJwksUri", + "ES256_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void supportAuthenticationToTokenEndpointWithAsymmetricallySignedJWTsES256( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String sectorIdentifierUri, final String clientJwksUri, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("OC5:FeatureTest-Support Authentication to Token Endpoint with Asymmetrically Signed JWTs (ES256)"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri", "clientJwksUri", + "ES384_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void supportAuthenticationToTokenEndpointWithAsymmetricallySignedJWTsES384( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String sectorIdentifierUri, final String clientJwksUri, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("OC5:FeatureTest-Support Authentication to Token Endpoint with Asymmetrically Signed JWTs (ES384)"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri", "clientJwksUri", + "ES512_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void supportAuthenticationToTokenEndpointWithAsymmetricallySignedJWTsES512( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String sectorIdentifierUri, final String clientJwksUri, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret) throws Exception { + showTitle("OC5:FeatureTest-Support Authentication to Token Endpoint with Asymmetrically Signed JWTs (ES512)"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportAuthenticationToTokenEndpointWithSymmetricallySignedJWTs.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportAuthenticationToTokenEndpointWithSymmetricallySignedJWTs.java new file mode 100644 index 00000000..e9a5a384 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportAuthenticationToTokenEndpointWithSymmetricallySignedJWTs.java @@ -0,0 +1,254 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Support Authentication to Token Endpoint with Symmetrically Signed JWTs + * + * @author Javier Rojas Blum + * @version June 17, 2016 + */ +public class SupportAuthenticationToTokenEndpointWithSymmetricallySignedJWTs extends BaseTest { + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void supportAuthenticationToTokenEndpointWithSymmetricallySignedJWTsHS256( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Support Authentication to Token Endpoint with Symmetrically Signed JWTs (HS256)"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void supportAuthenticationToTokenEndpointWithSymmetricallySignedJWTsHS384( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Support Authentication to Token Endpoint with Symmetrically Signed JWTs (HS384)"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setAlgorithm(SignatureAlgorithm.HS384); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void supportAuthenticationToTokenEndpointWithSymmetricallySignedJWTsHS512( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Support Authentication to Token Endpoint with Symmetrically Signed JWTs (HS512)"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setAlgorithm(SignatureAlgorithm.HS512); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportClaimsRequestSpecifyingSubValue.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportClaimsRequestSpecifyingSubValue.java new file mode 100644 index 00000000..38bf2e6f --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportClaimsRequestSpecifyingSubValue.java @@ -0,0 +1,229 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.client.model.authorize.Claim; +import org.gluu.oxauth.client.model.authorize.ClaimValue; +import org.gluu.oxauth.client.model.authorize.JwtAuthorizationRequest; +import org.gluu.oxauth.model.authorize.AuthorizeErrorResponseType; +import org.gluu.oxauth.model.common.Prompt; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Support claims Request Specifying sub Value + * If that user is logged in, the request succeeds, otherwise it fails. + * + * @author Javier Rojas Blum + * @version May 30, 2018 + */ +public class SupportClaimsRequestSpecifyingSubValue extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void supportClaimsRequestSpecifyingSubValueSucceed( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Support claims Request Specifying sub Value (succeed)"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setClaims(Arrays.asList( + JwtClaimName.GIVEN_NAME, + JwtClaimName.FAMILY_NAME)); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + List scopes = Arrays.asList("openid", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + // 2. Request authorization (first time) + AuthorizationRequest authorizationRequest1 = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest1.setState(state); + + AuthorizeClient authorizeClient1 = new AuthorizeClient(authorizationEndpoint); + authorizeClient1.setRequest(authorizationRequest1); + + AuthorizationResponse authorizationResponse1 = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest1, userId, userSecret); + + assertNotNull(authorizationResponse1.getLocation(), "The location is null"); + assertNotNull(authorizationResponse1.getIdToken(), "The ID Token is null"); + assertNotNull(authorizationResponse1.getAccessToken(), "The Access Token is null"); + assertNotNull(authorizationResponse1.getState(), "The state is null"); + assertNotNull(authorizationResponse1.getScope(), "The scope is null"); + + String sessionId = authorizationResponse1.getSessionId(); + + // 3. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + AuthorizationRequest authorizationRequest2 = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest2.getPrompts().add(Prompt.NONE); + authorizationRequest2.setState(state); + authorizationRequest2.setSessionId(sessionId); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest2, SignatureAlgorithm.HS256, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.SUBJECT_IDENTIFIER, ClaimValue.createSingleValue(userId))); + + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest2.setRequest(authJwt); + + AuthorizeClient authorizeClient2 = new AuthorizeClient(authorizationEndpoint); + authorizeClient2.setRequest(authorizationRequest2); + AuthorizationResponse authorizationResponse2 = authorizeClient2.exec(); + + assertNotNull(authorizationResponse2.getLocation(), "The location is null"); + assertNotNull(authorizationResponse2.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse2.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse2.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse2.getState(), "The state is null"); + + String idToken = authorizationResponse2.getIdToken(); + String accessToken = authorizationResponse2.getAccessToken(); + + // 4. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 5. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void supportClaimsRequestSpecifyingSubValueFail( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Support claims Request Specifying sub Value (fail)"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest, SignatureAlgorithm.HS256, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.SUBJECT_IDENTIFIER, ClaimValue.createSingleValue("WRONG_USER_ID"))); + + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getErrorType(), "The error type is null"); + assertEquals(authorizationResponse.getErrorType(), AuthorizeErrorResponseType.USER_MISMATCHED); + assertNotNull(authorizationResponse.getErrorDescription(), "The error description is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportCodeResponseType.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportCodeResponseType.java new file mode 100644 index 00000000..d06c24d9 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportCodeResponseType.java @@ -0,0 +1,81 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Support code Response Type + * + * @author Javier Rojas Blum + * @version June 19, 2015 + */ +public class SupportCodeResponseType extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void supportCodeResponseType( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Support code Response Type"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportCombinationOfCodeIdTokenTokenResponseTypes.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportCombinationOfCodeIdTokenTokenResponseTypes.java new file mode 100644 index 00000000..91799031 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportCombinationOfCodeIdTokenTokenResponseTypes.java @@ -0,0 +1,81 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Support Combination of code id token token Response Types + * + * @author Javier Rojas Blum + * @version June 19, 2015 + */ +public class SupportCombinationOfCodeIdTokenTokenResponseTypes extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void supportCombinationOfCodeIdTokenTokenResponseTypes( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Support Combination of code id token token Response Types"); + + List responseTypes = Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN, ResponseType.TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getAccessToken()); + assertNotNull(authorizationResponse.getState()); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportCombinationOfCodeTokenResponseTypes.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportCombinationOfCodeTokenResponseTypes.java new file mode 100644 index 00000000..111e7fa8 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportCombinationOfCodeTokenResponseTypes.java @@ -0,0 +1,81 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Support Combination of code token Response Types + * + * @author Javier Rojas Blum + * @version June 19, 2015 + */ +public class SupportCombinationOfCodeTokenResponseTypes extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void supportCombinationOfCodeTokenResponseTypes( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Support Combination of code token Response Types"); + + List responseTypes = Arrays.asList(ResponseType.CODE, ResponseType.TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getAccessToken()); + assertNotNull(authorizationResponse.getState()); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportCombinationOfIdTokenCodeResponseTypes.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportCombinationOfIdTokenCodeResponseTypes.java new file mode 100644 index 00000000..e4ac94a5 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportCombinationOfIdTokenCodeResponseTypes.java @@ -0,0 +1,81 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Support Combination of id token code Response Types + * + * @author Javier Rojas Blum + * @version June 19, 2015 + */ +public class SupportCombinationOfIdTokenCodeResponseTypes extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void supportCombinationOfIdTokenCodeResponseTypes( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Support Combination of id token code Response Types"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN, ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportCombinationOfIdTokenTokenResponseTypes.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportCombinationOfIdTokenTokenResponseTypes.java new file mode 100644 index 00000000..dd8f8f15 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportCombinationOfIdTokenTokenResponseTypes.java @@ -0,0 +1,81 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Support Combination of id token token Response Types + * + * @author Javier Rojas Blum + * @version June 19, 2015 + */ +public class SupportCombinationOfIdTokenTokenResponseTypes extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void supportCombinationOfIdTokenTokenResponseTypes( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Support Combination of id token token Response Types"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN, ResponseType.TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getAccessToken()); + assertNotNull(authorizationResponse.getState()); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportDisplayValuePage.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportDisplayValuePage.java new file mode 100644 index 00000000..4d1710fb --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportDisplayValuePage.java @@ -0,0 +1,81 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.Display; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Support display value page + * + * @author Javier Rojas Blum + * @version June 19, 2015 + */ +public class SupportDisplayValuePage extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void supportDisplayValuePage( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Support display value page"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setDisplay(Display.PAGE); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportDisplayValuePopup.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportDisplayValuePopup.java new file mode 100644 index 00000000..f4efba67 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportDisplayValuePopup.java @@ -0,0 +1,81 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.Display; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Support display value popup + * + * @author Javier Rojas Blum + * @version June 19, 2015 + */ +public class SupportDisplayValuePopup extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void supportDisplayValuePage( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Support display value page"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setDisplay(Display.POPUP); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportIdTokenResponseType.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportIdTokenResponseType.java new file mode 100644 index 00000000..1dced1a4 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportIdTokenResponseType.java @@ -0,0 +1,79 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Support id token Response Type + * + * @author Javier Rojas Blum + * @version June 19, 2015 + */ +public class SupportIdTokenResponseType extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void supportIdTokenResponseType( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Support id token Response Type"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportPromptValueLogin.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportPromptValueLogin.java new file mode 100644 index 00000000..1f9efd96 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportPromptValueLogin.java @@ -0,0 +1,153 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.Prompt; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Support prompt value login + * + * @author Javier Rojas Blum + * @version August 9, 2017 + */ +public class SupportPromptValueLogin extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void supportPromptValueLogin( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Support prompt value login"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = newRegisterClient(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + String sessionId; + { + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + assertNotNull(authorizationResponse.getScope()); + + String authorizationCode = authorizationResponse.getCode(); + sessionId = authorizationResponse.getSessionId(); + + // 3. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = newTokenClient(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + { + // 4. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.getPrompts().add(Prompt.LOGIN); + authorizationRequest.setSessionId(sessionId); + + AuthorizationResponse authorizationResponse = authenticateResourceOwner( + authorizationEndpoint, authorizationRequest, userId, userSecret, false); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + assertNotNull(authorizationResponse.getScope()); + + String authorizationCode = authorizationResponse.getCode(); + + // 5. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = newTokenClient(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportPromptValueNone.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportPromptValueNone.java new file mode 100644 index 00000000..a823a6db --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportPromptValueNone.java @@ -0,0 +1,159 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.Prompt; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Support prompt value none + * + * @author Javier Rojas Blum + * @version August 9, 2017 + */ +public class SupportPromptValueNone extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationPromptNone( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Support prompt value none"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + String sessionId; + { + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + assertNotNull(authorizationResponse.getScope()); + + String authorizationCode = authorizationResponse.getCode(); + sessionId = authorizationResponse.getSessionId(); + + // 3. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + { + // 4. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.getPrompts().add(Prompt.NONE); + authorizationRequest.setSessionId(sessionId); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + assertNotNull(authorizationResponse.getScope()); + + String authorizationCode = authorizationResponse.getCode(); + + // 5. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportRegistrationRead.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportRegistrationRead.java new file mode 100644 index 00000000..6d39c016 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportRegistrationRead.java @@ -0,0 +1,113 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.gluu.oxauth.model.register.RegisterRequestParam.APPLICATION_TYPE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.CLIENT_NAME; +import static org.gluu.oxauth.model.register.RegisterRequestParam.CONTACTS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ID_TOKEN_SIGNED_RESPONSE_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.JWKS_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.LOGO_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.POLICY_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REQUEST_OBJECT_SIGNING_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REQUEST_URIS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SCOPE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SECTOR_IDENTIFIER_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SUBJECT_TYPE; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.common.SubjectType; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Support Registration Read + * + * @author Javier Rojas Blum + * @version November 29, 2017 + */ +public class SupportRegistrationRead extends BaseTest { + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void supportRegistrationRead( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Support Registration Read"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest1 = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest1.setContacts(Arrays.asList("javier@gluu.org", "javier.rojas.blum@gmail.com")); + registerRequest1.setLogoUri("http://www.gluu.org/wp-content/themes/gluursn/images/logo.png"); + registerRequest1.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest1.setPolicyUri("http://www.gluu.org/policy"); + registerRequest1.setJwksUri("http://www.gluu.org/jwks"); + registerRequest1.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest1.setSubjectType(SubjectType.PUBLIC); + registerRequest1.setRequestObjectSigningAlg(SignatureAlgorithm.RS256); + registerRequest1.setRequestUris(Arrays.asList("http://www.gluu.org/request")); + + RegisterClient registerClient1 = new RegisterClient(registrationEndpoint); + registerClient1.setRequest(registerRequest1); + RegisterResponse registerResponse1 = registerClient1.exec(); + + showClient(registerClient1); + assertEquals(registerResponse1.getStatus(), 200, "Unexpected response code: " + registerResponse1.getEntity()); + assertNotNull(registerResponse1.getClientId()); + assertNotNull(registerResponse1.getClientSecret()); + assertNotNull(registerResponse1.getRegistrationAccessToken()); + assertNotNull(registerResponse1.getClientSecretExpiresAt()); + assertNotNull(registerResponse1.getClaims().get(SCOPE.toString())); + + String clientId = registerResponse1.getClientId(); + String registrationAccessToken = registerResponse1.getRegistrationAccessToken(); + String registrationClientUri = registerResponse1.getRegistrationClientUri(); + + // 2. Client Read + RegisterRequest registerRequest2 = new RegisterRequest(registrationAccessToken); + + RegisterClient registerClient2 = new RegisterClient(registrationClientUri); + registerClient2.setRequest(registerRequest2); + RegisterResponse registerResponse2 = registerClient2.exec(); + + showClient(registerClient2); + assertEquals(registerResponse2.getStatus(), 200, "Unexpected response code: " + registerResponse2.getEntity()); + assertNotNull(registerResponse2.getClientId()); + assertNotNull(registerResponse2.getClientSecret()); + assertNotNull(registerResponse2.getRegistrationAccessToken()); + assertNotNull(registerResponse2.getRegistrationClientUri()); + assertNotNull(registerResponse2.getClientSecretExpiresAt()); + assertNotNull(registerResponse2.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(registerResponse2.getClaims().get(POLICY_URI.toString())); + assertNotNull(registerResponse2.getClaims().get(REQUEST_OBJECT_SIGNING_ALG.toString())); + assertNotNull(registerResponse2.getClaims().get(CONTACTS.toString())); + assertNotNull(registerResponse2.getClaims().get(SECTOR_IDENTIFIER_URI.toString())); + assertNotNull(registerResponse2.getClaims().get(SUBJECT_TYPE.toString())); + assertNotNull(registerResponse2.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(registerResponse2.getClaims().get(JWKS_URI.toString())); + assertNotNull(registerResponse2.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(registerResponse2.getClaims().get(LOGO_URI.toString())); + assertNotNull(registerResponse2.getClaims().get(REQUEST_URIS.toString())); + assertNotNull(registerResponse2.getClaims().get(SCOPE.toString())); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportRequestFile.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportRequestFile.java new file mode 100644 index 00000000..148be5fa --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportRequestFile.java @@ -0,0 +1,139 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.fail; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.model.authorize.Claim; +import org.gluu.oxauth.client.model.authorize.ClaimValue; +import org.gluu.oxauth.client.model.authorize.JwtAuthorizationRequest; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.Base64Util; +import org.gluu.oxauth.model.util.JwtUtil; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Support Request File + * + * @author Javier Rojas Blum + * @version July 31, 2016 + */ +public class SupportRequestFile extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri", "requestFileBasePath", "requestFileBaseUrl"}) + @Test // This tests requires a place to publish a request object via HTTPS + public void requestFileMethod(final String userId, final String userSecret, final String redirectUri, + final String redirectUris, final String sectorIdentifierUri, + final String requestFileBasePath, final String requestFileBaseUrl) throws Exception { + showTitle("OC5:FeatureTest-Support Request File"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Writing a request object in a file + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + try { + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest, SignatureAlgorithm.HS256, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{"basic"}))); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + String hash = Base64Util.base64urlencode(JwtUtil.getMessageDigestSHA256(authJwt)); + String fileName = UUID.randomUUID().toString() + ".txt"; + String filePath = requestFileBasePath + File.separator + fileName; + String fileUrl = requestFileBaseUrl + "/" + fileName + "#" + hash; + FileWriter fw = new FileWriter(filePath); + BufferedWriter bw = new BufferedWriter(fw); + bw.write(authJwt); + bw.close(); + fw.close(); + authorizationRequest.setRequestUri(fileUrl); + System.out.println("Request JWT: " + authJwt); + System.out.println("Request File Path: " + filePath); + System.out.println("Request File URL: " + fileUrl); + } catch (IOException e) { + e.printStackTrace(); + fail(e.getMessage()); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + fail(e.getMessage()); + } catch (NoSuchProviderException e) { + e.printStackTrace(); + fail(e.getMessage()); + } + + // 3. Request authorization + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getAccessToken()); + assertNotNull(authorizationResponse.getState()); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportRequestsContainingNonce.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportRequestsContainingNonce.java new file mode 100644 index 00000000..e726d737 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportRequestsContainingNonce.java @@ -0,0 +1,117 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Support Requests Containing nonce + * + * @author Javier Rojas Blum + * @version November 3, 2016 + */ +public class SupportRequestsContainingNonce extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void supportRequestsContainingNonce( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Support Requests Containing nonce"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertEquals(nonce, jwt.getClaims().getClaimAsString(JwtClaimName.NONCE)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportRequestsWithoutNonce.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportRequestsWithoutNonce.java new file mode 100644 index 00000000..e36ca4ce --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportRequestsWithoutNonce.java @@ -0,0 +1,111 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Support Requests Without nonce + * + * @author Javier Rojas Blum + * @version November 3, 2016 + */ +public class SupportRequestsWithoutNonce extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void supportRequestsWithoutNonce( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Support Requests Without nonce"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization and receive the authorization code. + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + String nonce = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportScopeRequestingAddressClaims.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportScopeRequestingAddressClaims.java new file mode 100644 index 00000000..38a70987 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportScopeRequestingAddressClaims.java @@ -0,0 +1,121 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Support scope Requesting address Claims + * + * @author Javier Rojas Blum + * @version June 19, 2015 + */ +public class SupportScopeRequestingAddressClaims extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void supportScopeRequestingAddressClaims( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Support scope Requesting address Claims"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "address"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + + String accessToken = tokenResponse.getAccessToken(); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportScopeRequestingAllBasicClaims.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportScopeRequestingAllBasicClaims.java new file mode 100644 index 00000000..0c459ab1 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportScopeRequestingAllBasicClaims.java @@ -0,0 +1,121 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Support scope Requesting All Basic Claims + * + * @author Javier Rojas Blum + * @version June 19, 2015 + */ +public class SupportScopeRequestingAllBasicClaims extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void supportScopeRequestingAllBasicClaims( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Support scope Requesting All Basic Claims"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "email", "address", "phone"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + + String accessToken = tokenResponse.getAccessToken(); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportScopeRequestingEmailClaims.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportScopeRequestingEmailClaims.java new file mode 100644 index 00000000..bfa99254 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportScopeRequestingEmailClaims.java @@ -0,0 +1,121 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Support scope Requesting email Claims + * + * @author Javier Rojas Blum + * @version June 19, 2015 + */ +public class SupportScopeRequestingEmailClaims extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void supportScopeRequestingEmailClaims( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Support scope Requesting email Claims"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + + String accessToken = tokenResponse.getAccessToken(); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportScopeRequestingNoSpecificClaims.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportScopeRequestingNoSpecificClaims.java new file mode 100644 index 00000000..4a80521e --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportScopeRequestingNoSpecificClaims.java @@ -0,0 +1,140 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Support scope Requesting No Specific Claims + * + * @author Javier Rojas Blum + * @version June 19, 2015 + */ +public class SupportScopeRequestingNoSpecificClaims extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void supportScopeRequestingNoSpecificClaims( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Support scope Requesting No Specific Claims"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + + String accessToken = tokenResponse.getAccessToken(); + String idToken = tokenResponse.getIdToken(); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + + // 5. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportScopeRequestingPhoneClaims.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportScopeRequestingPhoneClaims.java new file mode 100644 index 00000000..74bc5351 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportScopeRequestingPhoneClaims.java @@ -0,0 +1,121 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Support scope Requesting phone Claims + * + * @author Javier Rojas Blum + * @version June 19, 2015 + */ +public class SupportScopeRequestingPhoneClaims extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void supportScopeRequestingPhoneClaims( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Support scope Requesting phone Claims"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "phone"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + + String accessToken = tokenResponse.getAccessToken(); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportScopeRequestingProfileClaims.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportScopeRequestingProfileClaims.java new file mode 100644 index 00000000..629c36b1 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportScopeRequestingProfileClaims.java @@ -0,0 +1,130 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Support scope Requesting profile Claims + * + * @author Javier Rojas Blum + * @version October 14, 2019 + */ +public class SupportScopeRequestingProfileClaims extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void supportScopeRequestingProfileClaims( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Support scope Requesting profile Claims"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = newRegisterClient(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = newTokenClient(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + + String accessToken = tokenResponse.getAccessToken(); + + // 4. Request user info + UserInfoResponse userInfoResponse = requestUserInfo(accessToken); + + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.WEBSITE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.BIRTHDATE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GENDER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PROFILE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PREFERRED_USERNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.MIDDLE_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.UPDATED_AT)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NICKNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportTokenResponseType.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportTokenResponseType.java new file mode 100644 index 00000000..35b295de --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportTokenResponseType.java @@ -0,0 +1,81 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Support token Response Type + * + * @author Javier Rojas Blum + * @version June 19, 2015 + */ +public class SupportTokenResponseType extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void supportTokenResponseType( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Support token Response Type"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getAccessToken()); + assertNotNull(authorizationResponse.getState()); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportWebFingerDiscovery.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportWebFingerDiscovery.java new file mode 100644 index 00000000..c29a7ccc --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportWebFingerDiscovery.java @@ -0,0 +1,29 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertTrue; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.BaseTest; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Support WebFinger Discovery + * + * @author Javier Rojas Blum + * @version 0.9, 06/09/2014 + */ +public class SupportWebFingerDiscovery extends BaseTest { + + @Test + public void supportWebFingerDiscovery() { + showTitle("OC5:FeatureTest-Support WebFinger Discovery"); + + assertTrue(StringUtils.isNotBlank(configurationEndpoint)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/Supports3rdPartyInitLogin.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/Supports3rdPartyInitLogin.java new file mode 100644 index 00000000..d3199688 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/Supports3rdPartyInitLogin.java @@ -0,0 +1,59 @@ +package org.gluu.oxauth.interop; + +import static org.gluu.oxauth.model.common.GrantType.AUTHORIZATION_CODE; +import static org.gluu.oxauth.model.common.ResponseType.CODE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.APPLICATION_TYPE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.INITIATE_LOGIN_URI; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OP-3rd_party-init-login + * + * @author Javier Rojas Blum + * @version October 22, 2019 + */ +public class Supports3rdPartyInitLogin extends BaseTest { + + @Parameters({"redirectUri", "clientJwksUri", "initiateLoginUri", "postLogoutRedirectUri"}) + @Test + public void supports3rdPartyInitLogin(final String redirectUri, final String clientJwksUri, final String initiateLoginUri, final String postLogoutRedirectUri) throws Exception { + showTitle("supports3rdPartyInitLogin"); + + // 1. Register Client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUri)); + registerRequest.setContacts(Arrays.asList("javier@gluu.org")); + registerRequest.setGrantTypes(Arrays.asList(AUTHORIZATION_CODE)); + registerRequest.setResponseTypes(Arrays.asList(CODE)); + registerRequest.setInitiateLoginUri(initiateLoginUri); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setPostLogoutRedirectUris(Arrays.asList(postLogoutRedirectUri)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + assertEquals(registerResponse.getClaims().get(APPLICATION_TYPE.toString()), ApplicationType.WEB.toString()); + assertEquals(registerResponse.getClaims().get(INITIATE_LOGIN_URI.toString()), initiateLoginUri); + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/Supports3rdPartyInitLoginNoHttps.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/Supports3rdPartyInitLoginNoHttps.java new file mode 100644 index 00000000..01d49656 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/Supports3rdPartyInitLoginNoHttps.java @@ -0,0 +1,54 @@ +package org.gluu.oxauth.interop; + +import static org.gluu.oxauth.model.common.GrantType.AUTHORIZATION_CODE; +import static org.gluu.oxauth.model.common.ResponseType.CODE; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OP-3rd_party-init-login-nohttps + * + * @author Javier Rojas Blum + * @version October 22, 2019 + */ +public class Supports3rdPartyInitLoginNoHttps extends BaseTest { + + @Parameters({"redirectUri", "clientJwksUri", "postLogoutRedirectUri"}) + @Test + public void supports3rdPartyInitLoginNoHttps(final String redirectUri, final String clientJwksUri, final String postLogoutRedirectUri) throws Exception { + showTitle("supports3rdPartyInitLoginNoHttps"); + + // 1. Register Client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUri)); + registerRequest.setContacts(Arrays.asList("javier@gluu.org")); + registerRequest.setGrantTypes(Arrays.asList(AUTHORIZATION_CODE)); + registerRequest.setResponseTypes(Arrays.asList(CODE)); + registerRequest.setInitiateLoginUri("http://client.example.com/start-3rd-party-initiated-sso"); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setPostLogoutRedirectUris(Arrays.asList(postLogoutRedirectUri)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 400, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getEntity(), "The entity is null"); + assertNotNull(registerResponse.getErrorType(), "The error type is null"); + assertNotNull(registerResponse.getErrorDescription(), "The error description is null"); + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportsCombiningClaimsRequestedWithScopeAndRequestObject.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportsCombiningClaimsRequestedWithScopeAndRequestObject.java new file mode 100644 index 00000000..c33369ec --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportsCombiningClaimsRequestedWithScopeAndRequestObject.java @@ -0,0 +1,146 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.client.model.authorize.Claim; +import org.gluu.oxauth.client.model.authorize.ClaimValue; +import org.gluu.oxauth.client.model.authorize.JwtAuthorizationRequest; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Supports Combining Claims Requested with scope and Request Object + * + * @author Javier Rojas Blum + * @version May 30, 2018 + */ +public class SupportsCombiningClaimsRequestedWithScopeAndRequestObject extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void supportsCombiningClaimsRequestedWithScopeAndRequestObject( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Supports Combining Claims Requested with scope and Request Object"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setClaims(Arrays.asList( + JwtClaimName.GIVEN_NAME, + JwtClaimName.FAMILY_NAME)); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest, SignatureAlgorithm.HS256, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createNull())); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportsReturningClaimsInIdToken.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportsReturningClaimsInIdToken.java new file mode 100644 index 00000000..126c57a7 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportsReturningClaimsInIdToken.java @@ -0,0 +1,145 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.client.model.authorize.Claim; +import org.gluu.oxauth.client.model.authorize.ClaimValue; +import org.gluu.oxauth.client.model.authorize.JwtAuthorizationRequest; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Supports Returning Claims in ID Token + * + * @author Javier Rojas Blum + * @version July 4, 2018 + */ +public class SupportsReturningClaimsInIdToken extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void supportsReturningClaimsInIdToken( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Supports Returning Claims in ID Token"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setClaims(Arrays.asList( + JwtClaimName.NAME, + JwtClaimName.EMAIL)); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.HS256, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NAME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.NAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EMAIL)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportsReturningDifferentClaimsInIdTokenAndUserInfoEndpoint.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportsReturningDifferentClaimsInIdTokenAndUserInfoEndpoint.java new file mode 100644 index 00000000..3402ff5c --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/SupportsReturningDifferentClaimsInIdTokenAndUserInfoEndpoint.java @@ -0,0 +1,151 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.client.model.authorize.Claim; +import org.gluu.oxauth.client.model.authorize.ClaimValue; +import org.gluu.oxauth.client.model.authorize.JwtAuthorizationRequest; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Supports Returning Different Claims in ID Token and UserInfo Endpoint + * + * @author Javier Rojas Blum + * @version May 30, 2018 + */ +public class SupportsReturningDifferentClaimsInIdTokenAndUserInfoEndpoint extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void supportsReturningDifferentClaimsInIdTokenAndUserInfoEndpoint( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Supports Returning Different Claims in ID Token and UserInfo Endpoint"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setClaims(Arrays.asList( + JwtClaimName.NAME, + JwtClaimName.EMAIL, + JwtClaimName.GIVEN_NAME, + JwtClaimName.FAMILY_NAME)); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest, SignatureAlgorithm.HS256, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NAME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createNull())); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.NAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EMAIL)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/UserInfoEndpoint.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/UserInfoEndpoint.java new file mode 100644 index 00000000..f5c027fe --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/UserInfoEndpoint.java @@ -0,0 +1,28 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertTrue; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.BaseTest; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-UserInfo Endpoint + * + * @author Javier Rojas Blum Date: 07.15.2013 + */ +public class UserInfoEndpoint extends BaseTest { + + @Test + public void userInfoEndpoint() { + showTitle("OC5:FeatureTest-UserInfo Endpoint"); + + assertTrue(StringUtils.isNotBlank(userInfoEndpoint)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/UserInfoEndpointAccessWithFormEncodedBodyMethod.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/UserInfoEndpointAccessWithFormEncodedBodyMethod.java new file mode 100644 index 00000000..edf50e02 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/UserInfoEndpointAccessWithFormEncodedBodyMethod.java @@ -0,0 +1,107 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoRequest; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.common.AuthorizationMethod; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-UserInfo Endpoint Access with Form-Encoded Body Method + * + * @author Javier Rojas Blum + * @version June 19, 2015 + */ +public class UserInfoEndpointAccessWithFormEncodedBodyMethod extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void userInfoEndpointAccessWithFormEncodedBodyMethod( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-UserInfo Endpoint Access with Form-Encoded Body Method"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getAccessToken()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + userInfoRequest.setAuthorizationMethod(AuthorizationMethod.FORM_ENCODED_BODY_PARAMETER); + + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/UserInfoEndpointAccessWithHeaderMethod.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/UserInfoEndpointAccessWithHeaderMethod.java new file mode 100644 index 00000000..6defcb2a --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/UserInfoEndpointAccessWithHeaderMethod.java @@ -0,0 +1,100 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-UserInfo Endpoint Access with Header Method + * + * @author Javier Rojas Blum + * @version June 19, 2015 + */ +public class UserInfoEndpointAccessWithHeaderMethod extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void userInfoEndpointAccessWithHeaderMethod( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-UserInfo Endpoint Access with Header Method"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getAccessToken()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/UsesAsymmetricIdTokenSignatures.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/UsesAsymmetricIdTokenSignatures.java new file mode 100644 index 00000000..f712204d --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/UsesAsymmetricIdTokenSignatures.java @@ -0,0 +1,374 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.signature.ECDSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.ECDSASigner; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Uses Asymmetric ID Token Signatures + * + * @author Javier Rojas Blum + * @version November 3, 2016 + */ +public class UsesAsymmetricIdTokenSignatures extends BaseTest { + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void usesAsymmetricIdTokenSignaturesRS256( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Uses Asymmetric ID Token Signatures RS256"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS256); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + assertTrue(rsaSigner.validate(jwt)); + } + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void usesAsymmetricIdTokenSignaturesRS384( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Uses Asymmetric ID Token Signatures RS384"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS384); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS384, publicKey); + assertTrue(rsaSigner.validate(jwt)); + } + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void usesAsymmetricIdTokenSignaturesRS512( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Uses Asymmetric ID Token Signatures RS512"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS512); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS512, publicKey); + assertTrue(rsaSigner.validate(jwt)); + } + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void usesAsymmetricIdTokenSignaturesES256( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Uses Asymmetric ID Token Signatures ES256"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.ES256); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + ECDSAPublicKey publicKey = JwkClient.getECDSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + ECDSASigner ecdsaSigner = new ECDSASigner(SignatureAlgorithm.ES256, publicKey); + assertTrue(ecdsaSigner.validate(jwt)); + } + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void usesAsymmetricIdTokenSignaturesES384( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Uses Asymmetric ID Token Signatures ES384"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.ES384); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + ECDSAPublicKey publicKey = JwkClient.getECDSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + ECDSASigner ecdsaSigner = new ECDSASigner(SignatureAlgorithm.ES384, publicKey); + assertTrue(ecdsaSigner.validate(jwt)); + } + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void usesAsymmetricIdTokenSignaturesES512( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Uses Asymmetric ID Token Signatures ES512"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.ES512); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + ECDSAPublicKey publicKey = JwkClient.getECDSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + ECDSASigner ecdsaSigner = new ECDSASigner(SignatureAlgorithm.ES512, publicKey); + assertTrue(ecdsaSigner.validate(jwt)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/UsesDiscovery.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/UsesDiscovery.java new file mode 100644 index 00000000..1e1b8d25 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/UsesDiscovery.java @@ -0,0 +1,34 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertTrue; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.BaseTest; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Uses Discovery + * + * @author Javier Rojas Blum Date: 09.02.2013 + */ +public class UsesDiscovery extends BaseTest { + + @Test + public void usesDiscovery() { + showTitle("OC5:FeatureTest-Uses Discovery"); + + assertTrue(StringUtils.isNotBlank(authorizationEndpoint)); + assertTrue(StringUtils.isNotBlank(tokenEndpoint)); + assertTrue(StringUtils.isNotBlank(userInfoEndpoint)); + assertTrue(StringUtils.isNotBlank(checkSessionIFrame)); + assertTrue(StringUtils.isNotBlank(endSessionEndpoint)); + assertTrue(StringUtils.isNotBlank(jwksUri)); + assertTrue(StringUtils.isNotBlank(registrationEndpoint)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/UsesDynamicRegistration.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/UsesDynamicRegistration.java new file mode 100644 index 00000000..76705fe9 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/UsesDynamicRegistration.java @@ -0,0 +1,63 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.SubjectType; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Uses Dynamic Registration + * + * @author Javier Rojas Blum Date: 09.03.2013 + */ +public class UsesDynamicRegistration extends BaseTest { + + @Parameters({"redirectUris", "sectorIdentifierUri", "clientJwksUri"}) + @Test + public void usesDynamicRegistration(final String redirectUris, final String sectorIdentifierUri, + final String clientJwksUri) throws Exception { + showTitle("OC5:FeatureTest-Uses Dynamic Registration"); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setContacts(Arrays.asList("javier@gluu.org", "javier.rojas.blum@gmail.com")); + registerRequest.setLogoUri("http://www.gluu.org/wp-content/themes/gluursn/images/logo.png"); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setPolicyUri("http://www.gluu.org/policy"); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setSubjectType(SubjectType.PUBLIC); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.RS256); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getRegistrationClientUri()); + assertNotNull(response.getClientIdIssuedAt()); + assertNotNull(response.getClientSecretExpiresAt()); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/UsesSymmetricIdTokenSignatures.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/UsesSymmetricIdTokenSignatures.java new file mode 100644 index 00000000..feca4c4e --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/UsesSymmetricIdTokenSignatures.java @@ -0,0 +1,198 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.HMACSigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Uses Symmetric ID Token Signatures + * + * @author Javier Rojas Blum + * @version November 3, 2016 + */ +public class UsesSymmetricIdTokenSignatures extends BaseTest { + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void usesSymmetricIdTokenSignaturesHS256( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Uses Symmetric ID Token Signatures HS256"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.HS256); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request Authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + HMACSigner hmacSigner = new HMACSigner(SignatureAlgorithm.HS256, clientSecret); + assertTrue(hmacSigner.validate(jwt)); + } + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void usesSymmetricIdTokenSignaturesHS384( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Uses Symmetric ID Token Signatures HS384"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.HS384); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request Authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + HMACSigner hmacSigner = new HMACSigner(SignatureAlgorithm.HS384, clientSecret); + assertTrue(hmacSigner.validate(jwt)); + } + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void usesSymmetricIdTokenSignaturesHS512( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Uses Symmetric ID Token Signatures HS512"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.HS512); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request Authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + HMACSigner hmacSigner = new HMACSigner(SignatureAlgorithm.HS512, clientSecret); + assertTrue(hmacSigner.validate(jwt)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/VerifiesCorrectAtHashWhenImplicitFlowUsed.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/VerifiesCorrectAtHashWhenImplicitFlowUsed.java new file mode 100644 index 00000000..6260c3dc --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/VerifiesCorrectAtHashWhenImplicitFlowUsed.java @@ -0,0 +1,115 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Verifies Correct at hash when Implicit Flow Used + * + * @author Javier Rojas Blum + * @version November 3, 2016 + */ +public class VerifiesCorrectAtHashWhenImplicitFlowUsed extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void verifiesCorrectAtHashWhenImplicitFlowUsed( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Verifies Correct at hash when Implicit Flow Used"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getAccessToken()); + assertNotNull(authorizationResponse.getTokenType()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + + String accessToken = authorizationResponse.getAccessToken(); + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate access_token and id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + assertTrue(rsaSigner.validateAccessToken(accessToken, jwt)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/VerifiesCorrectCHashWhenCodeFlowUsed.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/VerifiesCorrectCHashWhenCodeFlowUsed.java new file mode 100644 index 00000000..2ad47446 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/interop/VerifiesCorrectCHashWhenCodeFlowUsed.java @@ -0,0 +1,114 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.interop; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * OC5:FeatureTest-Verifies Correct c hash when Code Flow Used + * + * @author Javier Rojas Blum + * @version November 3, 2016 + */ +public class VerifiesCorrectCHashWhenCodeFlowUsed extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void verifiesCorrectCHashWhenCodeFlowUsed( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("OC5:FeatureTest-Verifies Correct c hash when Code Flow Used"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + String nonce = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The code is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String code = authorizationResponse.getCode(); + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate code and id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.CODE_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + assertTrue(rsaSigner.validateAuthorizationCode(code, jwt)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/json/JsonApplierTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/json/JsonApplierTest.java new file mode 100644 index 00000000..9acaa7b6 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/json/JsonApplierTest.java @@ -0,0 +1,55 @@ +package org.gluu.oxauth.json; + +import static org.testng.Assert.assertEquals; + +import java.util.HashMap; +import java.util.Map; + +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.model.json.JsonApplier; +import org.json.JSONArray; +import org.json.JSONObject; +import org.testng.annotations.Test; + +import com.google.common.collect.Lists; + +/** + * @author Yuriy Zabrovarnyy + */ +public class JsonApplierTest { + + @Test + public void apply_forListAndJSONObjectAsTarget_shouldTransferPropertyToTarget() { + RegisterRequest request = new RegisterRequest(); + request.setAdditionalAudience(Lists.newArrayList("aud1", "aud2")); + + JSONObject target = new JSONObject(); + + JsonApplier.getInstance().apply(request, target); + + assertEquals(new JSONArray(Lists.newArrayList("aud1", "aud2")), target.getJSONArray("additional_audience")); + } + + @Test + public void apply_forListAndMapAsTarget_shouldTransferPropertyToTarget() { + RegisterRequest request = new RegisterRequest(); + request.setAdditionalAudience(Lists.newArrayList("aud1", "aud2")); + + Map target = new HashMap<>(); + + JsonApplier.getInstance().apply(request, target); + + assertEquals(new JSONArray(Lists.newArrayList("aud1", "aud2")).toString(), target.get("additional_audience")); + } + + @Test + public void apply_forListAndJavaObjectAsTarget_shouldTransferPropertyToTarget() { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("additional_audience", new JSONArray(Lists.newArrayList("aud1", "aud2"))); + + final RegisterRequest registerRequest = new RegisterRequest(); + JsonApplier.getInstance().apply(jsonObject, registerRequest); + + assertEquals(Lists.newArrayList("aud1", "aud2"), registerRequest.getAdditionalAudience()); + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/load/LoadConstants.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/load/LoadConstants.java new file mode 100644 index 00000000..88b06a43 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/load/LoadConstants.java @@ -0,0 +1,21 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.load; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 03/12/2013 + */ + +public class LoadConstants { + + private LoadConstants() { + } + + public static final int INVOCATION_COUNT = 100; + public static final int THREAD_POOL_SIZE = 200; +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/load/ObtainAccessTokenLoadTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/load/ObtainAccessTokenLoadTest.java new file mode 100644 index 00000000..1109d439 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/load/ObtainAccessTokenLoadTest.java @@ -0,0 +1,119 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.load; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.ClientUtils; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.Prompt; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.gluu.oxauth.model.util.Util; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * DON'T INCLUDE IT IN TEST SUITE. + * + * @author Yuriy Zabrovarnyy + * @version 0.9, 03/12/2013 + */ + +public class ObtainAccessTokenLoadTest extends BaseTest { + + // Think twice before invoking this test ;). Leads to OpenDJ (Berkley DB) failure + // Caused by: LDAPSearchException(resultCode=80 (other), numEntries=0, numReferences=0, errorMessage='Database exception: (JE 4.1.10) JAVA_ERROR: Java Error occurred, recovery may not be possible.') + // http://ox.gluu.org/doku.php?id=oxauth:profiling#obtain_access_token_-_2000_invocations_within_200_concurrent_threads + @Parameters({"userId", "userSecret", "redirectUris"}) + @Test(invocationCount = 1000, threadPoolSize = 100) + public void obtainAccessToken(final String userId, final String userSecret, String redirectUris) throws Exception { + showTitle("requestClientAssociate1"); + + redirectUris = "https://client.example.com/cb"; + + final List responseTypes = new ArrayList(); + responseTypes.add(ResponseType.CODE); + responseTypes.add(ResponseType.ID_TOKEN); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + final String clientId = response.getClientId(); + final String clientSecret = response.getClientSecret(); + + // 1. Request authorization and receive the authorization code. + + final List scopes = Arrays.asList("openid", "profile", "address", "email"); + + final AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUris, null); + request.setState("af0ifjsldkj"); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + request.getPrompts().add(Prompt.NONE); + + final AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + final AuthorizationResponse response1 = authorizeClient.exec(); + + ClientUtils.showClient(authorizeClient); + + final String scope = response1.getScope(); + final String authorizationCode = response1.getCode(); + assertTrue(Util.allNotBlank(authorizationCode)); + + + // 2. Request access token using the authorization code. + final TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUris); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + tokenRequest.setScope(scope); + + final TokenClient tokenClient1 = new TokenClient(tokenEndpoint); + tokenClient1.setRequest(tokenRequest); + final TokenResponse response2 = tokenClient1.exec(); + ClientUtils.showClient(authorizeClient); + + assertTrue(response2.getStatus() == 200); + final String patToken = response2.getAccessToken(); + final String patRefreshToken = response2.getRefreshToken(); + assertTrue(Util.allNotBlank(patToken, patRefreshToken)); + } + +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/load/RegistrationLoadTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/load/RegistrationLoadTest.java new file mode 100644 index 00000000..64746733 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/load/RegistrationLoadTest.java @@ -0,0 +1,59 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.load; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * DON'T INCLUDE IT IN TEST SUITE. + * + * @author Yuriy Zabrovarnyy + * @version 0.9, 03/12/2013 + */ + +public class RegistrationLoadTest extends BaseTest { + + @Parameters({"redirectUris"}) + @Test(invocationCount = 1000, threadPoolSize = 100) + public void registerClient(final String redirectUris) throws Exception { + showTitle("requestClientAssociate1"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + RegisterResponse response = registerClient.execRegister(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + RegisterRequest readClientRequest = new RegisterRequest(response.getRegistrationAccessToken()); + + RegisterClient readClient = new RegisterClient(response.getRegistrationClientUri()); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/load/UserInfoLoadTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/load/UserInfoLoadTest.java new file mode 100644 index 00000000..6d17e0bb --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/load/UserInfoLoadTest.java @@ -0,0 +1,91 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.load; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.common.Prompt; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * DON'T INCLUDE IT IN TEST SUITE. + * + * @author Yuriy Zabrovarnyy + * @version June 19, 2015 + */ + +public class UserInfoLoadTest extends BaseTest { + + @Parameters({"userId", "userSecret", "clientId", "redirectUri"}) + @Test(invocationCount = 1000, threadPoolSize = 100) + public void requestUserInfoImplicitFlow(final String userId, final String userSecret, + final String clientId, final String redirectUri) throws Exception { + showTitle("requestUserInfoImplicitFlow"); + + // 1. Request authorization + List responseTypes = new ArrayList(); + responseTypes.add(ResponseType.TOKEN); + responseTypes.add(ResponseType.ID_TOKEN); + List scopes = new ArrayList(); + scopes.add("openid"); + scopes.add("profile"); + scopes.add("address"); + scopes.add("email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + request.setState(state); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + request.getPrompts().add(Prompt.NONE); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response1 = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response1.getStatus(), 302, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getLocation(), "The location is null"); + assertNotNull(response1.getAccessToken(), "The access token is null"); + assertNotNull(response1.getState(), "The state is null"); + assertNotNull(response1.getTokenType(), "The token type is null"); + assertNotNull(response1.getExpiresIn(), "The expires in value is null"); + assertNotNull(response1.getScope(), "The scope must be null"); + assertNotNull(response1.getIdToken(), "The id token must be null"); + + String accessToken = response1.getAccessToken(); + + // 2. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response2 = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(response2.getStatus(), 200, "Unexpected response code: " + response2.getStatus()); + assertNotNull(response2.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response2.getClaim(JwtClaimName.NAME)); + assertNotNull(response2.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response2.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response2.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response2.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response2.getClaim(JwtClaimName.LOCALE)); + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/load/benchmark/BenchmarkRequestAccessToken.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/load/benchmark/BenchmarkRequestAccessToken.java new file mode 100644 index 00000000..b05f0c4a --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/load/benchmark/BenchmarkRequestAccessToken.java @@ -0,0 +1,123 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.load.benchmark; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.load.benchmark.suite.BenchmarkTestListener; +import org.gluu.oxauth.load.benchmark.suite.BenchmarkTestSuiteListener; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.common.SubjectType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.Reporter; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Listeners; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Yuriy Movchan + * @version November 29, 2017 + */ + +@Listeners({BenchmarkTestSuiteListener.class, BenchmarkTestListener.class}) +public class BenchmarkRequestAccessToken extends BaseTest { + + private String clientId; + private String clientSecret; + + @Parameters({"userId", "userSecret", "redirectUris", "sectorIdentifierUri"}) + @BeforeClass + public void registerClient(final String userId, final String userSecret, String redirectUris, String sectorIdentifierUri) throws Exception { + Reporter.log("Register client", true); + + List responseTypes = Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN); + List scopes = Arrays.asList("openid", "profile", "address", "email", "user_name"); + + RegisterResponse registerResponse = registerClient(redirectUris, responseTypes, scopes, sectorIdentifierUri); + + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + this.clientId = registerResponse.getClientId(); + this.clientSecret = registerResponse.getClientSecret(); + } + + @Parameters({"userId", "userSecret"}) + @Test(invocationCount = 200, threadPoolSize = 1) + public void requestAccessTokenPassword1(final String userId, final String userSecret) throws Exception { + requestAccessTokenPassword(userId, userSecret, this.clientId, this.clientSecret); + } + + @Parameters({"userId", "userSecret"}) + @Test(invocationCount = 200, threadPoolSize = 5, dependsOnMethods = {"requestAccessTokenPassword1"}) + public void requestAccessTokenPassword2(final String userId, final String userSecret) throws Exception { + requestAccessTokenPassword(userId, userSecret, this.clientId, this.clientSecret); + } + + @Parameters({"userId", "userSecret"}) + @Test(invocationCount = 200, threadPoolSize = 2, dependsOnMethods = {"requestAccessTokenPassword2"}) + public void requestAccessTokenPassword4(final String userId, final String userSecret) throws Exception { + requestAccessTokenPassword(userId, userSecret, this.clientId, this.clientSecret); + } + + private void requestAccessTokenPassword(final String userId, final String userSecret, String clientId, String clientSecret) throws Exception { + // Request Resource Owner Credentials Grant + String scope = "openid"; + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + TokenResponse response1 = tokenClient.execResourceOwnerPasswordCredentialsGrant(userId, userSecret, scope, clientId, clientSecret); + + assertEquals(response1.getStatus(), 200, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getEntity(), "The entity is null"); + assertNotNull(response1.getAccessToken(), "The access token is null"); + assertNotNull(response1.getTokenType(), "The token type is null"); + assertNotNull(response1.getRefreshToken(), "The refresh token is null"); + assertNotNull(response1.getScope(), "The scope is null"); + assertNotNull(response1.getIdToken(), "The id token is null"); + } + + private RegisterResponse registerClient( + final String redirectUris, List responseTypes, List scopes, String sectorIdentifierUri) { + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth benchmark test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setScope(scopes); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + return registerResponse; + } + +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/load/benchmark/BenchmarkRequestAuthorization.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/load/benchmark/BenchmarkRequestAuthorization.java new file mode 100644 index 00000000..11d19431 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/load/benchmark/BenchmarkRequestAuthorization.java @@ -0,0 +1,135 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.load.benchmark; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.load.benchmark.suite.BenchmarkTestListener; +import org.gluu.oxauth.load.benchmark.suite.BenchmarkTestSuiteListener; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.common.SubjectType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.Reporter; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Listeners; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Yuriy Movchan + * @author Javier Rojas Blum + * @version November 29, 2017 + */ + +@Listeners({BenchmarkTestSuiteListener.class, BenchmarkTestListener.class}) +public class BenchmarkRequestAuthorization extends BaseTest { + + private String clientId; + private String clientSecret; + + @Parameters({"userId", "userSecret", "redirectUris", "sectorIdentifierUri"}) + @BeforeClass + public void registerClient(final String userId, final String userSecret, String redirectUris, String sectorIdentifierUri) throws Exception { + Reporter.log("Register client", true); + + List responseTypes = Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN); + List scopes = Arrays.asList("openid", "profile", "address", "email", "user_name"); + + RegisterResponse registerResponse = registerClient(redirectUris, responseTypes, scopes, sectorIdentifierUri); + + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + this.clientId = registerResponse.getClientId(); + this.clientSecret = registerResponse.getClientSecret(); + } + + @Parameters({"userId", "userSecret", "redirectUri"}) + @Test(invocationCount = 200, threadPoolSize = 1) + public void testAuthorization1(final String userId, final String userSecret, final String redirectUri) throws Exception { + testAuthorizationImpl(userId, userSecret, this.clientId, redirectUri, false); + } + + @Parameters({"userId", "userSecret", "redirectUri"}) + @Test(invocationCount = 200, threadPoolSize = 5, dependsOnMethods = {"testAuthorization1"}) + public void testAuthorization2(final String userId, final String userSecret, final String redirectUri) throws Exception { + testAuthorizationImpl(userId, userSecret, this.clientId, redirectUri, true); + } + + @Parameters({"userId", "userSecret", "redirectUri"}) + @Test(invocationCount = 200, threadPoolSize = 2, dependsOnMethods = {"testAuthorization2"}) + public void testAuthorization3(final String userId, final String userSecret, final String redirectUri) throws Exception { + testAuthorizationImpl(userId, userSecret, this.clientId, redirectUri, true); + } + + private void testAuthorizationImpl(final String userId, final String userSecret, final String clientId, final String redirectUri, boolean useNewDriver) { + List responseTypes = Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN); + List scopes = Arrays.asList("openid", "profile", "address", "email", "user_name"); + String nonce = UUID.randomUUID().toString(); + + AuthorizationResponse response = requestAuthorization(userId, userSecret, redirectUri, responseTypes, scopes, clientId, nonce, useNewDriver); + + assertNotNull(response.getLocation(), "The location is null"); + assertNotNull(response.getCode(), "The authorization code is null"); + assertNotNull(response.getState(), "The state is null"); + assertNotNull(response.getScope(), "The scope is null"); + } + + private AuthorizationResponse requestAuthorization(final String userId, final String userSecret, final String redirectUri, + List responseTypes, List scopes, String clientId, String nonce, boolean useNewDriver) { + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret, true, useNewDriver); + + return authorizationResponse; + } + + private RegisterResponse registerClient( + final String redirectUris, List responseTypes, List scopes, String sectorIdentifierUri) { + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth benchmark test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setScope(scopes); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + return registerResponse; + } + +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/load/benchmark/suite/BenchmarkTestListener.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/load/benchmark/suite/BenchmarkTestListener.java new file mode 100644 index 00000000..f02697dd --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/load/benchmark/suite/BenchmarkTestListener.java @@ -0,0 +1,117 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.load.benchmark.suite; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.gluu.util.StringHelper; +import org.testng.ITestContext; +import org.testng.ITestListener; +import org.testng.ITestNGMethod; +import org.testng.ITestResult; +import org.testng.Reporter; + +/** + * @author Yuriy Movchan + * @version 0.1, 03/17/2015 + */ +public class BenchmarkTestListener implements ITestListener { + + private List methodNames; + private Map methodTakes; + private Map methodInvoked; + + private Lock lock = new ReentrantLock(); + + @Override + public void onTestStart(ITestResult result) { + } + + @Override + public void onTestSuccess(ITestResult result) { + final String methodName = result.getMethod().getMethodName(); + final long takes = result.getEndMillis() - result.getStartMillis(); + + Long totalTakes; + Long totalInvoked; + + lock.lock(); + try { + if (methodTakes.containsKey(methodName)) { + totalTakes = methodTakes.get(methodName); + totalInvoked = methodInvoked.get(methodName); + + totalTakes += takes; + totalInvoked++; + } else { + methodNames.add(methodName); + + totalTakes = takes; + totalInvoked = 1L; + } + methodTakes.put(methodName, totalTakes); + methodInvoked.put(methodName, totalInvoked); + } finally { + lock.unlock(); + } + } + + @Override + public void onTestFailure(ITestResult result) { + } + + @Override + public void onTestSkipped(ITestResult result) { + } + + @Override + public void onTestFailedButWithinSuccessPercentage(ITestResult result) { + } + + @Override + public void onStart(ITestContext context) { + Reporter.log("Test '" + context.getName() + "' started ...", true); + + this.methodNames = new ArrayList(); + this.methodTakes = new HashMap(); + this.methodInvoked = new HashMap(); + } + + @Override + public void onFinish(ITestContext context) { + final long takes = (context.getEndDate().getTime() - context.getStartDate().getTime()) / 1000; + Reporter.log("Test '" + context.getName() + "' finished in " + takes + " seconds", true); + Reporter.log("================================================================================", true); + + for (String methodName : this.methodNames) { + final long methodTakes = this.methodTakes.get(methodName); + final long methodInvoked = this.methodInvoked.get(methodName); + final long methodThreads = getMethodThreqads(context, methodName); + + long oneExecutionMethodTakes = methodTakes == 0 ? 0 : methodTakes / methodInvoked; + Reporter.log("BENCHMARK REPORT | " + " Method: '" + methodName + "' | Takes:" + methodTakes + " | Invoked: " + methodInvoked + " | Threads: " + methodThreads + " | Average method execution: " + oneExecutionMethodTakes , true); + } + Reporter.log("================================================================================", true); + + } + + private long getMethodThreqads(ITestContext context, String methodName) { + ITestNGMethod[] allTestMethods = context.getAllTestMethods(); + for (int i = 0; i < allTestMethods.length; i++) { + if (StringHelper.equalsIgnoreCase(allTestMethods[i].getMethodName(), methodName)) { + return allTestMethods[i].getThreadPoolSize(); + } + } + + return 1; + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/load/benchmark/suite/BenchmarkTestSuiteListener.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/load/benchmark/suite/BenchmarkTestSuiteListener.java new file mode 100644 index 00000000..8344b960 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/load/benchmark/suite/BenchmarkTestSuiteListener.java @@ -0,0 +1,33 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.load.benchmark.suite; + +import org.testng.ISuite; +import org.testng.ISuiteListener; +import org.testng.Reporter; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 03/07/2014 + */ + +public class BenchmarkTestSuiteListener implements ISuiteListener { + + private long start; + + @Override + public void onStart(ISuite suite) { + this.start = System.currentTimeMillis(); + Reporter.log("Suite '" + suite.getName() + "' started ...", true); + } + + @Override + public void onFinish(ISuite suite) { + final long takes = (System.currentTimeMillis() - start) / 1000; + Reporter.log("Suite '" + suite.getName() + "' finished in " + takes + " seconds", true); + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/page/AbstractPage.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/page/AbstractPage.java new file mode 100644 index 00000000..cde7d14b --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/page/AbstractPage.java @@ -0,0 +1,86 @@ +package org.gluu.oxauth.page; + +import static org.testng.Assert.fail; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.Set; + +import org.gluu.oxauth.model.common.Holder; +import org.gluu.oxauth.model.util.Util; +import org.openqa.selenium.By; +import org.openqa.selenium.Cookie; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.WebDriverWait; + +import com.google.common.base.Preconditions; + +/** + * @author Yuriy Zabrovarnyy + */ +public class AbstractPage implements Page { + + protected PageConfig config; + + public AbstractPage(PageConfig config) { + Preconditions.checkNotNull(config); + this.config = config; + } + + public void navigate(String url) { + try { + final WebDriver driver = config.getDriver(); + output("Navigate URL: " + url); + //printCookies(); + driver.navigate().to(URLDecoder.decode(url, Util.UTF8_STRING_ENCODING)); + } catch (UnsupportedEncodingException ex) { + fail("Failed to decode the URL."); + } + } + + public void printCookies() { + final Set cookies = driver().manage().getCookies(); + if (cookies == null || cookies.isEmpty()) { + output("Cookies: no cookies"); + return; + } + + output("Cookies: "); + cookies.forEach(cookie -> System.out.println(" " + cookie)); + } + + public WebDriver driver() { + return config.getDriver(); + } + + public String config(String key) { + return config.value(key); + } + + public WebElement elementById(String id) { + return driver().findElement(By.id(config(id))); + } + + public WebElement elementByElementId(String elementId) { + return driver().findElement(By.id(elementId)); + } + + public String waitForPageSwitch(String previousUrl) { + return waitForPageSwitch(driver(), previousUrl); + } + + public static String waitForPageSwitch(WebDriver currentDriver, String previousURL) { + Holder currentUrl = new Holder<>(); + WebDriverWait wait = new WebDriverWait(currentDriver, PageConfig.WAIT_OPERATION_TIMEOUT); + wait.until((WebDriver d) -> { + currentUrl.setT(d.getCurrentUrl()); + return !currentUrl.getT().equals(previousURL); + }); + return currentUrl.getT(); + } + + public static void output(String str) { + System.out.println(str); // switch to logger? + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/page/DeviceAuthzPage.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/page/DeviceAuthzPage.java new file mode 100644 index 00000000..609f165d --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/page/DeviceAuthzPage.java @@ -0,0 +1,29 @@ +package org.gluu.oxauth.page; + +import org.openqa.selenium.WebElement; + +public class DeviceAuthzPage extends AbstractPage { + + private static final String FORM_USER_CODE_PART_1_ID = "deviceAuthzForm:userCodePart1"; + private static final String FORM_USER_CODE_PART_2_ID = "deviceAuthzForm:userCodePart2"; + private static final String FORM_CONTINUE_BUTTON_ID = "deviceAuthzForm:continueButton"; + + public DeviceAuthzPage(PageConfig config) { + super(config); + } + + public void fillUserCode(String userCode) { + final String[] userCodeParts = userCode.split("-"); + + WebElement userCodePart1 = elementByElementId(FORM_USER_CODE_PART_1_ID); + userCodePart1.sendKeys(userCodeParts[0]); + + WebElement userCodePart2 = elementByElementId(FORM_USER_CODE_PART_2_ID); + userCodePart2.sendKeys(userCodeParts[1]); + } + + public void clickContinueButton() { + WebElement continueButton = elementByElementId(FORM_CONTINUE_BUTTON_ID); + continueButton.click(); + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/page/LoginPage.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/page/LoginPage.java new file mode 100644 index 00000000..b41c24f2 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/page/LoginPage.java @@ -0,0 +1,42 @@ +package org.gluu.oxauth.page; + +import org.apache.commons.lang.StringUtils; +import org.openqa.selenium.WebElement; + +/** + * @author Yuriy Zabrovarnyy + */ +public class LoginPage extends AbstractPage { + + public LoginPage(PageConfig config) { + super(config); + } + + public WebElement getUsernameField() { + return elementById("loginFormUsername"); + } + + public WebElement getPasswordField() { + return elementById("loginFormPassword"); + } + + public WebElement getLoginButton() { + return elementById("loginFormLoginButton"); + } + + public void enterUsername(String username) { + if (StringUtils.isBlank(username)) { + return; + } + + getUsernameField().sendKeys(username); + } + + public void enterPassword(String userSecret) { + if (StringUtils.isBlank(userSecret)) { + return; + } + + getPasswordField().sendKeys(userSecret); + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/page/Page.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/page/Page.java new file mode 100644 index 00000000..0e1d3cb3 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/page/Page.java @@ -0,0 +1,7 @@ +package org.gluu.oxauth.page; + +/** + * @author Yuriy Zabrovarnyy + */ +public interface Page { +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/page/PageConfig.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/page/PageConfig.java new file mode 100644 index 00000000..e97450df --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/page/PageConfig.java @@ -0,0 +1,41 @@ +package org.gluu.oxauth.page; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.lang.StringUtils; +import org.openqa.selenium.WebDriver; + +import com.google.common.base.Preconditions; + +/** + * @author Yuriy Zabrovarnyy + */ +public class PageConfig { + + public static int WAIT_OPERATION_TIMEOUT = 60; + + private final WebDriver driver; + private final Map testKeys = new HashMap<>(); + + public PageConfig(WebDriver driver) { + Preconditions.checkNotNull(driver); + this.driver = driver; + } + + public WebDriver getDriver() { + return driver; + } + + public Map getTestKeys() { + return testKeys; + } + + public String value(String key) { + final String value = testKeys.get(key); + if (StringUtils.isBlank(value)) { + throw new IllegalArgumentException("Unknown key: " + key); + } + return value; + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/page/SelectPage.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/page/SelectPage.java new file mode 100644 index 00000000..bdeddd56 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/page/SelectPage.java @@ -0,0 +1,68 @@ +package org.gluu.oxauth.page; + +import java.util.List; +import java.util.stream.Collectors; + +import org.gluu.oxauth.BaseTest; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.WebDriverWait; + +/** + * @author Yuriy Zabrovarnyy + */ +public class SelectPage extends AbstractPage { + + public SelectPage(PageConfig config) { + super(config); + } + + public WebElement getLoginAsAnotherUserButton() { + return driver().findElement(By.id("selectForm:loginButton")); + } + + public List getAccountButtons() { + return driver().findElements(By.tagName("a")).stream().filter(webElement -> { + final String onclick = webElement.getAttribute("onclick"); + return onclick != null && onclick.contains("accountButtons"); + }).collect(Collectors.toList()); + } + + public WebElement getAccountButton(String name) { + return getAccountButtons().stream().filter(webElement -> webElement.getText().equals(name)).findFirst().get(); + } + + public LoginPage clickOnLoginAsAnotherUser() { + final WebDriver driver = driver(); + + output("Removed session_id"); + driver.manage().deleteCookieNamed("session_id"); // emulate browser + + final String previousUrl = driver.getCurrentUrl(); + output("Clicked Login as another user button"); + getLoginAsAnotherUserButton().click(); + waitForPageSwitch(previousUrl); + + navigate(driver.getCurrentUrl()); + if (BaseTest.ENABLE_REDIRECT_TO_LOGIN_PAGE) { + new WebDriverWait(driver, PageConfig.WAIT_OPERATION_TIMEOUT) + .until((WebDriver d) -> !d.getCurrentUrl().contains("/authorize")); + } + return new LoginPage(config); + } + + public SelectPage switchAccount(WebElement element) { + output("Switching account to: " + element.getText()); + final String url = driver().getCurrentUrl(); + element.click(); + waitForPageSwitch(url); + return this; + } + + public static SelectPage navigate(PageConfig config, String authorizationUrlWithPromptSelectAccount) { + final SelectPage page = new SelectPage(config); + page.navigate(authorizationUrlWithPromptSelectAccount); + return page; + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AccessTokenAsJwtHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AccessTokenAsJwtHttpTest.java new file mode 100644 index 00000000..538c781f --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AccessTokenAsJwtHttpTest.java @@ -0,0 +1,108 @@ +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Yuriy Zabrovarnyy + */ +public class AccessTokenAsJwtHttpTest extends BaseTest { + + /** + * Test for the complete Authorization Code Flow. + */ + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void accessTokenAsJwt( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("accessTokenAsJwt"); + + List responseTypes = Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN, ResponseType.TOKEN); + List scopes = Arrays.asList("openid", "profile", "address", "email", "phone", "user_name"); + + RegisterResponse registerResponse = registerClient(redirectUri, responseTypes, scopes); + + String clientId = registerResponse.getClientId(); + + // Request authorization and receive the authorization code. + String nonce = UUID.randomUUID().toString(); + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, scopes, clientId, nonce); + + String accessToken = authorizationResponse.getAccessToken(); + + // Validate access token as jwt + Jwt jwt = Jwt.parse(accessToken); + assertEquals(clientId, jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString("scope")); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + } + + private RegisterResponse registerClient(String redirectUris, List responseTypes, List scopes) throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "access token as JWT test", StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setScope(scopes); + registerRequest.setAccessTokenAsJwt(true); + registerRequest.setAccessTokenSigningAlg(SignatureAlgorithm.RS512); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setExecutor(clientEngine(true)); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + return registerResponse; + } + + private AuthorizationResponse requestAuthorization(final String userId, final String userSecret, final String redirectUri, + List responseTypes, List scopes, String clientId, String nonce) { + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + return authorizationResponse; + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AddressClaimsTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AddressClaimsTest.java new file mode 100644 index 00000000..97ae5674 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AddressClaimsTest.java @@ -0,0 +1,2193 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.security.PrivateKey; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.JwkResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoRequest; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.client.model.authorize.Claim; +import org.gluu.oxauth.client.model.authorize.ClaimValue; +import org.gluu.oxauth.client.model.authorize.JwtAuthorizationRequest; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.encryption.BlockEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.encryption.KeyEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.signature.ECDSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jwe.Jwe; +import org.gluu.oxauth.model.jwk.Algorithm; +import org.gluu.oxauth.model.jws.ECDSASigner; +import org.gluu.oxauth.model.jws.HMACSigner; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.JwtUtil; +import org.gluu.oxauth.model.util.StringUtils; +import org.gluu.oxauth.model.util.Util; +import org.json.JSONObject; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * Note: In order to run this tests, set legacyIdTokenClaims to true. + * + * @author Javier Rojas Blum + * @version March 8, 2019 + */ +public class AddressClaimsTest extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri", "RS256_keyId", "clientJwksUri"}) + @Test + public void authorizationRequestDefault( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri, final String keyId, final String clientJwksUri) throws Exception { + showTitle("authorizationRequestDefault"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "address"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.RS256, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwt.getClaims().getClaim(JwtClaimName.ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_REGION)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_COUNTRY, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION))); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void authorizationRequestHS256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestHS256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.HS256); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.HS256); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.HS256); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "address"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.HS256, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwt.getClaims().getClaim(JwtClaimName.ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_REGION)); + + HMACSigner hmacSigner = new HMACSigner(SignatureAlgorithm.HS256, clientSecret); + assertTrue(hmacSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_COUNTRY, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION))); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void authorizationRequestHS384( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestHS384"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.HS384); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.HS384); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.HS384); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "address"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.HS384, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwt.getClaims().getClaim(JwtClaimName.ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_REGION)); + + HMACSigner hmacSigner = new HMACSigner(SignatureAlgorithm.HS384, clientSecret); + assertTrue(hmacSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_COUNTRY, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION))); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void authorizationRequestHS512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestHS512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.HS512); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.HS512); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.HS512); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "address"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.HS512, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwt.getClaims().getClaim(JwtClaimName.ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_REGION)); + + HMACSigner hmacSigner = new HMACSigner(SignatureAlgorithm.HS512, clientSecret); + assertTrue(hmacSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_COUNTRY, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION))); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri", "RS256_keyId", "clientJwksUri"}) + @Test + public void authorizationRequestRS256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri, final String keyId, final String clientJwksUri) throws Exception { + showTitle("authorizationRequestRS256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS256); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.RS256); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.RS256); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + registerRequest.setJwksUri(clientJwksUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "address"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.RS256, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwt.getClaims().getClaim(JwtClaimName.ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_REGION)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_COUNTRY, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION))); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri", "RS384_keyId", "clientJwksUri"}) + @Test + public void authorizationRequestRS384( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri, final String keyId, final String clientJwksUri) throws Exception { + showTitle("authorizationRequestRS384"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS384); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.RS384); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.RS384); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + registerRequest.setJwksUri(clientJwksUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "address"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.RS384, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwt.getClaims().getClaim(JwtClaimName.ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_REGION)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS384, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_COUNTRY, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION))); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri", "RS512_keyId", "clientJwksUri"}) + @Test + public void authorizationRequestRS512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri, final String keyId, final String clientJwksUri) throws Exception { + showTitle("authorizationRequestRS512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS512); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.RS512); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.RS512); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "address"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.RS512, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwt.getClaims().getClaim(JwtClaimName.ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_REGION)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS512, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_COUNTRY, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION))); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri", "ES256_keyId", "clientJwksUri"}) + @Test + public void authorizationRequestES256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri, final String keyId, final String clientJwksUri) throws Exception { + showTitle("authorizationRequestES256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.ES256); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.ES256); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.ES256); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "address"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.ES256, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwt.getClaims().getClaim(JwtClaimName.ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_REGION)); + + ECDSAPublicKey publicKey = JwkClient.getECDSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + ECDSASigner ecdsaSigner = new ECDSASigner(SignatureAlgorithm.ES256, publicKey); + + assertTrue(ecdsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_COUNTRY, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION))); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri", "ES384_keyId", "clientJwksUri"}) + @Test + public void authorizationRequestES384( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri, final String keyId, final String clientJwksUri) throws Exception { + showTitle("authorizationRequestES384"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.ES384); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.ES384); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.ES384); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "address"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.ES384, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwt.getClaims().getClaim(JwtClaimName.ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_REGION)); + + ECDSAPublicKey publicKey = JwkClient.getECDSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + ECDSASigner ecdsaSigner = new ECDSASigner(SignatureAlgorithm.ES384, publicKey); + + assertTrue(ecdsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_COUNTRY, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION))); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri", "ES512_keyId", "clientJwksUri"}) + @Test + public void authorizationRequestES512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri, final String keyId, final String clientJwksUri) throws Exception { + showTitle("authorizationRequestES512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.ES512); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.ES512); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.ES512); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "address"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.ES512, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwt.getClaims().getClaim(JwtClaimName.ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_REGION)); + + ECDSAPublicKey publicKey = JwkClient.getECDSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + ECDSASigner ecdsaSigner = new ECDSASigner(SignatureAlgorithm.ES512, publicKey); + + assertTrue(ecdsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_COUNTRY, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION))); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri", "PS256_keyId", "clientJwksUri"}) + @Test + public void authorizationRequestPS256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri, final String keyId, final String clientJwksUri) throws Exception { + showTitle("authorizationRequestPS256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.PS256); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.PS256); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.PS256); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "address"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.PS256, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwt.getClaims().getClaim(JwtClaimName.ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_REGION)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.PS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_COUNTRY, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION))); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri", "PS384_keyId", "clientJwksUri"}) + @Test + public void authorizationRequestPS384( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri, final String keyId, final String clientJwksUri) throws Exception { + showTitle("authorizationRequestPS384"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.PS384); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.PS384); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.PS384); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "address"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.PS384, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwt.getClaims().getClaim(JwtClaimName.ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_REGION)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.PS384, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_COUNTRY, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION))); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri", "PS512_keyId", "clientJwksUri"}) + @Test + public void authorizationRequestPS512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri, final String keyId, final String clientJwksUri) throws Exception { + showTitle("authorizationRequestPS512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.PS512); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.PS512); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.PS512); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "address"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.PS512, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwt.getClaims().getClaim(JwtClaimName.ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(jwt.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_REGION)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.PS512, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_COUNTRY, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION))); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void authorizationRequestAlgA128KWEncA128GCM( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestAlgA128KWEncA128GCM"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.A128KW); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A128GCM); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.A128KW); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A128GCM); + registerRequest.setRequestObjectEncryptionAlg(KeyEncryptionAlgorithm.A128KW); + registerRequest.setRequestObjectEncryptionEnc(BlockEncryptionAlgorithm.A128GCM); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "address"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest, + KeyEncryptionAlgorithm.A128KW, + BlockEncryptionAlgorithm.A128GCM, + clientSecret); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwe jwe = Jwe.parse(idToken, null, clientSecret.getBytes(Util.UTF8_STRING_ENCODING)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwe.getClaims().getClaim(JwtClaimName.ADDRESS)); + assertNotNull(jwe.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwe.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwe.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(jwe.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_REGION)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_COUNTRY, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION))); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void authorizationRequestAlgA256KWEncA256GCM( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestAlgA256KWEncA256GCM"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.A256KW); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A256GCM); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.A256KW); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A256GCM); + registerRequest.setRequestObjectEncryptionAlg(KeyEncryptionAlgorithm.A256KW); + registerRequest.setRequestObjectEncryptionEnc(BlockEncryptionAlgorithm.A256GCM); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "address"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest, + KeyEncryptionAlgorithm.A256KW, + BlockEncryptionAlgorithm.A256GCM, + clientSecret); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwe jwe = Jwe.parse(idToken, null, clientSecret.getBytes(Util.UTF8_STRING_ENCODING)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwe.getClaims().getClaim(JwtClaimName.ADDRESS)); + assertNotNull(jwe.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwe.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwe.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(jwe.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_REGION)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_COUNTRY, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION))); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", + "dnName", "keyStoreFile", "keyStoreSecret", "RSA1_5_keyId", + "clientJwksUri", "sectorIdentifierUri"}) + @Test + public void authorizationRequestAlgRSA15EncA128CBCPLUSHS256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String clientKeyId, + final String clientJwksUri, final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestAlgRSA15EncA128CBCPLUSHS256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A128CBC_PLUS_HS256); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A128CBC_PLUS_HS256); + registerRequest.setRequestObjectEncryptionAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setRequestObjectEncryptionEnc(BlockEncryptionAlgorithm.A128CBC_PLUS_HS256); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Choose encryption key + JwkClient jwkClient = new JwkClient(jwksUri); + JwkResponse jwkResponse = jwkClient.exec(); + String serverKeyId = jwkResponse.getKeyId(Algorithm.RSA1_5); + assertNotNull(serverKeyId); + + // 3. Request authorization + JSONObject jwks = JwtUtil.getJSONWebKeys(jwksUri); + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "address"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, + KeyEncryptionAlgorithm.RSA1_5, BlockEncryptionAlgorithm.A128CBC_PLUS_HS256, cryptoProvider); + jwtAuthorizationRequest.setKeyId(serverKeyId); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(jwks); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 4. Validate id_token + PrivateKey privateKey = cryptoProvider.getPrivateKey(clientKeyId); + + Jwe jwe = Jwe.parse(idToken, privateKey, null); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwe.getClaims().getClaim(JwtClaimName.ADDRESS)); + assertNotNull(jwe.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwe.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwe.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(jwe.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_REGION)); + + // 5. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setPrivateKey(privateKey); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_COUNTRY, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION))); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", + "dnName", "keyStoreFile", "keyStoreSecret", "RSA1_5_keyId", + "clientJwksUri", "sectorIdentifierUri"}) + @Test + public void authorizationRequestAlgRSA15EncA256CBCPLUSHS512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String clientKeyId, + final String clientJwksUri, final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestAlgRSA15EncA256CBCPLUSHS512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A256CBC_PLUS_HS512); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A256CBC_PLUS_HS512); + registerRequest.setRequestObjectEncryptionAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setRequestObjectEncryptionEnc(BlockEncryptionAlgorithm.A256CBC_PLUS_HS512); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Choose encryption key + JwkClient jwkClient = new JwkClient(jwksUri); + JwkResponse jwkResponse = jwkClient.exec(); + String serverKeyId = jwkResponse.getKeyId(Algorithm.RSA1_5); + assertNotNull(serverKeyId); + + // 3. Request authorization + JSONObject jwks = JwtUtil.getJSONWebKeys(jwksUri); + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "address"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, + KeyEncryptionAlgorithm.RSA1_5, BlockEncryptionAlgorithm.A256CBC_PLUS_HS512, cryptoProvider); + jwtAuthorizationRequest.setKeyId(serverKeyId); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(jwks); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 4. Validate id_token + PrivateKey privateKey = cryptoProvider.getPrivateKey(clientKeyId); + + Jwe jwe = Jwe.parse(idToken, privateKey, null); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwe.getClaims().getClaim(JwtClaimName.ADDRESS)); + assertNotNull(jwe.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwe.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwe.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(jwe.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_REGION)); + + // 5. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setPrivateKey(privateKey); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_COUNTRY, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION))); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", + "dnName", "keyStoreFile", "keyStoreSecret", "RSA_OAEP_keyId", + "clientJwksUri", "sectorIdentifierUri"}) + @Test + public void authorizationRequestAlgRSAOAEPEncA256GCM( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String clientKeyId, + final String clientJwksUri, final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestAlgRSAOAEPEncA256GCM"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA_OAEP); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A256GCM); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA_OAEP); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A256GCM); + registerRequest.setRequestObjectEncryptionAlg(KeyEncryptionAlgorithm.RSA_OAEP); + registerRequest.setRequestObjectEncryptionEnc(BlockEncryptionAlgorithm.A256GCM); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Choose encryption key + JwkClient jwkClient = new JwkClient(jwksUri); + JwkResponse jwkResponse = jwkClient.exec(); + String serverKeyId = jwkResponse.getKeyId(Algorithm.RSA_OAEP); + assertNotNull(serverKeyId); + + // 3. Request authorization + JSONObject jwks = JwtUtil.getJSONWebKeys(jwksUri); + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "address"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, + KeyEncryptionAlgorithm.RSA_OAEP, BlockEncryptionAlgorithm.A256GCM, cryptoProvider); + jwtAuthorizationRequest.setKeyId(serverKeyId); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(jwks); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 4. Validate id_token + PrivateKey privateKey = cryptoProvider.getPrivateKey(clientKeyId); + + Jwe jwe = Jwe.parse(idToken, privateKey, null); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwe.getClaims().getClaim(JwtClaimName.ADDRESS)); + assertNotNull(jwe.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(jwe.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(jwe.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(jwe.getClaims().getClaimAsJSON(JwtClaimName.ADDRESS).has(JwtClaimName.ADDRESS_REGION)); + + // 5. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setPrivateKey(privateKey); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS).containsAll(Arrays.asList( + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_COUNTRY, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION))); + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ApplicationTypeRestrictionHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ApplicationTypeRestrictionHttpTest.java new file mode 100644 index 00000000..7d48d424 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ApplicationTypeRestrictionHttpTest.java @@ -0,0 +1,514 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.gluu.oxauth.model.register.RegisterRequestParam.APPLICATION_TYPE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.CLIENT_NAME; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ID_TOKEN_SIGNED_RESPONSE_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REDIRECT_URIS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.RESPONSE_TYPES; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SCOPE; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.common.SubjectType; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Javier Rojas Blum + * @version November 29, 2017 + */ +public class ApplicationTypeRestrictionHttpTest extends BaseTest { + + /** + * Register a client without specify an Application Type. + * Read client to check whether it is using the default Application Type web. + */ + @Parameters({"redirectUris", "sectorIdentifierUri"}) + @Test + public void omittedApplicationType(final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("omittedApplicationType"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(null, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertEquals(readClientResponse.getClaims().get(APPLICATION_TYPE.toString()), ApplicationType.WEB.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + } + + /** + * Register a client with Application Type web. + * Read client to check whether it is using the Application Type web. + */ + @Parameters({"redirectUris", "sectorIdentifierUri"}) + @Test + public void applicationTypeWeb(final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("applicationTypeWeb"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertEquals(readClientResponse.getClaims().get(APPLICATION_TYPE.toString()), ApplicationType.WEB.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + } + + /** + * Fail: Register a client with Application Type web and Redirect URI with the schema HTTP. + */ + @Test + public void applicationTypeWebFail1() throws Exception { + showTitle("applicationTypeWebFail1"); + + final String redirectUris = "http://client.example.com/cb"; + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + RegisterResponse registerResponse = registerClient.execRegister(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 400, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getEntity(), "The entity is null"); + assertNotNull(registerResponse.getErrorType(), "The error type is null"); + assertNotNull(registerResponse.getErrorDescription(), "The error description is null"); + } + + /** + * Register a client with Application Type native. + * Read client to check whether it is using the Application Type native. + */ + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret"}) + @Test + public void applicationTypeNativeSubjectTypePublic( + final String redirectUris, final String redirectUri, final String userId, final String userSecret) throws Exception { + showTitle("applicationTypeNativeSubjectTypePublic"); + + // 1. Register client + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + List scopes = Arrays.asList("openid", "profile", "address", "email", "user_name"); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.NATIVE, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setScope(scopes); + registerRequest.setSubjectType(SubjectType.PUBLIC); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read
 + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertEquals(readClientResponse.getClaims().get(APPLICATION_TYPE.toString()), ApplicationType.NATIVE.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization and receive the authorization code. + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + assertNotNull(authorizationResponse.getScope()); + + String scope = authorizationResponse.getScope(); + String authorizationCode = authorizationResponse.getCode(); + String idToken = authorizationResponse.getIdToken(); + + // 4. Request access token using the authorization code.
 + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient1 = new TokenClient(tokenEndpoint); + tokenClient1.setRequest(tokenRequest); + TokenResponse tokenResponse1 = tokenClient1.exec(); + + showClient(tokenClient1); + assertEquals(tokenResponse1.getStatus(), 200); + assertNotNull(tokenResponse1.getEntity()); + assertNotNull(tokenResponse1.getAccessToken()); + assertNotNull(tokenResponse1.getExpiresIn()); + assertNotNull(tokenResponse1.getTokenType()); + assertNotNull(tokenResponse1.getRefreshToken()); + + String refreshToken = tokenResponse1.getRefreshToken(); + + // 5. Validate id_token
 + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.CODE_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.OX_OPENID_CONNECT_VERSION)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 6. Request new access token using the refresh token. + TokenClient tokenClient2 = new TokenClient(tokenEndpoint); + TokenResponse tokenResponse2 = tokenClient2.execRefreshToken(scope, refreshToken, clientId, clientSecret); + + showClient(tokenClient2); + assertEquals(tokenResponse2.getStatus(), 200); + assertNotNull(tokenResponse2.getEntity()); + assertNotNull(tokenResponse2.getAccessToken()); + assertNotNull(tokenResponse2.getTokenType()); + assertNotNull(tokenResponse2.getRefreshToken()); + assertNotNull(tokenResponse2.getScope()); + + String accessToken = tokenResponse2.getAccessToken(); + + // 7. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void applicationTypeNativeSubjectTypePairwise( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("applicationTypeNativeSubjectTypePairwise"); + + // 1. Register client + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + List scopes = Arrays.asList("openid", "profile", "address", "email", "user_name"); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.NATIVE, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setScope(scopes); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read
 + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertEquals(readClientResponse.getClaims().get(APPLICATION_TYPE.toString()), ApplicationType.NATIVE.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization and receive the authorization code. + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + assertNotNull(authorizationResponse.getScope()); + + String scope = authorizationResponse.getScope(); + String authorizationCode = authorizationResponse.getCode(); + String idToken = authorizationResponse.getIdToken(); + + // 4. Request access token using the authorization code.
 + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient1 = new TokenClient(tokenEndpoint); + tokenClient1.setRequest(tokenRequest); + TokenResponse tokenResponse1 = tokenClient1.exec(); + + showClient(tokenClient1); + assertEquals(tokenResponse1.getStatus(), 200); + assertNotNull(tokenResponse1.getEntity()); + assertNotNull(tokenResponse1.getAccessToken()); + assertNotNull(tokenResponse1.getExpiresIn()); + assertNotNull(tokenResponse1.getTokenType()); + assertNotNull(tokenResponse1.getRefreshToken()); + + String refreshToken = tokenResponse1.getRefreshToken(); + + // 5. Validate id_token
 + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.CODE_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.OX_OPENID_CONNECT_VERSION)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 6. Request new access token using the refresh token. + TokenClient tokenClient2 = new TokenClient(tokenEndpoint); + TokenResponse tokenResponse2 = tokenClient2.execRefreshToken(scope, refreshToken, clientId, clientSecret); + + showClient(tokenClient2); + assertEquals(tokenResponse2.getStatus(), 200); + assertNotNull(tokenResponse2.getEntity()); + assertNotNull(tokenResponse2.getAccessToken()); + assertNotNull(tokenResponse2.getTokenType()); + assertNotNull(tokenResponse2.getRefreshToken()); + assertNotNull(tokenResponse2.getScope()); + + String accessToken = tokenResponse2.getAccessToken(); + + // 7. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + } + + /** + * Fail: Register a client with Application Type native and Redirect URI with the schema HTTPS. + */ + @Test(enabled = false) +//allowed to register redirect_uris with custom schema to conform "OAuth 2.0 for Native Apps" spec + public void applicationTypeNativeFail1() throws Exception { + showTitle("applicationTypeNativeFail1"); + + final String redirectUris = "https://client.example.com/cb"; + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + RegisterResponse registerResponse = registerClient.execRegister(ApplicationType.NATIVE, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 400, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getEntity(), "The entity is null"); + assertNotNull(registerResponse.getErrorType(), "The error type is null"); + assertNotNull(registerResponse.getErrorDescription(), "The error description is null"); + } + + /** + * Fail: Register a client with Application Type native and Redirect URI with the host different than localhost. + */ + @Parameters({"redirectUris"}) + @Test(enabled = false) +//allowed to register redirect_uris with custom schema to conform "OAuth 2.0 for Native Apps" spec + public void applicationTypeNativeFail2(final String redirectUris) throws Exception { + showTitle("applicationTypeNativeFail2"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + RegisterResponse registerResponse = registerClient.execRegister(ApplicationType.NATIVE, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 400, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getEntity(), "The entity is null"); + assertNotNull(registerResponse.getErrorType(), "The error type is null"); + assertNotNull(registerResponse.getErrorDescription(), "The error description is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AuthorizationCodeFlowHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AuthorizationCodeFlowHttpTest.java new file mode 100644 index 00000000..f7696094 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AuthorizationCodeFlowHttpTest.java @@ -0,0 +1,896 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.gluu.oxauth.model.register.RegisterRequestParam.APPLICATION_TYPE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.CLIENT_NAME; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ID_TOKEN_SIGNED_RESPONSE_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REDIRECT_URIS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.RESPONSE_TYPES; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SCOPE; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.Asserter; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.common.SubjectType; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * Test cases for the authorization code flow (HTTP) + * + * @author Javier Rojas Blum + * @version May 14, 2019 + */ +public class AuthorizationCodeFlowHttpTest extends BaseTest { + + /** + * Test for the complete Authorization Code Flow. + */ + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void authorizationCodeFlow( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationCodeFlow"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + List scopes = Arrays.asList("openid", "profile", "address", "email", "phone", "user_name"); + + // 1. Register client + RegisterResponse registerResponse = registerClient(redirectUris, responseTypes, scopes, sectorIdentifierUri); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization and receive the authorization code. + String nonce = UUID.randomUUID().toString(); + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, scopes, clientId, nonce); + + String scope = authorizationResponse.getScope(); + String authorizationCode = authorizationResponse.getCode(); + String idToken = authorizationResponse.getIdToken(); + + // 3. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient1 = newTokenClient(tokenRequest); + tokenClient1.setRequest(tokenRequest); + TokenResponse tokenResponse1 = tokenClient1.exec(); + + showClient(tokenClient1); + assertEquals(tokenResponse1.getStatus(), 200, "Unexpected response code: " + tokenResponse1.getStatus()); + assertNotNull(tokenResponse1.getEntity(), "The entity is null"); + assertNotNull(tokenResponse1.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse1.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse1.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse1.getRefreshToken(), "The refresh token is null"); + + String refreshToken = tokenResponse1.getRefreshToken(); + + // 4. Validate id_token + Jwt jwt = Jwt.parse(idToken); + Asserter.assertIdToken(jwt, JwtClaimName.CODE_HASH); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID), clientEngine(true)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 5. Request new access token using the refresh token. + TokenClient tokenClient2 = new TokenClient(tokenEndpoint); + tokenClient2.setExecutor(clientEngine(true)); + TokenResponse tokenResponse2 = tokenClient2.execRefreshToken(scope, refreshToken, clientId, clientSecret); + + showClient(tokenClient2); + assertEquals(tokenResponse2.getStatus(), 200, "Unexpected response code: " + tokenResponse2.getStatus()); + assertNotNull(tokenResponse2.getEntity(), "The entity is null"); + assertNotNull(tokenResponse2.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse2.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse2.getRefreshToken(), "The refresh token is null"); + assertNotNull(tokenResponse2.getScope(), "The scope is null"); + + String accessToken = tokenResponse2.getAccessToken(); + + // 6. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setExecutor(clientEngine(true)); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.BIRTHDATE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GENDER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.MIDDLE_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NICKNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PREFERRED_USERNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PROFILE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.WEBSITE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PHONE_NUMBER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PHONE_NUMBER_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.USER_NAME)); + assertNull(userInfoResponse.getClaim("org_name")); + assertNull(userInfoResponse.getClaim("work_phone")); + } + + /** + * Test for the complete Authorization Code Flow. + * Register just the openid scope. + * Request authorization with scopes openid, profile, address, email, phone, user_name. + * Expected result is just prompt the user to authorize openid scope. + */ + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void authorizationCodeFlowNegativeTest( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationCodeFlowNegativeTest"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + List registerScopes = Arrays.asList("openid"); + + // 1. Register client + RegisterResponse registerResponse = registerClient(redirectUris, responseTypes, registerScopes, sectorIdentifierUri); + + assertTrue(registerResponse.getClaims().containsKey(SCOPE.toString())); + assertNotNull(registerResponse.getClaims().get(SCOPE.toString())); + assertEquals(registerResponse.getClaims().get(SCOPE.toString()), "openid"); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization and receive the authorization code. + String nonce = UUID.randomUUID().toString(); + List scopes = Arrays.asList("openid", "profile", "address", "email", "phone", "user_name"); + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, scopes, clientId, nonce); + + assertEquals(authorizationResponse.getScope(), "openid"); + + String scope = authorizationResponse.getScope(); + String authorizationCode = authorizationResponse.getCode(); + String idToken = authorizationResponse.getIdToken(); + + // 3. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient1 = new TokenClient(tokenEndpoint); + tokenClient1.setRequest(tokenRequest); + TokenResponse tokenResponse1 = tokenClient1.exec(); + + showClient(tokenClient1); + assertEquals(tokenResponse1.getStatus(), 200, "Unexpected response code: " + tokenResponse1.getStatus()); + assertNotNull(tokenResponse1.getEntity(), "The entity is null"); + assertNotNull(tokenResponse1.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse1.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse1.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse1.getRefreshToken(), "The refresh token is null"); + + String refreshToken = tokenResponse1.getRefreshToken(); + + // 4. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.CODE_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.OX_OPENID_CONNECT_VERSION)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 5. Request new access token using the refresh token. + TokenClient tokenClient2 = new TokenClient(tokenEndpoint); + TokenResponse tokenResponse2 = tokenClient2.execRefreshToken(scope, refreshToken, clientId, clientSecret); + + showClient(tokenClient2); + assertEquals(tokenResponse2.getStatus(), 200, "Unexpected response code: " + tokenResponse2.getStatus()); + assertNotNull(tokenResponse2.getEntity(), "The entity is null"); + assertNotNull(tokenResponse2.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse2.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse2.getRefreshToken(), "The refresh token is null"); + assertNotNull(tokenResponse2.getScope(), "The scope is null"); + assertEquals(tokenResponse2.getScope(), "openid"); + + String accessToken = tokenResponse2.getAccessToken(); + + // 6. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNull(userInfoResponse.getClaim(JwtClaimName.BIRTHDATE)); + assertNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNull(userInfoResponse.getClaim(JwtClaimName.GENDER)); + assertNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNull(userInfoResponse.getClaim(JwtClaimName.MIDDLE_NAME)); + assertNull(userInfoResponse.getClaim(JwtClaimName.NICKNAME)); + assertNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNull(userInfoResponse.getClaim(JwtClaimName.PREFERRED_USERNAME)); + assertNull(userInfoResponse.getClaim(JwtClaimName.PROFILE)); + assertNull(userInfoResponse.getClaim(JwtClaimName.WEBSITE)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNull(userInfoResponse.getClaim(JwtClaimName.PHONE_NUMBER)); + assertNull(userInfoResponse.getClaim(JwtClaimName.PHONE_NUMBER_VERIFIED)); + assertNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNull(userInfoResponse.getClaim(JwtClaimName.USER_NAME)); + assertNull(userInfoResponse.getClaim("org_name")); + assertNull(userInfoResponse.getClaim("work_phone")); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void authorizationCodeWithNotAllowedScopeFlow( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationCodeWithNotAllowedScopeFlow"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + List scopes = Arrays.asList("openid", "profile", "address", "email", "user_name"); + + // 1. Register client + RegisterResponse registerResponse = registerClient(redirectUris, responseTypes, scopes, sectorIdentifierUri); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization and receive the authorization code. + List authorizationScopes = Arrays.asList("openid", "profile", "address", "email", "user_name", "mobile_phone"); + String nonce = UUID.randomUUID().toString(); + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, authorizationScopes, clientId, nonce); + + String idToken = authorizationResponse.getIdToken(); + String authorizationCode = authorizationResponse.getCode(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + Asserter.assertIdToken(jwt, JwtClaimName.CODE_HASH); + + // 4. Request access token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + + String accessToken = tokenResponse.getAccessToken(); + + // 5. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim("user_name")); + assertNull(userInfoResponse.getClaim("phone_mobile_number")); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void authorizationCodeDynamicScopeFlow( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationCodeDynamicScopeFlow"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + List scopes = Arrays.asList("openid", "profile", "address", "email", "user_name", "org_name", "work_phone"); + + // 1. Register client + RegisterResponse registerResponse = registerClient(redirectUris, responseTypes, scopes, sectorIdentifierUri); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization and receive the authorization code. + String nonce = UUID.randomUUID().toString(); + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, scopes, clientId, nonce); + + String idToken = authorizationResponse.getIdToken(); + String authorizationCode = authorizationResponse.getCode(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.CODE_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.OX_OPENID_CONNECT_VERSION)); + + // 4. Request access token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + + String accessToken = tokenResponse.getAccessToken(); + + // 5. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim("user_name")); + assertNotNull(userInfoResponse.getClaim("org_name")); + assertNotNull(userInfoResponse.getClaim("work_phone")); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void authorizationCodeFlowWithOptionalNonce( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationCodeFlowWithOptionalNonce"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization and receive the authorization code. + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + String nonce = UUID.randomUUID().toString(); + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + + String scope = authorizationResponse.getScope(); + String authorizationCode = authorizationResponse.getCode(); + String idToken = authorizationResponse.getIdToken(); + + // 3. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient1 = new TokenClient(tokenEndpoint); + tokenClient1.setRequest(tokenRequest); + TokenResponse tokenResponse1 = tokenClient1.exec(); + + showClient(tokenClient1); + assertEquals(tokenResponse1.getStatus(), 200, "Unexpected response code: " + tokenResponse1.getStatus()); + assertNotNull(tokenResponse1.getEntity(), "The entity is null"); + assertNotNull(tokenResponse1.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse1.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse1.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse1.getRefreshToken(), "The refresh token is null"); + + String refreshToken = tokenResponse1.getRefreshToken(); + + // 4. Validate id_token + Jwt jwt = Jwt.parse(idToken); + + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.NONCE)); + assertEquals(jwt.getClaims().getClaimAsString(JwtClaimName.NONCE), nonce); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.CODE_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.OX_OPENID_CONNECT_VERSION)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 5. Request new access token using the refresh token. + TokenClient tokenClient2 = new TokenClient(tokenEndpoint); + TokenResponse tokenResponse2 = tokenClient2.execRefreshToken(scope, refreshToken, clientId, clientSecret); + + showClient(tokenClient2); + assertEquals(tokenResponse2.getStatus(), 200, "Unexpected response code: " + tokenResponse2.getStatus()); + assertNotNull(tokenResponse2.getEntity(), "The entity is null"); + assertNotNull(tokenResponse2.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse2.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse2.getRefreshToken(), "The refresh token is null"); + assertNotNull(tokenResponse2.getScope(), "The scope is null"); + } + + /** + * When an authorization code is used more than once, all the tokens issued + * for that authorization code must be revoked. + */ + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void revokeTokens(final String userId, final String userSecret, final String redirectUris, + final String redirectUri, final String sectorIdentifierUri) throws Exception { + showTitle("revokeTokens"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization and receive the authorization code. + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + String nonce = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNotNull(authorizationResponse.getIdToken(), "The id token is null"); + + String scope = authorizationResponse.getScope(); + String authorizationCode = authorizationResponse.getCode(); + String idToken = authorizationResponse.getIdToken(); + + // 4. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.CODE_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 5. Request access token using the authorization code. + TokenClient tokenClient1 = new TokenClient(tokenEndpoint); + TokenResponse response2 = tokenClient1.execAuthorizationCode(authorizationCode, redirectUri, + clientId, clientSecret); + + showClient(tokenClient1); + assertEquals(response2.getStatus(), 200, "Unexpected response code: " + response2.getStatus()); + assertNotNull(response2.getEntity(), "The entity is null"); + assertNotNull(response2.getAccessToken(), "The access token is null"); + assertNotNull(response2.getTokenType(), "The token type is null"); + assertNotNull(response2.getRefreshToken(), "The refresh token is null"); + + String accessToken = response2.getAccessToken(); + String refreshToken = response2.getRefreshToken(); + + // 6. Request access token using the same authorization code one more time. This call must fail. + TokenClient tokenClient2 = new TokenClient(tokenEndpoint); + TokenResponse response4 = tokenClient2.execAuthorizationCode(authorizationCode, redirectUri, + clientId, clientSecret); + + showClient(tokenClient2); + assertEquals(response4.getStatus(), 400, "Unexpected response code: " + response4.getStatus()); + assertNotNull(response4.getEntity(), "The entity is null"); + assertNotNull(response4.getErrorType(), "The error type is null"); + assertNotNull(response4.getErrorDescription(), "The error description is null"); + + // 7. Request new access token using the refresh token. This call must fail too. + TokenClient tokenClient3 = new TokenClient(tokenEndpoint); + TokenResponse response5 = tokenClient3.execRefreshToken(scope, refreshToken, clientId, clientSecret); + + showClient(tokenClient3); + assertEquals(response5.getStatus(), 400, "Unexpected response code: " + response5.getStatus()); + assertNotNull(response5.getEntity(), "The entity is null"); + assertNotNull(response5.getErrorType(), "The error type is null"); + assertNotNull(response5.getErrorDescription(), "The error description is null"); + + // 8. Request user info should fail + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response7 = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(response7.getStatus(), 401, "Unexpected response code: " + response7.getStatus()); + assertNotNull(response7.getErrorType(), "Unexpected result: errorType not found"); + assertNotNull(response7.getErrorDescription(), "Unexpected result: errorDescription not found"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void authorizationCodeFlowLoginHint( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationCodeFlowLoginHint"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + List scopes = Arrays.asList("openid", "profile", "address", "email", "user_name"); + + // 1. Register client + RegisterResponse registerResponse = registerClient(redirectUris, responseTypes, scopes, sectorIdentifierUri); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization and receive the authorization code. + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setLoginHint(userId); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); // put userId explicitly, window.onload function result is not same as in browser (tested with chrome and FF) + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + + String scope = authorizationResponse.getScope(); + String authorizationCode = authorizationResponse.getCode(); + String idToken = authorizationResponse.getIdToken(); + + // 3. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient1 = new TokenClient(tokenEndpoint); + tokenClient1.setRequest(tokenRequest); + TokenResponse tokenResponse1 = tokenClient1.exec(); + + showClient(tokenClient1); + assertEquals(tokenResponse1.getStatus(), 200, "Unexpected response code: " + tokenResponse1.getStatus()); + assertNotNull(tokenResponse1.getEntity(), "The entity is null"); + assertNotNull(tokenResponse1.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse1.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse1.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse1.getRefreshToken(), "The refresh token is null"); + + String refreshToken = tokenResponse1.getRefreshToken(); + + // 4. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.CODE_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.OX_OPENID_CONNECT_VERSION)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 5. Request new access token using the refresh token. + TokenClient tokenClient2 = new TokenClient(tokenEndpoint); + TokenResponse tokenResponse2 = tokenClient2.execRefreshToken(scope, refreshToken, clientId, clientSecret); + + showClient(tokenClient2); + assertEquals(tokenResponse2.getStatus(), 200, "Unexpected response code: " + tokenResponse2.getStatus()); + assertNotNull(tokenResponse2.getEntity(), "The entity is null"); + assertNotNull(tokenResponse2.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse2.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse2.getRefreshToken(), "The refresh token is null"); + assertNotNull(tokenResponse2.getScope(), "The scope is null"); + + String accessToken = tokenResponse2.getAccessToken(); + + // 6. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim("user_name")); + assertNull(userInfoResponse.getClaim("org_name")); + assertNull(userInfoResponse.getClaim("work_phone")); + } + + private AuthorizationResponse requestAuthorization(final String userId, final String userSecret, final String redirectUri, + List responseTypes, List scopes, String clientId, String nonce) { + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + return authorizationResponse; + } + + private RegisterResponse registerClient( + final String redirectUris, List responseTypes, List scopes, String sectorIdentifierUri) { + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setScope(scopes); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = newRegisterClient(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + return registerResponse; + } + + @Parameters({"userId", "userSecret", "redirectUri"}) + @Test(enabled = false) // retain claims script has to be enabled and client pre-configured (not avaiable in test suite) + public void retainClaimAuthorizationCodeFlow(final String userId, final String userSecret, final String redirectUri) throws Exception { + showTitle("authorizationCodeFlow"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + List scopes = Arrays.asList("openid", "profile", "address", "email", "phone", "user_name"); + + String clientId = "0008-525a95a3-5fe1-4ecf-878c-06f438e3f500"; + String clientSecret = "V9RKUZOtfk92";//registerResponse.getClientSecret(); + + // 2. Request authorization and receive the authorization code. + String nonce = UUID.randomUUID().toString(); + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, scopes, clientId, nonce); + + String scope = authorizationResponse.getScope(); + String authorizationCode = authorizationResponse.getCode(); + String idToken = authorizationResponse.getIdToken(); + + // 3. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient1 = newTokenClient(tokenRequest); + tokenClient1.setRequest(tokenRequest); + TokenResponse tokenResponse1 = tokenClient1.exec(); + + showClient(tokenClient1); + assertEquals(tokenResponse1.getStatus(), 200, "Unexpected response code: " + tokenResponse1.getStatus()); + assertNotNull(tokenResponse1.getEntity(), "The entity is null"); + assertNotNull(tokenResponse1.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse1.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse1.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse1.getRefreshToken(), "The refresh token is null"); + + String refreshToken = tokenResponse1.getRefreshToken(); + + // 4. Validate id_token + Jwt jwt = Jwt.parse(idToken); + Asserter.assertIdToken(jwt, JwtClaimName.CODE_HASH); + + // 5. Request new access token using the refresh token. + TokenClient tokenClient2 = new TokenClient(tokenEndpoint); + tokenClient2.setExecutor(clientEngine(true)); + TokenResponse tokenResponse2 = tokenClient2.execRefreshToken(scope, refreshToken, clientId, clientSecret); + + showClient(tokenClient2); + assertEquals(tokenResponse2.getStatus(), 200, "Unexpected response code: " + tokenResponse2.getStatus()); + assertNotNull(tokenResponse2.getEntity(), "The entity is null"); + assertNotNull(tokenResponse2.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse2.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse2.getRefreshToken(), "The refresh token is null"); + assertNotNull(tokenResponse2.getScope(), "The scope is null"); + + String accessToken = tokenResponse2.getAccessToken(); + System.out.println("AT2: " + accessToken); + + Jwt at2Jwt = Jwt.parse(accessToken); + System.out.println("AT2 claims: " + at2Jwt.getClaims().toJsonString()); + } + +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AuthorizationResponseCustomHeaderTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AuthorizationResponseCustomHeaderTest.java new file mode 100644 index 00000000..731ffd93 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AuthorizationResponseCustomHeaderTest.java @@ -0,0 +1,115 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.Prompt; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.ITestContext; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +/** + * @author Javier Rojas Blum + * @version December 26, 2016 + */ +public class AuthorizationResponseCustomHeaderTest extends BaseTest { + + @Test(dataProvider = "requestAuthorizationCustomHeaderDataProvider") + public void requestAuthorizationCustomHeader( + final List responseTypes, final String userId, final String userSecret, + final String redirectUris, final String redirectUri, final String sectorIdentifierUri) throws Exception { + showTitle("AuthorizationResponseCustomHeaderTest"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization and receive the authorization code. + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + String nonce = UUID.randomUUID().toString(); + + Map customResponseHeaders = new HashMap(); + customResponseHeaders.put("CustomHeader1", "custom_header_value_1"); + customResponseHeaders.put("CustomHeader2", "custom_header_value_2"); + customResponseHeaders.put("CustomHeader3", "custom_header_value_3"); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.setAuthUsername(userId); + authorizationRequest.setAuthPassword(userSecret); + authorizationRequest.getPrompts().add(Prompt.NONE); + authorizationRequest.setCustomResponseHeaders(customResponseHeaders); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(authorizationResponse.getStatus(), 302); + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getHeaders()); + assertTrue(authorizationResponse.getHeaders().containsKey("CustomHeader1")); + assertTrue(authorizationResponse.getHeaders().containsKey("CustomHeader2")); + assertTrue(authorizationResponse.getHeaders().containsKey("CustomHeader3")); + } + + @DataProvider(name = "requestAuthorizationCustomHeaderDataProvider") + public Object[][] omittedResponseTypesFailDataProvider(ITestContext context) { + String userId = context.getCurrentXmlTest().getParameter("userId"); + String userSecret = context.getCurrentXmlTest().getParameter("userSecret"); + String redirectUris = context.getCurrentXmlTest().getParameter("redirectUris"); + String redirectUri = context.getCurrentXmlTest().getParameter("redirectUri"); + String sectorIdentifierUri = context.getCurrentXmlTest().getParameter("sectorIdentifierUri"); + + return new Object[][]{ + {Arrays.asList(ResponseType.CODE), userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri}, + {Arrays.asList(ResponseType.TOKEN), userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri}, + {Arrays.asList(ResponseType.ID_TOKEN), userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri}, + {Arrays.asList(ResponseType.CODE, ResponseType.TOKEN), userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri}, + {Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN), userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri}, + {Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN), userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri}, + {Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri}, + }; + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AuthorizationResponseModeHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AuthorizationResponseModeHttpTest.java new file mode 100644 index 00000000..0814da79 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AuthorizationResponseModeHttpTest.java @@ -0,0 +1,1149 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseMode; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Javier Rojas Blum + * @version November 2, 2016 + */ +public class AuthorizationResponseModeHttpTest extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void defaultResponseModeBasicCode( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("defaultResponseModeBasicCode"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertEquals(authorizationResponse.getResponseMode(), ResponseMode.QUERY); + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void fragmentResponseModeBasicCode( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("fragmentResponseModeBasicCode"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setResponseMode(ResponseMode.FRAGMENT); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertEquals(authorizationResponse.getResponseMode(), ResponseMode.FRAGMENT); + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void queryResponseModeBasicCode( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("queryResponseModeBasicCode"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setResponseMode(ResponseMode.QUERY); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertEquals(authorizationResponse.getResponseMode(), ResponseMode.QUERY); + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void formPostResponseModeBasicCode( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("formPostResponseModeBasicCode"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setResponseMode(ResponseMode.FORM_POST); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertEquals(authorizationResponse.getResponseMode(), ResponseMode.FORM_POST); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void defaultResponseModeImplicitIdToken( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("defaultResponseModeImplicitIdToken"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertEquals(authorizationResponse.getResponseMode(), ResponseMode.FRAGMENT); + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void fragmentResponseModeImplicitIdToken( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("fragmentResponseModeImplicitIdToken"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setResponseMode(ResponseMode.FRAGMENT); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertEquals(authorizationResponse.getResponseMode(), ResponseMode.FRAGMENT); + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void queryResponseModeImplicitIdToken( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("queryResponseModeImplicitIdToken"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setResponseMode(ResponseMode.QUERY); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertEquals(authorizationResponse.getResponseMode(), ResponseMode.QUERY); + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void formPostResponseModeImplicitIdToken( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("formPostResponseModeImplicitIdToken"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setResponseMode(ResponseMode.FORM_POST); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertEquals(authorizationResponse.getResponseMode(), ResponseMode.FORM_POST); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void defaultResponseModeImplicitIdTokenToken( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("defaultResponseModeImplicitIdTokenToken"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN, ResponseType.TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertEquals(authorizationResponse.getResponseMode(), ResponseMode.FRAGMENT); + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getAccessToken()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void fragmentResponseModeImplicitIdTokenToken( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("fragmentResponseModeImplicitIdTokenToken"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN, ResponseType.TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setResponseMode(ResponseMode.FRAGMENT); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertEquals(authorizationResponse.getResponseMode(), ResponseMode.FRAGMENT); + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getAccessToken()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void queryResponseModeImplicitIdTokenToken( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("queryResponseModeImplicitIdTokenToken"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN, ResponseType.TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setResponseMode(ResponseMode.QUERY); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertEquals(authorizationResponse.getResponseMode(), ResponseMode.QUERY); + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getAccessToken()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void formPostResponseModeImplicitIdTokenToken( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("formPostResponseModeImplicitIdTokenToken"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN, ResponseType.TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setResponseMode(ResponseMode.FORM_POST); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertEquals(authorizationResponse.getResponseMode(), ResponseMode.FORM_POST); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void defaultResponseModeHybridCodeIdToken( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("defaultResponseModeHybridCodeIdToken"); + + List responseTypes = Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertEquals(authorizationResponse.getResponseMode(), ResponseMode.FRAGMENT); + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void fragmentResponseModeHybridCodeIdToken( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("fragmentResponseModeHybridCodeIdToken"); + + List responseTypes = Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setResponseMode(ResponseMode.FRAGMENT); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertEquals(authorizationResponse.getResponseMode(), ResponseMode.FRAGMENT); + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void queryResponseModeHybridCodeIdToken( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("queryResponseModeHybridCodeIdToken"); + + List responseTypes = Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setResponseMode(ResponseMode.QUERY); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertEquals(authorizationResponse.getResponseMode(), ResponseMode.QUERY); + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void formPostResponseModeHybridCodeIdToken( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("formPostResponseModeHybridCodeIdToken"); + + List responseTypes = Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setResponseMode(ResponseMode.FORM_POST); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertEquals(authorizationResponse.getResponseMode(), ResponseMode.FORM_POST); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void defaultResponseModeHybridCodeIdTokenToken( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("defaultResponseModeHybridCodeIdTokenToken"); + + List responseTypes = Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN, ResponseType.TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertEquals(authorizationResponse.getResponseMode(), ResponseMode.FRAGMENT); + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getAccessToken()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void fragmentResponseModeHybridCodeIdTokenToken( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("fragmentResponseModeHybridCodeIdTokenToken"); + + List responseTypes = Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN, ResponseType.TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setResponseMode(ResponseMode.FRAGMENT); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertEquals(authorizationResponse.getResponseMode(), ResponseMode.FRAGMENT); + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getAccessToken()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void queryResponseModeHybridCodeIdTokenToken( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("queryResponseModeHybridCodeIdTokenToken"); + + List responseTypes = Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN, ResponseType.TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setResponseMode(ResponseMode.QUERY); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertEquals(authorizationResponse.getResponseMode(), ResponseMode.QUERY); + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getAccessToken()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void formPostResponseModeHybridCodeIdTokenToken( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("formPostResponseModeHybridCodeIdTokenToken"); + + List responseTypes = Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN, ResponseType.TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setResponseMode(ResponseMode.FORM_POST); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertEquals(authorizationResponse.getResponseMode(), ResponseMode.FORM_POST); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void defaultResponseModeHybridCodeToken( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("defaultResponseModeHybridCodeToken"); + + List responseTypes = Arrays.asList(ResponseType.CODE, ResponseType.TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertEquals(authorizationResponse.getResponseMode(), ResponseMode.FRAGMENT); + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getAccessToken()); + assertNotNull(authorizationResponse.getState()); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void fragmentResponseModeHybridCodeToken( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("fragmentResponseModeHybridCodeToken"); + + List responseTypes = Arrays.asList(ResponseType.CODE, ResponseType.TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setResponseMode(ResponseMode.FRAGMENT); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertEquals(authorizationResponse.getResponseMode(), ResponseMode.FRAGMENT); + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getAccessToken()); + assertNotNull(authorizationResponse.getState()); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void queryResponseModeHybridCodeToken( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("queryResponseModeHybridCodeToken"); + + List responseTypes = Arrays.asList(ResponseType.CODE, ResponseType.TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setResponseMode(ResponseMode.QUERY); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertEquals(authorizationResponse.getResponseMode(), ResponseMode.QUERY); + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getAccessToken()); + assertNotNull(authorizationResponse.getState()); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void formPostResponseModeHybridCodeToken( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("formPostResponseModeHybridCodeToken"); + + List responseTypes = Arrays.asList(ResponseType.CODE, ResponseType.TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setResponseMode(ResponseMode.FORM_POST); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertEquals(authorizationResponse.getResponseMode(), ResponseMode.FORM_POST); + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AuthorizationSupportCustomParams.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AuthorizationSupportCustomParams.java new file mode 100644 index 00000000..65529113 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AuthorizationSupportCustomParams.java @@ -0,0 +1,90 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Javier Rojas Blum + * @version November 23, 2017 + */ +public class AuthorizationSupportCustomParams extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void authorizationSupportCustomParams( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationSupportCustomParams"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "test"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.addCustomParameter("customParam1", "value1"); + authorizationRequest.addCustomParameter("customParam2", "value2"); + authorizationRequest.addCustomParameter("customParam3", "value3"); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + // NOTE: After complete successfully this test, check whether the stored session in LDAP has the 3 custom params + // stored in its session attributes list. + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AuthorizeRestWebServiceHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AuthorizeRestWebServiceHttpTest.java new file mode 100644 index 00000000..1cda42d7 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AuthorizeRestWebServiceHttpTest.java @@ -0,0 +1,3017 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.gluu.oxauth.model.register.RegisterRequestParam.APPLICATION_TYPE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.CLIENT_NAME; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ID_TOKEN_SIGNED_RESPONSE_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REDIRECT_URIS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.RESPONSE_TYPES; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SCOPE; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.Asserter; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.client.model.authorize.Claim; +import org.gluu.oxauth.client.model.authorize.ClaimValue; +import org.gluu.oxauth.client.model.authorize.JwtAuthorizationRequest; +import org.gluu.oxauth.model.authorize.AuthorizeErrorResponseType; +import org.gluu.oxauth.model.common.Prompt; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.common.SubjectType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * Functional tests for Authorize Web Services (HTTP) + * + * @author Javier Rojas Blum + * @version August 29, 2018 + */ +public class AuthorizeRestWebServiceHttpTest extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationCode(final String userId, final String userSecret, final String redirectUris, final String redirectUri, final String sectorIdentifierUri) { + showTitle("requestAuthorizationCode"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization and receive the authorization code. + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationCodeUserBasicAuth( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationCodeUserBasicAuth"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization and receive the authorization code. + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationCodeNoRedirection( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationCodeNoRedirection"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization and receive the authorization code. + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setUseNoRedirectHeader(true); // Use Alternate Method for redirect + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + showClient(authorizeClient); + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + } + + @Parameters({"userId", "userSecret"}) + @Test + public void requestAuthorizationCodeFail1(final String userId, final String userSecret) throws Exception { + showTitle("requestAuthorizationCodeFail1"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, null, null, null, null); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response.getStatus(), 400, "Unexpected response code: " + response.getStatus()); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationCodeFail2( + final String userId, final String userSecret, final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationCodeFail2"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + String redirectUri = "https://INVALID_REDIRECT_URI"; + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + request.setState(state); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + request.getPrompts().add(Prompt.NONE); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response.getStatus(), 400, "Unexpected response code: " + response.getStatus()); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUri"}) + @Test + public void requestAuthorizationCodeFail3(final String redirectUri) throws Exception { + showTitle("requestAuthorizationCodeFail3"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Request authorization with an invalid Client ID. + String clientId = "@!1111!0008!INVALID_VALUE"; + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(authorizationResponse.getStatus(), 401, "Unexpected response code: " + authorizationResponse.getStatus()); + assertEquals(authorizationResponse.getErrorType(), AuthorizeErrorResponseType.UNAUTHORIZED_CLIENT); + assertNotNull(authorizationResponse.getErrorType(), "The error type is null"); + assertNotNull(authorizationResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationCodeFail4( + final String userId, final String userSecret, final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationCodeFail4"); + + List responseTypes = Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList("openid", "email"); + String nonce = UUID.randomUUID().toString(); + String redirectUri = "https://evil.com/oxLicenceAdmin"; + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(authorizationResponse.getStatus(), 400, "Unexpected response code: " + authorizationResponse.getStatus()); + assertNotNull(authorizationResponse.getErrorType(), "The error type is null"); + assertNotNull(authorizationResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationToken( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationToken"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The access token is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getTokenType(), "The token type is null"); + assertNotNull(authorizationResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(authorizationResponse.getScope(), "The scope must be null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationTokenUserBasicAuth( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationTokenUserBasicAuth"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The access token is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getTokenType(), "The token type is null"); + assertNotNull(authorizationResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(authorizationResponse.getScope(), "The scope must be null"); + } + + @Parameters({"userId", "userSecret", "redirectUri"}) + @Test + public void requestAuthorizationTokenFail1( + final String userId, final String userSecret, final String redirectUri) throws Exception { + showTitle("requestAuthorizationTokenFail1"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, null, null, redirectUri, null); + request.setState(state); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response.getStatus(), 400, "Unexpected response code: " + response.getStatus()); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + assertNotNull(response.getState(), "The state is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationTokenFail2( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationTokenFail2"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = null; + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + request.setState(state); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + request.getPrompts().add(Prompt.NONE); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response.getStatus(), 302, "Unexpected response code: " + response.getStatus()); + assertNotNull(response.getLocation(), "The location is null"); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + assertNotNull(response.getState(), "The state is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationTokenIdToken( + final String userId, final String userSecret, + final String redirectUris, final String redirectUri, final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationTokenIdToken"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String accessToken = authorizationResponse.getAccessToken(); + String idToken = authorizationResponse.getIdToken(); + + // 2. Validate access_token and id_token + Jwt jwt = Jwt.parse(idToken); + Asserter.assertIdToken(jwt, JwtClaimName.ACCESS_TOKEN_HASH); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + assertTrue(rsaSigner.validateAccessToken(accessToken, jwt)); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationTokenIdTokenUserBasicAuth( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationTokenIdTokenUserBasicAuth"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String accessToken = authorizationResponse.getAccessToken(); + String idToken = authorizationResponse.getIdToken(); + + // 2. Validate access_token and id_token + Jwt jwt = Jwt.parse(idToken); + Asserter.assertIdToken(jwt, JwtClaimName.ACCESS_TOKEN_HASH); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + assertTrue(rsaSigner.validateAccessToken(accessToken, jwt)); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationCodeIdToken( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationCodeIdToken"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + String nonce = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The code is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String code = authorizationResponse.getCode(); + String idToken = authorizationResponse.getIdToken(); + + // 4. Validate code and id_token + Jwt jwt = Jwt.parse(idToken); + Asserter.assertIdToken(jwt, JwtClaimName.CODE_HASH); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + assertTrue(rsaSigner.validateAuthorizationCode(code, jwt)); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationCodeIdTokenUserBasicAuth( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationCodeIdTokenUserBasicAuth"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + String nonce = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The code is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String code = authorizationResponse.getCode(); + String idToken = authorizationResponse.getIdToken(); + + // 4. Validate code and id_token + Jwt jwt = Jwt.parse(idToken); + Asserter.assertIdToken(jwt, JwtClaimName.CODE_HASH); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + assertTrue(rsaSigner.validateAuthorizationCode(code, jwt)); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationTokenCode( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationTokenCode"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The code is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationTokenCodeUserBasicAuth( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationTokenCodeUserBasicAuth"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The code is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationTokenCodeIdToken( + final String userId, final String userSecret, + final String redirectUris, final String redirectUri, final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationTokenCodeIdToken"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.CODE, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getCode(), "The code is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String code = authorizationResponse.getCode(); + String accessToken = authorizationResponse.getAccessToken(); + String idToken = authorizationResponse.getIdToken(); + + // 4. Validate access_token and id_token + Jwt jwt = Jwt.parse(idToken); + Asserter.assertIdToken(jwt, JwtClaimName.CODE_HASH, JwtClaimName.ACCESS_TOKEN_HASH); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + assertTrue(rsaSigner.validateAuthorizationCode(code, jwt)); + assertTrue(rsaSigner.validateAccessToken(accessToken, jwt)); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationTokenCodeIdTokenUserBasicAuth( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationTokenCodeIdTokenUserBasicAuth"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.CODE, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getCode(), "The code is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String code = authorizationResponse.getCode(); + String accessToken = authorizationResponse.getAccessToken(); + String idToken = authorizationResponse.getIdToken(); + + // 4. Validate access_token and id_token + Jwt jwt = Jwt.parse(idToken); + Asserter.assertIdToken(jwt, JwtClaimName.CODE_HASH, JwtClaimName.ACCESS_TOKEN_HASH); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + assertTrue(rsaSigner.validateAuthorizationCode(code, jwt)); + assertTrue(rsaSigner.validateAccessToken(accessToken, jwt)); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationIdToken( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationIdToken"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationIdTokenUserBasicAuth( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationIdTokenUserBasicAuth"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationWithoutScope( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationWithoutScope"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = new ArrayList(); // Empty scopes list + String state = UUID.randomUUID().toString(); + String nonce = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The code is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationPromptNoneTrustedClient( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationPromptNoneTrustedClient"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + request.setState(state); + request.getPrompts().add(Prompt.NONE); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response.getStatus(), 302, "Unexpected response code: " + response.getStatus()); + assertNotNull(response.getLocation(), "The location is null"); + assertNotNull(response.getCode(), "The code is null"); + assertNotNull(response.getState(), "The state is null"); + } + + @Parameters({"redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationPromptNoneFail( + final String redirectUris, final String redirectUri, final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationPromptNoneFail"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + request.setState(state); + request.getPrompts().add(Prompt.NONE); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response.getStatus(), 302, "Unexpected response code: " + response.getStatus()); + assertNotNull(response.getLocation(), "The location is null"); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + assertNotNull(response.getState(), "The state is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationPromptLogin( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationPromptLogin"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + authorizationRequest.getPrompts().add(Prompt.LOGIN); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationPromptConsent( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationPromptConsent"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + authorizationRequest.getPrompts().add(Prompt.CONSENT); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationPromptConsentTrustedClient( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationPromptConsentTrustedClient"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + authorizationRequest.getPrompts().add(Prompt.CONSENT); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationPromptLoginConsent( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationPromptLoginConsent"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + authorizationRequest.getPrompts().add(Prompt.LOGIN); + authorizationRequest.getPrompts().add(Prompt.CONSENT); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationPromptLoginConsentTrustedClient( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationPromptLoginConsentTrustedClient"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + authorizationRequest.getPrompts().add(Prompt.LOGIN); + authorizationRequest.getPrompts().add(Prompt.CONSENT); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationPromptNoneLoginConsentFail( + final String userId, final String userSecret, + final String redirectUris, final String redirectUri, final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationPromptLoginConsent"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + request.setState(state); + request.getPrompts().add(Prompt.NONE); + request.getPrompts().add(Prompt.LOGIN); + request.getPrompts().add(Prompt.CONSENT); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response.getStatus(), 302, "Unexpected response code: " + response.getStatus()); + assertNotNull(response.getLocation(), "The location is null"); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + assertNotNull(response.getState(), "The state is null"); + } + + @Parameters({"redirectUri", "userId", "userSecret"}) + @Test + public void requestAuthorizationCodeWithoutRedirectUri( + final String redirectUri, final String userId, final String userSecret) throws Exception { + showTitle("requestAuthorizationCodeWithoutRedirectUri"); + + List redirectUriList = Arrays.asList(redirectUri.split(StringUtils.SPACE)); + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", redirectUriList); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + } + + @Parameters({"redirectUri", "userId", "userSecret"}) + @Test + public void requestAuthorizationCodeWithoutRedirectUriUserBasicAuth( + final String redirectUri, final String userId, final String userSecret) throws Exception { + showTitle("requestAuthorizationCodeWithoutRedirectUriUserBasicAuth"); + + List redirectUriList = Arrays.asList(redirectUri.split(StringUtils.SPACE)); + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", redirectUriList); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSubjectType(SubjectType.PUBLIC); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, null, null); + authorizationRequest.setState(state); + authorizationRequest.setAuthUsername(userId); + authorizationRequest.setAuthPassword(userSecret); + authorizationRequest.getPrompts().add(Prompt.NONE); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(authorizationResponse.getStatus(), 302, "Unexpected response code: " + authorizationResponse.getStatus()); + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + } + + @Parameters({"redirectUris", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationCodeWithoutRedirectUriFail( + final String redirectUris, final String userId, final String userSecret, final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationCodeWithoutRedirectUriFail"); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, null, null); + authorizationRequest.setState(state); + authorizationRequest.setAuthUsername(userId); + authorizationRequest.setAuthPassword(userSecret); + authorizationRequest.getPrompts().add(Prompt.NONE); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(authorizationResponse.getStatus(), 400, "Unexpected response code: " + authorizationResponse.getStatus()); + assertNotNull(authorizationResponse.getErrorType(), "The error type is null"); + assertNotNull(authorizationResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationAccessToken( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationAccessToken"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest1 = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest1.setState(state); + authorizationRequest1.setNonce(nonce); + + AuthorizationResponse authorizationResponse1 = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest1, userId, userSecret); + + assertNotNull(authorizationResponse1.getLocation(), "The location is null"); + assertNotNull(authorizationResponse1.getAccessToken(), "The access token is null"); + assertNotNull(authorizationResponse1.getState(), "The state is null"); + assertNotNull(authorizationResponse1.getTokenType(), "The token type is null"); + assertNotNull(authorizationResponse1.getExpiresIn(), "The expires in value is null"); + assertNotNull(authorizationResponse1.getScope(), "The scope must be null"); + + String accessToken = authorizationResponse1.getAccessToken(); + + // 4. Downstream client may be authorized by oxAuth presenting the access token and client credentials + responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + + nonce = UUID.randomUUID().toString(); + state = UUID.randomUUID().toString(); +/* + AuthorizationRequest authorizationRequest2 = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest2.setAccessToken(accessToken); + authorizationRequest2.setState(state); + authorizationRequest2.getPrompts().add(Prompt.NONE); + + AuthorizeClient authorizeClient2 = new AuthorizeClient(authorizationEndpoint); + authorizeClient2.setRequest(authorizationRequest2); + AuthorizationResponse authorizationResponse2 = authorizeClient2.exec(); + + showClient(authorizeClient2); + assertEquals(authorizationResponse2.getStatus(), 302, "Unexpected response code: " + authorizationResponse2.getStatus()); + assertNotNull(authorizationResponse2.getLocation(), "The location is null"); + assertNotNull(authorizationResponse2.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse2.getState(), "The state is null"); + assertNotNull(authorizationResponse2.getScope(), "The scope is null"); + + String authorizationCode = authorizationResponse2.getCode(); + + // 5. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient1 = new TokenClient(tokenEndpoint); + tokenClient1.setRequest(tokenRequest); + TokenResponse response3 = tokenClient1.exec(); + + showClient(tokenClient1); + assertEquals(response3.getStatus(), 200, "Unexpected response code: " + response3.getStatus()); + assertNotNull(response3.getEntity(), "The entity is null"); + assertNotNull(response3.getAccessToken(), "The access token is null"); + assertNotNull(response3.getExpiresIn(), "The expires in value is null"); + assertNotNull(response3.getTokenType(), "The token type is null"); + assertNotNull(response3.getRefreshToken(), "The refresh token is null");*/ + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationAccessTokenUserBasicAuth( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationAccessTokenUserBasicAuth"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest1 = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest1.setState(state); + + AuthorizationResponse authorizationResponse1 = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest1, userId, userSecret); + + assertNotNull(authorizationResponse1.getLocation(), "The location is null"); + assertNotNull(authorizationResponse1.getAccessToken(), "The access token is null"); + assertNotNull(authorizationResponse1.getState(), "The state is null"); + assertNotNull(authorizationResponse1.getTokenType(), "The token type is null"); + assertNotNull(authorizationResponse1.getExpiresIn(), "The expires in value is null"); + assertNotNull(authorizationResponse1.getScope(), "The scope must be null"); + + String accessToken = authorizationResponse1.getAccessToken(); + + // 4. Downstream client may be authorized by oxAuth presenting the access token and client credentials + responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + + nonce = UUID.randomUUID().toString(); + state = UUID.randomUUID().toString(); +/* + AuthorizationRequest authorizationRequest2 = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest2.setAccessToken(accessToken); + authorizationRequest2.setState(state); + authorizationRequest2.getPrompts().add(Prompt.NONE); + + AuthorizeClient authorizeClient2 = new AuthorizeClient(authorizationEndpoint); + authorizeClient2.setRequest(authorizationRequest2); + AuthorizationResponse authorizationResponse2 = authorizeClient2.exec(); + + showClient(authorizeClient2); + assertEquals(authorizationResponse2.getStatus(), 302, "Unexpected response code: " + authorizationResponse2.getStatus()); + assertNotNull(authorizationResponse2.getLocation(), "The location is null"); + assertNotNull(authorizationResponse2.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse2.getState(), "The state is null"); + assertNotNull(authorizationResponse2.getScope(), "The scope is null"); + + String authorizationCode = authorizationResponse2.getCode(); + + // 5. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient1 = new TokenClient(tokenEndpoint); + tokenClient1.setRequest(tokenRequest); + TokenResponse response3 = tokenClient1.exec(); + + showClient(tokenClient1); + assertEquals(response3.getStatus(), 200, "Unexpected response code: " + response3.getStatus()); + assertNotNull(response3.getEntity(), "The entity is null"); + assertNotNull(response3.getAccessToken(), "The access token is null"); + assertNotNull(response3.getExpiresIn(), "The expires in value is null"); + assertNotNull(response3.getTokenType(), "The token type is null"); + assertNotNull(response3.getRefreshToken(), "The refresh token is null");*/ + } + + @Parameters({"redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationAccessTokenFail( + final String redirectUris, final String redirectUri, final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationAccessTokenFail"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = null; + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + request.setAccessToken("INVALID_ACCESS_TOKEN"); + request.setState(state); + request.getPrompts().add(Prompt.NONE); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response.getStatus(), 302, "Unexpected response code: " + response.getStatus()); + assertNotNull(response.getLocation(), "The location is null"); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + assertNotNull(response.getState(), "The state is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationDenyAccessThenGrantAccess( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationDenyAccessThenGrantAccess"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = newRegisterClient(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String sessionId = null; + + // 2. Request authorization, authenticate resource owner and deny access + { + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + authorizationRequest.setAcrValues(Arrays.asList("basic")); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndDenyAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getErrorType()); + assertEquals(authorizationResponse.getErrorType(), AuthorizeErrorResponseType.ACCESS_DENIED); + assertNotNull(authorizationResponse.getErrorDescription()); + assertNotNull(authorizationResponse.getState()); + + sessionId = authorizationResponse.getSessionId(); + } + + // 3. Request authorization and deny access (resource owner is already authenticated) + { + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + authorizationRequest.setSessionId(sessionId); + authorizationRequest.setAcrValues(Arrays.asList("basic")); + + AuthorizationResponse authorizationResponse = authorizationRequestAndDenyAccess( + authorizationEndpoint, authorizationRequest); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getErrorType()); + assertEquals(authorizationResponse.getErrorType(), AuthorizeErrorResponseType.ACCESS_DENIED); + assertNotNull(authorizationResponse.getErrorDescription()); + assertNotNull(authorizationResponse.getState()); + } + + // 4. Request authorization and grant access (resource owner is already authenticated) + { + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + authorizationRequest.setSessionId(sessionId); + authorizationRequest.setAcrValues(Arrays.asList("basic")); + + AuthorizationResponse authorizationResponse = authorizationRequestAndGrantAccess( + authorizationEndpoint, authorizationRequest); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + } + } + + /** + * If a client has only openid scope and pairwise id, person should not have to authorize. + */ + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + //@Test // Before run this test, set skipAuthorizationForOpenIdScopeAndPairwiseId = true + public void requestAuthorizationForOpenIdScopeAndPairwiseId( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationForOpenIdScopeAndPairwiseId"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization and receive the authorization code. + List scopes = Arrays.asList("openid"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwner( + authorizationEndpoint, authorizationRequest, userId, userSecret, false); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationUILocales( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationUILocales"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.setNonce(nonce); + authorizationRequest.setUiLocales(Arrays.asList("es")); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The access token is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getTokenType(), "The token type is null"); + assertNotNull(authorizationResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(authorizationResponse.getScope(), "The scope must be null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationObjectUILocales( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationObjectUILocales"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = newRegisterClient(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.setUiLocales(Arrays.asList("es")); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest, SignatureAlgorithm.HS256, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{"basic"}))); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationAccessTokenSubjectTypePublic( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationAccessTokenSubjectTypePublic"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setSubjectType(SubjectType.PUBLIC); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest1 = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest1.setState(state); + authorizationRequest1.setNonce(nonce); + + AuthorizationResponse authorizationResponse1 = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest1, userId, userSecret); + + assertNotNull(authorizationResponse1.getLocation(), "The location is null"); + assertNotNull(authorizationResponse1.getAccessToken(), "The access token is null"); + assertNotNull(authorizationResponse1.getState(), "The state is null"); + assertNotNull(authorizationResponse1.getTokenType(), "The token type is null"); + assertNotNull(authorizationResponse1.getExpiresIn(), "The expires in value is null"); + assertNotNull(authorizationResponse1.getScope(), "The scope must be null"); + + String idToken = authorizationResponse1.getIdToken(); + + // 2. Validate access_token and id_token + Jwt jwt = Jwt.parse(idToken); + Asserter.assertIdToken(jwt, JwtClaimName.ACCESS_TOKEN_HASH); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AuthorizeSessionIdRestWebServiceHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AuthorizeSessionIdRestWebServiceHttpTest.java new file mode 100644 index 00000000..6e1a0593 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/AuthorizeSessionIdRestWebServiceHttpTest.java @@ -0,0 +1,142 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.gluu.oxauth.model.register.RegisterRequestParam.APPLICATION_TYPE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.CLIENT_NAME; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ID_TOKEN_SIGNED_RESPONSE_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REDIRECT_URIS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.RESPONSE_TYPES; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SCOPE; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * Functional tests for checking Sessions in Authorize Web Services workflow (HTTP) + * + * @author Yuriy Movchan + * @author Javier Rojas Blum + * @version November 29, 2017 + */ +public class AuthorizeSessionIdRestWebServiceHttpTest extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestSessionIdAuthorizationCode1( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestSessionIdAuthorizationCode1"); + + requestSessionIdAuthorizationCode(userId, userSecret, redirectUris, redirectUri, authorizationEndpoint, + authorizationEndpoint, sectorIdentifierUri); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestSessionIdAuthorizationCode2( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestSessionIdAuthorizationCode2"); + + requestSessionIdAuthorizationCode(userId, userSecret, redirectUris, redirectUri, authorizationPageEndpoint, + authorizationEndpoint, sectorIdentifierUri); + } + + private void requestSessionIdAuthorizationCode( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String authorizationEndpoint1, final String authorizationEndpoint2, final String sectorIdentifierUri) { + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization but not enter credentials. + // Store session_id parameter value + List scopes1 = Arrays.asList("openid", "profile", "address", "email"); + String state1 = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest1 = new AuthorizationRequest(responseTypes, clientId, scopes1, redirectUri, null); + authorizationRequest1.setState(state1); + String sessionId = waitForResourceOwnerAndGrantLoginForm(authorizationEndpoint1, authorizationRequest1, false); + assertNotNull(sessionId, "The session_id is null"); + + // 4. Request authorization and receive the authorization code. + // Application should returns new session_id + List scopes2 = Arrays.asList("openid", "profile", "address", "email"); + String state2 = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest2 = new AuthorizationRequest(responseTypes, clientId, scopes2, redirectUri, null); + authorizationRequest2.setState(state2); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint2, authorizationRequest2, userId, userSecret, false); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNotEquals(sessionId, authorizationResponse.getSessionId(), "The session_id is the same for 2 different authorization requests"); + } + +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ClientAuthenticationFilterHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ClientAuthenticationFilterHttpTest.java new file mode 100644 index 00000000..d2aca30b --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ClientAuthenticationFilterHttpTest.java @@ -0,0 +1,189 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.Prompt; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Javier Rojas Blum + * @version September 3, 2018 + */ +public class ClientAuthenticationFilterHttpTest extends BaseTest { + + private String clientId; + private String customAttrValue1; + + @Parameters({"redirectUris", "sectorIdentifierUri"}) + @Test + public void requestClientRegistrationWithCustomAttributes( + final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("requestClientRegistrationWithCustomAttributes"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + customAttrValue1 = UUID.randomUUID().toString(); + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setGrantTypes(grantTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.addCustomAttribute("myCustomAttr1", customAttrValue1); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + clientId = response.getClientId(); + } + + @Parameters({"userId", "userSecret", "redirectUri"}) + @Test(dependsOnMethods = "requestClientRegistrationWithCustomAttributes") + public void requestAccessTokenCustomClientAuth1(final String userId, final String userSecret, + final String redirectUri) throws Exception { + showTitle("requestAccessTokenCustomClientAuth1"); + + // 1. Request authorization and receive the authorization code. + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + List scopes = Arrays.asList("openid", "profile", "address", "email"); + + String state = UUID.randomUUID().toString(); + String nonce = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.setAuthUsername(userId); + authorizationRequest.setAuthPassword(userSecret); + authorizationRequest.getPrompts().add(Prompt.NONE); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(authorizationResponse.getStatus(), 302, "Unexpected response code: " + authorizationResponse.getStatus()); + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The code is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String authorizationCode = authorizationResponse.getCode(); + String idToken = authorizationResponse.getIdToken(); + + // 2. Validate code and id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.CODE_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + assertTrue(rsaSigner.validateAuthorizationCode(authorizationCode, jwt)); + + // 3. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_POST); + tokenRequest.addCustomParameter("myCustomAttr1", customAttrValue1); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + @Parameters({"userId", "userSecret"}) + @Test(dependsOnMethods = "requestClientRegistrationWithCustomAttributes") + public void requestAccessTokenCustomClientAuth2(final String userId, final String userSecret) throws Exception { + showTitle("requestAccessTokenCustomClientAuth2"); + + String username = userId; + String password = userSecret; + + TokenRequest tokenRequest = new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + tokenRequest.setUsername(username); + tokenRequest.setPassword(password); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_POST); + tokenRequest.addCustomParameter("myCustomAttr1", customAttrValue1); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ClientCredentialsGrantHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ClientCredentialsGrantHttpTest.java new file mode 100644 index 00000000..8c243261 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ClientCredentialsGrantHttpTest.java @@ -0,0 +1,1956 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; + +import java.util.Arrays; +import java.util.List; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.ClientInfoClient; +import org.gluu.oxauth.client.ClientInfoResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.token.TokenErrorResponseType; +import org.gluu.oxauth.model.userinfo.UserInfoErrorResponseType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Javier Rojas Blum + * @version February 8, 2019 + */ +public class ClientCredentialsGrantHttpTest extends BaseTest { + + @Parameters({"redirectUris", "sectorIdentifierUri"}) + @Test + public void defaultAuthenticationMethod(final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("defaultAuthenticationMethod"); + + List scopes = Arrays.asList("clientinfo"); + List grantTypes = Arrays.asList( + GrantType.CLIENT_CREDENTIALS + ); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setGrantTypes(grantTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request Client Credentials Grant + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity()); + assertNotNull(tokenResponse.getAccessToken()); + assertNotNull(tokenResponse.getTokenType()); + assertNotNull(tokenResponse.getScope()); + assertNull(tokenResponse.getRefreshToken()); + + String accessToken = tokenResponse.getAccessToken(); + + // 3. Request client info + ClientInfoClient clientInfoClient = new ClientInfoClient(clientInfoEndpoint); + ClientInfoResponse clientInfoResponse = clientInfoClient.execClientInfo(accessToken); + + showClient(clientInfoClient); + assertEquals(clientInfoResponse.getStatus(), 200, "Unexpected response code: " + clientInfoResponse.getStatus()); + assertNotNull(clientInfoResponse.getClaim("displayName"), "Unexpected result: displayName not found"); + assertNotNull(clientInfoResponse.getClaim("inum"), "Unexpected result: inum not found"); + } + + @Parameters({"redirectUris", "sectorIdentifierUri"}) + @Test + public void defaultAuthenticationMethodFail(final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("defaultAuthenticationMethodFail"); + + List scopes = Arrays.asList("clientinfo"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Client Credentials Grant + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword("INVALID_CLIENT_SECRET"); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType()); + assertEquals(tokenResponse.getErrorType(), TokenErrorResponseType.INVALID_CLIENT); + assertNotNull(tokenResponse.getErrorDescription()); + } + + @Parameters({"redirectUris", "sectorIdentifierUri"}) + @Test + public void clientSecretBasicAuthenticationMethod(final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("clientSecretBasicAuthenticationMethod"); + + List scopes = Arrays.asList("openid", "profile", "address", "email", "clientinfo"); + List grantTypes = Arrays.asList( + GrantType.CLIENT_CREDENTIALS + ); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setGrantTypes(grantTypes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request Client Credentials Grant + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity()); + assertNotNull(tokenResponse.getAccessToken()); + assertNotNull(tokenResponse.getTokenType()); + assertNotNull(tokenResponse.getScope()); + assertNull(tokenResponse.getRefreshToken()); + + String accessToken = tokenResponse.getAccessToken(); + + // 3. Request client info + ClientInfoClient clientInfoClient = new ClientInfoClient(clientInfoEndpoint); + ClientInfoResponse clientInfoResponse = clientInfoClient.execClientInfo(accessToken); + + showClient(clientInfoClient); + assertEquals(clientInfoResponse.getStatus(), 200, "Unexpected response code: " + clientInfoResponse.getStatus()); + assertNotNull(clientInfoResponse.getClaim("displayName"), "Unexpected result: displayName not found"); + assertNotNull(clientInfoResponse.getClaim("inum"), "Unexpected result: inum not found"); + + // 4. Request user info should fail + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 403); + assertEquals(userInfoResponse.getErrorType(), UserInfoErrorResponseType.INSUFFICIENT_SCOPE); + } + + @Parameters({"redirectUris", "sectorIdentifierUri"}) + @Test + public void clientSecretBasicAuthenticationMethodFail(final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("clientSecretBasicAuthenticationMethodFail"); + + List scopes = Arrays.asList("clientinfo"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Client Credentials Grant + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword("INVALID_CLIENT_SECRET"); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType()); + assertEquals(tokenResponse.getErrorType(), TokenErrorResponseType.INVALID_CLIENT); + assertNotNull(tokenResponse.getErrorDescription()); + } + + @Parameters({"redirectUris", "sectorIdentifierUri"}) + @Test + public void clientSecretPostAuthenticationMethod(final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("clientSecretPostAuthenticationMethod"); + + List scopes = Arrays.asList("clientinfo"); + List grantTypes = Arrays.asList( + GrantType.CLIENT_CREDENTIALS + ); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setGrantTypes(grantTypes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_POST); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request Client Credentials Grant + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_POST); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity()); + assertNotNull(tokenResponse.getAccessToken()); + assertNotNull(tokenResponse.getTokenType()); + assertNotNull(tokenResponse.getScope()); + assertNull(tokenResponse.getRefreshToken()); + + String accessToken = tokenResponse.getAccessToken(); + + // 3. Request client info + ClientInfoClient clientInfoClient = new ClientInfoClient(clientInfoEndpoint); + ClientInfoResponse clientInfoResponse = clientInfoClient.execClientInfo(accessToken); + + showClient(clientInfoClient); + assertEquals(clientInfoResponse.getStatus(), 200, "Unexpected response code: " + clientInfoResponse.getStatus()); + assertNotNull(clientInfoResponse.getClaim("displayName"), "Unexpected result: displayName not found"); + assertNotNull(clientInfoResponse.getClaim("inum"), "Unexpected result: inum not found"); + } + + @Parameters({"redirectUris", "sectorIdentifierUri"}) + @Test + public void clientSecretPostAuthenticationMethodFail1(final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("clientSecretPostAuthenticationMethodFail1"); + + List scopes = Arrays.asList("clientinfo"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_POST); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Client Credentials Grant + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword("INVALID_CLIENT_SECRET"); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_POST); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType()); + assertEquals(tokenResponse.getErrorType(), TokenErrorResponseType.INVALID_CLIENT); + assertNotNull(tokenResponse.getErrorDescription()); + } + + @Parameters({"redirectUris", "sectorIdentifierUri"}) + @Test + public void clientSecretPostAuthenticationMethodFail2(final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("clientSecretPostAuthenticationMethodFail2"); + + List scopes = Arrays.asList("clientinfo"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_POST); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Client Credentials Grant + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(null); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_POST); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType()); + assertEquals(tokenResponse.getErrorType(), TokenErrorResponseType.INVALID_CLIENT); + assertNotNull(tokenResponse.getErrorDescription()); + } + + @Parameters({"redirectUris", "sectorIdentifierUri"}) + @Test + public void clientSecretPostAuthenticationMethodFail3(final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("clientSecretPostAuthenticationMethodFail3"); + + List scopes = Arrays.asList("clientinfo"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_POST); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Client Credentials Grant + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(null); + tokenRequest.setAuthPassword(null); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_POST); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType()); + assertEquals(tokenResponse.getErrorType(), TokenErrorResponseType.INVALID_CLIENT); + assertNotNull(tokenResponse.getErrorDescription()); + } + + @Parameters({"redirectUris", "sectorIdentifierUri"}) + @Test + public void clientSecretJwtAuthenticationMethodHS256(final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("clientSecretJwtAuthenticationMethodHS256"); + + List scopes = Arrays.asList("clientinfo"); + List grantTypes = Arrays.asList( + GrantType.CLIENT_CREDENTIALS + ); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setGrantTypes(grantTypes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request Client Credentials Grant + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.HS256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity()); + assertNotNull(tokenResponse.getAccessToken()); + assertNotNull(tokenResponse.getTokenType()); + assertNotNull(tokenResponse.getScope()); + assertNull(tokenResponse.getRefreshToken()); + + String accessToken = tokenResponse.getAccessToken(); + + // 3. Request client info + ClientInfoClient clientInfoClient = new ClientInfoClient(clientInfoEndpoint); + ClientInfoResponse clientInfoResponse = clientInfoClient.execClientInfo(accessToken); + + showClient(clientInfoClient); + assertEquals(clientInfoResponse.getStatus(), 200, "Unexpected response code: " + clientInfoResponse.getStatus()); + assertNotNull(clientInfoResponse.getClaim("displayName"), "Unexpected result: displayName not found"); + assertNotNull(clientInfoResponse.getClaim("inum"), "Unexpected result: inum not found"); + } + + @Parameters({"redirectUris", "sectorIdentifierUri"}) + @Test + public void clientSecretJwtAuthenticationMethodHS256Fail(final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("clientSecretJwtAuthenticationMethodHS256Fail"); + + List scopes = Arrays.asList("clientinfo"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Client Credentials Grant + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword("INVALID_CLIENT_SECRET"); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.HS256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType()); + assertEquals(tokenResponse.getErrorType(), TokenErrorResponseType.INVALID_CLIENT); + assertNotNull(tokenResponse.getErrorDescription()); + } + + @Parameters({"redirectUris", "sectorIdentifierUri"}) + @Test + public void clientSecretJwtAuthenticationMethodHS384(final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("clientSecretJwtAuthenticationMethodHS384"); + + List scopes = Arrays.asList("clientinfo"); + List grantTypes = Arrays.asList( + GrantType.CLIENT_CREDENTIALS + ); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setGrantTypes(grantTypes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request Client Credentials Grant + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.HS384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity()); + assertNotNull(tokenResponse.getAccessToken()); + assertNotNull(tokenResponse.getTokenType()); + assertNotNull(tokenResponse.getScope()); + assertNull(tokenResponse.getRefreshToken()); + + String accessToken = tokenResponse.getAccessToken(); + + // 3. Request client info + ClientInfoClient clientInfoClient = new ClientInfoClient(clientInfoEndpoint); + ClientInfoResponse clientInfoResponse = clientInfoClient.execClientInfo(accessToken); + + showClient(clientInfoClient); + assertEquals(clientInfoResponse.getStatus(), 200, "Unexpected response code: " + clientInfoResponse.getStatus()); + assertNotNull(clientInfoResponse.getClaim("displayName"), "Unexpected result: displayName not found"); + assertNotNull(clientInfoResponse.getClaim("inum"), "Unexpected result: inum not found"); + } + + @Parameters({"redirectUris", "sectorIdentifierUri"}) + @Test + public void clientSecretJwtAuthenticationMethodHS384Fail(final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("clientSecretJwtAuthenticationMethodHS384Fail"); + + List scopes = Arrays.asList("clientinfo"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Client Credentials Grant + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword("INVALID_CLIENT_SECRET"); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.HS384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType()); + assertEquals(tokenResponse.getErrorType(), TokenErrorResponseType.INVALID_CLIENT); + assertNotNull(tokenResponse.getErrorDescription()); + } + + @Parameters({"redirectUris", "sectorIdentifierUri"}) + @Test + public void clientSecretJwtAuthenticationMethodHS512(final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("clientSecretJwtAuthenticationMethodHS512"); + + List scopes = Arrays.asList("clientinfo"); + List grantTypes = Arrays.asList( + GrantType.CLIENT_CREDENTIALS + ); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setGrantTypes(grantTypes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request Client Credentials Grant + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.HS512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity()); + assertNotNull(tokenResponse.getAccessToken()); + assertNotNull(tokenResponse.getTokenType()); + assertNotNull(tokenResponse.getScope()); + assertNull(tokenResponse.getRefreshToken()); + + String accessToken = tokenResponse.getAccessToken(); + + // 3. Request client info + ClientInfoClient clientInfoClient = new ClientInfoClient(clientInfoEndpoint); + ClientInfoResponse clientInfoResponse = clientInfoClient.execClientInfo(accessToken); + + showClient(clientInfoClient); + assertEquals(clientInfoResponse.getStatus(), 200, "Unexpected response code: " + clientInfoResponse.getStatus()); + assertNotNull(clientInfoResponse.getClaim("displayName"), "Unexpected result: displayName not found"); + assertNotNull(clientInfoResponse.getClaim("inum"), "Unexpected result: inum not found"); + } + + @Parameters({"redirectUris", "sectorIdentifierUri"}) + @Test + public void clientSecretJwtAuthenticationMethodHS512Fail(final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("clientSecretJwtAuthenticationMethodHS512Fail"); + + List scopes = Arrays.asList("clientinfo"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request Client Credentials Grant + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword("INVALID_CLIENT_SECRET"); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.HS512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType()); + assertEquals(tokenResponse.getErrorType(), TokenErrorResponseType.INVALID_CLIENT); + assertNotNull(tokenResponse.getErrorDescription()); + } + + @Parameters({"redirectUris", "clientJwksUri", "RS256_keyId", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void privateKeyJwtAuthenticationMethodRS256( + final String redirectUris, final String clientJwksUri, final String keyId, final String dnName, + final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("privateKeyJwtAuthenticationMethodRS256"); + + List scopes = Arrays.asList("clientinfo"); + List grantTypes = Arrays.asList( + GrantType.CLIENT_CREDENTIALS + ); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setGrantTypes(grantTypes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Client Credentials Grant + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity()); + assertNotNull(tokenResponse.getAccessToken()); + assertNotNull(tokenResponse.getTokenType()); + assertNotNull(tokenResponse.getScope()); + assertNull(tokenResponse.getRefreshToken()); + + String accessToken = tokenResponse.getAccessToken(); + + // 3. Request client info + ClientInfoClient clientInfoClient = new ClientInfoClient(clientInfoEndpoint); + ClientInfoResponse clientInfoResponse = clientInfoClient.execClientInfo(accessToken); + + showClient(clientInfoClient); + assertEquals(clientInfoResponse.getStatus(), 200, "Unexpected response code: " + clientInfoResponse.getStatus()); + assertNotNull(clientInfoResponse.getClaim("displayName"), "Unexpected result: displayName not found"); + assertNotNull(clientInfoResponse.getClaim("inum"), "Unexpected result: inum not found"); + } + + @Parameters({"redirectUris", "clientJwksUri", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void privateKeyJwtAuthenticationMethodRS256Fail( + final String redirectUris, final String clientJwksUri, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("privateKeyJwtAuthenticationMethodRS256Fail"); + + List scopes = Arrays.asList("clientinfo"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Client Credentials Grant + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId("RS256SIG_INVALID_KEYID"); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType()); + assertEquals(tokenResponse.getErrorType(), TokenErrorResponseType.INVALID_CLIENT); + assertNotNull(tokenResponse.getErrorDescription()); + } + + @Parameters({"redirectUris", "clientJwksUri", "RS384_keyId", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void privateKeyJwtAuthenticationMethodRS384( + final String redirectUris, final String clientJwksUri, final String keyId, final String dnName, + final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("privateKeyJwtAuthenticationMethodRS384"); + + List scopes = Arrays.asList("clientinfo"); + List grantTypes = Arrays.asList( + GrantType.CLIENT_CREDENTIALS + ); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setGrantTypes(grantTypes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Client Credentials Grant + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity()); + assertNotNull(tokenResponse.getAccessToken()); + assertNotNull(tokenResponse.getTokenType()); + assertNotNull(tokenResponse.getScope()); + assertNull(tokenResponse.getRefreshToken()); + + String accessToken = tokenResponse.getAccessToken(); + + // 3. Request client info + ClientInfoClient clientInfoClient = new ClientInfoClient(clientInfoEndpoint); + ClientInfoResponse clientInfoResponse = clientInfoClient.execClientInfo(accessToken); + + showClient(clientInfoClient); + assertEquals(clientInfoResponse.getStatus(), 200, "Unexpected response code: " + clientInfoResponse.getStatus()); + assertNotNull(clientInfoResponse.getClaim("displayName"), "Unexpected result: displayName not found"); + assertNotNull(clientInfoResponse.getClaim("inum"), "Unexpected result: inum not found"); + } + + @Parameters({"redirectUris", "clientJwksUri", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void privateKeyJwtAuthenticationMethodRS384Fail( + final String redirectUris, final String clientJwksUri, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("privateKeyJwtAuthenticationMethodRS384Fail"); + + List scopes = Arrays.asList("clientinfo"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Client Credentials Grant + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId("RS384SIG_INVALID_KEYID"); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType()); + assertEquals(tokenResponse.getErrorType(), TokenErrorResponseType.INVALID_CLIENT); + assertNotNull(tokenResponse.getErrorDescription()); + } + + @Parameters({"redirectUris", "clientJwksUri", "RS512_keyId", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void privateKeyJwtAuthenticationMethodRS512( + final String redirectUris, final String clientJwksUri, final String keyId, final String dnName, + final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("privateKeyJwtAuthenticationMethodRS512"); + + List scopes = Arrays.asList("clientinfo"); + List grantTypes = Arrays.asList( + GrantType.CLIENT_CREDENTIALS + ); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setGrantTypes(grantTypes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Client Credentials Grant + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity()); + assertNotNull(tokenResponse.getAccessToken()); + assertNotNull(tokenResponse.getTokenType()); + assertNotNull(tokenResponse.getScope()); + assertNull(tokenResponse.getRefreshToken()); + + String accessToken = tokenResponse.getAccessToken(); + + // 3. Request client info + ClientInfoClient clientInfoClient = new ClientInfoClient(clientInfoEndpoint); + ClientInfoResponse clientInfoResponse = clientInfoClient.execClientInfo(accessToken); + + showClient(clientInfoClient); + assertEquals(clientInfoResponse.getStatus(), 200, "Unexpected response code: " + clientInfoResponse.getStatus()); + assertNotNull(clientInfoResponse.getClaim("displayName"), "Unexpected result: displayName not found"); + assertNotNull(clientInfoResponse.getClaim("inum"), "Unexpected result: inum not found"); + } + + @Parameters({"redirectUris", "clientJwksUri", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void privateKeyJwtAuthenticationMethodRS512Fail( + final String redirectUris, final String clientJwksUri, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("privateKeyJwtAuthenticationMethodRS512Fail"); + + List scopes = Arrays.asList("clientinfo"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Client Credentials Grant + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId("RS512SIG_INVALID_KEYID"); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType()); + assertEquals(tokenResponse.getErrorType(), TokenErrorResponseType.INVALID_CLIENT); + assertNotNull(tokenResponse.getErrorDescription()); + } + + @Parameters({"redirectUris", "clientJwksUri", "ES256_keyId", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void privateKeyJwtAuthenticationMethodES256( + final String redirectUris, final String clientJwksUri, final String keyId, final String dnName, + final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("privateKeyJwtAuthenticationMethodES256"); + + List scopes = Arrays.asList("clientinfo"); + List grantTypes = Arrays.asList( + GrantType.CLIENT_CREDENTIALS + ); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setGrantTypes(grantTypes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Client Credentials Grant + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity()); + assertNotNull(tokenResponse.getAccessToken()); + assertNotNull(tokenResponse.getTokenType()); + assertNotNull(tokenResponse.getScope()); + assertNull(tokenResponse.getRefreshToken()); + + String accessToken = tokenResponse.getAccessToken(); + + // 3. Request client info + ClientInfoClient clientInfoClient = new ClientInfoClient(clientInfoEndpoint); + ClientInfoResponse clientInfoResponse = clientInfoClient.execClientInfo(accessToken); + + showClient(clientInfoClient); + assertEquals(clientInfoResponse.getStatus(), 200, "Unexpected response code: " + clientInfoResponse.getStatus()); + assertNotNull(clientInfoResponse.getClaim("displayName"), "Unexpected result: displayName not found"); + assertNotNull(clientInfoResponse.getClaim("inum"), "Unexpected result: inum not found"); + } + + @Parameters({"redirectUris", "clientJwksUri", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void privateKeyJwtAuthenticationMethodES256Fail( + final String redirectUris, final String clientJwksUri, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("privateKeyJwtAuthenticationMethodES256Fail"); + + List scopes = Arrays.asList("clientinfo"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Client Credentials Grant + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId("ES256SIG_INVALID_KEYID"); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType()); + assertEquals(tokenResponse.getErrorType(), TokenErrorResponseType.INVALID_CLIENT); + assertNotNull(tokenResponse.getErrorDescription()); + } + + @Parameters({"redirectUris", "clientJwksUri", "ES384_keyId", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void privateKeyJwtAuthenticationMethodES384( + final String redirectUris, final String clientJwksUri, final String keyId, final String dnName, + final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("privateKeyJwtAuthenticationMethodES384"); + + List scopes = Arrays.asList("clientinfo"); + List grantTypes = Arrays.asList( + GrantType.CLIENT_CREDENTIALS + ); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setGrantTypes(grantTypes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Client Credentials Grant + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity()); + assertNotNull(tokenResponse.getAccessToken()); + assertNotNull(tokenResponse.getTokenType()); + assertNotNull(tokenResponse.getScope()); + assertNull(tokenResponse.getRefreshToken()); + + String accessToken = tokenResponse.getAccessToken(); + + // 3. Request client info + ClientInfoClient clientInfoClient = new ClientInfoClient(clientInfoEndpoint); + ClientInfoResponse clientInfoResponse = clientInfoClient.execClientInfo(accessToken); + + showClient(clientInfoClient); + assertEquals(clientInfoResponse.getStatus(), 200, "Unexpected response code: " + clientInfoResponse.getStatus()); + assertNotNull(clientInfoResponse.getClaim("displayName"), "Unexpected result: displayName not found"); + assertNotNull(clientInfoResponse.getClaim("inum"), "Unexpected result: inum not found"); + } + + @Parameters({"redirectUris", "clientJwksUri", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void privateKeyJwtAuthenticationMethodES384Fail( + final String redirectUris, final String clientJwksUri, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("privateKeyJwtAuthenticationMethodES384Fail"); + + List scopes = Arrays.asList("clientinfo"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Client Credentials Grant + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId("ES384SIG_INVALID_KEYID"); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType()); + assertEquals(tokenResponse.getErrorType(), TokenErrorResponseType.INVALID_CLIENT); + assertNotNull(tokenResponse.getErrorDescription()); + } + + @Parameters({"redirectUris", "clientJwksUri", "ES512_keyId", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void privateKeyJwtAuthenticationMethodES512( + final String redirectUris, final String clientJwksUri, final String keyId, final String dnName, + final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("privateKeyJwtAuthenticationMethodES512"); + + List scopes = Arrays.asList("clientinfo"); + List grantTypes = Arrays.asList( + GrantType.CLIENT_CREDENTIALS + ); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setGrantTypes(grantTypes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Client Credentials Grant + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity()); + assertNotNull(tokenResponse.getAccessToken()); + assertNotNull(tokenResponse.getTokenType()); + assertNotNull(tokenResponse.getScope()); + assertNull(tokenResponse.getRefreshToken()); + + String accessToken = tokenResponse.getAccessToken(); + + // 3. Request client info + ClientInfoClient clientInfoClient = new ClientInfoClient(clientInfoEndpoint); + ClientInfoResponse clientInfoResponse = clientInfoClient.execClientInfo(accessToken); + + showClient(clientInfoClient); + assertEquals(clientInfoResponse.getStatus(), 200, "Unexpected response code: " + clientInfoResponse.getStatus()); + assertNotNull(clientInfoResponse.getClaim("displayName"), "Unexpected result: displayName not found"); + assertNotNull(clientInfoResponse.getClaim("inum"), "Unexpected result: inum not found"); + } + + @Parameters({"redirectUris", "clientJwksUri", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void privateKeyJwtAuthenticationMethodES512Fail( + final String redirectUris, final String clientJwksUri, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("privateKeyJwtAuthenticationMethodES512Fail"); + + List scopes = Arrays.asList("clientinfo"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Client Credentials Grant + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId("ES512SIG_INVALID_KEYID"); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType()); + assertEquals(tokenResponse.getErrorType(), TokenErrorResponseType.INVALID_CLIENT); + assertNotNull(tokenResponse.getErrorDescription()); + } + + @Parameters({"redirectUris", "clientJwksUri", "PS256_keyId", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void privateKeyJwtAuthenticationMethodPS256( + final String redirectUris, final String clientJwksUri, final String keyId, final String dnName, + final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("privateKeyJwtAuthenticationMethodPS256"); + + List scopes = Arrays.asList("clientinfo"); + List grantTypes = Arrays.asList( + GrantType.CLIENT_CREDENTIALS + ); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setGrantTypes(grantTypes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Client Credentials Grant + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.PS256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity()); + assertNotNull(tokenResponse.getAccessToken()); + assertNotNull(tokenResponse.getTokenType()); + assertNotNull(tokenResponse.getScope()); + assertNull(tokenResponse.getRefreshToken()); + + String accessToken = tokenResponse.getAccessToken(); + + // 3. Request client info + ClientInfoClient clientInfoClient = new ClientInfoClient(clientInfoEndpoint); + ClientInfoResponse clientInfoResponse = clientInfoClient.execClientInfo(accessToken); + + showClient(clientInfoClient); + assertEquals(clientInfoResponse.getStatus(), 200, "Unexpected response code: " + clientInfoResponse.getStatus()); + assertNotNull(clientInfoResponse.getClaim("displayName"), "Unexpected result: displayName not found"); + assertNotNull(clientInfoResponse.getClaim("inum"), "Unexpected result: inum not found"); + } + + @Parameters({"redirectUris", "clientJwksUri", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void privateKeyJwtAuthenticationMethodPS256Fail( + final String redirectUris, final String clientJwksUri, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("privateKeyJwtAuthenticationMethodPS256Fail"); + + List scopes = Arrays.asList("clientinfo"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Client Credentials Grant + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.PS256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId("PS256SIG_INVALID_KEYID"); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType()); + assertEquals(tokenResponse.getErrorType(), TokenErrorResponseType.INVALID_CLIENT); + assertNotNull(tokenResponse.getErrorDescription()); + } + + @Parameters({"redirectUris", "clientJwksUri", "PS384_keyId", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void privateKeyJwtAuthenticationMethodPS384( + final String redirectUris, final String clientJwksUri, final String keyId, final String dnName, + final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("privateKeyJwtAuthenticationMethodPS384"); + + List scopes = Arrays.asList("clientinfo"); + List grantTypes = Arrays.asList( + GrantType.CLIENT_CREDENTIALS + ); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setGrantTypes(grantTypes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Client Credentials Grant + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.PS384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity()); + assertNotNull(tokenResponse.getAccessToken()); + assertNotNull(tokenResponse.getTokenType()); + assertNotNull(tokenResponse.getScope()); + assertNull(tokenResponse.getRefreshToken()); + + String accessToken = tokenResponse.getAccessToken(); + + // 3. Request client info + ClientInfoClient clientInfoClient = new ClientInfoClient(clientInfoEndpoint); + ClientInfoResponse clientInfoResponse = clientInfoClient.execClientInfo(accessToken); + + showClient(clientInfoClient); + assertEquals(clientInfoResponse.getStatus(), 200, "Unexpected response code: " + clientInfoResponse.getStatus()); + assertNotNull(clientInfoResponse.getClaim("displayName"), "Unexpected result: displayName not found"); + assertNotNull(clientInfoResponse.getClaim("inum"), "Unexpected result: inum not found"); + } + + @Parameters({"redirectUris", "clientJwksUri", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void privateKeyJwtAuthenticationMethodPS384Fail( + final String redirectUris, final String clientJwksUri, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("privateKeyJwtAuthenticationMethodPS384Fail"); + + List scopes = Arrays.asList("clientinfo"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Client Credentials Grant + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.PS384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId("PS384SIG_INVALID_KEYID"); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType()); + assertEquals(tokenResponse.getErrorType(), TokenErrorResponseType.INVALID_CLIENT); + assertNotNull(tokenResponse.getErrorDescription()); + } + + @Parameters({"redirectUris", "clientJwksUri", "PS512_keyId", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void privateKeyJwtAuthenticationMethodPS512( + final String redirectUris, final String clientJwksUri, final String keyId, final String dnName, + final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("privateKeyJwtAuthenticationMethodPS512"); + + List scopes = Arrays.asList("clientinfo"); + List grantTypes = Arrays.asList( + GrantType.CLIENT_CREDENTIALS + ); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setGrantTypes(grantTypes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Client Credentials Grant + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.PS512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity()); + assertNotNull(tokenResponse.getAccessToken()); + assertNotNull(tokenResponse.getTokenType()); + assertNotNull(tokenResponse.getScope()); + assertNull(tokenResponse.getRefreshToken()); + + String accessToken = tokenResponse.getAccessToken(); + + // 3. Request client info + ClientInfoClient clientInfoClient = new ClientInfoClient(clientInfoEndpoint); + ClientInfoResponse clientInfoResponse = clientInfoClient.execClientInfo(accessToken); + + showClient(clientInfoClient); + assertEquals(clientInfoResponse.getStatus(), 200, "Unexpected response code: " + clientInfoResponse.getStatus()); + assertNotNull(clientInfoResponse.getClaim("displayName"), "Unexpected result: displayName not found"); + assertNotNull(clientInfoResponse.getClaim("inum"), "Unexpected result: inum not found"); + } + + @Parameters({"redirectUris", "clientJwksUri", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void privateKeyJwtAuthenticationMethodPS512Fail( + final String redirectUris, final String clientJwksUri, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("privateKeyJwtAuthenticationMethodPS512Fail"); + + List scopes = Arrays.asList("clientinfo"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setScope(scopes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Client Credentials Grant + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.CLIENT_CREDENTIALS); + tokenRequest.setScope("clientinfo"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.PS512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId("PS512SIG_INVALID_KEYID"); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType()); + assertEquals(tokenResponse.getErrorType(), TokenErrorResponseType.INVALID_CLIENT); + assertNotNull(tokenResponse.getErrorDescription()); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ClientInfoRestWebServiceHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ClientInfoRestWebServiceHttpTest.java new file mode 100644 index 00000000..be69324d --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ClientInfoRestWebServiceHttpTest.java @@ -0,0 +1,197 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.ClientInfoClient; +import org.gluu.oxauth.client.ClientInfoResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * Functional tests for Client Info Web Services (HTTP) + * + * @author Javier Rojas Blum + * @version March 9, 2019 + */ +public class ClientInfoRestWebServiceHttpTest extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestClientInfoImplicitFlow( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestClientInfoImplicitFlow"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = new ArrayList(); + scopes.add("clientinfo"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The access token is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getTokenType(), "The token type is null"); + assertNotNull(authorizationResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(authorizationResponse.getScope(), "The scope must be null"); + assertNotNull(authorizationResponse.getIdToken(), "The id token must be null"); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request client info + ClientInfoClient clientInfoClient = new ClientInfoClient(clientInfoEndpoint); + ClientInfoResponse clientInfoResponse = clientInfoClient.execClientInfo(accessToken); + + showClient(clientInfoClient); + assertEquals(clientInfoResponse.getStatus(), 200, "Unexpected response code: " + clientInfoResponse.getStatus()); + assertNotNull(clientInfoResponse.getClaim("displayName"), "Unexpected result: displayName not found"); + assertNotNull(clientInfoResponse.getClaim("inum"), "Unexpected result: inum not found"); + assertNotNull(clientInfoResponse.getClaim("oxAuthAppType"), "Unexpected result: oxAuthAppType not found"); + assertNotNull(clientInfoResponse.getClaim("oxAuthIdTokenSignedResponseAlg"), "Unexpected result: oxAuthIdTokenSignedResponseAlg not found"); + assertNotNull(clientInfoResponse.getClaim("oxAuthRedirectURI"), "Unexpected result: oxAuthRedirectURI not found"); + assertNotNull(clientInfoResponse.getClaim("oxAuthScope"), "Unexpected result: oxAuthScope not found"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "sectorIdentifierUri"}) + @Test + public void requestClientInfoPasswordFlow( + final String userId, final String userSecret, final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("requestClientInfoPasswordFlow"); + + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(grantTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + String username = userId; + String password = userSecret; + String scope = "clientinfo"; + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + TokenResponse response1 = tokenClient.execResourceOwnerPasswordCredentialsGrant(username, password, scope, + clientId, clientSecret); + + showClient(tokenClient); + assertEquals(response1.getStatus(), 200, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getEntity(), "The entity is null"); + assertNotNull(response1.getAccessToken(), "The access token is null"); + assertNotNull(response1.getTokenType(), "The token type is null"); + assertNotNull(response1.getScope(), "The scope is null"); + + String accessToken = response1.getAccessToken(); + + // 3. Request client info + ClientInfoClient clientInfoClient = new ClientInfoClient(clientInfoEndpoint); + ClientInfoResponse response2 = clientInfoClient.execClientInfo(accessToken); + + showClient(clientInfoClient); + assertEquals(response2.getStatus(), 200, "Unexpected response code: " + response2.getStatus()); + assertNotNull(response2.getClaim("displayName"), "Unexpected result: displayName not found"); + assertNotNull(response2.getClaim("inum"), "Unexpected result: inum not found"); + assertNotNull(response2.getClaim("oxAuthAppType"), "Unexpected result: oxAuthAppType not found"); + assertNotNull(response2.getClaim("oxAuthIdTokenSignedResponseAlg"), "Unexpected result: oxAuthIdTokenSignedResponseAlg not found"); + assertNotNull(response2.getClaim("oxAuthRedirectURI"), "Unexpected result: oxAuthRedirectURI not found"); + assertNotNull(response2.getClaim("oxAuthScope"), "Unexpected result: oxAuthScope not found"); + } + + @Test + public void requestClientInfoInvalidRequest() throws Exception { + showTitle("requestClientInfoInvalidRequest"); + + ClientInfoClient clientInfoClient = new ClientInfoClient(clientInfoEndpoint); + ClientInfoResponse response = clientInfoClient.execClientInfo(null); + + showClient(clientInfoClient); + assertEquals(response.getStatus(), 400, "Unexpected response code: " + response.getStatus()); + assertNotNull(response.getErrorType(), "Unexpected result: errorType not found"); + assertNotNull(response.getErrorDescription(), "Unexpected result: errorDescription not found"); + } + + @Test + public void requestClientInfoInvalidToken() throws Exception { + showTitle("requestClientInfoInvalidToken"); + + ClientInfoClient clientInfoClient = new ClientInfoClient(clientInfoEndpoint); + ClientInfoResponse response = clientInfoClient.execClientInfo("INVALID-TOKEN"); + + showClient(clientInfoClient); + assertEquals(response.getStatus(), 400, "Unexpected response code: " + response.getStatus()); + assertNotNull(response.getErrorType(), "Unexpected result: errorType not found"); + assertNotNull(response.getErrorDescription(), "Unexpected result: errorDescription not found"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ClientSecretBasicTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ClientSecretBasicTest.java new file mode 100644 index 00000000..44640400 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ClientSecretBasicTest.java @@ -0,0 +1,234 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +import java.net.URLEncoder; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.apache.commons.codec.binary.Base64; +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.common.SubjectType; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.gluu.oxauth.model.util.Util; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * http://tools.ietf.org/html/rfc2617#section-2 + * + * @author Javier Rojas Blum + * @version January 26. 2018 + */ +public class ClientSecretBasicTest extends BaseTest { + + @Test + public void testEncode1() { + showTitle("testEncode1"); + + String clientId = "Aladdin"; + String clientSecret = "open sesame"; + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + assertEquals(tokenRequest.getEncodedCredentials(), "QWxhZGRpbjpvcGVuK3Nlc2FtZQ=="); + } + + @Test + public void testEncode2() { + showTitle("testEncode2"); + + String clientId = "a+b"; + String clientSecret = "c+d"; + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + assertEquals(tokenRequest.getEncodedCredentials(), "YSUyQmI6YyUyQmQ="); + } + + @Test + public void testEncode3() { + showTitle("testEncode3"); + + String clientId = "@!12AD!0008!6D30.23D7"; + String clientSecret = "P@55W0rd!"; + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + assertEquals(tokenRequest.getEncodedCredentials(), "JTQwJTIxMTJBRCUyMTAwMDglMjE2RDMwLjIzRDc6UCU0MDU1VzByZCUyMQ=="); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void authorizationCodeFlow( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationCodeFlow"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + List scopes = Arrays.asList("openid", "profile", "address", "email", "phone", "user_name"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setScope(scopes); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization and receive the authorization code. + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + + String scope = authorizationResponse.getScope(); + String authorizationCode = authorizationResponse.getCode(); + String idToken = authorizationResponse.getIdToken(); + + // 3. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + String expectedEncodedCredentials = Base64.encodeBase64String(Util.getBytes( + URLEncoder.encode(clientId, Util.UTF8_STRING_ENCODING) + + ":" + + URLEncoder.encode(clientSecret, Util.UTF8_STRING_ENCODING))); + assertEquals(tokenRequest.getEncodedCredentials(), expectedEncodedCredentials); + + TokenClient tokenClient1 = new TokenClient(tokenEndpoint); + tokenClient1.setRequest(tokenRequest); + TokenResponse tokenResponse1 = tokenClient1.exec(); + + showClient(tokenClient1); + assertEquals(tokenResponse1.getStatus(), 200, "Unexpected response code: " + tokenResponse1.getStatus()); + assertNotNull(tokenResponse1.getEntity(), "The entity is null"); + assertNotNull(tokenResponse1.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse1.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse1.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse1.getRefreshToken(), "The refresh token is null"); + + String refreshToken = tokenResponse1.getRefreshToken(); + + // 4. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.CODE_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.OX_OPENID_CONNECT_VERSION)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 5. Request new access token using the refresh token. + TokenClient tokenClient2 = new TokenClient(tokenEndpoint); + TokenResponse tokenResponse2 = tokenClient2.execRefreshToken(scope, refreshToken, clientId, clientSecret); + + showClient(tokenClient2); + assertEquals(tokenResponse2.getStatus(), 200, "Unexpected response code: " + tokenResponse2.getStatus()); + assertNotNull(tokenResponse2.getEntity(), "The entity is null"); + assertNotNull(tokenResponse2.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse2.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse2.getRefreshToken(), "The refresh token is null"); + assertNotNull(tokenResponse2.getScope(), "The scope is null"); + + String accessToken = tokenResponse2.getAccessToken(); + + // 6. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PHONE_NUMBER_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.USER_NAME)); + assertNull(userInfoResponse.getClaim("org_name")); + assertNull(userInfoResponse.getClaim("work_phone")); + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ClientSpecificAccessTokenExpiration.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ClientSpecificAccessTokenExpiration.java new file mode 100644 index 00000000..1113d82d --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ClientSpecificAccessTokenExpiration.java @@ -0,0 +1,163 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.common.SubjectType; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Javier Rojas Blum + * @version May 14, 2019 + */ +public class ClientSpecificAccessTokenExpiration extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void authorizationCodeFlow( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationCodeFlow"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + List scopes = Arrays.asList("openid", "profile", "address", "email", "phone", "user_name"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setScope(scopes); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setAccessTokenLifetime(3); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization and receive the authorization code. + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + + String accessToken = tokenResponse.getAccessToken(); + + // 4. Request user info + { + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.BIRTHDATE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GENDER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.MIDDLE_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NICKNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PREFERRED_USERNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PROFILE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.WEBSITE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PHONE_NUMBER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PHONE_NUMBER_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.USER_NAME)); + assertNull(userInfoResponse.getClaim("org_name")); + assertNull(userInfoResponse.getClaim("work_phone")); + } + + Thread.sleep(5000); + + // 5. Request user info + { + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 401, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getErrorType(), "Unexpected result: errorType not found"); + assertNotNull(userInfoResponse.getErrorDescription(), "Unexpected result: errorDescription not found"); + } + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ClientTestUtil.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ClientTestUtil.java new file mode 100644 index 00000000..f583ec1a --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ClientTestUtil.java @@ -0,0 +1,33 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.gluu.oxauth.model.uma.TestUtil.assertNotBlank; +import static org.testng.Assert.assertNotNull; + +import org.gluu.oxauth.client.RegisterResponse; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 16/10/2013 + */ + +public class ClientTestUtil { + + private ClientTestUtil() { + } + + public static void assert_(RegisterResponse p_response) { + assertNotNull(p_response); + assertNotBlank(p_response.getClientId()); + assertNotBlank(p_response.getClientSecret()); + assertNotBlank(p_response.getRegistrationAccessToken()); + assertNotBlank(p_response.getRegistrationClientUri()); + assertNotNull(p_response.getClientIdIssuedAt()); + assertNotNull(p_response.getClientSecretExpiresAt()); + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ClientWhiteListBlackListRedirectUris.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ClientWhiteListBlackListRedirectUris.java new file mode 100644 index 00000000..1ae72d18 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ClientWhiteListBlackListRedirectUris.java @@ -0,0 +1,127 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.gluu.oxauth.model.register.RegisterRequestParam.SCOPE; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; + +import javax.ws.rs.HttpMethod; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.gluu.oxauth.model.util.URLPatternList; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Javier Rojas Blum + * @version November 29, 2017 + */ +public class ClientWhiteListBlackListRedirectUris extends BaseTest { + + private String registrationAccessToken1; + private String registrationClientUri1; + + @Test + public void testUrlPatterList() { + showTitle("testUrlPatterList"); + + List urlPatterns = Arrays.asList( + "*.gluu.org/foo*bar", + "https://example.org/foo/bar.html", + "*.attacker.com/*"); + + URLPatternList urlPatternList = new URLPatternList(urlPatterns); + assertFalse(urlPatternList.isUrlListed("gluu.org")); + assertFalse(urlPatternList.isUrlListed("www.gluu.org")); + assertTrue(urlPatternList.isUrlListed("http://gluu.org/foo/bar")); + assertTrue(urlPatternList.isUrlListed("https://mail.gluu.org/foo/bar")); + assertTrue(urlPatternList.isUrlListed("http://www.gluu.org/foobar")); + assertTrue(urlPatternList.isUrlListed("https://www.gluu.org/foo/baz/bar")); + assertFalse(urlPatternList.isUrlListed("http://example.org")); + assertFalse(urlPatternList.isUrlListed("http://example.org/foo/bar.html")); + assertTrue(urlPatternList.isUrlListed("https://example.org/foo/bar.html")); + assertTrue(urlPatternList.isUrlListed("http://attacker.com")); + assertTrue(urlPatternList.isUrlListed("https://www.attacker.com")); + assertTrue(urlPatternList.isUrlListed("https://www.attacker.com/foo/bar")); + } + + @Test + public void requestClientAssociateInBlackList() throws Exception { + showTitle("requestClientAssociateInBlackList"); + + final String redirectUris = "https://www.attacker.com"; + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + RegisterResponse response = registerClient.execRegister(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + + showClient(registerClient); + assertEquals(response.getStatus(), 400, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getEntity(), "The entity is null"); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "sectorIdentifierUri"}) + @Test + public void requestClientAssociate(final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("requestClientAssociate"); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + registerClient.setExecutor(clientEngine(true)); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + assertNotNull(response.getClaims().get(SCOPE.toString())); + + registrationAccessToken1 = response.getRegistrationAccessToken(); + registrationClientUri1 = response.getRegistrationClientUri(); + } + + @Test(dependsOnMethods = "requestClientAssociate") + public void requestClientUpdate() throws Exception { + showTitle("requestClientUpdate"); + + final String redirectUris = "https://www.attacker.com"; + + final RegisterRequest registerRequest = new RegisterRequest(registrationAccessToken1); + registerRequest.setHttpMethod(HttpMethod.PUT); + registerRequest.setRedirectUris(StringUtils.spaceSeparatedToList(redirectUris)); + + final RegisterClient registerClient = new RegisterClient(registrationClientUri1); + registerClient.setRequest(registerRequest); + registerClient.setExecutor(clientEngine(true)); + final RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 400, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getEntity(), "The entity is null"); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ConfigurationRestWebServiceHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ConfigurationRestWebServiceHttpTest.java new file mode 100644 index 00000000..09b757dc --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ConfigurationRestWebServiceHttpTest.java @@ -0,0 +1,124 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.List; +import java.util.Map; + +import org.apache.http.impl.client.CloseableHttpClient; +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.OpenIdConfigurationClient; +import org.gluu.oxauth.client.OpenIdConfigurationResponse; +import org.gluu.oxauth.client.OpenIdConnectDiscoveryClient; +import org.gluu.oxauth.client.OpenIdConnectDiscoveryResponse; +import org.gluu.oxauth.dev.HostnameVerifierType; +import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient43Engine; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * Functional tests for OpenId Configuration Web Services (HTTP) + * + * @author Javier Rojas Blum + * @version July 10, 2019 + */ +public class ConfigurationRestWebServiceHttpTest extends BaseTest { + + @Test + @Parameters({"swdResource"}) + public void requestOpenIdConfiguration(final String resource) throws Exception { + showTitle("OpenID Connect Discovery"); + + OpenIdConnectDiscoveryClient openIdConnectDiscoveryClient = new OpenIdConnectDiscoveryClient(resource); + + CloseableHttpClient httpClient = createHttpClient(HostnameVerifierType.ALLOW_ALL); + OpenIdConnectDiscoveryResponse openIdConnectDiscoveryResponse; + try { + openIdConnectDiscoveryResponse = openIdConnectDiscoveryClient.exec(new ApacheHttpClient43Engine(httpClient)); + } finally { + httpClient.close(); + } + + showClient(openIdConnectDiscoveryClient); + assertEquals(openIdConnectDiscoveryResponse.getStatus(), 200, "Unexpected response code"); + assertNotNull(openIdConnectDiscoveryResponse.getSubject()); + assertTrue(openIdConnectDiscoveryResponse.getLinks().size() > 0); + + String configurationEndpoint = openIdConnectDiscoveryResponse.getLinks().get(0).getHref() + + "/.well-known/openid-configuration"; + + showTitle("OpenID Connect Configuration"); + + OpenIdConfigurationClient client = new OpenIdConfigurationClient(configurationEndpoint); + OpenIdConfigurationResponse response = client.execOpenIdConfiguration(); + + showClient(client); + assertEquals(response.getStatus(), 200, "Unexpected response code"); + assertNotNull(response.getIssuer(), "The issuer is null"); + assertNotNull(response.getAuthorizationEndpoint(), "The authorizationEndpoint is null"); + assertNotNull(response.getTokenEndpoint(), "The tokenEndpoint is null"); + assertNotNull(response.getRevocationEndpoint(), "The tokenRevocationEndpoint is null"); + assertNotNull(response.getUserInfoEndpoint(), "The userInfoEndPoint is null"); + assertNotNull(response.getClientInfoEndpoint(), "The clientInfoEndPoint is null"); + assertNotNull(response.getCheckSessionIFrame(), "The checkSessionIFrame is null"); + assertNotNull(response.getEndSessionEndpoint(), "The endSessionEndpoint is null"); + assertNotNull(response.getJwksUri(), "The jwksUri is null"); + assertNotNull(response.getRegistrationEndpoint(), "The registrationEndpoint is null"); + assertNotNull(response.getIntrospectionEndpoint(), "The introspectionEndpoint is null"); + assertNotNull(response.getIdGenerationEndpoint(), "The idGenerationEndpoint is null"); + + assertTrue(response.getScopesSupported().size() > 0, "The scopesSupported is empty"); + assertTrue(response.getScopeToClaimsMapping().size() > 0, "The scope to claims mapping is empty"); + assertTrue(response.getResponseTypesSupported().size() > 0, "The responseTypesSupported is empty"); + assertTrue(response.getResponseModesSupported().size() > 0, "The responseModesSupported is empty"); + assertTrue(response.getGrantTypesSupported().size() > 0, "The grantTypesSupported is empty"); + assertTrue(response.getAcrValuesSupported().size() >= 0, "The acrValuesSupported is empty"); + assertTrue(response.getSubjectTypesSupported().size() > 0, "The subjectTypesSupported is empty"); + assertTrue(response.getUserInfoSigningAlgValuesSupported().size() > 0, "The userInfoSigningAlgValuesSupported is empty"); + assertTrue(response.getUserInfoEncryptionAlgValuesSupported().size() > 0, "The userInfoEncryptionAlgValuesSupported is empty"); + assertTrue(response.getUserInfoEncryptionEncValuesSupported().size() > 0, "The userInfoEncryptionEncValuesSupported is empty"); + assertTrue(response.getIdTokenSigningAlgValuesSupported().size() > 0, "The idTokenSigningAlgValuesSupported is empty"); + assertTrue(response.getIdTokenEncryptionAlgValuesSupported().size() > 0, "The idTokenEncryptionAlgValuesSupported is empty"); + assertTrue(response.getIdTokenEncryptionEncValuesSupported().size() > 0, "The idTokenEncryptionEncValuesSupported is empty"); + assertTrue(response.getRequestObjectSigningAlgValuesSupported().size() > 0, "The requestObjectSigningAlgValuesSupported is empty"); + assertTrue(response.getRequestObjectEncryptionAlgValuesSupported().size() > 0, "The requestObjectEncryptionAlgValuesSupported is empty"); + assertTrue(response.getRequestObjectEncryptionEncValuesSupported().size() > 0, "The requestObjectEncryptionEncValuesSupported is empty"); + assertTrue(response.getTokenEndpointAuthMethodsSupported().size() > 0, "The tokenEndpointAuthMethodsSupported is empty"); + assertTrue(response.getTokenEndpointAuthSigningAlgValuesSupported().size() > 0, "The tokenEndpointAuthSigningAlgValuesSupported is empty"); + + assertTrue(response.getDisplayValuesSupported().size() > 0, "The displayValuesSupported is empty"); + assertTrue(response.getClaimTypesSupported().size() > 0, "The claimTypesSupported is empty"); + assertTrue(response.getClaimsSupported().size() > 0, "The claimsSupported is empty"); + assertNotNull(response.getServiceDocumentation(), "The serviceDocumentation is null"); + assertTrue(response.getClaimsLocalesSupported().size() > 0, "The claimsLocalesSupported is empty"); + assertTrue(response.getUiLocalesSupported().size() > 0, "The uiLocalesSupported is empty"); + assertTrue(response.getClaimsParameterSupported(), "The claimsParameterSupported is false"); + assertTrue(response.getRequestParameterSupported(), "The requestParameterSupported is false"); + assertTrue(response.getRequestUriParameterSupported(), "The requestUriParameterSupported is false"); + assertFalse(response.getRequireRequestUriRegistration(), "The requireRequestUriRegistration is true"); + assertNotNull(response.getOpPolicyUri(), "The opPolicyUri is null"); + assertNotNull(response.getOpTosUri(), "The opTosUri is null"); + + // oxAuth #917: Add dynamic scopes and claims to discovery + Map> scopeToClaims = response.getScopeToClaimsMapping(); + List scopesSupported = response.getScopesSupported(); + List claimsSupported = response.getClaimsSupported(); + for (Map.Entry> scopeEntry : scopeToClaims.entrySet()) { + assertTrue(scopesSupported.contains(scopeEntry.getKey()), + "The scopes supported list does not contain the scope: " + scopeEntry.getKey()); + for (String claimEntry : scopeEntry.getValue()) { + assertTrue(claimsSupported.contains(claimEntry), + "The claims supported list does not contain the claim: " + claimEntry); + } + } + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/EnableClientToRestrictJavascriptOrigin.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/EnableClientToRestrictJavascriptOrigin.java new file mode 100644 index 00000000..1b91f5de --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/EnableClientToRestrictJavascriptOrigin.java @@ -0,0 +1,198 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.common.SubjectType; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.register.RegisterRequestParam; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Javier Rojas Blum + * @version March 20, 2018 + */ +public class EnableClientToRestrictJavascriptOrigin extends BaseTest { + + /** + * Test for the complete Authorization Code Flow. + */ + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void enableClientToRestrictJavascriptOrigin( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("enableClientToRestrictJavascriptOrigin"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + List scopes = Arrays.asList("openid", "profile", "address", "email", "phone", "user_name"); + + // 1. Register client + List authorizedOrigins = Arrays.asList("https://ce.gluu.info:8443"); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setScope(scopes); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setAuthorizedOrigins(authorizedOrigins); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + assertTrue(registerResponse.getClaims().containsKey(RegisterRequestParam.AUTHORIZED_ORIGINS.toString())); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization and receive the authorization code. + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + + String scope = authorizationResponse.getScope(); + String authorizationCode = authorizationResponse.getCode(); + String idToken = authorizationResponse.getIdToken(); + + // 3. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient1 = new TokenClient(tokenEndpoint); + tokenClient1.setRequest(tokenRequest); + TokenResponse tokenResponse1 = tokenClient1.exec(); + + showClient(tokenClient1); + assertEquals(tokenResponse1.getStatus(), 200, "Unexpected response code: " + tokenResponse1.getStatus()); + assertNotNull(tokenResponse1.getEntity(), "The entity is null"); + assertNotNull(tokenResponse1.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse1.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse1.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse1.getRefreshToken(), "The refresh token is null"); + + String refreshToken = tokenResponse1.getRefreshToken(); + + // 4. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.CODE_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.OX_OPENID_CONNECT_VERSION)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 5. Request new access token using the refresh token. + TokenClient tokenClient2 = new TokenClient(tokenEndpoint); + TokenResponse tokenResponse2 = tokenClient2.execRefreshToken(scope, refreshToken, clientId, clientSecret); + + showClient(tokenClient2); + assertEquals(tokenResponse2.getStatus(), 200, "Unexpected response code: " + tokenResponse2.getStatus()); + assertNotNull(tokenResponse2.getEntity(), "The entity is null"); + assertNotNull(tokenResponse2.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse2.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse2.getRefreshToken(), "The refresh token is null"); + assertNotNull(tokenResponse2.getScope(), "The scope is null"); + + String accessToken = tokenResponse2.getAccessToken(); + + // 6. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.BIRTHDATE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GENDER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.MIDDLE_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NICKNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PREFERRED_USERNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PROFILE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.WEBSITE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PHONE_NUMBER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PHONE_NUMBER_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.USER_NAME)); + assertNull(userInfoResponse.getClaim("org_name")); + assertNull(userInfoResponse.getClaim("work_phone")); + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/EncodeClaimsInStateParameter.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/EncodeClaimsInStateParameter.java new file mode 100644 index 00000000..41fc6598 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/EncodeClaimsInStateParameter.java @@ -0,0 +1,1772 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.gluu.oxauth.model.jwt.JwtStateClaimName.ADDITIONAL_CLAIMS; +import static org.gluu.oxauth.model.jwt.JwtStateClaimName.JTI; +import static org.gluu.oxauth.model.jwt.JwtStateClaimName.KID; +import static org.gluu.oxauth.model.jwt.JwtStateClaimName.RFP; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.security.PrivateKey; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.model.JwtState; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.AbstractCryptoProvider; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.encryption.BlockEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.encryption.KeyEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jwe.Jwe; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.JwtUtil; +import org.gluu.oxauth.model.util.StringUtils; +import org.json.JSONObject; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Javier Rojas Blum + * @version February 8, 2019 + */ +public class EncodeClaimsInStateParameter extends BaseTest { + + private final String additionalClaims = "{first_name: 'Javier', last_name: 'Rojas', age: 34, more: ['foo', 'bar']}"; + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void encodeClaimsInStateParameterHS256( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("encodeClaimsInStateParameterHS256"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(SignatureAlgorithm.HS256, clientSecret, cryptoProvider); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + String encodedState = jwtState.getEncodedJwt(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(encodedState); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String state = authorizationResponse.getState(); + + // 3. Validate state + Jwt jwt = Jwt.parse(state); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), null, + null, clientSecret, SignatureAlgorithm.HS256); + assertTrue(validJwt); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void encodeClaimsInStateParameterHS384( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("encodeClaimsInStateParameterHS384"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(SignatureAlgorithm.HS384, clientSecret, cryptoProvider); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + String encodedState = jwtState.getEncodedJwt(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(encodedState); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String state = authorizationResponse.getState(); + + // 3. Validate state + Jwt jwt = Jwt.parse(state); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), null, + null, clientSecret, SignatureAlgorithm.HS384); + assertTrue(validJwt); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void encodeClaimsInStateParameterHS512( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("encodeClaimsInStateParameterHS512"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(SignatureAlgorithm.HS512, clientSecret, cryptoProvider); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + String encodedState = jwtState.getEncodedJwt(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(encodedState); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String state = authorizationResponse.getState(); + + // 3. Validate state + Jwt jwt = Jwt.parse(state); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), null, + null, clientSecret, SignatureAlgorithm.HS512); + assertTrue(validJwt); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri", + "keyStoreFile", "keyStoreSecret", "dnName", "RS256_keyId"}) + @Test + public void encodeClaimsInStateParameterRS256( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri, final String keyStoreFile, final String keyStoreSecret, + final String dnName, final String keyId) throws Exception { + showTitle("encodeClaimsInStateParameterRS256"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(SignatureAlgorithm.RS256, cryptoProvider); + jwtState.setKeyId(keyId); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + String encodedState = jwtState.getEncodedJwt(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(encodedState); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String state = authorizationResponse.getState(); + + // 3. Validate state + Jwt jwt = Jwt.parse(state); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), keyId, + null, null, SignatureAlgorithm.RS256); + assertTrue(validJwt); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri", + "keyStoreFile", "keyStoreSecret", "dnName", "RS384_keyId"}) + @Test + public void encodeClaimsInStateParameterRS384( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri, final String keyStoreFile, final String keyStoreSecret, + final String dnName, final String keyId) throws Exception { + showTitle("encodeClaimsInStateParameterRS384"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(SignatureAlgorithm.RS384, cryptoProvider); + jwtState.setKeyId(keyId); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + String encodedState = jwtState.getEncodedJwt(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(encodedState); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String state = authorizationResponse.getState(); + + // 3. Validate state + Jwt jwt = Jwt.parse(state); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), keyId, + null, null, SignatureAlgorithm.RS384); + assertTrue(validJwt); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri", + "keyStoreFile", "keyStoreSecret", "dnName", "RS512_keyId"}) + @Test + public void encodeClaimsInStateParameterRS512( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri, final String keyStoreFile, final String keyStoreSecret, + final String dnName, final String keyId) throws Exception { + showTitle("encodeClaimsInStateParameterRS512"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(SignatureAlgorithm.RS512, cryptoProvider); + jwtState.setKeyId(keyId); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + String encodedState = jwtState.getEncodedJwt(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(encodedState); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String state = authorizationResponse.getState(); + + // 3. Validate state + Jwt jwt = Jwt.parse(state); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), keyId, + null, null, SignatureAlgorithm.RS512); + assertTrue(validJwt); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri", + "keyStoreFile", "keyStoreSecret", "dnName", "ES256_keyId"}) + @Test + public void encodeClaimsInStateParameterES256( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri, final String keyStoreFile, final String keyStoreSecret, + final String dnName, final String keyId) throws Exception { + showTitle("encodeClaimsInStateParameterES256"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(SignatureAlgorithm.ES256, cryptoProvider); + jwtState.setKeyId(keyId); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + String encodedState = jwtState.getEncodedJwt(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(encodedState); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String state = authorizationResponse.getState(); + + // 3. Validate state + Jwt jwt = Jwt.parse(state); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), keyId, + null, null, SignatureAlgorithm.ES256); + assertTrue(validJwt); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri", + "keyStoreFile", "keyStoreSecret", "dnName", "ES384_keyId"}) + @Test + public void encodeClaimsInStateParameterES384( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri, final String keyStoreFile, final String keyStoreSecret, + final String dnName, final String keyId) throws Exception { + showTitle("encodeClaimsInStateParameterES384"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(SignatureAlgorithm.ES384, cryptoProvider); + jwtState.setKeyId(keyId); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + String encodedState = jwtState.getEncodedJwt(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(encodedState); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String state = authorizationResponse.getState(); + + // 3. Validate state + Jwt jwt = Jwt.parse(state); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), keyId, + null, null, SignatureAlgorithm.ES384); + assertTrue(validJwt); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri", + "keyStoreFile", "keyStoreSecret", "dnName", "ES512_keyId"}) + @Test + public void encodeClaimsInStateParameterES512( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri, final String keyStoreFile, final String keyStoreSecret, + final String dnName, final String keyId) throws Exception { + showTitle("encodeClaimsInStateParameterES512"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(SignatureAlgorithm.ES512, cryptoProvider); + jwtState.setKeyId(keyId); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + String encodedState = jwtState.getEncodedJwt(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(encodedState); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String state = authorizationResponse.getState(); + + // 3. Validate state + Jwt jwt = Jwt.parse(state); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), keyId, + null, null, SignatureAlgorithm.ES512); + assertTrue(validJwt); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri", + "keyStoreFile", "keyStoreSecret", "dnName", "PS256_keyId"}) + @Test + public void encodeClaimsInStateParameterPS256( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri, final String keyStoreFile, final String keyStoreSecret, + final String dnName, final String keyId) throws Exception { + showTitle("encodeClaimsInStateParameterPS256"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(SignatureAlgorithm.PS256, cryptoProvider); + jwtState.setKeyId(keyId); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + String encodedState = jwtState.getEncodedJwt(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(encodedState); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String state = authorizationResponse.getState(); + + // 3. Validate state + Jwt jwt = Jwt.parse(state); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), keyId, + null, null, SignatureAlgorithm.PS256); + assertTrue(validJwt); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri", + "keyStoreFile", "keyStoreSecret", "dnName", "PS384_keyId"}) + @Test + public void encodeClaimsInStateParameterPS384( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri, final String keyStoreFile, final String keyStoreSecret, + final String dnName, final String keyId) throws Exception { + showTitle("encodeClaimsInStateParameterPS384"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(SignatureAlgorithm.PS384, cryptoProvider); + jwtState.setKeyId(keyId); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + String encodedState = jwtState.getEncodedJwt(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(encodedState); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String state = authorizationResponse.getState(); + + // 3. Validate state + Jwt jwt = Jwt.parse(state); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), keyId, + null, null, SignatureAlgorithm.PS384); + assertTrue(validJwt); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri", + "keyStoreFile", "keyStoreSecret", "dnName", "PS512_keyId"}) + @Test + public void encodeClaimsInStateParameterPS512( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri, final String keyStoreFile, final String keyStoreSecret, + final String dnName, final String keyId) throws Exception { + showTitle("encodeClaimsInStateParameterPS512"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(SignatureAlgorithm.PS512, cryptoProvider); + jwtState.setKeyId(keyId); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + String encodedState = jwtState.getEncodedJwt(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(encodedState); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String state = authorizationResponse.getState(); + + // 3. Validate state + Jwt jwt = Jwt.parse(state); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), keyId, + null, null, SignatureAlgorithm.PS512); + assertTrue(validJwt); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri", + "keyStoreFile", "keyStoreSecret", "dnName", "RS256_keyId", "clientJwksUri"}) + @Test + public void encodeClaimsInStateParameterAlgRSAOAEPEncA256GCM( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri, final String keyStoreFile, final String keyStoreSecret, + final String dnName, final String keyId, final String clientJwksUri) throws Exception { + showTitle("encodeClaimsInStateParameterAlgRSAOAEPEncA256GCM"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + JSONObject jwks = JwtUtil.getJSONWebKeys(clientJwksUri); + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(KeyEncryptionAlgorithm.RSA_OAEP, BlockEncryptionAlgorithm.A256GCM, cryptoProvider); + jwtState.setKeyId(keyId); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + String encodedState = jwtState.getEncodedJwt(jwks); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(encodedState); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String state = authorizationResponse.getState(); + + // 3. Decrypt state + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + Jwe jwe = Jwe.parse(state, privateKey, null); + assertNotNull(jwe.getClaims().getClaimAsString(KID)); + assertNotNull(jwe.getClaims().getClaimAsString(RFP)); + assertNotNull(jwe.getClaims().getClaimAsString(JTI)); + assertNotNull(jwe.getClaims().getClaimAsJSON(ADDITIONAL_CLAIMS)); + + JSONObject addClaims = jwe.getClaims().getClaimAsJSON(ADDITIONAL_CLAIMS); + assertEquals(addClaims.getString("first_name"), "Javier"); + assertEquals(addClaims.getString("last_name"), "Rojas"); + assertEquals(addClaims.getInt("age"), 34); + assertNotNull(addClaims.getJSONArray("more")); + assertEquals(addClaims.getJSONArray("more").length(), 2); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri", + "keyStoreFile", "keyStoreSecret", "dnName", "RS256_keyId", "clientJwksUri"}) + @Test + public void encodeClaimsInStateParameterAlgRSA15EncA128CBCPLUSHS256( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri, final String keyStoreFile, final String keyStoreSecret, + final String dnName, final String keyId, final String clientJwksUri) throws Exception { + showTitle("encodeClaimsInStateParameterAlgRSA15EncA128CBCPLUSHS256"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + JSONObject jwks = JwtUtil.getJSONWebKeys(clientJwksUri); + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(KeyEncryptionAlgorithm.RSA1_5, BlockEncryptionAlgorithm.A128CBC_PLUS_HS256, cryptoProvider); + jwtState.setKeyId(keyId); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + String encodedState = jwtState.getEncodedJwt(jwks); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(encodedState); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String state = authorizationResponse.getState(); + + // 3. Decrypt state + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + Jwe jwe = Jwe.parse(state, privateKey, null); + assertNotNull(jwe.getClaims().getClaimAsString(KID)); + assertNotNull(jwe.getClaims().getClaimAsString(RFP)); + assertNotNull(jwe.getClaims().getClaimAsString(JTI)); + assertNotNull(jwe.getClaims().getClaimAsJSON(ADDITIONAL_CLAIMS)); + + JSONObject addClaims = jwe.getClaims().getClaimAsJSON(ADDITIONAL_CLAIMS); + assertEquals(addClaims.getString("first_name"), "Javier"); + assertEquals(addClaims.getString("last_name"), "Rojas"); + assertEquals(addClaims.getInt("age"), 34); + assertNotNull(addClaims.getJSONArray("more")); + assertEquals(addClaims.getJSONArray("more").length(), 2); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri", + "keyStoreFile", "keyStoreSecret", "dnName", "RS256_keyId", "clientJwksUri"}) + @Test + public void encodeClaimsInStateParameterAlgRSA15EncA256CBCPLUSHS512( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri, final String keyStoreFile, final String keyStoreSecret, + final String dnName, final String keyId, final String clientJwksUri) throws Exception { + showTitle("encodeClaimsInStateParameterAlgRSA15EncA256CBCPLUSHS512"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + JSONObject jwks = JwtUtil.getJSONWebKeys(clientJwksUri); + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(KeyEncryptionAlgorithm.RSA1_5, BlockEncryptionAlgorithm.A256CBC_PLUS_HS512, cryptoProvider); + jwtState.setKeyId(keyId); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + String encodedState = jwtState.getEncodedJwt(jwks); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(encodedState); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String state = authorizationResponse.getState(); + + // 3. Decrypt state + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + Jwe jwe = Jwe.parse(state, privateKey, null); + assertNotNull(jwe.getClaims().getClaimAsString(KID)); + assertNotNull(jwe.getClaims().getClaimAsString(RFP)); + assertNotNull(jwe.getClaims().getClaimAsString(JTI)); + assertNotNull(jwe.getClaims().getClaimAsJSON(ADDITIONAL_CLAIMS)); + + JSONObject addClaims = jwe.getClaims().getClaimAsJSON(ADDITIONAL_CLAIMS); + assertEquals(addClaims.getString("first_name"), "Javier"); + assertEquals(addClaims.getString("last_name"), "Rojas"); + assertEquals(addClaims.getInt("age"), 34); + assertNotNull(addClaims.getJSONArray("more")); + assertEquals(addClaims.getJSONArray("more").length(), 2); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void encodeClaimsInStateParameterAlgA128KWEncA128GCM( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("encodeClaimsInStateParameterAlgA128KWEncA128GCM"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(KeyEncryptionAlgorithm.A128KW, BlockEncryptionAlgorithm.A128GCM, clientSecret); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + String encodedState = jwtState.getEncodedJwt(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(encodedState); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String state = authorizationResponse.getState(); + + // 3. Decrypt state + Jwe jwe = Jwe.parse(state, null, clientSecret.getBytes()); + assertNotNull(jwe.getClaims().getClaimAsString(RFP)); + assertNotNull(jwe.getClaims().getClaimAsString(JTI)); + assertNotNull(jwe.getClaims().getClaimAsJSON(ADDITIONAL_CLAIMS)); + + JSONObject addClaims = jwe.getClaims().getClaimAsJSON(ADDITIONAL_CLAIMS); + assertEquals(addClaims.getString("first_name"), "Javier"); + assertEquals(addClaims.getString("last_name"), "Rojas"); + assertEquals(addClaims.getInt("age"), 34); + assertNotNull(addClaims.getJSONArray("more")); + assertEquals(addClaims.getJSONArray("more").length(), 2); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void encodeClaimsInStateParameterAlgA256KWEncA256GCM( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("encodeClaimsInStateParameterAlgA256KWEncA256GCM"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(KeyEncryptionAlgorithm.A256KW, BlockEncryptionAlgorithm.A256GCM, clientSecret); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + String encodedState = jwtState.getEncodedJwt(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(encodedState); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String state = authorizationResponse.getState(); + + // 3. Decrypt state + Jwe jwe = Jwe.parse(state, null, clientSecret.getBytes()); + assertNotNull(jwe.getClaims().getClaimAsString(RFP)); + assertNotNull(jwe.getClaims().getClaimAsString(JTI)); + assertNotNull(jwe.getClaims().getClaimAsJSON(ADDITIONAL_CLAIMS)); + + JSONObject addClaims = jwe.getClaims().getClaimAsJSON(ADDITIONAL_CLAIMS); + assertEquals(addClaims.getString("first_name"), "Javier"); + assertEquals(addClaims.getString("last_name"), "Rojas"); + assertEquals(addClaims.getInt("age"), 34); + assertNotNull(addClaims.getJSONArray("more")); + assertEquals(addClaims.getJSONArray("more").length(), 2); + } + + @Test + public void jwtStateNONETest() throws Exception { + showTitle("jwtStateNONETest"); + + AbstractCryptoProvider cryptoProvider = createCryptoProviderWithAllowedNone(); + + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(SignatureAlgorithm.NONE, cryptoProvider); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + + String encodedState = jwtState.getEncodedJwt(); + assertNotNull(encodedState); + System.out.println("Encoded State: " + encodedState); + + Jwt jwt = Jwt.parse(encodedState); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), null, + null, null, SignatureAlgorithm.NONE); + assertTrue(validJwt); + } + + @Test + public void jwtStateHS256Test() throws Exception { + showTitle("jwtStateHS256Test"); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + String sharedKey = "shared_key"; + + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(SignatureAlgorithm.HS256, sharedKey, cryptoProvider); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + + String encodedState = jwtState.getEncodedJwt(); + assertNotNull(encodedState); + System.out.println("Signed JWS State: " + encodedState); + + Jwt jwt = Jwt.parse(encodedState); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), null, + null, sharedKey, SignatureAlgorithm.HS256); + assertTrue(validJwt); + } + + @Test + public void jwtStateHS384Test() throws Exception { + showTitle("jwtStateHS384Test"); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + String sharedKey = "shared_key"; + + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(SignatureAlgorithm.HS384, sharedKey, cryptoProvider); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + + String encodedState = jwtState.getEncodedJwt(); + assertNotNull(encodedState); + System.out.println("Signed JWS State: " + encodedState); + + Jwt jwt = Jwt.parse(encodedState); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), null, + null, sharedKey, SignatureAlgorithm.HS384); + assertTrue(validJwt); + } + + @Test + public void jwtStateHS512Test() throws Exception { + showTitle("jwtStateHS512Test"); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + String sharedKey = "shared_key"; + + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(SignatureAlgorithm.HS512, sharedKey, cryptoProvider); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + + String encodedState = jwtState.getEncodedJwt(); + assertNotNull(encodedState); + System.out.println("Signed JWS State: " + encodedState); + + Jwt jwt = Jwt.parse(encodedState); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), null, + null, sharedKey, SignatureAlgorithm.HS512); + assertTrue(validJwt); + } + + @Parameters({"keyStoreFile", "keyStoreSecret", "dnName", "RS256_keyId"}) + @Test + public void jwtStateRS256Test(final String keyStoreFile, final String keyStoreSecret, + final String dnName, final String keyId) throws Exception { + showTitle("jwtStateRS256Test"); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(SignatureAlgorithm.RS256, cryptoProvider); + jwtState.setKeyId(keyId); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + + String encodedState = jwtState.getEncodedJwt(); + assertNotNull(encodedState); + System.out.println("Signed JWS State: " + encodedState); + + Jwt jwt = Jwt.parse(encodedState); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), keyId, + null, null, SignatureAlgorithm.RS256); + assertTrue(validJwt); + } + + @Parameters({"keyStoreFile", "keyStoreSecret", "dnName", "RS384_keyId"}) + @Test + public void jwtStateRS384Test(final String keyStoreFile, final String keyStoreSecret, + final String dnName, final String keyId) throws Exception { + showTitle("jwtStateRS384Test"); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(SignatureAlgorithm.RS384, cryptoProvider); + jwtState.setKeyId(keyId); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + + String encodedState = jwtState.getEncodedJwt(); + assertNotNull(encodedState); + System.out.println("Signed JWS State: " + encodedState); + + Jwt jwt = Jwt.parse(encodedState); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), keyId, + null, null, SignatureAlgorithm.RS384); + assertTrue(validJwt); + } + + @Parameters({"keyStoreFile", "keyStoreSecret", "dnName", "RS512_keyId"}) + @Test + public void jwtStateRS512Test(final String keyStoreFile, final String keyStoreSecret, + final String dnName, final String keyId) throws Exception { + showTitle("jwtStateRS512Test"); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(SignatureAlgorithm.RS512, cryptoProvider); + jwtState.setKeyId(keyId); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + + String encodedState = jwtState.getEncodedJwt(); + assertNotNull(encodedState); + System.out.println("Signed JWS State: " + encodedState); + + Jwt jwt = Jwt.parse(encodedState); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), keyId, + null, null, SignatureAlgorithm.RS512); + assertTrue(validJwt); + } + + @Parameters({"keyStoreFile", "keyStoreSecret", "dnName", "ES256_keyId"}) + @Test + public void jwtStateES256Test(final String keyStoreFile, final String keyStoreSecret, + final String dnName, final String keyId) throws Exception { + showTitle("jwtStateES256Test"); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(SignatureAlgorithm.ES256, cryptoProvider); + jwtState.setKeyId(keyId); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + + String encodedState = jwtState.getEncodedJwt(); + assertNotNull(encodedState); + System.out.println("Signed JWS State: " + encodedState); + + Jwt jwt = Jwt.parse(encodedState); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), keyId, + null, null, SignatureAlgorithm.ES256); + assertTrue(validJwt); + } + + @Parameters({"keyStoreFile", "keyStoreSecret", "dnName", "ES384_keyId"}) + @Test + public void jwtStateES384Test(final String keyStoreFile, final String keyStoreSecret, + final String dnName, final String keyId) throws Exception { + showTitle("jwtStateES384Test"); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(SignatureAlgorithm.ES384, cryptoProvider); + jwtState.setKeyId(keyId); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + + String encodedState = jwtState.getEncodedJwt(); + assertNotNull(encodedState); + System.out.println("Signed JWS State: " + encodedState); + + Jwt jwt = Jwt.parse(encodedState); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), keyId, + null, null, SignatureAlgorithm.ES384); + assertTrue(validJwt); + } + + @Parameters({"keyStoreFile", "keyStoreSecret", "dnName", "ES512_keyId"}) + @Test + public void jwtStateES512Test(final String keyStoreFile, final String keyStoreSecret, + final String dnName, final String keyId) throws Exception { + showTitle("jwtStateES512Test"); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(SignatureAlgorithm.ES512, cryptoProvider); + jwtState.setKeyId(keyId); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + + String encodedState = jwtState.getEncodedJwt(); + assertNotNull(encodedState); + System.out.println("Signed JWS State: " + encodedState); + + Jwt jwt = Jwt.parse(encodedState); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), keyId, + null, null, SignatureAlgorithm.ES512); + assertTrue(validJwt); + } + + @Parameters({"keyStoreFile", "keyStoreSecret", "dnName", "PS256_keyId"}) + @Test + public void jwtStatePS256Test(final String keyStoreFile, final String keyStoreSecret, + final String dnName, final String keyId) throws Exception { + showTitle("jwtStatePS256Test"); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(SignatureAlgorithm.PS256, cryptoProvider); + jwtState.setKeyId(keyId); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + + String encodedState = jwtState.getEncodedJwt(); + assertNotNull(encodedState); + System.out.println("Signed JWS State: " + encodedState); + + Jwt jwt = Jwt.parse(encodedState); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), keyId, + null, null, SignatureAlgorithm.PS256); + assertTrue(validJwt); + } + + @Parameters({"keyStoreFile", "keyStoreSecret", "dnName", "PS384_keyId"}) + @Test + public void jwtStatePS384Test(final String keyStoreFile, final String keyStoreSecret, + final String dnName, final String keyId) throws Exception { + showTitle("jwtStatePS384Test"); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(SignatureAlgorithm.PS384, cryptoProvider); + jwtState.setKeyId(keyId); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + + String encodedState = jwtState.getEncodedJwt(); + assertNotNull(encodedState); + System.out.println("Signed JWS State: " + encodedState); + + Jwt jwt = Jwt.parse(encodedState); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), keyId, + null, null, SignatureAlgorithm.PS384); + assertTrue(validJwt); + } + + @Parameters({"keyStoreFile", "keyStoreSecret", "dnName", "PS512_keyId"}) + @Test + public void jwtStatePS512Test(final String keyStoreFile, final String keyStoreSecret, + final String dnName, final String keyId) throws Exception { + showTitle("jwtStatePS512Test"); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(SignatureAlgorithm.PS512, cryptoProvider); + jwtState.setKeyId(keyId); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + + String encodedState = jwtState.getEncodedJwt(); + assertNotNull(encodedState); + System.out.println("Signed JWS State: " + encodedState); + + Jwt jwt = Jwt.parse(encodedState); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), keyId, + null, null, SignatureAlgorithm.PS512); + assertTrue(validJwt); + } + + @Parameters({"keyStoreFile", "keyStoreSecret", "dnName", "RS256_keyId", "clientJwksUri"}) + @Test + public void jwtStateAlgRSAOAEPEncA256GCMTest( + final String keyStoreFile, final String keyStoreSecret, final String dnName, final String keyId, + final String clientJwksUri) throws Exception { + showTitle("jwtStateAlgRSAOAEPEncA256GCMTest"); + + JSONObject jwks = JwtUtil.getJSONWebKeys(clientJwksUri); + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(KeyEncryptionAlgorithm.RSA_OAEP, BlockEncryptionAlgorithm.A256GCM, cryptoProvider); + jwtState.setKeyId(keyId); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + + String encodedState = jwtState.getEncodedJwt(jwks); + assertNotNull(encodedState); + System.out.println("Encrypted JWE State: " + encodedState); + + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + Jwe jwe = Jwe.parse(encodedState, privateKey, null); + assertNotNull(jwe.getClaims().getClaimAsString(KID)); + assertNotNull(jwe.getClaims().getClaimAsString(RFP)); + assertNotNull(jwe.getClaims().getClaimAsString(JTI)); + assertNotNull(jwe.getClaims().getClaimAsJSON(ADDITIONAL_CLAIMS)); + + JSONObject addClaims = jwe.getClaims().getClaimAsJSON(ADDITIONAL_CLAIMS); + assertEquals(addClaims.getString("first_name"), "Javier"); + assertEquals(addClaims.getString("last_name"), "Rojas"); + assertEquals(addClaims.getInt("age"), 34); + assertNotNull(addClaims.getJSONArray("more")); + assertEquals(addClaims.getJSONArray("more").length(), 2); + } + + @Parameters({"keyStoreFile", "keyStoreSecret", "dnName", "RS256_keyId", "clientJwksUri"}) + @Test + public void jwtStateAlgRSA15EncA128CBCPLUSHS256Test( + final String keyStoreFile, final String keyStoreSecret, final String dnName, final String keyId, + final String clientJwksUri) throws Exception { + showTitle("jwtStateAlgRSA15EncA128CBCPLUSHS256Test"); + + JSONObject jwks = JwtUtil.getJSONWebKeys(clientJwksUri); + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(KeyEncryptionAlgorithm.RSA1_5, BlockEncryptionAlgorithm.A128CBC_PLUS_HS256, cryptoProvider); + jwtState.setKeyId(keyId); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + + String encodedState = jwtState.getEncodedJwt(jwks); + assertNotNull(encodedState); + System.out.println("Encrypted JWE State: " + encodedState); + + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + Jwe jwe = Jwe.parse(encodedState, privateKey, null); + assertNotNull(jwe.getClaims().getClaimAsString(KID)); + assertNotNull(jwe.getClaims().getClaimAsString(RFP)); + assertNotNull(jwe.getClaims().getClaimAsString(JTI)); + assertNotNull(jwe.getClaims().getClaimAsJSON(ADDITIONAL_CLAIMS)); + + JSONObject addClaims = jwe.getClaims().getClaimAsJSON(ADDITIONAL_CLAIMS); + assertEquals(addClaims.getString("first_name"), "Javier"); + assertEquals(addClaims.getString("last_name"), "Rojas"); + assertEquals(addClaims.getInt("age"), 34); + assertNotNull(addClaims.getJSONArray("more")); + assertEquals(addClaims.getJSONArray("more").length(), 2); + } + + @Parameters({"keyStoreFile", "keyStoreSecret", "dnName", "RS256_keyId", "clientJwksUri"}) + @Test + public void jwtStateAlgRSA15EncA256CBCPLUSHS512Test( + final String keyStoreFile, final String keyStoreSecret, final String dnName, final String keyId, + final String clientJwksUri) throws Exception { + showTitle("jwtStateAlgRSA15EncA256CBCPLUSHS512Test"); + + JSONObject jwks = JwtUtil.getJSONWebKeys(clientJwksUri); + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(KeyEncryptionAlgorithm.RSA1_5, BlockEncryptionAlgorithm.A256CBC_PLUS_HS512, cryptoProvider); + jwtState.setKeyId(keyId); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + + String encodedState = jwtState.getEncodedJwt(jwks); + assertNotNull(encodedState); + System.out.println("Encrypted JWE State: " + encodedState); + + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + Jwe jwe = Jwe.parse(encodedState, privateKey, null); + assertNotNull(jwe.getClaims().getClaimAsString(KID)); + assertNotNull(jwe.getClaims().getClaimAsString(RFP)); + assertNotNull(jwe.getClaims().getClaimAsString(JTI)); + assertNotNull(jwe.getClaims().getClaimAsJSON(ADDITIONAL_CLAIMS)); + + JSONObject addClaims = jwe.getClaims().getClaimAsJSON(ADDITIONAL_CLAIMS); + assertEquals(addClaims.getString("first_name"), "Javier"); + assertEquals(addClaims.getString("last_name"), "Rojas"); + assertEquals(addClaims.getInt("age"), 34); + assertNotNull(addClaims.getJSONArray("more")); + assertEquals(addClaims.getJSONArray("more").length(), 2); + } + + @Test + public void jwtStateAlgA128KWEncA128GCMTest() throws Exception { + showTitle("jwtStateAlgA128KWEncA128GCMTest"); + + String sharedKey = "shared_key"; + + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(KeyEncryptionAlgorithm.A128KW, BlockEncryptionAlgorithm.A128GCM, sharedKey); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + + String encodedState = jwtState.getEncodedJwt(); + assertNotNull(encodedState); + System.out.println("Encrypted JWE State: " + encodedState); + + Jwe jwe = Jwe.parse(encodedState, null, sharedKey.getBytes()); + assertNotNull(jwe.getClaims().getClaimAsString(RFP)); + assertNotNull(jwe.getClaims().getClaimAsString(JTI)); + assertNotNull(jwe.getClaims().getClaimAsJSON(ADDITIONAL_CLAIMS)); + + JSONObject addClaims = jwe.getClaims().getClaimAsJSON(ADDITIONAL_CLAIMS); + assertEquals(addClaims.getString("first_name"), "Javier"); + assertEquals(addClaims.getString("last_name"), "Rojas"); + assertEquals(addClaims.getInt("age"), 34); + assertNotNull(addClaims.getJSONArray("more")); + assertEquals(addClaims.getJSONArray("more").length(), 2); + } + + @Test + public void jwtStateAlgA256KWEncA256GCMTest() throws Exception { + showTitle("jwtStateAlgA256KWEncA256GCMTest"); + + String sharedKey = "shared_key"; + + String rfp = UUID.randomUUID().toString(); + String jti = UUID.randomUUID().toString(); + + JwtState jwtState = new JwtState(KeyEncryptionAlgorithm.A256KW, BlockEncryptionAlgorithm.A256GCM, sharedKey); + jwtState.setRfp(rfp); + jwtState.setJti(jti); + jwtState.setAdditionalClaims(new JSONObject(additionalClaims)); + + String encodedState = jwtState.getEncodedJwt(); + assertNotNull(encodedState); + System.out.println("Encrypted JWE State: " + encodedState); + + Jwe jwe = Jwe.parse(encodedState, null, sharedKey.getBytes()); + assertNotNull(jwe.getClaims().getClaimAsString(RFP)); + assertNotNull(jwe.getClaims().getClaimAsString(JTI)); + assertNotNull(jwe.getClaims().getClaimAsJSON(ADDITIONAL_CLAIMS)); + + JSONObject addClaims = jwe.getClaims().getClaimAsJSON(ADDITIONAL_CLAIMS); + assertEquals(addClaims.getString("first_name"), "Javier"); + assertEquals(addClaims.getString("last_name"), "Rojas"); + assertEquals(addClaims.getInt("age"), 34); + assertNotNull(addClaims.getJSONArray("more")); + assertEquals(addClaims.getJSONArray("more").length(), 2); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/EndSessionRestWebServiceHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/EndSessionRestWebServiceHttpTest.java new file mode 100644 index 00000000..7ffa2cd6 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/EndSessionRestWebServiceHttpTest.java @@ -0,0 +1,261 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import javax.ws.rs.core.Response.Status; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.EndSessionClient; +import org.gluu.oxauth.client.EndSessionRequest; +import org.gluu.oxauth.client.EndSessionResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.session.EndSessionErrorResponseType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +import com.google.common.collect.Lists; + +/** + * Functional tests for End Session Web Services (HTTP) + * + * @author Javier Rojas Blum + * @version August 9, 2017 + */ +public class EndSessionRestWebServiceHttpTest extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "postLogoutRedirectUri", "logoutUri", "sectorIdentifierUri"}) + @Test + public void requestEndSession( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String postLogoutRedirectUri, final String logoutUri, final String sectorIdentifierUri) throws Exception { + showTitle("requestEndSession by id_token"); + + // 1. OpenID Connect Dynamic Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN)); + registerRequest.setPostLogoutRedirectUris(Arrays.asList(postLogoutRedirectUri)); + registerRequest.setFrontChannelLogoutUris(Lists.newArrayList(logoutUri)); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + String clientId = response.getClientId(); + + // 2. Request authorization + List responseTypes = new ArrayList(); + responseTypes.add(ResponseType.TOKEN); + responseTypes.add(ResponseType.ID_TOKEN); + List scopes = new ArrayList(); + scopes.add("openid"); + scopes.add("profile"); + scopes.add("address"); + scopes.add("email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The access token is null"); + assertEquals(authorizationResponse.getState(), state); + assertNotNull(authorizationResponse.getTokenType(), "The token type is null"); + assertNotNull(authorizationResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(authorizationResponse.getScope(), "The scope must be null"); + assertNotNull(authorizationResponse.getSessionId(), "The session_id is null"); + assertNotNull(authorizationResponse.getSid(), "The sid is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. End session + String endSessionId1 = UUID.randomUUID().toString(); + EndSessionRequest endSessionRequest1 = new EndSessionRequest(idToken, postLogoutRedirectUri, endSessionId1); + endSessionRequest1.setSid(authorizationResponse.getSid()); + + EndSessionClient endSessionClient = new EndSessionClient(endSessionEndpoint); + endSessionClient.setRequest(endSessionRequest1); + + EndSessionResponse endSessionResponse1 = endSessionClient.exec(); + + showClient(endSessionClient); + assertEquals(endSessionResponse1.getStatus(), 200); + assertNotNull(endSessionResponse1.getHtmlPage(), "The HTML page is null"); + + // silly validation of html content returned by server but at least it verifies that logout_uri and post_logout_uri are present + assertTrue(endSessionResponse1.getHtmlPage().contains(""), "The HTML page is null"); + assertTrue(endSessionResponse1.getHtmlPage().contains(logoutUri), "logout_uri is not present on html page"); + assertTrue(endSessionResponse1.getHtmlPage().contains(postLogoutRedirectUri), "postLogoutRedirectUri is not present on html page"); + // assertEquals(endSessionResponse.getState(), endSessionId); // commented out, for http-based logout we get html page + + // 4. End session with an already ended session + String endSessionId2 = UUID.randomUUID().toString(); + EndSessionRequest endSessionRequest2 = new EndSessionRequest(idToken, postLogoutRedirectUri, endSessionId2); + endSessionRequest2.setSid(authorizationResponse.getSid()); + + EndSessionClient endSessionClient2 = new EndSessionClient(endSessionEndpoint); + endSessionClient2.setRequest(endSessionRequest2); + + EndSessionResponse endSessionResponse2 = endSessionClient2.exec(); + + showClient(endSessionClient2); + assertStatusOrRedirect(endSessionResponse2.getStatus(), Status.BAD_REQUEST.getStatusCode()); + assertEquals(endSessionResponse2.getErrorType(), EndSessionErrorResponseType.INVALID_GRANT_AND_SESSION); + } + + public static void assertStatusOrRedirect(int actualStatus, int expectedStatus) { + assertTrue(actualStatus == expectedStatus || actualStatus == Status.FOUND.getStatusCode()); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "postLogoutRedirectUri", "logoutUri", "sectorIdentifierUri"}) + @Test + public void requestEndSessionWithSessionId( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String postLogoutRedirectUri, final String logoutUri, final String sectorIdentifierUri) throws Exception { + showTitle("requestEndSession by session_id"); + + // 1. OpenID Connect Dynamic Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN)); + registerRequest.setPostLogoutRedirectUris(Arrays.asList(postLogoutRedirectUri)); + registerRequest.setFrontChannelLogoutUris(Lists.newArrayList(logoutUri)); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + String clientId = response.getClientId(); + + // 2. Request authorization + List responseTypes = new ArrayList(); + responseTypes.add(ResponseType.TOKEN); + responseTypes.add(ResponseType.ID_TOKEN); + List scopes = new ArrayList(); + scopes.add("openid"); + scopes.add("profile"); + scopes.add("address"); + scopes.add("email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The access token is null"); + assertEquals(authorizationResponse.getState(), state); + assertNotNull(authorizationResponse.getTokenType(), "The token type is null"); + assertNotNull(authorizationResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(authorizationResponse.getScope(), "The scope must be null"); + assertNotNull(authorizationResponse.getSessionId(), "The session_id is null"); + + // 3. End session + String endSessionId1 = UUID.randomUUID().toString(); + EndSessionRequest endSessionRequest1 = new EndSessionRequest(null, postLogoutRedirectUri, endSessionId1); + endSessionRequest1.setSid(authorizationResponse.getSid()); + + EndSessionClient endSessionClient = new EndSessionClient(endSessionEndpoint); + endSessionClient.setRequest(endSessionRequest1); + + EndSessionResponse endSessionResponse1 = endSessionClient.exec(); + + showClient(endSessionClient); + assertEquals(endSessionResponse1.getStatus(), 200); + assertNotNull(endSessionResponse1.getHtmlPage(), "The HTML page is null"); + + // silly validation of html content returned by server but at least it verifies that logout_uri and post_logout_uri are present + assertTrue(endSessionResponse1.getHtmlPage().contains(""), "The HTML page is null"); + assertTrue(endSessionResponse1.getHtmlPage().contains(logoutUri), "logout_uri is not present on html page"); + assertTrue(endSessionResponse1.getHtmlPage().contains(postLogoutRedirectUri), "postLogoutRedirectUri is not present on html page"); + // assertEquals(endSessionResponse.getState(), endSessionId); // commented out, for http-based logout we get html page + + // 4. End session with an already ended session + String endSessionId2 = UUID.randomUUID().toString(); + EndSessionRequest endSessionRequest2 = new EndSessionRequest(null, postLogoutRedirectUri, endSessionId2); + endSessionRequest2.setSid(authorizationResponse.getSid()); + + EndSessionClient endSessionClient2 = new EndSessionClient(endSessionEndpoint); + endSessionClient2.setRequest(endSessionRequest2); + + EndSessionResponse endSessionResponse2 = endSessionClient2.exec(); + + showClient(endSessionClient2); + assertStatusOrRedirect(endSessionResponse2.getStatus(), Status.BAD_REQUEST.getStatusCode()); + assertEquals(endSessionResponse2.getErrorType(), EndSessionErrorResponseType.INVALID_GRANT_AND_SESSION); + } + + @Test + public void requestEndSessionFail1() throws Exception { + showTitle("requestEndSessionFail1"); + + EndSessionClient endSessionClient = new EndSessionClient(endSessionEndpoint); + EndSessionResponse response = endSessionClient.execEndSession(null, null, null); + + showClient(endSessionClient); + assertEquals(response.getStatus(), 400, "Unexpected response code. Entity: " + response.getEntity()); + assertNotNull(response.getEntity(), "The entity is null"); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + } + + @Parameters({"postLogoutRedirectUri"}) + @Test + public void requestEndSessionFail2(final String postLogoutRedirectUri) throws Exception { + showTitle("requestEndSessionFail2"); + + String state = UUID.randomUUID().toString(); + + EndSessionClient endSessionClient = new EndSessionClient(endSessionEndpoint); + EndSessionResponse response = endSessionClient.execEndSession("INVALID_ACCESS_TOKEN", postLogoutRedirectUri, state); + + showClient(endSessionClient); + assertStatusOrRedirect(response.getStatus(), Status.BAD_REQUEST.getStatusCode()); + assertNotNull(response.getEntity(), "The entity is null"); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/GluuConfigurationWebServiceHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/GluuConfigurationWebServiceHttpTest.java new file mode 100644 index 00000000..1f01e722 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/GluuConfigurationWebServiceHttpTest.java @@ -0,0 +1,25 @@ +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.GluuConfigurationClient; +import org.gluu.oxauth.client.GluuConfigurationResponse; +import org.testng.annotations.Test; + +/** + * Created by eugeniuparvan on 8/12/16. + */ +public class GluuConfigurationWebServiceHttpTest extends BaseTest { + + @Test + public void requestGluuConfiguration() throws Exception { + GluuConfigurationClient client = new GluuConfigurationClient(gluuConfigurationEndpoint); + GluuConfigurationResponse response = client.execGluuConfiguration(); + + showClient(client); + assertEquals(response.getStatus(), 200, "Unexpected response code. Entity: " + response.getEntity()); + assertNotNull(response.getEntity(), "The entity is null"); + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/GrantTypesRestrictionHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/GrantTypesRestrictionHttpTest.java new file mode 100644 index 00000000..20348f0a --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/GrantTypesRestrictionHttpTest.java @@ -0,0 +1,750 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.gluu.oxauth.model.register.RegisterRequestParam.APPLICATION_TYPE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SCOPE; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.EndSessionClient; +import org.gluu.oxauth.client.EndSessionRequest; +import org.gluu.oxauth.client.EndSessionResponse; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.common.SubjectType; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.ITestContext; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import com.google.common.collect.Lists; + +/** + * @author Javier Rojas Blum + * @version November 29, 2017 + */ +public class GrantTypesRestrictionHttpTest extends BaseTest { + + @Test(dataProvider = "grantTypesRestrictionDataProvider") + public void grantTypesRestriction( + final List responseTypes, final List expectedResponseTypes, + final List grantTypes, final List expectedGrantTypes, + final String userId, final String userSecret, + final String redirectUris, final String redirectUri, final String sectorIdentifierUri, + final String postLogoutRedirectUri, final String logoutUri) throws Exception { + showTitle("grantTypesRestriction"); + + List scopes = Arrays.asList("openid", "profile", "address", "email", "user_name"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setGrantTypes(grantTypes); + registerRequest.setScope(scopes); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setPostLogoutRedirectUris(Arrays.asList(postLogoutRedirectUri)); + registerRequest.setFrontChannelLogoutUris(Lists.newArrayList(logoutUri)); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + assertNotNull(registerResponse.getResponseTypes()); + assertTrue(registerResponse.getResponseTypes().containsAll(expectedResponseTypes)); + assertNotNull(registerResponse.getGrantTypes()); + assertTrue(registerResponse.getGrantTypes().containsAll(expectedGrantTypes)); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readRequest); + RegisterResponse readResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(readResponse.getStatus(), 200); + assertNotNull(readResponse.getClientId()); + assertNotNull(readResponse.getClientSecret()); + assertNotNull(readResponse.getRegistrationAccessToken()); + assertNotNull(readResponse.getRegistrationClientUri()); + assertNotNull(readResponse.getClientSecretExpiresAt()); + assertNotNull(readResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readResponse.getClaims().get(SCOPE.toString())); + assertNotNull(readResponse.getResponseTypes()); + assertTrue(readResponse.getResponseTypes().containsAll(expectedResponseTypes)); + assertNotNull(readResponse.getGrantTypes()); + assertTrue(readResponse.getGrantTypes().containsAll(expectedGrantTypes)); + + // 3. Request authorization + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(expectedResponseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + if (expectedResponseTypes.size() == 0) { + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(authorizationResponse.getStatus(), 302); + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getErrorType()); + assertNotNull(authorizationResponse.getErrorDescription()); + assertNotNull(authorizationResponse.getState()); + + return; + } + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + String scope = authorizationResponse.getScope(); + String authorizationCode = null; + String accessToken = null; + String idToken = null; + String refreshToken = null; + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getState()); + assertNotNull(authorizationResponse.getScope()); + if (expectedResponseTypes.contains(ResponseType.CODE)) { + assertNotNull(authorizationResponse.getCode()); + + authorizationCode = authorizationResponse.getCode(); + } + if (expectedResponseTypes.contains(ResponseType.TOKEN)) { + assertNotNull(authorizationResponse.getAccessToken()); + + accessToken = authorizationResponse.getAccessToken(); + } + if (expectedResponseTypes.contains(ResponseType.ID_TOKEN)) { + assertNotNull(authorizationResponse.getIdToken()); + + idToken = authorizationResponse.getIdToken(); + + // 4. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + if (expectedResponseTypes.contains(ResponseType.CODE)) { + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.CODE_HASH)); + assertTrue(rsaSigner.validateAuthorizationCode(authorizationCode, jwt)); + } + if (expectedResponseTypes.contains(ResponseType.TOKEN)) { + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertTrue(rsaSigner.validateAccessToken(accessToken, jwt)); + } + } + + if (expectedResponseTypes.contains(ResponseType.CODE)) { + // 5. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200); + assertNotNull(tokenResponse.getEntity()); + assertNotNull(tokenResponse.getAccessToken()); + assertNotNull(tokenResponse.getExpiresIn()); + assertNotNull(tokenResponse.getTokenType()); + + if (expectedGrantTypes.contains(GrantType.REFRESH_TOKEN)) { + assertNotNull(tokenResponse.getRefreshToken()); + + refreshToken = tokenResponse.getRefreshToken(); + + // 6. Request new access token using the refresh token. + TokenClient refreshTokenClient = new TokenClient(tokenEndpoint); + TokenResponse refreshTokenResponse = refreshTokenClient.execRefreshToken(scope, refreshToken, clientId, clientSecret); + + showClient(refreshTokenClient); + assertEquals(refreshTokenResponse.getStatus(), 200); + assertNotNull(refreshTokenResponse.getEntity()); + assertNotNull(refreshTokenResponse.getAccessToken()); + assertNotNull(refreshTokenResponse.getTokenType()); + assertNotNull(refreshTokenResponse.getRefreshToken()); + assertNotNull(refreshTokenResponse.getScope()); + + accessToken = refreshTokenResponse.getAccessToken(); + } else { + assertNull(tokenResponse.getRefreshToken()); + } + } + + if (accessToken != null) { + // 7. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + + if (idToken != null) { + // 8. End session + String endSessionId = UUID.randomUUID().toString(); + EndSessionRequest endSessionRequest = new EndSessionRequest(idToken, postLogoutRedirectUri, endSessionId); + endSessionRequest.setSid(authorizationResponse.getSid()); + + EndSessionClient endSessionClient = new EndSessionClient(endSessionEndpoint); + endSessionClient.setRequest(endSessionRequest); + + EndSessionResponse endSessionResponse = endSessionClient.exec(); + + showClient(endSessionClient); + assertEquals(endSessionResponse.getStatus(), 200); + assertNotNull(endSessionResponse.getHtmlPage()); + + // silly validation of html content returned by server but at least it verifies that logout_uri and post_logout_uri are present + assertTrue(endSessionResponse.getHtmlPage().contains("")); + assertTrue(endSessionResponse.getHtmlPage().contains(logoutUri)); + assertTrue(endSessionResponse.getHtmlPage().contains(postLogoutRedirectUri)); + // assertEquals(endSessionResponse.getState(), endSessionId); // commented out, for http-based logout we get html page + } + } + } + + @DataProvider(name = "grantTypesRestrictionDataProvider") + public Object[][] omittedResponseTypesFailDataProvider(ITestContext context) { + String userId = context.getCurrentXmlTest().getParameter("userId"); + String userSecret = context.getCurrentXmlTest().getParameter("userSecret"); + String redirectUris = context.getCurrentXmlTest().getParameter("redirectUris"); + String redirectUri = context.getCurrentXmlTest().getParameter("redirectUri"); + String sectorIdentifierUri = context.getCurrentXmlTest().getParameter("sectorIdentifierUri"); + String postLogoutRedirectUri = context.getCurrentXmlTest().getParameter("postLogoutRedirectUri"); + String logoutUri = context.getCurrentXmlTest().getParameter("logoutUri"); + + return new Object[][]{ + { + Arrays.asList(), + Arrays.asList(ResponseType.CODE), + Arrays.asList(), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.CODE), + Arrays.asList(ResponseType.CODE), + Arrays.asList(), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.TOKEN), + Arrays.asList(ResponseType.TOKEN), + Arrays.asList(), + Arrays.asList(GrantType.IMPLICIT), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.ID_TOKEN), + Arrays.asList(), + Arrays.asList(GrantType.IMPLICIT), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(), + Arrays.asList(GrantType.IMPLICIT), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN), + Arrays.asList(), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.IMPLICIT, GrantType.REFRESH_TOKEN), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN), + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN), + Arrays.asList(), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.IMPLICIT, GrantType.REFRESH_TOKEN), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.IMPLICIT, GrantType.REFRESH_TOKEN), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + // + { + Arrays.asList(), + Arrays.asList(ResponseType.CODE), + Arrays.asList(GrantType.AUTHORIZATION_CODE), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(), + Arrays.asList(ResponseType.TOKEN), + Arrays.asList(GrantType.IMPLICIT), + Arrays.asList(GrantType.IMPLICIT), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(), + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.IMPLICIT), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(), + Arrays.asList(), + Arrays.asList(GrantType.REFRESH_TOKEN), + Arrays.asList(GrantType.REFRESH_TOKEN), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(), + Arrays.asList(), + Arrays.asList(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS), + Arrays.asList(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(), + Arrays.asList(), + Arrays.asList(GrantType.CLIENT_CREDENTIALS), + Arrays.asList(GrantType.CLIENT_CREDENTIALS), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(), + Arrays.asList(), + Arrays.asList(GrantType.OXAUTH_UMA_TICKET), + Arrays.asList(GrantType.OXAUTH_UMA_TICKET), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + // + { + Arrays.asList(ResponseType.CODE), + Arrays.asList(ResponseType.CODE), + Arrays.asList(GrantType.AUTHORIZATION_CODE), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.CODE), + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN), + Arrays.asList(GrantType.IMPLICIT), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.CODE), + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.IMPLICIT), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.CODE), + Arrays.asList(ResponseType.CODE), + Arrays.asList(GrantType.REFRESH_TOKEN), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.CODE), + Arrays.asList(ResponseType.CODE), + Arrays.asList(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.CODE), + Arrays.asList(ResponseType.CODE), + Arrays.asList(GrantType.CLIENT_CREDENTIALS), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.CLIENT_CREDENTIALS), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.CODE), + Arrays.asList(ResponseType.CODE), + Arrays.asList(GrantType.OXAUTH_UMA_TICKET), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.OXAUTH_UMA_TICKET), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + // + { + Arrays.asList(ResponseType.TOKEN), + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN), + Arrays.asList(GrantType.AUTHORIZATION_CODE), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.TOKEN), + Arrays.asList(ResponseType.TOKEN), + Arrays.asList(GrantType.IMPLICIT), + Arrays.asList(GrantType.IMPLICIT), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.TOKEN), + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.IMPLICIT), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.TOKEN), + Arrays.asList(ResponseType.TOKEN), + Arrays.asList(GrantType.REFRESH_TOKEN), + Arrays.asList(GrantType.IMPLICIT, GrantType.REFRESH_TOKEN), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.TOKEN), + Arrays.asList(ResponseType.TOKEN), + Arrays.asList(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS), + Arrays.asList(GrantType.IMPLICIT, GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.TOKEN), + Arrays.asList(ResponseType.TOKEN), + Arrays.asList(GrantType.CLIENT_CREDENTIALS), + Arrays.asList(GrantType.IMPLICIT, GrantType.CLIENT_CREDENTIALS), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.TOKEN), + Arrays.asList(ResponseType.TOKEN), + Arrays.asList(GrantType.OXAUTH_UMA_TICKET), + Arrays.asList(GrantType.IMPLICIT, GrantType.OXAUTH_UMA_TICKET), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + // + { + Arrays.asList(ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN), + Arrays.asList(GrantType.AUTHORIZATION_CODE), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.ID_TOKEN), + Arrays.asList(GrantType.IMPLICIT), + Arrays.asList(GrantType.IMPLICIT), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.IMPLICIT), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.ID_TOKEN), + Arrays.asList(GrantType.REFRESH_TOKEN), + Arrays.asList(GrantType.IMPLICIT, GrantType.REFRESH_TOKEN), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.ID_TOKEN), + Arrays.asList(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS), + Arrays.asList(GrantType.IMPLICIT, GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.ID_TOKEN), + Arrays.asList(GrantType.CLIENT_CREDENTIALS), + Arrays.asList(GrantType.IMPLICIT, GrantType.CLIENT_CREDENTIALS), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.ID_TOKEN), + Arrays.asList(GrantType.OXAUTH_UMA_TICKET), + Arrays.asList(GrantType.IMPLICIT, GrantType.OXAUTH_UMA_TICKET), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + // + { + Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(GrantType.AUTHORIZATION_CODE), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(GrantType.IMPLICIT), + Arrays.asList(GrantType.IMPLICIT), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.IMPLICIT), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(GrantType.REFRESH_TOKEN), + Arrays.asList(GrantType.IMPLICIT, GrantType.REFRESH_TOKEN), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS), + Arrays.asList(GrantType.IMPLICIT, GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(GrantType.CLIENT_CREDENTIALS), + Arrays.asList(GrantType.IMPLICIT, GrantType.CLIENT_CREDENTIALS), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(GrantType.OXAUTH_UMA_TICKET), + Arrays.asList(GrantType.IMPLICIT, GrantType.OXAUTH_UMA_TICKET), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + // + { + Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN), + Arrays.asList(GrantType.AUTHORIZATION_CODE), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(GrantType.IMPLICIT), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.IMPLICIT), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(GrantType.REFRESH_TOKEN), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.IMPLICIT, GrantType.REFRESH_TOKEN), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT, GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(GrantType.CLIENT_CREDENTIALS), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT, GrantType.CLIENT_CREDENTIALS), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(GrantType.OXAUTH_UMA_TICKET), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT, GrantType.OXAUTH_UMA_TICKET), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + // + { + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN), + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN), + Arrays.asList(GrantType.AUTHORIZATION_CODE), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN), + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN), + Arrays.asList(GrantType.IMPLICIT), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN), + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.IMPLICIT), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN), + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN), + Arrays.asList(GrantType.REFRESH_TOKEN), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.IMPLICIT, GrantType.REFRESH_TOKEN), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN), + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN), + Arrays.asList(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT, GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN), + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN), + Arrays.asList(GrantType.CLIENT_CREDENTIALS), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT, GrantType.CLIENT_CREDENTIALS), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN), + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN), + Arrays.asList(GrantType.OXAUTH_UMA_TICKET), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT, GrantType.OXAUTH_UMA_TICKET), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + // + { + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(GrantType.AUTHORIZATION_CODE), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(GrantType.IMPLICIT), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.IMPLICIT), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(GrantType.REFRESH_TOKEN), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.IMPLICIT, GrantType.REFRESH_TOKEN), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT, GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(GrantType.CLIENT_CREDENTIALS), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT, GrantType.CLIENT_CREDENTIALS), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + { + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), + Arrays.asList(GrantType.OXAUTH_UMA_TICKET), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT, GrantType.OXAUTH_UMA_TICKET), + userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri + }, + }; + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/IndividualClaimsRequestsTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/IndividualClaimsRequestsTest.java new file mode 100644 index 00000000..c141b9ca --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/IndividualClaimsRequestsTest.java @@ -0,0 +1,2414 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +import java.security.PrivateKey; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.JwkResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.client.model.authorize.Claim; +import org.gluu.oxauth.client.model.authorize.ClaimValue; +import org.gluu.oxauth.client.model.authorize.JwtAuthorizationRequest; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.AbstractCryptoProvider; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.encryption.BlockEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.encryption.KeyEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.signature.ECDSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jwe.Jwe; +import org.gluu.oxauth.model.jwk.Algorithm; +import org.gluu.oxauth.model.jws.ECDSASigner; +import org.gluu.oxauth.model.jws.HMACSigner; +import org.gluu.oxauth.model.jws.PlainTextSignature; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.JwtUtil; +import org.gluu.oxauth.model.util.StringUtils; +import org.gluu.oxauth.model.util.Util; +import org.json.JSONObject; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Javier Rojas Blum + * @version February 12, 2019 + */ +public class IndividualClaimsRequestsTest extends BaseTest { + + public static final String ACR_VALUE = "basic"; + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestClaimsIndividuallyRequestObjectSigningAlgNoneUserInfoSignedResponseJson( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestClaimsIndividuallyRequestObjectSigningAlgNoneUserInfoSignedResponseJson"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.NONE); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.NONE); + registerRequest.setClaims(Arrays.asList( + JwtClaimName.NAME, + JwtClaimName.NICKNAME, + JwtClaimName.GIVEN_NAME, + JwtClaimName.FAMILY_NAME, + JwtClaimName.PICTURE, + JwtClaimName.ZONEINFO, + JwtClaimName.LOCALE, + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_POSTAL_CODE, + JwtClaimName.ADDRESS_COUNTRY)); + + RegisterClient registerClient = newRegisterClient(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + AbstractCryptoProvider cryptoProvider = createCryptoProviderWithAllowedNone(); + + List scopes = Arrays.asList("openid", "clientinfo"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest, SignatureAlgorithm.NONE, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ZONEINFO, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.LOCALE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_LOCALITY, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_REGION, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_POSTAL_CODE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NAME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.NAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.NICKNAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.GIVEN_NAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.FAMILY_NAME)); + assertNull(jwt.getClaims().getClaimAsString(JwtClaimName.EMAIL)); + assertNull(jwt.getClaims().getClaimAsString(JwtClaimName.EMAIL_VERIFIED)); + + PlainTextSignature signer = new PlainTextSignature(); + assertTrue(signer.validate(jwt)); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NICKNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_REGION)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestClaimsIndividuallyRequestObjectSigningAlgNoneUserInfoSignedResponsAlgNone( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestClaimsIndividuallyRequestObjectSigningAlgNoneUserInfoSignedResponsAlgNone"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.NONE); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.NONE); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.NONE); + registerRequest.setClaims(Arrays.asList( + JwtClaimName.NAME, + JwtClaimName.NICKNAME, + JwtClaimName.GIVEN_NAME, + JwtClaimName.FAMILY_NAME, + JwtClaimName.PICTURE, + JwtClaimName.ZONEINFO, + JwtClaimName.LOCALE, + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_POSTAL_CODE, + JwtClaimName.ADDRESS_COUNTRY)); + + RegisterClient registerClient = newRegisterClient(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + AbstractCryptoProvider cryptoProvider = createCryptoProviderWithAllowedNone(); + + List scopes = Arrays.asList("openid", "clientinfo"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest, SignatureAlgorithm.NONE, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ZONEINFO, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.LOCALE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_LOCALITY, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_REGION, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_POSTAL_CODE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NAME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.NAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.NICKNAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.GIVEN_NAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.FAMILY_NAME)); + assertNull(jwt.getClaims().getClaimAsString(JwtClaimName.EMAIL)); + assertNull(jwt.getClaims().getClaimAsString(JwtClaimName.EMAIL_VERIFIED)); + + PlainTextSignature signer = new PlainTextSignature(); + assertTrue(signer.validate(jwt)); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NICKNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_REGION)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestClaimsIndividuallyRequestObjectSigningAlgHS256UserInfoSignedResponseAlgHS256( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestClaimsIndividuallyRequestObjectSigningAlgHS256UserInfoSignedResponseAlgHS256"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.HS256); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.HS256); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.HS256); + registerRequest.setClaims(Arrays.asList( + JwtClaimName.NAME, + JwtClaimName.NICKNAME, + JwtClaimName.GIVEN_NAME, + JwtClaimName.FAMILY_NAME, + JwtClaimName.PICTURE, + JwtClaimName.ZONEINFO, + JwtClaimName.LOCALE, + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_POSTAL_CODE, + JwtClaimName.ADDRESS_COUNTRY)); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid", "clientinfo"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest, SignatureAlgorithm.HS256, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ZONEINFO, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.LOCALE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_LOCALITY, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_REGION, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_POSTAL_CODE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NAME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.NAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.NICKNAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.GIVEN_NAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.FAMILY_NAME)); + assertNull(jwt.getClaims().getClaimAsString(JwtClaimName.EMAIL)); + assertNull(jwt.getClaims().getClaimAsString(JwtClaimName.EMAIL_VERIFIED)); + + HMACSigner hmacSigner = new HMACSigner(SignatureAlgorithm.HS256, clientSecret); + assertTrue(hmacSigner.validate(jwt)); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NICKNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_REGION)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestClaimsIndividuallyRequestObjectSigningAlgHS384UserInfoSignedResponseAlgHS384( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestClaimsIndividuallyRequestObjectSigningAlgHS384UserInfoSignedResponseAlgHS384"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.HS384); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.HS384); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.HS384); + registerRequest.setClaims(Arrays.asList( + JwtClaimName.NAME, + JwtClaimName.NICKNAME, + JwtClaimName.GIVEN_NAME, + JwtClaimName.FAMILY_NAME, + JwtClaimName.PICTURE, + JwtClaimName.ZONEINFO, + JwtClaimName.LOCALE, + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_POSTAL_CODE, + JwtClaimName.ADDRESS_COUNTRY)); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid", "clientinfo"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest, SignatureAlgorithm.HS384, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ZONEINFO, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.LOCALE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_LOCALITY, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_REGION, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_POSTAL_CODE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NAME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.NAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.NICKNAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.GIVEN_NAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.FAMILY_NAME)); + assertNull(jwt.getClaims().getClaimAsString(JwtClaimName.EMAIL)); + assertNull(jwt.getClaims().getClaimAsString(JwtClaimName.EMAIL_VERIFIED)); + + HMACSigner hmacSigner = new HMACSigner(SignatureAlgorithm.HS384, clientSecret); + assertTrue(hmacSigner.validate(jwt)); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NICKNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_REGION)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestClaimsIndividuallyRequestObjectSigningAlgHS512UserInfoSignedResponseAlgHS512( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestClaimsIndividuallyRequestObjectSigningAlgHS512UserInfoSignedResponseAlgHS512"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.HS512); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.HS512); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.HS512); + registerRequest.setClaims(Arrays.asList( + JwtClaimName.NAME, + JwtClaimName.NICKNAME, + JwtClaimName.GIVEN_NAME, + JwtClaimName.FAMILY_NAME, + JwtClaimName.PICTURE, + JwtClaimName.ZONEINFO, + JwtClaimName.LOCALE, + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_POSTAL_CODE, + JwtClaimName.ADDRESS_COUNTRY)); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid", "clientinfo"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest, SignatureAlgorithm.HS512, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ZONEINFO, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.LOCALE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_LOCALITY, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_REGION, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_POSTAL_CODE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NAME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.NAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.NICKNAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.GIVEN_NAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.FAMILY_NAME)); + assertNull(jwt.getClaims().getClaimAsString(JwtClaimName.EMAIL)); + assertNull(jwt.getClaims().getClaimAsString(JwtClaimName.EMAIL_VERIFIED)); + + HMACSigner hmacSigner = new HMACSigner(SignatureAlgorithm.HS512, clientSecret); + assertTrue(hmacSigner.validate(jwt)); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NICKNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_REGION)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri", "RS256_keyId", + "dnName", "keyStoreFile", "keyStoreSecret", "clientJwksUri"}) + @Test + public void requestClaimsIndividuallyRequestObjectSigningAlgRS256UserInfoSignedResponseAlgRS256( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String clientJwksUri) throws Exception { + showTitle("requestClaimsIndividuallyRequestObjectSigningAlgRS256UserInfoSignedResponseAlgRS256"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS256); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.RS256); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.RS256); + registerRequest.setClaims(Arrays.asList( + JwtClaimName.NAME, + JwtClaimName.NICKNAME, + JwtClaimName.GIVEN_NAME, + JwtClaimName.FAMILY_NAME, + JwtClaimName.PICTURE, + JwtClaimName.ZONEINFO, + JwtClaimName.LOCALE, + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_POSTAL_CODE, + JwtClaimName.ADDRESS_COUNTRY)); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "clientinfo"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest, SignatureAlgorithm.RS256, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ZONEINFO, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.LOCALE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_LOCALITY, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_REGION, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_POSTAL_CODE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NAME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.NAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.NICKNAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.GIVEN_NAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.FAMILY_NAME)); + assertNull(jwt.getClaims().getClaimAsString(JwtClaimName.EMAIL)); + assertNull(jwt.getClaims().getClaimAsString(JwtClaimName.EMAIL_VERIFIED)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NICKNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_REGION)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri", "RS384_keyId", + "dnName", "keyStoreFile", "keyStoreSecret", "clientJwksUri"}) + @Test + public void requestClaimsIndividuallyRequestObjectSigningAlgRS384UserInfoSignedResponseAlgRS384( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String clientJwksUri) throws Exception { + showTitle("requestClaimsIndividuallyRequestObjectSigningAlgRS384UserInfoSignedResponseAlgRS384"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS384); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.RS384); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.RS384); + registerRequest.setClaims(Arrays.asList( + JwtClaimName.NAME, + JwtClaimName.NICKNAME, + JwtClaimName.GIVEN_NAME, + JwtClaimName.FAMILY_NAME, + JwtClaimName.PICTURE, + JwtClaimName.ZONEINFO, + JwtClaimName.LOCALE, + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_POSTAL_CODE, + JwtClaimName.ADDRESS_COUNTRY)); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "clientinfo"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest, SignatureAlgorithm.RS384, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ZONEINFO, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.LOCALE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_LOCALITY, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_REGION, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_POSTAL_CODE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NAME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.NAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.NICKNAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.GIVEN_NAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.FAMILY_NAME)); + assertNull(jwt.getClaims().getClaimAsString(JwtClaimName.EMAIL)); + assertNull(jwt.getClaims().getClaimAsString(JwtClaimName.EMAIL_VERIFIED)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS384, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NICKNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_REGION)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri", "RS512_keyId", + "dnName", "keyStoreFile", "keyStoreSecret", "clientJwksUri"}) + @Test + public void requestClaimsIndividuallyRequestObjectSigningAlgRS512UserInfoSignedResponseAlgRS512( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String clientJwksUri) throws Exception { + showTitle("requestClaimsIndividuallyRequestObjectSigningAlgRS512UserInfoSignedResponseAlgRS512"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS512); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.RS512); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.RS512); + registerRequest.setClaims(Arrays.asList( + JwtClaimName.NAME, + JwtClaimName.NICKNAME, + JwtClaimName.GIVEN_NAME, + JwtClaimName.FAMILY_NAME, + JwtClaimName.PICTURE, + JwtClaimName.ZONEINFO, + JwtClaimName.LOCALE, + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_POSTAL_CODE, + JwtClaimName.ADDRESS_COUNTRY)); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "clientinfo"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest, SignatureAlgorithm.RS512, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ZONEINFO, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.LOCALE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_LOCALITY, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_REGION, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_POSTAL_CODE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NAME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.NAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.NICKNAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.GIVEN_NAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.FAMILY_NAME)); + assertNull(jwt.getClaims().getClaimAsString(JwtClaimName.EMAIL)); + assertNull(jwt.getClaims().getClaimAsString(JwtClaimName.EMAIL_VERIFIED)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS512, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NICKNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_REGION)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri", "ES256_keyId", + "dnName", "keyStoreFile", "keyStoreSecret", "clientJwksUri"}) + @Test + public void requestClaimsIndividuallyRequestObjectSigningAlgES256UserInfoSignedResponseAlgES256( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String clientJwksUri) throws Exception { + showTitle("requestClaimsIndividuallyRequestObjectSigningAlgES256UserInfoSignedResponseAlgES256"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.ES256); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.ES256); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.ES256); + registerRequest.setClaims(Arrays.asList( + JwtClaimName.NAME, + JwtClaimName.NICKNAME, + JwtClaimName.GIVEN_NAME, + JwtClaimName.FAMILY_NAME, + JwtClaimName.PICTURE, + JwtClaimName.ZONEINFO, + JwtClaimName.LOCALE, + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_POSTAL_CODE, + JwtClaimName.ADDRESS_COUNTRY)); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "clientinfo"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest, SignatureAlgorithm.ES256, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ZONEINFO, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.LOCALE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_LOCALITY, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_REGION, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_POSTAL_CODE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NAME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.NAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.NICKNAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.GIVEN_NAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.FAMILY_NAME)); + assertNull(jwt.getClaims().getClaimAsString(JwtClaimName.EMAIL)); + assertNull(jwt.getClaims().getClaimAsString(JwtClaimName.EMAIL_VERIFIED)); + + ECDSAPublicKey publicKey = JwkClient.getECDSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + ECDSASigner ecdsaSigner = new ECDSASigner(SignatureAlgorithm.ES256, publicKey); + + assertTrue(ecdsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NICKNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_REGION)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri", "ES384_keyId", + "dnName", "keyStoreFile", "keyStoreSecret", "clientJwksUri"}) + @Test + public void requestClaimsIndividuallyRequestObjectSigningAlgES384UserInfoSignedResponseAlgES384( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String clientJwksUri) throws Exception { + showTitle("requestClaimsIndividuallyRequestObjectSigningAlgES384UserInfoSignedResponseAlgES384"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.ES384); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.ES384); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.ES384); + registerRequest.setClaims(Arrays.asList( + JwtClaimName.NAME, + JwtClaimName.NICKNAME, + JwtClaimName.GIVEN_NAME, + JwtClaimName.FAMILY_NAME, + JwtClaimName.PICTURE, + JwtClaimName.ZONEINFO, + JwtClaimName.LOCALE, + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_POSTAL_CODE, + JwtClaimName.ADDRESS_COUNTRY)); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "clientinfo"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest, SignatureAlgorithm.ES384, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ZONEINFO, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.LOCALE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_LOCALITY, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_REGION, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_POSTAL_CODE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NAME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.NAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.NICKNAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.GIVEN_NAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.FAMILY_NAME)); + assertNull(jwt.getClaims().getClaimAsString(JwtClaimName.EMAIL)); + assertNull(jwt.getClaims().getClaimAsString(JwtClaimName.EMAIL_VERIFIED)); + + ECDSAPublicKey publicKey = JwkClient.getECDSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + ECDSASigner ecdsaSigner = new ECDSASigner(SignatureAlgorithm.ES384, publicKey); + + assertTrue(ecdsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NICKNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_REGION)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri", "ES512_keyId", + "dnName", "keyStoreFile", "keyStoreSecret", "clientJwksUri"}) + @Test + public void requestClaimsIndividuallyRequestObjectSigningAlgES512UserInfoSignedResponseAlgES512( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String clientJwksUri) throws Exception { + showTitle("requestClaimsIndividuallyRequestObjectSigningAlgES512UserInfoSignedResponseAlgES512"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.ES512); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.ES512); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.ES512); + registerRequest.setClaims(Arrays.asList( + JwtClaimName.NAME, + JwtClaimName.NICKNAME, + JwtClaimName.GIVEN_NAME, + JwtClaimName.FAMILY_NAME, + JwtClaimName.PICTURE, + JwtClaimName.ZONEINFO, + JwtClaimName.LOCALE, + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_POSTAL_CODE, + JwtClaimName.ADDRESS_COUNTRY)); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "clientinfo"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest, SignatureAlgorithm.ES512, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ZONEINFO, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.LOCALE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_LOCALITY, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_REGION, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_POSTAL_CODE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NAME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.NAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.NICKNAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.GIVEN_NAME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.FAMILY_NAME)); + assertNull(jwt.getClaims().getClaimAsString(JwtClaimName.EMAIL)); + assertNull(jwt.getClaims().getClaimAsString(JwtClaimName.EMAIL_VERIFIED)); + + ECDSAPublicKey publicKey = JwkClient.getECDSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + ECDSASigner ecdsaSigner = new ECDSASigner(SignatureAlgorithm.ES512, publicKey); + + assertTrue(ecdsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NICKNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_REGION)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestClaimsIndividuallyRequestObjectEncryptionAlgA128KWEncA128GCMUserInfoEncryptedResponseAlgA128KWEncA128GCM( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestClaimsIndividuallyRequestObjectEncryptionAlgA128KWEncA128GCMUserInfoEncryptedResponseAlgA128KWEncA128GCM"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.A128KW); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A128GCM); + registerRequest.setRequestObjectEncryptionAlg(KeyEncryptionAlgorithm.A128KW); + registerRequest.setRequestObjectEncryptionEnc(BlockEncryptionAlgorithm.A128GCM); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.A128KW); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A128GCM); + registerRequest.setClaims(Arrays.asList( + JwtClaimName.NAME, + JwtClaimName.NICKNAME, + JwtClaimName.GIVEN_NAME, + JwtClaimName.FAMILY_NAME, + JwtClaimName.PICTURE, + JwtClaimName.ZONEINFO, + JwtClaimName.LOCALE, + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_POSTAL_CODE, + JwtClaimName.ADDRESS_COUNTRY)); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "clientinfo"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest, + KeyEncryptionAlgorithm.A128KW, + BlockEncryptionAlgorithm.A128GCM, + clientSecret); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ZONEINFO, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.LOCALE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_LOCALITY, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_REGION, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_POSTAL_CODE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NAME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwe jwe = Jwe.parse(idToken, null, clientSecret.getBytes(Util.UTF8_STRING_ENCODING)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.NAME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.NICKNAME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.GIVEN_NAME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.FAMILY_NAME)); + assertNull(jwe.getClaims().getClaimAsString(JwtClaimName.EMAIL)); + assertNull(jwe.getClaims().getClaimAsString(JwtClaimName.EMAIL_VERIFIED)); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NICKNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_REGION)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestClaimsIndividuallyRequestObjectEncryptionAlgA256KWEncA256GCMUserInfoEncryptedResponseAlgA256KWEncA256GCM( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestClaimsIndividuallyRequestObjectEncryptionAlgA256KWEncA256GCMUserInfoEncryptedResponseAlgA256KWEncA256GCM"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.A256KW); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A256GCM); + registerRequest.setRequestObjectEncryptionAlg(KeyEncryptionAlgorithm.A256KW); + registerRequest.setRequestObjectEncryptionEnc(BlockEncryptionAlgorithm.A256GCM); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.A256KW); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A256GCM); + registerRequest.setClaims(Arrays.asList( + JwtClaimName.NAME, + JwtClaimName.NICKNAME, + JwtClaimName.GIVEN_NAME, + JwtClaimName.FAMILY_NAME, + JwtClaimName.PICTURE, + JwtClaimName.ZONEINFO, + JwtClaimName.LOCALE, + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_POSTAL_CODE, + JwtClaimName.ADDRESS_COUNTRY)); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "clientinfo"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest, + KeyEncryptionAlgorithm.A256KW, + BlockEncryptionAlgorithm.A256GCM, + clientSecret); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ZONEINFO, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.LOCALE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_LOCALITY, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_REGION, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_POSTAL_CODE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NAME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwe jwe = Jwe.parse(idToken, null, clientSecret.getBytes(Util.UTF8_STRING_ENCODING)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.NAME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.NICKNAME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.GIVEN_NAME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.FAMILY_NAME)); + assertNull(jwe.getClaims().getClaimAsString(JwtClaimName.EMAIL)); + assertNull(jwe.getClaims().getClaimAsString(JwtClaimName.EMAIL_VERIFIED)); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NICKNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_REGION)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri", "dnName", "keyStoreFile", + "keyStoreSecret", "RSA1_5_keyId", "clientJwksUri"}) + @Test + public void requestClaimsIndividuallyRequestObjectEncryptionAlgRSA1_5EncA128CBC_PLUS_HS256UserInfoEncryptedResponseAlgRSA1_5EncA128CBC_PLUS_HS256( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String clientKeyId, final String clientJwksUri) throws Exception { + showTitle("requestClaimsIndividuallyRequestObjectEncryptionAlgRSA1_5EncA128CBC_PLUS_HS256UserInfoEncryptedResponseAlgRSA1_5EncA128CBC_PLUS_HS256"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A128CBC_PLUS_HS256); + registerRequest.setRequestObjectEncryptionAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setRequestObjectEncryptionEnc(BlockEncryptionAlgorithm.A128CBC_PLUS_HS256); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A128CBC_PLUS_HS256); + registerRequest.setClaims(Arrays.asList( + JwtClaimName.NAME, + JwtClaimName.NICKNAME, + JwtClaimName.GIVEN_NAME, + JwtClaimName.FAMILY_NAME, + JwtClaimName.PICTURE, + JwtClaimName.ZONEINFO, + JwtClaimName.LOCALE, + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_POSTAL_CODE, + JwtClaimName.ADDRESS_COUNTRY)); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Choose encryption key + JwkClient jwkClient = new JwkClient(jwksUri); + JwkResponse jwkResponse = jwkClient.exec(); + String serverKeyId = jwkResponse.getKeyId(Algorithm.RSA1_5); + assertNotNull(serverKeyId); + + // 3. Request authorization + JSONObject jwks = JwtUtil.getJSONWebKeys(jwksUri); + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "clientinfo"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, + KeyEncryptionAlgorithm.RSA1_5, BlockEncryptionAlgorithm.A128CBC_PLUS_HS256, cryptoProvider); + jwtAuthorizationRequest.setKeyId(serverKeyId); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ZONEINFO, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.LOCALE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_LOCALITY, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_REGION, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_POSTAL_CODE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NAME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(jwks); + authorizationRequest.setRequest(authJwt); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 4. Validate id_token + PrivateKey privateKey = cryptoProvider.getPrivateKey(clientKeyId); + + Jwe jwe = Jwe.parse(idToken, privateKey, null); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.NAME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.NICKNAME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.GIVEN_NAME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.FAMILY_NAME)); + assertNull(jwe.getClaims().getClaimAsString(JwtClaimName.EMAIL)); + assertNull(jwe.getClaims().getClaimAsString(JwtClaimName.EMAIL_VERIFIED)); + + // 5. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setPrivateKey(privateKey); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NICKNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_REGION)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri", "dnName", "keyStoreFile", + "keyStoreSecret", "RSA1_5_keyId", "clientJwksUri"}) + @Test + public void requestClaimsIndividuallyRequestObjectEncryptionAlgRSA1_5EncA256CBC_PLUS_HS512UserInfoEncryptedResponseAlgRSA1_5EncA256CBC_PLUS_HS512( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String clientKeyId, final String clientJwksUri) throws Exception { + showTitle("requestClaimsIndividuallyRequestObjectEncryptionAlgRSA1_5EncA256CBC_PLUS_HS512UserInfoEncryptedResponseAlgRSA1_5EncA256CBC_PLUS_HS512"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A256CBC_PLUS_HS512); + registerRequest.setRequestObjectEncryptionAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setRequestObjectEncryptionEnc(BlockEncryptionAlgorithm.A256CBC_PLUS_HS512); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A256CBC_PLUS_HS512); + registerRequest.setClaims(Arrays.asList( + JwtClaimName.NAME, + JwtClaimName.NICKNAME, + JwtClaimName.GIVEN_NAME, + JwtClaimName.FAMILY_NAME, + JwtClaimName.PICTURE, + JwtClaimName.ZONEINFO, + JwtClaimName.LOCALE, + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_POSTAL_CODE, + JwtClaimName.ADDRESS_COUNTRY)); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Choose encryption key + JwkClient jwkClient = new JwkClient(jwksUri); + JwkResponse jwkResponse = jwkClient.exec(); + String serverKeyId = jwkResponse.getKeyId(Algorithm.RSA1_5); + assertNotNull(serverKeyId); + + // 3. Request authorization + JSONObject jwks = JwtUtil.getJSONWebKeys(jwksUri); + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "clientinfo"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, + KeyEncryptionAlgorithm.RSA1_5, BlockEncryptionAlgorithm.A256CBC_PLUS_HS512, cryptoProvider); + jwtAuthorizationRequest.setKeyId(serverKeyId); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ZONEINFO, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.LOCALE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_LOCALITY, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_REGION, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_POSTAL_CODE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NAME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(jwks); + authorizationRequest.setRequest(authJwt); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 4. Validate id_token + PrivateKey privateKey = cryptoProvider.getPrivateKey(clientKeyId); + + Jwe jwe = Jwe.parse(idToken, privateKey, null); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.NAME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.NICKNAME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.GIVEN_NAME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.FAMILY_NAME)); + assertNull(jwe.getClaims().getClaimAsString(JwtClaimName.EMAIL)); + assertNull(jwe.getClaims().getClaimAsString(JwtClaimName.EMAIL_VERIFIED)); + + // 5. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setPrivateKey(privateKey); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NICKNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_REGION)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri", "dnName", "keyStoreFile", + "keyStoreSecret", "RSA_OAEP_keyId", "clientJwksUri"}) + @Test + public void requestClaimsIndividuallyRequestObjectEncryptionAlgRSA_OAEPEncA256GCMUserInfoEncryptedResponseAlgRSA_OAEPEncA256GCM( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String clientKeyId, final String clientJwksUri) throws Exception { + showTitle("requestClaimsIndividuallyRequestObjectEncryptionAlgRSA_OAEPEncA256GCMUserInfoEncryptedResponseAlgRSA_OAEPEncA256GCM"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA_OAEP); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A256GCM); + registerRequest.setRequestObjectEncryptionAlg(KeyEncryptionAlgorithm.RSA_OAEP); + registerRequest.setRequestObjectEncryptionEnc(BlockEncryptionAlgorithm.A256GCM); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA_OAEP); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A256GCM); + registerRequest.setClaims(Arrays.asList( + JwtClaimName.NAME, + JwtClaimName.NICKNAME, + JwtClaimName.GIVEN_NAME, + JwtClaimName.FAMILY_NAME, + JwtClaimName.PICTURE, + JwtClaimName.ZONEINFO, + JwtClaimName.LOCALE, + JwtClaimName.ADDRESS_STREET_ADDRESS, + JwtClaimName.ADDRESS_LOCALITY, + JwtClaimName.ADDRESS_REGION, + JwtClaimName.ADDRESS_POSTAL_CODE, + JwtClaimName.ADDRESS_COUNTRY)); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Choose encryption key + JwkClient jwkClient = new JwkClient(jwksUri); + JwkResponse jwkResponse = jwkClient.exec(); + String serverKeyId = jwkResponse.getKeyId(Algorithm.RSA_OAEP); + assertNotNull(serverKeyId); + + // 3. Request authorization + JSONObject jwks = JwtUtil.getJSONWebKeys(jwksUri); + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "clientinfo"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, + KeyEncryptionAlgorithm.RSA_OAEP, BlockEncryptionAlgorithm.A256GCM, cryptoProvider); + jwtAuthorizationRequest.setKeyId(serverKeyId); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ZONEINFO, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.LOCALE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_STREET_ADDRESS, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_LOCALITY, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_REGION, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_POSTAL_CODE, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.ADDRESS_COUNTRY, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NAME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.GIVEN_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.FAMILY_NAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(jwks); + authorizationRequest.setRequest(authJwt); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 4. Validate id_token + PrivateKey privateKey = cryptoProvider.getPrivateKey(clientKeyId); + + Jwe jwe = Jwe.parse(idToken, privateKey, null); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.NAME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.NICKNAME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.GIVEN_NAME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.FAMILY_NAME)); + assertNull(jwe.getClaims().getClaimAsString(JwtClaimName.EMAIL)); + assertNull(jwe.getClaims().getClaimAsString(JwtClaimName.EMAIL_VERIFIED)); + + // 5. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setPrivateKey(privateKey); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NICKNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNull(userInfoResponse.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_STREET_ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_LOCALITY)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_REGION)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS_COUNTRY)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/IntrospectionWsHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/IntrospectionWsHttpTest.java new file mode 100644 index 00000000..0e77b37f --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/IntrospectionWsHttpTest.java @@ -0,0 +1,77 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.BaseRequest; +import org.gluu.oxauth.client.service.ClientFactory; +import org.gluu.oxauth.client.service.IntrospectionService; +import org.gluu.oxauth.client.uma.wrapper.UmaClient; +import org.gluu.oxauth.model.common.IntrospectionResponse; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.uma.wrapper.Token; +import org.jboss.resteasy.client.jaxrs.ClientHttpEngine; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 17/09/2013 + */ + +public class IntrospectionWsHttpTest extends BaseTest { + + @Test + @Parameters({"umaPatClientId", "umaPatClientSecret"}) + public void bearer(final String umaPatClientId, final String umaPatClientSecret) throws Exception { + final Token authorization = UmaClient.requestPat(tokenEndpoint, umaPatClientId, umaPatClientSecret); + final Token tokenToIntrospect = UmaClient.requestPat(tokenEndpoint, umaPatClientId, umaPatClientSecret); + + final IntrospectionService introspectionService = ClientFactory.instance().createIntrospectionService(introspectionEndpoint); + final IntrospectionResponse introspectionResponse = introspectionService.introspectToken("Bearer " + authorization.getAccessToken(), tokenToIntrospect.getAccessToken()); + assertTrue(introspectionResponse != null && introspectionResponse.isActive()); + } + + @Test + @Parameters({"umaPatClientId", "umaPatClientSecret"}) + public void bearerWithResponseAsJwt(final String umaPatClientId, final String umaPatClientSecret) throws Exception { + final ClientHttpEngine engine = clientEngine(true); + final Token authorization = UmaClient.requestPat(tokenEndpoint, umaPatClientId, umaPatClientSecret, engine); + final Token tokenToIntrospect = UmaClient.requestPat(tokenEndpoint, umaPatClientId, umaPatClientSecret, engine); + + final IntrospectionService introspectionService = ClientFactory.instance().createIntrospectionService(introspectionEndpoint, engine); + final String jwtAsString = introspectionService.introspectTokenWithResponseAsJwt("Bearer " + authorization.getAccessToken(), tokenToIntrospect.getAccessToken(), true); + final Jwt jwt = Jwt.parse(jwtAsString); + assertTrue(Boolean.parseBoolean(jwt.getClaims().getClaimAsString("active"))); + } + + @Test + @Parameters({"umaPatClientId", "umaPatClientSecret"}) + public void basicAuthentication(final String umaPatClientId, final String umaPatClientSecret) throws Exception { + final Token tokenToIntrospect = UmaClient.requestPat(tokenEndpoint, umaPatClientId, umaPatClientSecret, clientEngine(true)); + + final IntrospectionService introspectionService = ClientFactory.instance().createIntrospectionService(introspectionEndpoint, clientEngine(true)); + final IntrospectionResponse introspectionResponse = introspectionService.introspectToken("Basic " + BaseRequest.getEncodedCredentials(umaPatClientId, umaPatClientSecret), tokenToIntrospect.getAccessToken()); + assertTrue(introspectionResponse != null && introspectionResponse.isActive()); + } + + @Test + @Parameters({"umaPatClientId", "umaPatClientSecret"}) + public void introspectWithValidAuthorizationButInvalidTokenShouldReturnActiveFalse(final String umaPatClientId, final String umaPatClientSecret) throws Exception { + final Token authorization = UmaClient.requestPat(tokenEndpoint, umaPatClientId, umaPatClientSecret, clientEngine(true)); + + final IntrospectionService introspectionService = ClientFactory.instance().createIntrospectionService(introspectionEndpoint, clientEngine(true)); + final IntrospectionResponse introspectionResponse = introspectionService.introspectToken("Bearer " + authorization.getAccessToken(), "invalid_token"); + assertNotNull(introspectionResponse); + assertFalse(introspectionResponse.isActive()); + } + +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/JwkRestWebServiceHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/JwkRestWebServiceHttpTest.java new file mode 100644 index 00000000..65e2cf9c --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/JwkRestWebServiceHttpTest.java @@ -0,0 +1,72 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.JwkResponse; +import org.gluu.oxauth.model.jwk.JSONWebKey; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * Functional tests for JWK Web Services (HTTP) + * + * @author Javier Rojas Blum + * @version June 25, 2016 + */ +public class JwkRestWebServiceHttpTest extends BaseTest { + + @Test + public void requestJwks() throws Exception { + showTitle("requestJwks"); + + JwkClient jwkClient = new JwkClient(jwksUri); + JwkResponse response = jwkClient.exec(); + + showClient(jwkClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getEntity(), "Unexpected result: entity is null"); + assertNotNull(response.getJwks(), "Unexpected result: jwks is null"); + assertNotNull(response.getJwks().getKeys(), "Unexpected result: keys is null"); + assertTrue(response.getJwks().getKeys().size() > 0, "Unexpected result: keys is empty"); + + for (JSONWebKey JSONWebKey : response.getJwks().getKeys()) { + assertNotNull(JSONWebKey.getKid(), "Unexpected result: kid is null"); + assertNotNull(JSONWebKey.getUse(), "Unexpected result: use is null"); + assertNotNull(JSONWebKey.getAlg(), "Unexpected result: alg is null"); + } + //assertEquals(response.getJwks().getKeys().size(), 11, "The list of keys are not all that could be supported."); + } + + @Parameters({"clientJwksUri"}) + @Test + public void requestClientJwks(final String clientJwksUri) throws Exception { + showTitle("requestJwks"); + + JwkClient jwkClient = new JwkClient(clientJwksUri); + JwkResponse response = jwkClient.exec(); + + showClient(jwkClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getEntity(), "Unexpected result: entity is null"); + assertNotNull(response.getJwks(), "Unexpected result: jwks is null"); + assertNotNull(response.getJwks().getKeys(), "Unexpected result: keys is null"); + assertTrue(response.getJwks().getKeys().size() > 0, "Unexpected result: keys is empty"); + + for (JSONWebKey JSONWebKey : response.getJwks().getKeys()) { + assertNotNull(JSONWebKey.getKid(), "Unexpected result: kid is null"); + assertNotNull(JSONWebKey.getUse(), "Unexpected result: use is null"); + assertNotNull(JSONWebKey.getAlg(), "Unexpected result: alg is null"); + } + //assertEquals(response.getJwks().getKeys().size(), 11, "The list of keys are not all that could be supported."); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/MultiStepAuthorizationCodeFlowHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/MultiStepAuthorizationCodeFlowHttpTest.java new file mode 100644 index 00000000..6325a934 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/MultiStepAuthorizationCodeFlowHttpTest.java @@ -0,0 +1,135 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.common.SubjectType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * Test cases for the multi step authorization code flow (HTTP) + * + * @author Yuriy Movchan + * @version November 29, 2017 + */ +public class MultiStepAuthorizationCodeFlowHttpTest extends BaseTest { + + /** + * Test for the complete multi-step Authorization Code Flow. + */ + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void authorizationMultiStepCodeFlow( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationMultiStepCodeFlow"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + List scopes = Arrays.asList("openid", "profile", "address", "email", "phone", "user_name"); + + // 1. Register client + RegisterResponse registerResponse = registerClient(redirectUris, responseTypes, scopes, sectorIdentifierUri); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization and receive the authorization code. + String nonce = UUID.randomUUID().toString(); + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, scopes, clientId, nonce, 2); + + String authorizationCode = authorizationResponse.getCode(); + + assertNotNull(authorizationCode, "The authorization code is null"); + assertNotNull(authorizationResponse.getScope(), "The authorization scope is null"); + assertNotNull(authorizationResponse.getIdToken(), "The authorization id_token is null"); + + // 3. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient1 = new TokenClient(tokenEndpoint); + tokenClient1.setRequest(tokenRequest); + TokenResponse tokenResponse1 = tokenClient1.exec(); + + showClient(tokenClient1); + assertEquals(tokenResponse1.getStatus(), 200, "Unexpected response code: " + tokenResponse1.getStatus()); + assertNotNull(tokenResponse1.getEntity(), "The entity is null"); + assertNotNull(tokenResponse1.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse1.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse1.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse1.getRefreshToken(), "The refresh token is null"); + } + + private AuthorizationResponse requestAuthorization(final String userId, final String userSecret, final String redirectUri, + List responseTypes, List scopes, String clientId, String nonce, int countAuthzSteps) { + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret, + true, false, countAuthzSteps); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + return authorizationResponse; + } + + private RegisterResponse registerClient( + final String redirectUris, List responseTypes, List scopes, String sectorIdentifierUri) { + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setScope(scopes); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + return registerResponse; + } + +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/MultivaluedClaims.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/MultivaluedClaims.java new file mode 100644 index 00000000..1a7e103c --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/MultivaluedClaims.java @@ -0,0 +1,3529 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.security.PrivateKey; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.JwkResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoRequest; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.client.model.authorize.Claim; +import org.gluu.oxauth.client.model.authorize.ClaimValue; +import org.gluu.oxauth.client.model.authorize.JwtAuthorizationRequest; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.AbstractCryptoProvider; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.encryption.BlockEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.encryption.KeyEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.signature.ECDSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jwe.Jwe; +import org.gluu.oxauth.model.jwk.Algorithm; +import org.gluu.oxauth.model.jws.ECDSASigner; +import org.gluu.oxauth.model.jws.HMACSigner; +import org.gluu.oxauth.model.jws.PlainTextSignature; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.JwtUtil; +import org.gluu.oxauth.model.util.StringUtils; +import org.gluu.oxauth.model.util.Util; +import org.json.JSONObject; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * Note: In order to run this tests, set legacyIdTokenClaims to true. + * + * @author Javier Rojas Blum + * @version March 8, 2019 + */ +public class MultivaluedClaims extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void authorizationRequestWithMultivaluedClaimNone( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestWithMultivaluedClaimNone"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.NONE); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.NONE); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "test"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwt.getClaims().getClaimAsStringList("member_of").size() > 1); + + PlainTextSignature signer = new PlainTextSignature(); + assertTrue(signer.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void authorizationRequestWithMultivaluedClaimHS256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestWithMultivaluedClaimHS256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.HS256); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.HS256); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "test"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwt.getClaims().getClaimAsStringList("member_of").size() > 1); + + HMACSigner hmacSigner = new HMACSigner(SignatureAlgorithm.HS256, clientSecret); + assertTrue(hmacSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void authorizationRequestWithMultivaluedClaimHS384( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestWithMultivaluedClaimHS384"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.HS384); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.HS384); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "test"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwt.getClaims().getClaimAsStringList("member_of").size() > 1); + + HMACSigner hmacSigner = new HMACSigner(SignatureAlgorithm.HS384, clientSecret); + assertTrue(hmacSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void authorizationRequestWithMultivaluedClaimHS512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestWithMultivaluedClaimHS512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.HS512); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.HS512); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "test"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwt.getClaims().getClaimAsStringList("member_of").size() > 1); + + HMACSigner hmacSigner = new HMACSigner(SignatureAlgorithm.HS512, clientSecret); + assertTrue(hmacSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void authorizationRequestWithMultivaluedClaimRS256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestWithMultivaluedClaimRS256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.RS256); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "test"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwt.getClaims().getClaimAsStringList("member_of").size() > 1); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void authorizationRequestWithMultivaluedClaimRS384( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestWithMultivaluedClaimRS384"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS384); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.RS384); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "test"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwt.getClaims().getClaimAsStringList("member_of").size() > 1); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS384, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void authorizationRequestWithMultivaluedClaimRS512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestWithMultivaluedClaimRS512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS512); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.RS512); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "test"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwt.getClaims().getClaimAsStringList("member_of").size() > 1); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS512, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void authorizationRequestWithMultivaluedClaimES256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestWithMultivaluedClaimES256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.ES256); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.ES256); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "test"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwt.getClaims().getClaimAsStringList("member_of").size() > 1); + + ECDSAPublicKey publicKey = JwkClient.getECDSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + ECDSASigner ecdsaSigner = new ECDSASigner(SignatureAlgorithm.ES256, publicKey); + + assertTrue(ecdsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void authorizationRequestWithMultivaluedClaimES384( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestWithMultivaluedClaimES384"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.ES384); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.ES384); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "test"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwt.getClaims().getClaimAsStringList("member_of").size() > 1); + + ECDSAPublicKey publicKey = JwkClient.getECDSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + ECDSASigner ecdsaSigner = new ECDSASigner(SignatureAlgorithm.ES384, publicKey); + + assertTrue(ecdsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void authorizationRequestWithMultivaluedClaimES512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestWithMultivaluedClaimES512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.ES512); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.ES512); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "test"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwt.getClaims().getClaimAsStringList("member_of").size() > 1); + + ECDSAPublicKey publicKey = JwkClient.getECDSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + ECDSASigner ecdsaSigner = new ECDSASigner(SignatureAlgorithm.ES512, publicKey); + + assertTrue(ecdsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void authorizationRequestWithMultivaluedClaimPS256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestWithMultivaluedClaimPS256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.PS256); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.PS256); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "test"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwt.getClaims().getClaimAsStringList("member_of").size() > 1); + + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.PS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void authorizationRequestWithMultivaluedClaimPS384( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestWithMultivaluedClaimPS384"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.PS384); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.PS384); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "test"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwt.getClaims().getClaimAsStringList("member_of").size() > 1); + + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.PS384, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void authorizationRequestWithMultivaluedClaimPS512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestWithMultivaluedClaimPS512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.PS512); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.PS512); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "test"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwt.getClaims().getClaimAsStringList("member_of").size() > 1); + + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.PS512, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void authorizationRequestWithMultivaluedClaimAlgA128KWEncA128GCM( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestWithMultivaluedClaimAlgA128KWEncA128GCM"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.A128KW); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A128GCM); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.A128KW); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A128GCM); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "test"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwe jwe = Jwe.parse(idToken, null, clientSecret.getBytes(Util.UTF8_STRING_ENCODING)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwe.getClaims().getClaimAsStringList("member_of").size() > 1); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void authorizationRequestWithMultivaluedClaimAlgA256KWEncA256GCM( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestWithMultivaluedClaimAlgA256KWEncA256GCM"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.A256KW); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A256GCM); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.A256KW); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A256GCM); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "test"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwe jwe = Jwe.parse(idToken, null, clientSecret.getBytes(Util.UTF8_STRING_ENCODING)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwe.getClaims().getClaimAsStringList("member_of").size() > 1); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", + "clientJwksUri", "RSA1_5_keyId", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void authorizationRequestWithMultivaluedClaimAlgRSA15EncA128CBCPLUSHS256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String clientJwksUri, final String keyId, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestWithMultivaluedClaimAlgRSA15EncA128CBCPLUSHS256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A128CBC_PLUS_HS256); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A128CBC_PLUS_HS256); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "test"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, null); + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + + Jwe jwe = Jwe.parse(idToken, privateKey, null); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwe.getClaims().getClaimAsStringList("member_of").size() > 1); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setPrivateKey(privateKey); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", + "clientJwksUri", "RSA1_5_keyId", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void authorizationRequestWithMultivaluedClaimAlgRSA15EncA256CBCPLUSHS512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String clientJwksUri, final String keyId, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestWithMultivaluedClaimAlgRSA15EncA256CBCPLUSHS512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A256CBC_PLUS_HS512); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A256CBC_PLUS_HS512); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "test"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, null); + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + + Jwe jwe = Jwe.parse(idToken, privateKey, null); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwe.getClaims().getClaimAsStringList("member_of").size() > 1); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setPrivateKey(privateKey); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", + "clientJwksUri", "RSA_OAEP_keyId", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void authorizationRequestWithMultivaluedClaimAlgRSAOAEPEncA256GCM( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String clientJwksUri, final String keyId, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestWithMultivaluedClaimAlgRSAOAEPEncA256GCM"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA_OAEP); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A256GCM); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA_OAEP); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A256GCM); + registerRequest.addCustomAttribute("oxIncludeClaimsInIdToken", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "test"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, null); + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + + Jwe jwe = Jwe.parse(idToken, privateKey, null); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwe.getClaims().getClaimAsStringList("member_of").size() > 1); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setPrivateKey(privateKey); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void authorizationRequestObjectWithMultivaluedClaimNone( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestObjectWithMultivaluedClaimNone"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.NONE); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.NONE); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.NONE); + registerRequest.setClaims(Arrays.asList("member_of")); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + AbstractCryptoProvider cryptoProvider = createCryptoProviderWithAllowedNone(); + + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.NONE, null, cryptoProvider); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim("member_of", ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim("member_of", ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwt.getClaims().getClaimAsStringList("member_of").size() > 1); + + PlainTextSignature signer = new PlainTextSignature(); + assertTrue(signer.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void authorizationRequestObjectWithMultivaluedClaimHS256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestObjectWithMultivaluedClaimHS256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.HS256); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.HS256); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.HS256); + registerRequest.setClaims(Arrays.asList("member_of")); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.HS256, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim("member_of", ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim("member_of", ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwt.getClaims().getClaimAsStringList("member_of").size() > 1); + + HMACSigner hmacSigner = new HMACSigner(SignatureAlgorithm.HS256, clientSecret); + assertTrue(hmacSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void authorizationRequestObjectWithMultivaluedClaimHS384( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestObjectWithMultivaluedClaimHS384"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.HS384); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.HS384); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.HS384); + registerRequest.setClaims(Arrays.asList("member_of")); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.HS384, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim("member_of", ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim("member_of", ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwt.getClaims().getClaimAsStringList("member_of").size() > 1); + + HMACSigner hmacSigner = new HMACSigner(SignatureAlgorithm.HS384, clientSecret); + assertTrue(hmacSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void authorizationRequestObjectWithMultivaluedClaimHS512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestObjectWithMultivaluedClaimHS512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.HS512); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.HS512); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.HS512); + registerRequest.setClaims(Arrays.asList("member_of")); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.HS512, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim("member_of", ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim("member_of", ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwt.getClaims().getClaimAsStringList("member_of").size() > 1); + + HMACSigner hmacSigner = new HMACSigner(SignatureAlgorithm.HS512, clientSecret); + assertTrue(hmacSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri", "RS256_keyId", "clientJwksUri"}) + @Test + public void authorizationRequestObjectWithMultivaluedClaimRS256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri, final String keyId, final String clientJwksUri) throws Exception { + showTitle("authorizationRequestObjectWithMultivaluedClaimRS256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS256); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.RS256); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.RS256); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setClaims(Arrays.asList("member_of")); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.RS256, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim("member_of", ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim("member_of", ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwt.getClaims().getClaimAsStringList("member_of").size() > 1); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri", "RS384_keyId", "clientJwksUri"}) + @Test + public void authorizationRequestObjectWithMultivaluedClaimRS384( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri, final String keyId, final String clientJwksUri) throws Exception { + showTitle("authorizationRequestObjectWithMultivaluedClaimRS384"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS384); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.RS384); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.RS384); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setClaims(Arrays.asList("member_of")); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.RS384, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim("member_of", ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim("member_of", ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwt.getClaims().getClaimAsStringList("member_of").size() > 1); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS384, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri", "RS512_keyId", "clientJwksUri"}) + @Test + public void authorizationRequestObjectWithMultivaluedClaimRS512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri, final String keyId, final String clientJwksUri) throws Exception { + showTitle("authorizationRequestObjectWithMultivaluedClaimRS512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS512); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.RS512); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.RS512); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setClaims(Arrays.asList("member_of")); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.RS512, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim("member_of", ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim("member_of", ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwt.getClaims().getClaimAsStringList("member_of").size() > 1); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS512, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri", "ES256_keyId", "clientJwksUri"}) + @Test + public void authorizationRequestObjectWithMultivaluedClaimES256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri, final String keyId, final String clientJwksUri) throws Exception { + showTitle("authorizationRequestObjectWithMultivaluedClaimES256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.ES256); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.ES256); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.ES256); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setClaims(Arrays.asList("member_of")); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.ES256, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim("member_of", ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim("member_of", ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwt.getClaims().getClaimAsStringList("member_of").size() > 1); + + ECDSAPublicKey publicKey = JwkClient.getECDSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + ECDSASigner ecdsaSigner = new ECDSASigner(SignatureAlgorithm.ES256, publicKey); + + assertTrue(ecdsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri", "ES384_keyId", "clientJwksUri"}) + @Test + public void authorizationRequestObjectWithMultivaluedClaimES384( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri, final String keyId, final String clientJwksUri) throws Exception { + showTitle("authorizationRequestObjectWithMultivaluedClaimES384"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.ES384); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.ES384); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.ES384); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setClaims(Arrays.asList("member_of")); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.ES384, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim("member_of", ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim("member_of", ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwt.getClaims().getClaimAsStringList("member_of").size() > 1); + + ECDSAPublicKey publicKey = JwkClient.getECDSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + ECDSASigner ecdsaSigner = new ECDSASigner(SignatureAlgorithm.ES384, publicKey); + + assertTrue(ecdsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri", "ES512_keyId", "clientJwksUri"}) + @Test + public void authorizationRequestObjectWithMultivaluedClaimES512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri, final String keyId, final String clientJwksUri) throws Exception { + showTitle("authorizationRequestObjectWithMultivaluedClaimES512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.ES512); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.ES512); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.ES512); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setClaims(Arrays.asList("member_of")); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.ES512, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim("member_of", ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim("member_of", ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwt.getClaims().getClaimAsStringList("member_of").size() > 1); + + ECDSAPublicKey publicKey = JwkClient.getECDSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + ECDSASigner ecdsaSigner = new ECDSASigner(SignatureAlgorithm.ES512, publicKey); + + assertTrue(ecdsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri", "PS256_keyId", "clientJwksUri"}) + @Test + public void authorizationRequestObjectWithMultivaluedClaimPS256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri, final String keyId, final String clientJwksUri) throws Exception { + showTitle("authorizationRequestObjectWithMultivaluedClaimPS256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.PS256); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.PS256); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.PS256); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setClaims(Arrays.asList("member_of")); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.PS256, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim("member_of", ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim("member_of", ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwt.getClaims().getClaimAsStringList("member_of").size() > 1); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.PS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri", "PS384_keyId", "clientJwksUri"}) + @Test + public void authorizationRequestObjectWithMultivaluedClaimPS384( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri, final String keyId, final String clientJwksUri) throws Exception { + showTitle("authorizationRequestObjectWithMultivaluedClaimPS384"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.PS384); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.PS384); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.PS384); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setClaims(Arrays.asList("member_of")); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.PS384, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim("member_of", ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim("member_of", ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwt.getClaims().getClaimAsStringList("member_of").size() > 1); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.PS384, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri", "PS512_keyId", "clientJwksUri"}) + @Test + public void authorizationRequestObjectWithMultivaluedClaimPS512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri, final String keyId, final String clientJwksUri) throws Exception { + showTitle("authorizationRequestObjectWithMultivaluedClaimPS512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.PS512); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.PS512); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.PS512); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setClaims(Arrays.asList("member_of")); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.PS512, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim("member_of", ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim("member_of", ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwt.getClaims().getClaimAsStringList("member_of").size() > 1); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.PS512, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void authorizationRequestObjectWithMultivaluedClaimAlgA128KWEncA128GCM( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestObjectWithMultivaluedClaimAlgA128KWEncA128GCM"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.A128KW); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A128GCM); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.A128KW); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A128GCM); + registerRequest.setRequestObjectEncryptionAlg(KeyEncryptionAlgorithm.A128KW); + registerRequest.setRequestObjectEncryptionEnc(BlockEncryptionAlgorithm.A128GCM); + registerRequest.setClaims(Arrays.asList("member_of")); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest, + KeyEncryptionAlgorithm.A128KW, + BlockEncryptionAlgorithm.A128GCM, + clientSecret); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim("member_of", ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim("member_of", ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwe jwe = Jwe.parse(idToken, null, clientSecret.getBytes(Util.UTF8_STRING_ENCODING)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwe.getClaims().getClaimAsStringList("member_of").size() > 1); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void authorizationRequestObjectWithMultivaluedClaimAlgA256KWEncA256GCM( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestObjectWithMultivaluedClaimAlgA256KWEncA256GCM"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.A256KW); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A256GCM); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.A256KW); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A256GCM); + registerRequest.setRequestObjectEncryptionAlg(KeyEncryptionAlgorithm.A256KW); + registerRequest.setRequestObjectEncryptionEnc(BlockEncryptionAlgorithm.A256GCM); + registerRequest.setClaims(Arrays.asList("member_of")); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest, + KeyEncryptionAlgorithm.A256KW, + BlockEncryptionAlgorithm.A256GCM, + clientSecret); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim("member_of", ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim("member_of", ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Validate id_token + Jwe jwe = Jwe.parse(idToken, null, clientSecret.getBytes(Util.UTF8_STRING_ENCODING)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwe.getClaims().getClaimAsStringList("member_of").size() > 1); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", + "dnName", "keyStoreFile", "keyStoreSecret", "RSA1_5_keyId", + "clientJwksUri", "sectorIdentifierUri"}) + @Test + public void authorizationRequestObjectWithMultivaluedClaimAlgRSA15EncA128CBCPLUSHS256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String clientKeyId, + final String clientJwksUri, final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestObjectWithMultivaluedClaimAlgRSA15EncA128CBCPLUSHS256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A128CBC_PLUS_HS256); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A128CBC_PLUS_HS256); + registerRequest.setRequestObjectEncryptionAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setRequestObjectEncryptionEnc(BlockEncryptionAlgorithm.A128CBC_PLUS_HS256); + registerRequest.setClaims(Arrays.asList("member_of")); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Choose encryption key + JwkClient jwkClient = new JwkClient(jwksUri); + JwkResponse jwkResponse = jwkClient.exec(); + String serverKeyId = jwkResponse.getKeyId(Algorithm.RSA1_5); + assertNotNull(serverKeyId); + + // 3. Request authorization + JSONObject jwks = JwtUtil.getJSONWebKeys(jwksUri); + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, + KeyEncryptionAlgorithm.RSA1_5, BlockEncryptionAlgorithm.A128CBC_PLUS_HS256, cryptoProvider); + jwtAuthorizationRequest.setKeyId(serverKeyId); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim("member_of", ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim("member_of", ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(jwks); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 4. Validate id_token + PrivateKey privateKey = cryptoProvider.getPrivateKey(clientKeyId); + + Jwe jwe = Jwe.parse(idToken, privateKey, null); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwe.getClaims().getClaimAsStringList("member_of").size() > 1); + + // 5. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setPrivateKey(privateKey); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", + "dnName", "keyStoreFile", "keyStoreSecret", "RSA1_5_keyId", + "clientJwksUri", "sectorIdentifierUri"}) + @Test + public void authorizationRequestObjectWithMultivaluedClaimAlgRSA15EncA256CBCPLUSHS512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String clientKeyId, + final String clientJwksUri, final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestObjectWithMultivaluedClaimAlgRSA15EncA256CBCPLUSHS512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A256CBC_PLUS_HS512); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A256CBC_PLUS_HS512); + registerRequest.setRequestObjectEncryptionAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setRequestObjectEncryptionEnc(BlockEncryptionAlgorithm.A256CBC_PLUS_HS512); + registerRequest.setClaims(Arrays.asList("member_of")); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Choose encryption key + JwkClient jwkClient = new JwkClient(jwksUri); + JwkResponse jwkResponse = jwkClient.exec(); + String serverKeyId = jwkResponse.getKeyId(Algorithm.RSA1_5); + assertNotNull(serverKeyId); + + // 3. Request authorization + JSONObject jwks = JwtUtil.getJSONWebKeys(jwksUri); + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, + KeyEncryptionAlgorithm.RSA1_5, BlockEncryptionAlgorithm.A256CBC_PLUS_HS512, cryptoProvider); + jwtAuthorizationRequest.setKeyId(serverKeyId); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim("member_of", ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim("member_of", ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(jwks); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 4. Validate id_token + PrivateKey privateKey = cryptoProvider.getPrivateKey(clientKeyId); + + Jwe jwe = Jwe.parse(idToken, privateKey, null); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwe.getClaims().getClaimAsStringList("member_of").size() > 1); + + // 5. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setPrivateKey(privateKey); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", + "dnName", "keyStoreFile", "keyStoreSecret", "RSA_OAEP_keyId", + "clientJwksUri", "sectorIdentifierUri"}) + @Test + public void authorizationRequestObjectWithMultivaluedClaimAlgRSAOAEPEncA256GCM( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String clientKeyId, + final String clientJwksUri, final String sectorIdentifierUri) throws Exception { + showTitle("authorizationRequestObjectWithMultivaluedClaimAlgRSAOAEPEncA256GCM"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA_OAEP); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A256GCM); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA_OAEP); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A256GCM); + registerRequest.setRequestObjectEncryptionAlg(KeyEncryptionAlgorithm.RSA_OAEP); + registerRequest.setRequestObjectEncryptionEnc(BlockEncryptionAlgorithm.A256GCM); + registerRequest.setClaims(Arrays.asList("member_of")); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Choose encryption key + JwkClient jwkClient = new JwkClient(jwksUri); + JwkResponse jwkResponse = jwkClient.exec(); + String serverKeyId = jwkResponse.getKeyId(Algorithm.RSA_OAEP); + assertNotNull(serverKeyId); + + // 3. Request authorization + JSONObject jwks = JwtUtil.getJSONWebKeys(jwksUri); + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, + KeyEncryptionAlgorithm.RSA_OAEP, BlockEncryptionAlgorithm.A256GCM, cryptoProvider); + jwtAuthorizationRequest.setKeyId(serverKeyId); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim("member_of", ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim("member_of", ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(jwks); + authorizationRequest.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + String accessToken = authorizationResponse.getAccessToken(); + + // 4. Validate id_token + PrivateKey privateKey = cryptoProvider.getPrivateKey(clientKeyId); + + Jwe jwe = Jwe.parse(idToken, privateKey, null); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsStringList("member_of")); + assertTrue(jwe.getClaims().getClaimAsStringList("member_of").size() > 1); + + // 5. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + userInfoClient.setPrivateKey(privateKey); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim("member_of")); + assertTrue(userInfoResponse.getClaim("member_of").size() > 1); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/OpenIDConnectDiscoveryHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/OpenIDConnectDiscoveryHttpTest.java new file mode 100644 index 00000000..480526ef --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/OpenIDConnectDiscoveryHttpTest.java @@ -0,0 +1,244 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.OpenIdConnectDiscoveryClient; +import org.gluu.oxauth.client.OpenIdConnectDiscoveryRequest; +import org.gluu.oxauth.client.OpenIdConnectDiscoveryResponse; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * Functional tests for SWD Web Services (HTTP) + * + * @author Javier Rojas Blum Date: 12.7.2011 + */ +public class OpenIDConnectDiscoveryHttpTest extends BaseTest { + + @Test + public void emailNormalization1() throws Exception { + String resource = "acct:joe@example.com"; + String expectedHost = "example.com"; + String expectedPath = null; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void emailNormalization2() throws Exception { + String resource = "joe@example.com"; + String expectedHost = "example.com"; + String expectedPath = null; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void emailNormalization3() throws Exception { + String resource = "acct:joe@example.com:8080"; + String expectedHost = "example.com:8080"; + String expectedPath = null; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void emailNormalization4() throws Exception { + String resource = "joe@example.com:8080"; + String expectedHost = "example.com:8080"; + String expectedPath = null; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void emailNormalization5() throws Exception { + String resource = "joe@localhost"; + String expectedHost = "localhost"; + String expectedPath = null; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void emailNormalization6() throws Exception { + String resource = "joe@localhost:8080"; + String expectedHost = "localhost:8080"; + String expectedPath = null; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void urlNormalization1() throws Exception { + String resource = "https://example.com"; + String expectedHost = "example.com"; + String expectedPath = null; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void urlNormalization2() throws Exception { + String resource = "https://example.com/joe"; + String expectedHost = "example.com"; + String expectedPath = "/joe"; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void urlNormalization3() throws Exception { + String resource = "https://example.com:8080/"; + String expectedHost = "example.com:8080"; + String expectedPath = null; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void urlNormalization4() throws Exception { + String resource = "https://example.com:8080/joe"; + String expectedHost = "example.com:8080"; + String expectedPath = "/joe"; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void urlNormalization5() throws Exception { + String resource = "https://example.com:8080/joe#fragment"; + String expectedHost = "example.com:8080"; + String expectedPath = "/joe"; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void urlNormalization6() throws Exception { + String resource = "https://example.com:8080/joe?param=value"; + String expectedHost = "example.com:8080"; + String expectedPath = "/joe"; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void urlNormalization7() throws Exception { + String resource = "https://example.com:8080/joe?param1=foo¶m2=bar#fragment"; + String expectedHost = "example.com:8080"; + String expectedPath = "/joe"; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void hostNormalization1() throws Exception { + String resource = "example.com"; + String expectedHost = "example.com"; + String expectedPath = null; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void hostNormalization2() throws Exception { + String resource = "example.com:8080"; + String expectedHost = "example.com:8080"; + String expectedPath = null; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void hostNormalization3() throws Exception { + String resource = "example.com/path"; + String expectedHost = "example.com"; + String expectedPath = "/path"; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Test + public void hostNormalization4() throws Exception { + String resource = "example.com:8080/path"; + String expectedHost = "example.com:8080"; + String expectedPath = "/path"; + + OpenIdConnectDiscoveryRequest openIdConnectDiscoveryRequest = new OpenIdConnectDiscoveryRequest(resource); + assertEquals(openIdConnectDiscoveryRequest.getResource(), resource); + assertEquals(openIdConnectDiscoveryRequest.getHost(), expectedHost); + assertEquals(openIdConnectDiscoveryRequest.getPath(), expectedPath); + } + + @Parameters({"swdResource"}) + @Test + public void requestOpenIdConnectDiscovery(final String resource) throws Exception { + showTitle("requestOpenIdConnectDiscovery"); + + OpenIdConnectDiscoveryClient client = new OpenIdConnectDiscoveryClient(resource); + OpenIdConnectDiscoveryResponse response = client.exec(); + + showClient(client); + assertEquals(response.getStatus(), 200, "Unexpected response code"); + assertNotNull(response.getSubject()); + assertTrue(response.getLinks().size() > 0); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/OpenIDRequestObjectHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/OpenIDRequestObjectHttpTest.java new file mode 100644 index 00000000..2bc0160a --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/OpenIDRequestObjectHttpTest.java @@ -0,0 +1,2890 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.*; +import org.gluu.oxauth.client.model.authorize.Claim; +import org.gluu.oxauth.client.model.authorize.ClaimValue; +import org.gluu.oxauth.client.model.authorize.JwtAuthorizationRequest; +import org.gluu.oxauth.model.common.Prompt; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.AbstractCryptoProvider; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.encryption.BlockEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.encryption.KeyEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jwk.Algorithm; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.Base64Util; +import org.gluu.oxauth.model.util.JwtUtil; +import org.gluu.oxauth.model.util.StringUtils; +import org.gluu.util.StringHelper; +import org.json.JSONObject; +import org.testng.annotations.Optional; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import static org.testng.Assert.*; + +/** + * Functional tests for OpenID Request Object (HTTP) + * + * @author Javier Rojas Blum + * @version February 12, 2019 + */ +public class OpenIDRequestObjectHttpTest extends BaseTest { + + public static final String ACR_VALUE = "basic"; + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestParameterMethod1( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestParameterMethod1"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest, SignatureAlgorithm.HS256, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestParameterMethod2( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestParameterMethod2"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest, SignatureAlgorithm.HS256, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response2 = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(response2.getStatus(), 200, "Unexpected response code: " + response2.getStatus()); + assertNotNull(response2.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response2.getClaim(JwtClaimName.NAME)); + assertNotNull(response2.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response2.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response2.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response2.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response2.getClaim(JwtClaimName.LOCALE)); + assertNotNull(response2.getClaim(JwtClaimName.ADDRESS)); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestParameterMethod3( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestParameterMethod3"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid"); + String state = "STATE0"; + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest, SignatureAlgorithm.HS256, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addUserInfoClaim(new Claim("name", ClaimValue.createNull())); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The code is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestParameterMethod4( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestParameterMethod4"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid"); + String state = UUID.randomUUID().toString(); + String nonce = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest, SignatureAlgorithm.HS384, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.SUBJECT_IDENTIFIER, ClaimValue.createSingleValue(userId))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestParameterMethod5( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestParameterMethod5"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid"); + String state = UUID.randomUUID().toString(); + String nonce = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest, SignatureAlgorithm.HS512, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.SUBJECT_IDENTIFIER, ClaimValue.createSingleValue(userId))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestParameterMethod6( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestParameterMethod6"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setClaims(Arrays.asList(JwtClaimName.NAME)); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.HS256, clientSecret, cryptoProvider); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + jwtAuthorizationRequest.addUserInfoClaim(new Claim("name", ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "clientJwksUri", + "RS256_keyId", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestParameterMethodRS256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String jwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("requestParameterMethodRS256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setJwksUri(jwksUri); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.RS256); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + String clientId = response.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + request.setState(state); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + request.getPrompts().add(Prompt.NONE); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(request, SignatureAlgorithm.RS256, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + request.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response1 = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response1.getStatus(), 302, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getLocation(), "The location is null"); + assertNotNull(response1.getAccessToken(), "The accessToken is null"); + assertNotNull(response1.getTokenType(), "The tokenType is null"); + assertNotNull(response1.getIdToken(), "The idToken is null"); + assertNotNull(response1.getState(), "The state is null"); + + String accessToken = response1.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response3 = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(response3.getStatus(), 200, "Unexpected response code: " + response3.getStatus()); + assertNotNull(response3.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response3.getClaim(JwtClaimName.NAME)); + assertNotNull(response3.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response3.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response3.getClaim(JwtClaimName.LOCALE)); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "clientJwksUri", + "RS384_keyId", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestParameterMethodRS384( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String jwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("requestParameterMethodRS384"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setJwksUri(jwksUri); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.RS384); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + String clientId = response.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + request.setState(state); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + request.getPrompts().add(Prompt.NONE); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + request, SignatureAlgorithm.RS384, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + request.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response1 = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response1.getStatus(), 302, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getLocation(), "The location is null"); + assertNotNull(response1.getAccessToken(), "The accessToken is null"); + assertNotNull(response1.getTokenType(), "The tokenType is null"); + assertNotNull(response1.getIdToken(), "The idToken is null"); + assertNotNull(response1.getState(), "The state is null"); + + String accessToken = response1.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response3 = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(response3.getStatus(), 200, "Unexpected response code: " + response3.getStatus()); + assertNotNull(response3.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response3.getClaim(JwtClaimName.NAME)); + assertNotNull(response3.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response3.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response3.getClaim(JwtClaimName.LOCALE)); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "clientJwksUri", + "RS512_keyId", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestParameterMethodRS512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String jwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("requestParameterMethodRS512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setJwksUri(jwksUri); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.RS512); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + String clientId = response.getClientId(); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + request.setState(state); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + request.getPrompts().add(Prompt.NONE); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(request, SignatureAlgorithm.RS512, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + request.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response1 = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response1.getStatus(), 302, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getLocation(), "The location is null"); + assertNotNull(response1.getAccessToken(), "The accessToken is null"); + assertNotNull(response1.getTokenType(), "The tokenType is null"); + assertNotNull(response1.getIdToken(), "The idToken is null"); + assertNotNull(response1.getState(), "The state is null"); + + String accessToken = response1.getAccessToken(); + + // Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response3 = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(response3.getStatus(), 200, "Unexpected response code: " + response3.getStatus()); + assertNotNull(response3.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response3.getClaim(JwtClaimName.NAME)); + assertNotNull(response3.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response3.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response3.getClaim(JwtClaimName.LOCALE)); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "clientJwksUri", + "ES256_keyId", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestParameterMethodES256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String jwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("requestParameterMethodES256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setJwksUri(jwksUri); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.ES256); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + String clientId = response.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + request.setState(state); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + request.getPrompts().add(Prompt.NONE); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(request, SignatureAlgorithm.ES256, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + request.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response1 = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response1.getStatus(), 302, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getLocation(), "The location is null"); + assertNotNull(response1.getAccessToken(), "The accessToken is null"); + assertNotNull(response1.getTokenType(), "The tokenType is null"); + assertNotNull(response1.getIdToken(), "The idToken is null"); + assertNotNull(response1.getState(), "The state is null"); + + String accessToken = response1.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response3 = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(response3.getStatus(), 200, "Unexpected response code: " + response3.getStatus()); + assertNotNull(response3.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response3.getClaim(JwtClaimName.NAME)); + assertNotNull(response3.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response3.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response3.getClaim(JwtClaimName.LOCALE)); + assertNotNull(response3.getClaim(JwtClaimName.ADDRESS)); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "clientJwksUri", + "ES384_keyId", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestParameterMethodES384( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String jwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("requestParameterMethodES384"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setJwksUri(jwksUri); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.ES384); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + registerClient.setExecutor(clientEngine(true)); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + String clientId = response.getClientId(); + String clientSecret = response.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + request.setState(state); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + request.getPrompts().add(Prompt.NONE); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(request, SignatureAlgorithm.ES384, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + request.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + authorizeClient.setExecutor(clientEngine(true)); + AuthorizationResponse response1 = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response1.getStatus(), 302, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getLocation(), "The location is null"); + assertNotNull(response1.getAccessToken(), "The accessToken is null"); + assertNotNull(response1.getTokenType(), "The tokenType is null"); + assertNotNull(response1.getIdToken(), "The idToken is null"); + assertNotNull(response1.getState(), "The state is null"); + + String accessToken = response1.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setExecutor(clientEngine(true)); + UserInfoResponse response3 = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(response3.getStatus(), 200, "Unexpected response code: " + response3.getStatus()); + assertNotNull(response3.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response3.getClaim(JwtClaimName.NAME)); + assertNotNull(response3.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response3.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response3.getClaim(JwtClaimName.LOCALE)); + assertNotNull(response3.getClaim(JwtClaimName.ADDRESS)); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "clientJwksUri", + "ES512_keyId", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestParameterMethodES512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String jwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("requestParameterMethodES512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setJwksUri(jwksUri); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.ES512); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + String clientId = response.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + request.setState(state); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + request.getPrompts().add(Prompt.NONE); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(request, SignatureAlgorithm.ES512, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + request.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response1 = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response1.getStatus(), 302, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getLocation(), "The location is null"); + assertNotNull(response1.getAccessToken(), "The accessToken is null"); + assertNotNull(response1.getTokenType(), "The tokenType is null"); + assertNotNull(response1.getIdToken(), "The idToken is null"); + assertNotNull(response1.getState(), "The state is null"); + + String accessToken = response1.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response3 = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(response3.getStatus(), 200, "Unexpected response code: " + response3.getStatus()); + assertNotNull(response3.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response3.getClaim(JwtClaimName.NAME)); + assertNotNull(response3.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response3.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response3.getClaim(JwtClaimName.LOCALE)); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "clientJwksUri", + "PS256_keyId", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestParameterMethodPS256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String jwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("requestParameterMethodPS256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setJwksUri(jwksUri); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.PS256); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + String clientId = response.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + request.setState(state); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + request.getPrompts().add(Prompt.NONE); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(request, SignatureAlgorithm.PS256, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + request.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response1 = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response1.getStatus(), 302, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getLocation(), "The location is null"); + assertNotNull(response1.getAccessToken(), "The accessToken is null"); + assertNotNull(response1.getTokenType(), "The tokenType is null"); + assertNotNull(response1.getIdToken(), "The idToken is null"); + assertNotNull(response1.getState(), "The state is null"); + + String accessToken = response1.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response3 = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(response3.getStatus(), 200, "Unexpected response code: " + response3.getStatus()); + assertNotNull(response3.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response3.getClaim(JwtClaimName.NAME)); + assertNotNull(response3.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response3.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response3.getClaim(JwtClaimName.LOCALE)); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "clientJwksUri", + "PS384_keyId", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestParameterMethodPS384( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String jwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("requestParameterMethodPS384"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setJwksUri(jwksUri); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.PS384); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + String clientId = response.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + request.setState(state); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + request.getPrompts().add(Prompt.NONE); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(request, SignatureAlgorithm.PS384, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + request.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response1 = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response1.getStatus(), 302, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getLocation(), "The location is null"); + assertNotNull(response1.getAccessToken(), "The accessToken is null"); + assertNotNull(response1.getTokenType(), "The tokenType is null"); + assertNotNull(response1.getIdToken(), "The idToken is null"); + assertNotNull(response1.getState(), "The state is null"); + + String accessToken = response1.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response3 = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(response3.getStatus(), 200, "Unexpected response code: " + response3.getStatus()); + assertNotNull(response3.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response3.getClaim(JwtClaimName.NAME)); + assertNotNull(response3.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response3.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response3.getClaim(JwtClaimName.LOCALE)); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "clientJwksUri", + "PS512_keyId", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestParameterMethodPS512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String jwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("requestParameterMethodPS512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setJwksUri(jwksUri); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.PS512); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + String clientId = response.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + request.setState(state); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + request.getPrompts().add(Prompt.NONE); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(request, SignatureAlgorithm.PS512, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + request.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response1 = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response1.getStatus(), 302, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getLocation(), "The location is null"); + assertNotNull(response1.getAccessToken(), "The accessToken is null"); + assertNotNull(response1.getTokenType(), "The tokenType is null"); + assertNotNull(response1.getIdToken(), "The idToken is null"); + assertNotNull(response1.getState(), "The state is null"); + + String accessToken = response1.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response3 = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(response3.getStatus(), 200, "Unexpected response code: " + response3.getStatus()); + assertNotNull(response3.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response3.getClaim(JwtClaimName.NAME)); + assertNotNull(response3.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response3.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response3.getClaim(JwtClaimName.LOCALE)); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "clientJwksUri", + "RS256_keyId", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestParameterMethodRS256X509Cert( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String jwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("requestParameterMethodRS256X509Cert"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setJwksUri(jwksUri); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.RS256); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + String clientId = response.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + request.setState(state); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + request.getPrompts().add(Prompt.NONE); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(request, SignatureAlgorithm.RS256, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + request.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response1 = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response1.getStatus(), 302, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getLocation(), "The location is null"); + assertNotNull(response1.getAccessToken(), "The accessToken is null"); + assertNotNull(response1.getTokenType(), "The tokenType is null"); + assertNotNull(response1.getIdToken(), "The idToken is null"); + assertNotNull(response1.getState(), "The state is null"); + + String accessToken = response1.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response3 = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(response3.getStatus(), 200, "Unexpected response code: " + response3.getStatus()); + assertNotNull(response3.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response3.getClaim(JwtClaimName.NAME)); + assertNotNull(response3.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response3.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response3.getClaim(JwtClaimName.LOCALE)); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "clientJwksUri", + "RS384_keyId", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestParameterMethodRS384X509Cert( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String jwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("requestParameterMethodRS384X509Cert"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setJwksUri(jwksUri); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.RS384); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + String clientId = response.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + request.setState(state); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + request.getPrompts().add(Prompt.NONE); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(request, SignatureAlgorithm.RS384, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + request.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response1 = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response1.getStatus(), 302, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getLocation(), "The location is null"); + assertNotNull(response1.getAccessToken(), "The accessToken is null"); + assertNotNull(response1.getTokenType(), "The tokenType is null"); + assertNotNull(response1.getIdToken(), "The idToken is null"); + assertNotNull(response1.getState(), "The state is null"); + + String accessToken = response1.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response3 = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(response3.getStatus(), 200, "Unexpected response code: " + response3.getStatus()); + assertNotNull(response3.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response3.getClaim(JwtClaimName.NAME)); + assertNotNull(response3.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response3.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response3.getClaim(JwtClaimName.LOCALE)); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "clientJwksUri", + "RS512_keyId", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestParameterMethodRS512X509Cert( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String jwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("requestParameterMethodRS512X509Cert"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setJwksUri(jwksUri); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.RS512); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + String clientId = response.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + request.setState(state); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + request.getPrompts().add(Prompt.NONE); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(request, SignatureAlgorithm.RS512, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + request.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response1 = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response1.getStatus(), 302, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getLocation(), "The location is null"); + assertNotNull(response1.getAccessToken(), "The accessToken is null"); + assertNotNull(response1.getTokenType(), "The tokenType is null"); + assertNotNull(response1.getIdToken(), "The idToken is null"); + assertNotNull(response1.getState(), "The state is null"); + + String accessToken = response1.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response3 = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(response3.getStatus(), 200, "Unexpected response code: " + response3.getStatus()); + assertNotNull(response3.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response3.getClaim(JwtClaimName.NAME)); + assertNotNull(response3.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response3.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response3.getClaim(JwtClaimName.LOCALE)); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "clientJwksUri", + "ES256_keyId", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestParameterMethodES256X509Cert( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String jwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("requestParameterMethodES256X509Cert"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setJwksUri(jwksUri); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.ES256); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + String clientId = response.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + request.setState(state); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + request.getPrompts().add(Prompt.NONE); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(request, SignatureAlgorithm.ES256, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + request.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response1 = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response1.getStatus(), 302, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getLocation(), "The location is null"); + assertNotNull(response1.getAccessToken(), "The accessToken is null"); + assertNotNull(response1.getTokenType(), "The tokenType is null"); + assertNotNull(response1.getIdToken(), "The idToken is null"); + assertNotNull(response1.getState(), "The state is null"); + + String accessToken = response1.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response3 = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(response3.getStatus(), 200, "Unexpected response code: " + response3.getStatus()); + assertNotNull(response3.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response3.getClaim(JwtClaimName.NAME)); + assertNotNull(response3.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response3.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response3.getClaim(JwtClaimName.LOCALE)); + assertNotNull(response3.getClaim(JwtClaimName.ADDRESS)); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "clientJwksUri", + "ES384_keyId", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestParameterMethodES384X509Cert( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String jwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("requestParameterMethodES384X509Cert"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setJwksUri(jwksUri); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.ES384); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + String clientId = response.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + request.setState(state); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + request.getPrompts().add(Prompt.NONE); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(request, SignatureAlgorithm.ES384, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + request.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response1 = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response1.getStatus(), 302, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getLocation(), "The location is null"); + assertNotNull(response1.getAccessToken(), "The accessToken is null"); + assertNotNull(response1.getTokenType(), "The tokenType is null"); + assertNotNull(response1.getIdToken(), "The idToken is null"); + assertNotNull(response1.getState(), "The state is null"); + + String accessToken = response1.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response3 = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(response3.getStatus(), 200, "Unexpected response code: " + response3.getStatus()); + assertNotNull(response3.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response3.getClaim(JwtClaimName.NAME)); + assertNotNull(response3.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response3.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response3.getClaim(JwtClaimName.LOCALE)); + assertNotNull(response3.getClaim(JwtClaimName.ADDRESS)); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "clientJwksUri", + "ES512_keyId", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestParameterMethodES512X509Cert( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String jwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("requestParameterMethodES512X509Cert"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setJwksUri(jwksUri); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.ES512); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + String clientId = response.getClientId(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + request.setState(state); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + request.getPrompts().add(Prompt.NONE); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(request, SignatureAlgorithm.ES512, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + request.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response1 = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response1.getStatus(), 302, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getLocation(), "The location is null"); + assertNotNull(response1.getAccessToken(), "The accessToken is null"); + assertNotNull(response1.getTokenType(), "The tokenType is null"); + assertNotNull(response1.getIdToken(), "The idToken is null"); + assertNotNull(response1.getState(), "The state is null"); + + String accessToken = response1.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response3 = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(response3.getStatus(), 200, "Unexpected response code: " + response3.getStatus()); + assertNotNull(response3.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response3.getClaim(JwtClaimName.NAME)); + assertNotNull(response3.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response3.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response3.getClaim(JwtClaimName.LOCALE)); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestParameterMethodFail1( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) { + try { + showTitle("requestParameterMethodFail1"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Authorization Request + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.setRequest("INVALID_REQUEST_OBJECT"); + authorizationRequest.setAuthUsername(userId); + authorizationRequest.setAuthPassword(userSecret); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + AuthorizationResponse response = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response.getStatus(), 302, "Unexpected response code: " + response.getStatus()); + assertNotNull(response.getLocation(), "The location is null"); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + assertNotNull(response.getState(), "The state is null"); + } catch (Exception e) { + fail(e.getMessage(), e); + } + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestParameterMethodFail2( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) { + try { + showTitle("requestParameterMethodFail2"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authorization Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.setAuthUsername(userId); + authorizationRequest.setAuthPassword(userSecret); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.HS256, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt + "INVALID_KEY"); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + AuthorizationResponse response = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response.getStatus(), 302, "Unexpected response code: " + response.getStatus()); + assertNotNull(response.getLocation(), "The location is null"); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + assertNotNull(response.getState(), "The state is null"); + } catch (Exception e) { + fail(e.getMessage(), e); + } + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestParameterMethodFail3( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) { + try { + showTitle("requestParameterMethodFail3"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authorization Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + request.setState(state); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(request, SignatureAlgorithm.HS256, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + jwtAuthorizationRequest.setClientId("INVALID_CLIENT_ID"); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + request.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response.getStatus(), 302, "Unexpected response code: " + response.getStatus()); + assertNotNull(response.getLocation(), "The location is null"); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + assertNotNull(response.getState(), "The state is null"); + } catch (Exception e) { + fail(e.getMessage(), e); + } + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestParameterMethodFail4( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) { + try { + showTitle("requestParameterMethodFail4"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authorization Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + request.setState(state); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(request, SignatureAlgorithm.HS256, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.SUBJECT_IDENTIFIER, ClaimValue.createSingleValue("INVALID_USER_ID"))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + request.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response.getStatus(), 302, "Unexpected response code: " + response.getStatus()); + assertNotNull(response.getLocation(), "The location is null"); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + assertNotNull(response.getState(), "The state is null"); + } catch (Exception e) { + fail(e.getMessage(), e); + } + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "requestFileBasePath", "requestFileBaseUrl", "sectorIdentifierUri"}) + @Test // This tests requires a place to publish a request object via HTTPS + public void requestFileMethod( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + @Optional final String requestFileBasePath, final String requestFileBaseUrl, final String sectorIdentifierUri) throws Exception { + showTitle("requestFileMethod"); + + if (StringHelper.isEmpty(requestFileBasePath)) { + return; + } + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request Authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + try { + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.HS256, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + String hash = Base64Util.base64urlencode(JwtUtil.getMessageDigestSHA256(authJwt)); + String fileName = UUID.randomUUID().toString() + ".txt"; + String filePath = requestFileBasePath + File.separator + fileName; + String fileUrl = requestFileBaseUrl + "/" + fileName;// + "#" + hash; + FileWriter fw = new FileWriter(filePath); + BufferedWriter bw = new BufferedWriter(fw); + bw.write(authJwt); + bw.close(); + fw.close(); + authorizationRequest.setRequestUri(fileUrl); + System.out.println("Request JWT: " + authJwt); + System.out.println("Request File Path: " + filePath); + System.out.println("Request File URL: " + fileUrl); + } catch (IOException e) { + e.printStackTrace(); + fail(e.getMessage()); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + fail(e.getMessage()); + } catch (NoSuchProviderException e) { + e.printStackTrace(); + fail(e.getMessage()); + } + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The accessToken is null"); + assertNotNull(authorizationResponse.getTokenType(), "The tokenType is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestFileMethodFail1( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) { + try { + showTitle("requestFileMethodFail1"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.setAuthUsername(userId); + authorizationRequest.setAuthPassword(userSecret); + + authorizationRequest.setRequest("FAKE_REQUEST"); + authorizationRequest.setRequestUri("FAKE_REQUEST_URI"); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + AuthorizationResponse response = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response.getStatus(), 302, "Unexpected response code: " + response.getStatus()); + assertNotNull(response.getLocation(), "The location is null"); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + assertNotNull(response.getState(), "The state is null"); + } catch (Exception e) { + fail(e.getMessage(), e); + } + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "requestFileBaseUrl", "sectorIdentifierUri"}) + @Test + public void requestFileMethodFail2( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String requestFileBaseUrl, final String sectorIdentifierUri) { + try { + showTitle("requestFileMethodFail2"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = newRegisterClient(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Authorization Request + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.setAuthUsername(userId); + authorizationRequest.setAuthPassword(userSecret); + + authorizationRequest.setRequestUri(requestFileBaseUrl + "/FAKE_REQUEST_URI"); + + AuthorizeClient authorizeClient = newAuthorizeClient(authorizationRequest); + AuthorizationResponse response = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response.getStatus(), 400, "Unexpected response code: " + response.getStatus()); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + assertNotNull(response.getState(), "The state is null"); + } catch (Exception e) { + fail(e.getMessage(), e); + } + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "requestFileBasePath", "requestFileBaseUrl", "sectorIdentifierUri"}) + @Test // This tests requires a place to publish a request object via HTTPS + public void requestFileMethodFail3( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + @Optional final String requestFileBasePath, final String requestFileBaseUrl, final String sectorIdentifierUri) throws Exception { + showTitle("requestFileMethodFail3"); + + if (StringHelper.isEmpty(requestFileBasePath)) { + return; + } + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Authorization Request + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.setAuthUsername(userId); + authorizationRequest.setAuthPassword(userSecret); + authorizationRequest.getPrompts().add(Prompt.NONE); + + try { + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(authorizationRequest, SignatureAlgorithm.HS256, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + String hash = "INVALID_HASH"; + String fileName = UUID.randomUUID().toString() + ".txt"; + String filePath = requestFileBasePath + File.separator + fileName; + String fileUrl = requestFileBaseUrl + "/" + fileName + "#" + hash; + FileWriter fw = new FileWriter(filePath); + BufferedWriter bw = new BufferedWriter(fw); + bw.write(authJwt); + bw.close(); + fw.close(); + authorizationRequest.setRequestUri(fileUrl); + System.out.println("Request JWT: " + authJwt); + System.out.println("Request File Path: " + filePath); + System.out.println("Request File URL: " + fileUrl); + } catch (IOException e) { + e.printStackTrace(); + fail(e.getMessage()); + } + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + AuthorizationResponse response = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response.getStatus(), 302, "Unexpected response code: " + response.getStatus()); + assertNotNull(response.getLocation(), "The location is null"); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + assertNotNull(response.getState(), "The state is null"); + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void requestParameterMethodAlgNone( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) { + try { + showTitle("requestParameterMethodAlgNone"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.NONE); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + String clientId = response.getClientId(); + + // 2. Request authorization + AbstractCryptoProvider cryptoProvider = createCryptoProviderWithAllowedNone(); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + request.setState(state); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + request.getPrompts().add(Prompt.NONE); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(request, SignatureAlgorithm.NONE, cryptoProvider); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + request.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response1 = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response1.getStatus(), 302, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getLocation(), "The location is null"); + assertNotNull(response1.getAccessToken(), "The accessToken is null"); + assertNotNull(response1.getTokenType(), "The tokenType is null"); + assertNotNull(response1.getIdToken(), "The idToken is null"); + assertNotNull(response1.getState(), "The state is null"); + + String accessToken = response1.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response3 = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(response3.getStatus(), 200, "Unexpected response code: " + response3.getStatus()); + assertNotNull(response3.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response3.getClaim(JwtClaimName.NAME)); + assertNotNull(response3.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response3.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response3.getClaim(JwtClaimName.LOCALE)); + assertNotNull(response3.getClaim(JwtClaimName.ADDRESS)); + } catch (Exception e) { + fail(e.getMessage(), e); + } + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void requestParameterMethodAlgRSAOAEPEncA256GCM( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) { + try { + showTitle("requestParameterMethodAlgRSAOAEPEncA256GCM"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + String clientId = response.getClientId(); + + // 2. Choose encryption key + JwkClient jwkClient = new JwkClient(jwksUri); + JwkResponse jwkResponse = jwkClient.exec(); + String keyId = jwkResponse.getKeyId(Algorithm.RSA_OAEP); + assertNotNull(keyId); + + // 3. Request authorization + JSONObject jwks = JwtUtil.getJSONWebKeys(jwksUri); + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + request.setState(state); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + request.getPrompts().add(Prompt.NONE); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(request, + KeyEncryptionAlgorithm.RSA_OAEP, BlockEncryptionAlgorithm.A256GCM, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(jwks); + request.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response1 = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response1.getStatus(), 302, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getLocation(), "The location is null"); + assertNotNull(response1.getAccessToken(), "The accessToken is null"); + assertNotNull(response1.getTokenType(), "The tokenType is null"); + assertNotNull(response1.getIdToken(), "The idToken is null"); + assertNotNull(response1.getState(), "The state is null"); + + String accessToken = response1.getAccessToken(); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response3 = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(response3.getStatus(), 200, "Unexpected response code: " + response3.getStatus()); + assertNotNull(response3.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response3.getClaim(JwtClaimName.NAME)); + assertNotNull(response3.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response3.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response3.getClaim(JwtClaimName.LOCALE)); + assertNotNull(response3.getClaim(JwtClaimName.ADDRESS)); + } catch (Exception e) { + fail(e.getMessage(), e); + } + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void requestParameterMethodAlgRSA15EncA128CBCPLUSHS256( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) { + try { + showTitle("requestParameterMethodAlgRSA15EncA128CBCPLUSHS256"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + String clientId = response.getClientId(); + + // 2. Choose encryption key + JwkClient jwkClient = new JwkClient(jwksUri); + JwkResponse jwkResponse = jwkClient.exec(); + String keyId = jwkResponse.getKeyId(Algorithm.RSA1_5); + assertNotNull(keyId); + + // 3. Request authorization + JSONObject jwks = JwtUtil.getJSONWebKeys(jwksUri); + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + request.setState(state); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + request.getPrompts().add(Prompt.NONE); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(request, + KeyEncryptionAlgorithm.RSA1_5, BlockEncryptionAlgorithm.A128CBC_PLUS_HS256, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(jwks); + request.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response1 = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response1.getStatus(), 302, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getLocation(), "The location is null"); + assertNotNull(response1.getAccessToken(), "The accessToken is null"); + assertNotNull(response1.getTokenType(), "The tokenType is null"); + assertNotNull(response1.getIdToken(), "The idToken is null"); + assertNotNull(response1.getState(), "The state is null"); + + String accessToken = response1.getAccessToken(); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response3 = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(response3.getStatus(), 200, "Unexpected response code: " + response3.getStatus()); + assertNotNull(response3.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response3.getClaim(JwtClaimName.NAME)); + assertNotNull(response3.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response3.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response3.getClaim(JwtClaimName.LOCALE)); + assertNotNull(response3.getClaim(JwtClaimName.ADDRESS)); + } catch (Exception e) { + fail(e.getMessage(), e); + } + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void requestParameterMethodAlgRSA15EncA256CBCPLUSHS512( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) { + try { + showTitle("requestParameterMethodAlgRSA15EncA256CBCPLUSHS512"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + String clientId = response.getClientId(); + + // 2. Choose encryption key + JwkClient jwkClient = new JwkClient(jwksUri); + JwkResponse jwkResponse = jwkClient.exec(); + String keyId = jwkResponse.getKeyId(Algorithm.RSA1_5); + assertNotNull(keyId); + + // 3. Request authorization + JSONObject jwks = JwtUtil.getJSONWebKeys(jwksUri); + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + request.setState(state); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + request.getPrompts().add(Prompt.NONE); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(request, + KeyEncryptionAlgorithm.RSA1_5, BlockEncryptionAlgorithm.A256CBC_PLUS_HS512, cryptoProvider); + jwtAuthorizationRequest.setKeyId(keyId); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(jwks); + request.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response1 = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response1.getStatus(), 302, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getLocation(), "The location is null"); + assertNotNull(response1.getAccessToken(), "The accessToken is null"); + assertNotNull(response1.getTokenType(), "The tokenType is null"); + assertNotNull(response1.getIdToken(), "The idToken is null"); + assertNotNull(response1.getState(), "The state is null"); + + String accessToken = response1.getAccessToken(); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response3 = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(response3.getStatus(), 200, "Unexpected response code: " + response3.getStatus()); + assertNotNull(response3.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response3.getClaim(JwtClaimName.NAME)); + assertNotNull(response3.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response3.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response3.getClaim(JwtClaimName.LOCALE)); + assertNotNull(response3.getClaim(JwtClaimName.ADDRESS)); + } catch (Exception e) { + fail(e.getMessage(), e); + } + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void requestParameterMethodAlgA128KWEncA128GCM( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) { + try { + showTitle("requestParameterMethodAlgA128KWEncA128GCM"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + String clientId = response.getClientId(); + String clientSecret = response.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + request.setState(state); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + request.getPrompts().add(Prompt.NONE); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + request, KeyEncryptionAlgorithm.A128KW, BlockEncryptionAlgorithm.A128GCM, clientSecret); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + request.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response1 = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response1.getStatus(), 302, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getLocation(), "The location is null"); + assertNotNull(response1.getAccessToken(), "The accessToken is null"); + assertNotNull(response1.getTokenType(), "The tokenType is null"); + assertNotNull(response1.getIdToken(), "The idToken is null"); + assertNotNull(response1.getState(), "The state is null"); + + String accessToken = response1.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response3 = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(response3.getStatus(), 200, "Unexpected response code: " + response3.getStatus()); + assertNotNull(response3.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response3.getClaim(JwtClaimName.NAME)); + assertNotNull(response3.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response3.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response3.getClaim(JwtClaimName.LOCALE)); + assertNotNull(response3.getClaim(JwtClaimName.ADDRESS)); + } catch (Exception e) { + fail(e.getMessage(), e); + } + } + + @Parameters({"userId", "userSecret", "redirectUri", "redirectUris", "sectorIdentifierUri"}) + @Test + public void requestParameterMethodAlgA256KWEncA256GCM( + final String userId, final String userSecret, final String redirectUri, final String redirectUris, + final String sectorIdentifierUri) { + try { + showTitle("requestParameterMethodAlgA256KWEncA256GCM"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + String clientId = response.getClientId(); + String clientSecret = response.getClientSecret(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + request.setState(state); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + request.getPrompts().add(Prompt.NONE); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + request, KeyEncryptionAlgorithm.A256KW, BlockEncryptionAlgorithm.A256GCM, clientSecret); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NAME, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.NICKNAME, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.EMAIL_VERIFIED, ClaimValue.createNull())); + jwtAuthorizationRequest.addUserInfoClaim(new Claim(JwtClaimName.PICTURE, ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_TIME, ClaimValue.createNull())); + jwtAuthorizationRequest.addIdTokenClaim(new Claim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, ClaimValue.createValueList(new String[]{ACR_VALUE}))); + jwtAuthorizationRequest.getIdTokenMember().setMaxAge(86400); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + request.setRequest(authJwt); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse response1 = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(response1.getStatus(), 302, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getLocation(), "The location is null"); + assertNotNull(response1.getAccessToken(), "The accessToken is null"); + assertNotNull(response1.getTokenType(), "The tokenType is null"); + assertNotNull(response1.getIdToken(), "The idToken is null"); + assertNotNull(response1.getState(), "The state is null"); + + String accessToken = response1.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response3 = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(response3.getStatus(), 200, "Unexpected response code: " + response3.getStatus()); + assertNotNull(response3.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response3.getClaim(JwtClaimName.NAME)); + assertNotNull(response3.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response3.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response3.getClaim(JwtClaimName.LOCALE)); + assertNotNull(response3.getClaim(JwtClaimName.ADDRESS)); + } catch (Exception e) { + fail(e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/PersistClientAuthorizationsHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/PersistClientAuthorizationsHttpTest.java new file mode 100644 index 00000000..5ddd4a13 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/PersistClientAuthorizationsHttpTest.java @@ -0,0 +1,565 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.Prompt; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Javier Rojas Blum + * @version November 10, 2017 + */ +public class PersistClientAuthorizationsHttpTest extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void persistentClientAuthorizations( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("persistentClientAuthorizations"); + + List responseTypes = Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + String sessionId = null; + { + // 2. Request authorization + // Scopes: openid, profile + // Authenticate user with login password then authorize. + List scopes = Arrays.asList("openid", "profile"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + + String authorizationCode = authorizationResponse.getCode(); + sessionId = authorizationResponse.getSessionId(); + + // 3. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity()); + assertNotNull(tokenResponse.getAccessToken()); + assertNotNull(tokenResponse.getExpiresIn()); + assertNotNull(tokenResponse.getTokenType()); + assertNotNull(tokenResponse.getRefreshToken()); + } + + { + // 4. Request authorization + // Scopes: openid, profile + // Authenticate user with login password, do not show authorize page because those scopes are already authorized. + List scopes = Arrays.asList("openid", "profile"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwner( + authorizationEndpoint, authorizationRequest, userId, userSecret, false); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + + String authorizationCode = authorizationResponse.getCode(); + + // 5. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity()); + assertNotNull(tokenResponse.getAccessToken()); + assertNotNull(tokenResponse.getExpiresIn()); + assertNotNull(tokenResponse.getTokenType()); + assertNotNull(tokenResponse.getRefreshToken()); + } + + { + // 6. Request authorization + // Scopes: openid, address, email + // Authenticate user with login password then authorize. + // It shows authorize page because those scopes are not yet authorized. + List scopes = Arrays.asList("openid", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret, false); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + + String authorizationCode = authorizationResponse.getCode(); + + // 7. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity()); + assertNotNull(tokenResponse.getAccessToken()); + assertNotNull(tokenResponse.getExpiresIn()); + assertNotNull(tokenResponse.getTokenType()); + assertNotNull(tokenResponse.getRefreshToken()); + } + + { + // 8. Request authorization + // Scopes: openid, profile, address, email + // Authenticate user with login password, do not show authorize page because those scopes are already authorized. + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwner( + authorizationEndpoint, authorizationRequest, userId, userSecret, false); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + + String authorizationCode = authorizationResponse.getCode(); + + // 9. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity()); + assertNotNull(tokenResponse.getAccessToken()); + assertNotNull(tokenResponse.getExpiresIn()); + assertNotNull(tokenResponse.getTokenType()); + assertNotNull(tokenResponse.getRefreshToken()); + } + + { + // 10. Request authorization + // Scopes: openid, profile, address, email + // Do not show authenticate page because we are including the session_id in the authorization request and the session is already authenticated. + // Do not show authorize page because those scopes are already authorized. + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.getPrompts().add(Prompt.NONE); + authorizationRequest.setSessionId(sessionId); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + assertNotNull(authorizationResponse.getScope()); + + String authorizationCode = authorizationResponse.getCode(); + + // 11. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void persistentClientAuthorizationsSameSession( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("persistentClientAuthorizationsSameSession"); + + List responseTypes = Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + String sessionId = null; + { + // 2. Request authorization + // Scopes: openid, profile + // Authenticate user with login password then authorize. + List scopes = Arrays.asList("openid", "profile"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + + String authorizationCode = authorizationResponse.getCode(); + sessionId = authorizationResponse.getSessionId(); + + // 3. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity()); + assertNotNull(tokenResponse.getAccessToken()); + assertNotNull(tokenResponse.getExpiresIn()); + assertNotNull(tokenResponse.getTokenType()); + assertNotNull(tokenResponse.getRefreshToken()); + } + + { + // 4. Request authorization + // Scopes: openid, profile + // Do not show authorize page because those scopes are already authorized. + List scopes = Arrays.asList("openid", "profile"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.setSessionId(sessionId); + + AuthorizationResponse authorizationResponse = authenticateResourceOwner( + authorizationEndpoint, authorizationRequest, null, null, false); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + + String authorizationCode = authorizationResponse.getCode(); + + // 5. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity()); + assertNotNull(tokenResponse.getAccessToken()); + assertNotNull(tokenResponse.getExpiresIn()); + assertNotNull(tokenResponse.getTokenType()); + assertNotNull(tokenResponse.getRefreshToken()); + } + + { + // 6. Request authorization + // Scopes: openid, address, email + // Shows authorize page because some scopes are not yet authorized. + List scopes = Arrays.asList("openid", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.setSessionId(sessionId); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, null, null, false); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + + String authorizationCode = authorizationResponse.getCode(); + + // 7. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity()); + assertNotNull(tokenResponse.getAccessToken()); + assertNotNull(tokenResponse.getExpiresIn()); + assertNotNull(tokenResponse.getTokenType()); + assertNotNull(tokenResponse.getRefreshToken()); + } + + { + // 8. Request authorization + // Scopes: openid, profile, address, email + // Do not show authorize page because those scopes are already authorized. + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.setSessionId(sessionId); + + AuthorizationResponse authorizationResponse = authenticateResourceOwner( + authorizationEndpoint, authorizationRequest, null, null, false); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + + String authorizationCode = authorizationResponse.getCode(); + + // 9. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity()); + assertNotNull(tokenResponse.getAccessToken()); + assertNotNull(tokenResponse.getExpiresIn()); + assertNotNull(tokenResponse.getTokenType()); + assertNotNull(tokenResponse.getRefreshToken()); + } + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void doNotPersistAuthorizationWhenPreAuthorized( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("doNotPersistAuthorizationWhenPreAuthorized"); + + List responseTypes = Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + // Scopes: openid, profile + // Authenticate user with login password, do not show authorize page because the client is pre-authorized. + List scopes = Arrays.asList("openid", "profile"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwner( + authorizationEndpoint, authorizationRequest, userId, userSecret, false); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getIdToken()); + assertNotNull(authorizationResponse.getState()); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity()); + assertNotNull(tokenResponse.getAccessToken()); + assertNotNull(tokenResponse.getExpiresIn()); + assertNotNull(tokenResponse.getTokenType()); + assertNotNull(tokenResponse.getRefreshToken()); + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/PkceHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/PkceHttpTest.java new file mode 100644 index 00000000..f3e05337 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/PkceHttpTest.java @@ -0,0 +1,216 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.gluu.oxauth.client.Asserter.assertOk; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.model.authorize.CodeVerifier; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Yuriy Zabrovarnyy + * @author Javier Rojas Blum + * @version March 3, 2017 + */ + +public class PkceHttpTest extends BaseTest { + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void tokenWithPkceCheck( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("tokenWithPkceCheck"); + + // 1. Register client + List responseTypes = Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_POST); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setResponseTypes(responseTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertOk(registerResponse); + assertNotNull(registerResponse.getRegistrationAccessToken()); + + // 3. Request authorization + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + String nonce = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, registerResponse.getClientId(), scopes, redirectUri, nonce); + authorizationRequest.setState(state); + CodeVerifier verifier = authorizationRequest.generateAndSetCodeChallengeWithMethod(); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getIdToken(), "The ID Token is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + + String authorizationCode = authorizationResponse.getCode(); + String idToken = authorizationResponse.getIdToken(); + + // 4. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.CODE_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 5. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(registerResponse.getClientId()); + tokenRequest.setAuthPassword(registerResponse.getClientSecret()); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_POST); + tokenRequest.setCodeVerifier(verifier.getCodeVerifier()); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + + } + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void invalidCodeVerifier( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("invalidCodeVerifier"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_POST); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertOk(registerResponse); + assertNotNull(registerResponse.getRegistrationAccessToken()); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, registerResponse.getClientId(), scopes, redirectUri, null); + authorizationRequest.setState(state); + authorizationRequest.generateAndSetCodeChallengeWithMethod(); // PKCE is set !!! + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token with invalid code verifier + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(registerResponse.getClientId()); + tokenRequest.setAuthPassword(registerResponse.getClientSecret()); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_POST); + tokenRequest.setCodeVerifier("invalid_code_verifier"); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNull(tokenResponse.getAccessToken(), "The access token is null"); + + // 5. Get Access Token without code verifier + tokenRequest.setCodeVerifier(null); + + tokenClient.setRequest(tokenRequest); + tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNull(tokenResponse.getAccessToken(), "The access token is null"); + + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/RegistrationRestWebServiceHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/RegistrationRestWebServiceHttpTest.java new file mode 100644 index 00000000..b756e197 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/RegistrationRestWebServiceHttpTest.java @@ -0,0 +1,699 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.gluu.oxauth.model.common.GrantType.AUTHORIZATION_CODE; +import static org.gluu.oxauth.model.common.GrantType.CLIENT_CREDENTIALS; +import static org.gluu.oxauth.model.common.GrantType.IMPLICIT; +import static org.gluu.oxauth.model.common.GrantType.OXAUTH_UMA_TICKET; +import static org.gluu.oxauth.model.common.GrantType.REFRESH_TOKEN; +import static org.gluu.oxauth.model.common.GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS; +import static org.gluu.oxauth.model.common.ResponseType.CODE; +import static org.gluu.oxauth.model.common.ResponseType.ID_TOKEN; +import static org.gluu.oxauth.model.common.ResponseType.TOKEN; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ACCESS_TOKEN_AS_JWT; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ACCESS_TOKEN_SIGNING_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.APPLICATION_TYPE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_LOGOUT_SESSION_REQUIRED; +import static org.gluu.oxauth.model.register.RegisterRequestParam.BACKCHANNEL_LOGOUT_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.CLIENT_NAME; +import static org.gluu.oxauth.model.register.RegisterRequestParam.CONTACTS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED; +import static org.gluu.oxauth.model.register.RegisterRequestParam.FRONT_CHANNEL_LOGOUT_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ID_TOKEN_ENCRYPTED_RESPONSE_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ID_TOKEN_ENCRYPTED_RESPONSE_ENC; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ID_TOKEN_SIGNED_RESPONSE_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.JWKS_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.LOGO_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.POLICY_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REDIRECT_URIS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REQUEST_OBJECT_ENCRYPTION_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REQUEST_OBJECT_ENCRYPTION_ENC; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REQUEST_OBJECT_SIGNING_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REQUEST_URIS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REQUIRE_AUTH_TIME; +import static org.gluu.oxauth.model.register.RegisterRequestParam.RPT_AS_JWT; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SCOPE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SECTOR_IDENTIFIER_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SOFTWARE_ID; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SOFTWARE_VERSION; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SUBJECT_TYPE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.TOKEN_ENDPOINT_AUTH_METHOD; +import static org.gluu.oxauth.model.register.RegisterRequestParam.TOKEN_ENDPOINT_AUTH_SIGNING_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.USERINFO_ENCRYPTED_RESPONSE_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.USERINFO_ENCRYPTED_RESPONSE_ENC; +import static org.gluu.oxauth.model.register.RegisterRequestParam.USERINFO_SIGNED_RESPONSE_ALG; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import javax.ws.rs.HttpMethod; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.common.SubjectType; +import org.gluu.oxauth.model.crypto.encryption.BlockEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.encryption.KeyEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.gluu.oxauth.model.util.Util; +import org.json.JSONArray; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +import com.google.common.collect.Lists; + +/** + * Functional tests for Client Registration Web Services (HTTP) + * + * @author Javier Rojas Blum + * @author Yuriy Zabrovarnyy + * @version May 28, 2020 + */ +public class RegistrationRestWebServiceHttpTest extends BaseTest { + + private String registrationAccessToken1; + private String registrationClientUri1; + private String registrationAccessToken2; + private String registrationClientUri2; + + @Parameters({"redirectUris", "sectorIdentifierUri"}) + @Test + public void requestClientAssociate1(final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("requestClientAssociate1"); + + // 1. Register Client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(Arrays.asList( + AUTHORIZATION_CODE, + IMPLICIT, + RESOURCE_OWNER_PASSWORD_CREDENTIALS, + CLIENT_CREDENTIALS, + REFRESH_TOKEN, + OXAUTH_UMA_TICKET)); + registerRequest.setResponseTypes(Arrays.asList( + CODE, + TOKEN, + ID_TOKEN + )); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client Update + String newClientName = "New Client Name"; + + RegisterRequest clientUpdateRequest = new RegisterRequest(registrationAccessToken); + clientUpdateRequest.setHttpMethod(HttpMethod.PUT); + clientUpdateRequest.setClientName(newClientName); + + RegisterClient clientUpdateClient = new RegisterClient(registrationClientUri); + clientUpdateClient.setRequest(clientUpdateRequest); + RegisterResponse clientUpdateResponse = clientUpdateClient.exec(); + + showClient(clientUpdateClient); + assertEquals(clientUpdateResponse.getStatus(), 200, "Unexpected response code: " + clientUpdateResponse.getEntity()); + assertEquals(clientUpdateResponse.getClaims().get(CLIENT_NAME.toString()), newClientName); + assertEquals(clientUpdateResponse.getClientId(), registerResponse.getClientId()); + assertEquals(clientUpdateResponse.getClientSecret(), registerResponse.getClientSecret()); + assertEquals(clientUpdateResponse.getRegistrationAccessToken(), registerResponse.getRegistrationAccessToken()); + assertEquals(clientUpdateResponse.getRegistrationClientUri(), registerResponse.getRegistrationClientUri()); + assertEquals(clientUpdateResponse.getClientIdIssuedAt(), registerResponse.getClientIdIssuedAt()); + assertEquals(clientUpdateResponse.getClientSecretExpiresAt(), registerResponse.getClientSecretExpiresAt()); + assertEquals(clientUpdateResponse.getResponseTypes(), registerResponse.getResponseTypes()); + assertEquals(clientUpdateResponse.getGrantTypes(), registerResponse.getGrantTypes()); + assertEquals(clientUpdateResponse.getClaims().get(REDIRECT_URIS.toString()), registerResponse.getClaims().get(REDIRECT_URIS.toString())); + assertEquals(clientUpdateResponse.getClaims().get(APPLICATION_TYPE.toString()), registerResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertEquals(clientUpdateResponse.getClaims().get(SECTOR_IDENTIFIER_URI.toString()), registerResponse.getClaims().get(SECTOR_IDENTIFIER_URI.toString())); + assertEquals(clientUpdateResponse.getClaims().get(SUBJECT_TYPE.toString()), registerResponse.getClaims().get(SUBJECT_TYPE.toString())); + assertEquals(clientUpdateResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString()), registerResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertEquals(clientUpdateResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), registerResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(clientUpdateResponse.getClaims().get(REQUIRE_AUTH_TIME.toString()), registerResponse.getClaims().get(REQUIRE_AUTH_TIME.toString())); + assertEquals(clientUpdateResponse.getClaims().get(RPT_AS_JWT.toString()), registerResponse.getClaims().get(RPT_AS_JWT.toString())); + assertEquals(clientUpdateResponse.getClaims().get(ACCESS_TOKEN_AS_JWT.toString()), registerResponse.getClaims().get(ACCESS_TOKEN_AS_JWT.toString())); + assertEquals(clientUpdateResponse.getClaims().get(ACCESS_TOKEN_SIGNING_ALG.toString()), registerResponse.getClaims().get(ACCESS_TOKEN_SIGNING_ALG.toString())); + assertEquals(clientUpdateResponse.getClaims().get(FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED.toString()), registerResponse.getClaims().get(FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED.toString())); + + assertNotNull(clientUpdateResponse.getClaims().get(SCOPE.toString())); + assertNotNull(registerResponse.getClaims().get(SCOPE.toString())); + + List clientUpdateResponseScopes = Util.splittedStringAsList(clientUpdateResponse.getClaims().get(SCOPE.toString()), " "); + List registerResponseScopes = Util.splittedStringAsList(registerResponse.getClaims().get(SCOPE.toString()), " "); + Collections.sort(clientUpdateResponseScopes); + Collections.sort(registerResponseScopes); + assertEquals(clientUpdateResponseScopes,registerResponseScopes); + } + + @Parameters({"redirectUris", "sectorIdentifierUri", "logoutUri"}) + @Test + public void requestClientAssociate2(final String redirectUris, final String sectorIdentifierUri, + final String logoutUri) throws Exception { + showTitle("requestClientAssociate2"); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setContacts(Arrays.asList("javier@gluu.org", "javier.rojas.blum@gmail.com")); + registerRequest.setScope(Arrays.asList("openid", "address", "profile", "email", "phone", "clientinfo", "invalid_scope")); + registerRequest.setLogoUri("http://www.gluu.org/wp-content/themes/gluursn/images/logo.png"); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setPolicyUri("http://www.gluu.org/policy"); + registerRequest.setJwksUri("http://www.gluu.org/jwks"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setRequestUris(Arrays.asList("http://www.gluu.org/request")); + registerRequest.setFrontChannelLogoutUris(Lists.newArrayList(logoutUri)); + registerRequest.setFrontChannelLogoutSessionRequired(true); + registerRequest.setBackchannelLogoutUris(Lists.newArrayList(logoutUri)); + registerRequest.setBackchannelLogoutSessionRequired(true); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS512); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A128CBC_PLUS_HS256); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.RS384); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.A128KW); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A128GCM); + registerRequest.setRequestObjectSigningAlg(SignatureAlgorithm.RS256); + registerRequest.setRequestObjectEncryptionAlg(KeyEncryptionAlgorithm.A256KW); + registerRequest.setRequestObjectEncryptionEnc(BlockEncryptionAlgorithm.A256CBC_PLUS_HS512); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.ES256); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + registerClient.setExecutor(clientEngine(true)); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + assertNotNull(response.getClaims().get(SCOPE.toString())); + assertTrue(Boolean.parseBoolean(response.getClaims().get(BACKCHANNEL_LOGOUT_SESSION_REQUIRED.toString()))); + assertEquals(logoutUri, new JSONArray(response.getClaims().get(BACKCHANNEL_LOGOUT_URI.toString())).getString(0)); + assertNotNull(response.getClaims().get(FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED.toString())); + assertTrue(Boolean.parseBoolean(response.getClaims().get(FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED.toString()))); + assertNotNull(response.getClaims().get(FRONT_CHANNEL_LOGOUT_URI.toString())); + assertEquals(logoutUri, new JSONArray(response.getClaims().get(FRONT_CHANNEL_LOGOUT_URI.toString())).getString(0)); + assertNotNull(response.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertEquals(SignatureAlgorithm.RS512, + SignatureAlgorithm.fromString(response.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString()))); + assertNotNull(response.getClaims().get(ID_TOKEN_ENCRYPTED_RESPONSE_ALG.toString())); + assertEquals(KeyEncryptionAlgorithm.RSA1_5, + KeyEncryptionAlgorithm.fromName(response.getClaims().get(ID_TOKEN_ENCRYPTED_RESPONSE_ALG.toString()))); + assertNotNull(response.getClaims().get(ID_TOKEN_ENCRYPTED_RESPONSE_ENC.toString())); + assertEquals(BlockEncryptionAlgorithm.A128CBC_PLUS_HS256, + BlockEncryptionAlgorithm.fromName(response.getClaims().get(ID_TOKEN_ENCRYPTED_RESPONSE_ENC.toString()))); + assertNotNull(response.getClaims().get(USERINFO_SIGNED_RESPONSE_ALG.toString())); + assertEquals(SignatureAlgorithm.RS384, + SignatureAlgorithm.fromString(response.getClaims().get(USERINFO_SIGNED_RESPONSE_ALG.toString()))); + assertNotNull(response.getClaims().get(USERINFO_ENCRYPTED_RESPONSE_ALG.toString())); + assertEquals(KeyEncryptionAlgorithm.A128KW, + KeyEncryptionAlgorithm.fromName(response.getClaims().get(USERINFO_ENCRYPTED_RESPONSE_ALG.toString()))); + assertNotNull(response.getClaims().get(USERINFO_ENCRYPTED_RESPONSE_ENC.toString())); + assertEquals(BlockEncryptionAlgorithm.A128GCM, + BlockEncryptionAlgorithm.fromName(response.getClaims().get(USERINFO_ENCRYPTED_RESPONSE_ENC.toString()))); + assertNotNull(response.getClaims().get(REQUEST_OBJECT_SIGNING_ALG.toString())); + assertEquals(SignatureAlgorithm.RS256, + SignatureAlgorithm.fromString(response.getClaims().get(REQUEST_OBJECT_SIGNING_ALG.toString()))); + assertNotNull(response.getClaims().get(REQUEST_OBJECT_ENCRYPTION_ALG.toString())); + assertEquals(KeyEncryptionAlgorithm.A256KW, + KeyEncryptionAlgorithm.fromName(response.getClaims().get(REQUEST_OBJECT_ENCRYPTION_ALG.toString()))); + assertNotNull(response.getClaims().get(REQUEST_OBJECT_ENCRYPTION_ENC.toString())); + assertEquals(BlockEncryptionAlgorithm.A256CBC_PLUS_HS512, + BlockEncryptionAlgorithm.fromName(response.getClaims().get(REQUEST_OBJECT_ENCRYPTION_ENC.toString()))); + assertNotNull(response.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(AuthenticationMethod.CLIENT_SECRET_JWT, + AuthenticationMethod.fromString(response.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()))); + assertNotNull(response.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(SignatureAlgorithm.ES256, + SignatureAlgorithm.fromString(response.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()))); + JSONArray scopesJsonArray = new JSONArray(StringUtils.spaceSeparatedToList(response.getClaims().get(SCOPE.toString()))); + List scopes = new ArrayList(); + for (int i = 0; i < scopesJsonArray.length(); i++) { + scopes.add(scopesJsonArray.get(i).toString()); + } + assertTrue(scopes.contains("openid")); + assertTrue(scopes.contains("address")); + assertTrue(scopes.contains("email")); + assertTrue(scopes.contains("profile")); + assertTrue(scopes.contains("phone")); + assertTrue(scopes.contains("clientinfo")); + + registrationAccessToken1 = response.getRegistrationAccessToken(); + registrationClientUri1 = response.getRegistrationClientUri(); + } + + @Parameters({"redirectUris", "sectorIdentifierUri"}) + @Test + public void requestClientAssociate3(final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("requestClientAssociate3"); + String softwareId = UUID.randomUUID().toString(); + String softwareVersion = "version_3.1.5"; + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setSoftwareId(softwareId); + registerRequest.setSoftwareVersion(softwareVersion); + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + assertTrue(response.getClaims().containsKey(SOFTWARE_ID.toString())); + assertEquals(response.getClaims().get(SOFTWARE_ID.toString()), softwareId); + assertTrue(response.getClaims().containsKey(SOFTWARE_VERSION.toString())); + assertEquals(response.getClaims().get(SOFTWARE_VERSION.toString()), softwareVersion); + } + + @Test(dependsOnMethods = "requestClientAssociate2") + public void requestClientUpdate() throws Exception { + showTitle("requestClientUpdate"); + + final String logoUriNewValue = "http://www.gluu.org/test/yuriy/logo.png"; + final String contact1NewValue = "yuriy@gluu.org"; + final String contact2NewValue = "yuriyz@gmail.com"; + + final RegisterRequest registerRequest = new RegisterRequest(registrationAccessToken1); + registerRequest.setHttpMethod(HttpMethod.PUT); + registerRequest.setContacts(Arrays.asList(contact1NewValue, contact2NewValue)); + registerRequest.setLogoUri(logoUriNewValue); + + final RegisterClient registerClient = new RegisterClient(registrationClientUri1); + registerClient.setRequest(registerRequest); + registerClient.setExecutor(clientEngine(true)); + final RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + + // check whether info is really updated + final String responseContacts = response.getClaims().get(CONTACTS.toString()); + final String responseLogoUri = response.getClaims().get(LOGO_URI.toString()); + + assertTrue(responseContacts.contains(contact1NewValue) && responseContacts.contains(contact2NewValue)); + assertNotNull(responseLogoUri.equals(logoUriNewValue)); + } + + @Test(dependsOnMethods = "requestClientAssociate2") + public void requestClientRead() throws Exception { + showTitle("requestClientRead"); + + RegisterRequest registerRequest = new RegisterRequest(registrationAccessToken1); + + RegisterClient registerClient = new RegisterClient(registrationClientUri1); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getRegistrationClientUri()); + assertNotNull(response.getClientSecretExpiresAt()); + assertNotNull(response.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(response.getClaims().get(POLICY_URI.toString())); + assertNotNull(response.getClaims().get(REQUEST_OBJECT_SIGNING_ALG.toString())); + assertNotNull(response.getClaims().get(CONTACTS.toString())); + assertNotNull(response.getClaims().get(SECTOR_IDENTIFIER_URI.toString())); + assertNotNull(response.getClaims().get(SUBJECT_TYPE.toString())); + assertNotNull(response.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(response.getClaims().get(JWKS_URI.toString())); + assertNotNull(response.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(response.getClaims().get(LOGO_URI.toString())); + assertNotNull(response.getClaims().get(REQUEST_URIS.toString())); + assertNotNull(response.getClaims().get(SCOPE.toString())); + } + + @Parameters({"redirectUris", "sectorIdentifierUri", "logoutUri"}) + @Test + public void requestClientAssociate3(final String redirectUris, final String sectorIdentifierUri, + final String logoutUri) throws Exception { + showTitle("requestClientAssociate3"); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setPostLogoutRedirectUris(Lists.newArrayList(logoutUri)); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); // + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setGrantTypes(Arrays.asList(GrantType.IMPLICIT)); + registerRequest.setResponseTypes(Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN)); + registerRequest.setScope(Arrays.asList("openid", "profile", "email")); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_POST); + registerRequest.setFrontChannelLogoutSessionRequired(true); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + registerClient.setExecutor(clientEngine(true)); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + assertNotNull(response.getClaims().get(SCOPE.toString())); + assertNotNull(response.getClaims().get(FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED.toString())); + assertTrue(Boolean.parseBoolean(response.getClaims().get(FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED.toString()))); + assertNotNull(response.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertEquals(SignatureAlgorithm.RS256, + SignatureAlgorithm.fromString(response.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString()))); + assertEquals(AuthenticationMethod.CLIENT_SECRET_POST, + AuthenticationMethod.fromString(response.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()))); + JSONArray scopesJsonArray = new JSONArray(StringUtils.spaceSeparatedToList(response.getClaims().get(SCOPE.toString()))); + List scopes = new ArrayList(); + for (int i = 0; i < scopesJsonArray.length(); i++) { + scopes.add(scopesJsonArray.get(i).toString()); + } + assertTrue(scopes.contains("openid")); + assertTrue(scopes.contains("email")); + assertTrue(scopes.contains("profile")); + + registrationAccessToken2 = response.getRegistrationAccessToken(); + registrationClientUri2 = response.getRegistrationClientUri(); + } + + @Test(dependsOnMethods = "requestClientAssociate3") + public void requestClientUpdate3() throws Exception { + showTitle("requestClientUpdate3"); + + final String clientName = "Dynamically Registered Client #1 update_1"; + + final RegisterRequest registerRequest = new RegisterRequest(registrationAccessToken2); + registerRequest.setHttpMethod(HttpMethod.PUT); + + registerRequest.setRedirectUris(Arrays.asList("https://localhost:8443/auth")); + registerRequest.setPostLogoutRedirectUris(Arrays.asList("https://localhost:8443/auth")); + registerRequest.setApplicationType(ApplicationType.WEB); + registerRequest.setClientName(clientName); + registerRequest.setSubjectType(SubjectType.PUBLIC); + registerRequest.setGrantTypes(Arrays.asList(GrantType.IMPLICIT)); + registerRequest.setResponseTypes(Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN)); + registerRequest.setScope(Arrays.asList("openid", "address", "profile", "email", "phone", "clientinfo", "invalid_scope")); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_POST); + registerRequest.setFrontChannelLogoutSessionRequired(true); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + + final RegisterClient registerClient = new RegisterClient(registrationClientUri2); + registerClient.setRequest(registerRequest); + registerClient.setExecutor(clientEngine(true)); + final RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + + assertTrue(response.getClaims().containsKey(CLIENT_NAME.toString())); + assertEquals(clientName, response.getClaims().get(CLIENT_NAME.toString())); + JSONArray scopesJsonArray = new JSONArray(StringUtils.spaceSeparatedToList(response.getClaims().get(SCOPE.toString()))); + List scopes = new ArrayList(); + for (int i = 0; i < scopesJsonArray.length(); i++) { + scopes.add(scopesJsonArray.get(i).toString()); + } + assertTrue(scopes.contains("openid")); + assertTrue(scopes.contains("address")); + assertTrue(scopes.contains("email")); + assertTrue(scopes.contains("profile")); + assertTrue(scopes.contains("phone")); + assertTrue(scopes.contains("clientinfo")); + } + + @Parameters({"redirectUris", "sectorIdentifierUri"}) + @Test + // ATTENTION : uncomment test annotation only if 112-customAttributes.ldif (located in server test resources) + // is loaded by ldap server. + public void requestClientRegistrationWithCustomAttributes( + final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("requestClientRegistrationWithCustomAttributes"); + + final RegisterRequest request = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + + // custom attribute must be declared in oxauth-config.xml in dynamic-registration-custom-attribute tag + request.addCustomAttribute("myCustomAttr1", "customAttrValue1"); + request.addCustomAttribute("myCustomAttr2", "customAttrValue2"); + request.setSectorIdentifierUri(sectorIdentifierUri); + + final RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(request); + final RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + } + + @Test + public void failRegistration_whenRedirectUriIsNotSetForResponseTypeCode() throws Exception { + showTitle("failRegistration_whenRedirectUriIsNotSetForResponseTypeCode"); + + RegisterRequest request = new RegisterRequest(); + request.setResponseTypes(Lists.newArrayList(ResponseType.CODE)); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setExecutor(clientEngine(true)); + registerClient.setRequest(request); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 400, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getEntity(), "The entity is null"); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + } + + @Test + public void requestClientRegistrationFail3() throws Exception { + showTitle("requestClientRegistrationFail3"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + RegisterResponse response = registerClient.execRegister(ApplicationType.WEB, "oxAuth test app", + Arrays.asList("https://client.example.com/cb#fail_fragment")); + + showClient(registerClient); + assertEquals(response.getStatus(), 400, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getEntity(), "The entity is null"); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris"}) + @Test + public void requestClientRegistrationFail4(final String redirectUris) throws Exception { + showTitle("requestClientRegistrationFail4"); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.NONE); // id_token signature cannot be none + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + registerClient.setExecutor(clientEngine(true)); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 400); + assertNotNull(response.getEntity()); + assertNotNull(response.getErrorType()); + assertNotNull(response.getErrorDescription()); + } + + @Parameters({"redirectUris"}) + @Test + public void registerWithCustomURI(final String redirectUris) throws Exception { + showTitle("registerWithCustomURI"); + + List redirectUriList = Lists.newArrayList(StringUtils.spaceSeparatedToList(redirectUris)); + redirectUriList.add("myschema://client.example.com/cb"); // URI with custom schema + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.NATIVE, "oxAuth native test app with custom schema in URI", + redirectUriList); + registerRequest.setSubjectType(SubjectType.PUBLIC); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setExecutor(clientEngine(true)); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + } + + @Parameters({"redirectUris", "sectorIdentifierUri"}) + @Test + public void registerWithApplicationTypeNativeAndSubjectTypePairwise( + final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("registerWithApplicationTypeNativeAndSubjectTypePairwise"); + + List redirectUriList = Lists.newArrayList(StringUtils.spaceSeparatedToList(redirectUris)); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.NATIVE, "oxAuth native test app", + redirectUriList); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setExecutor(clientEngine(true)); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + } + + @Parameters({"redirectUris"}) + @Test + public void registerWithHttp1(final String redirectUris) throws Exception { + showTitle("registerWithHttp1"); + + List redirectUriList = Lists.newArrayList(StringUtils.spaceSeparatedToList(redirectUris)); + redirectUriList.add("http://localhost/cb"); // URI with HTTP schema + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth web test app with HTTP schema in URI", + redirectUriList); + registerRequest.setSubjectType(SubjectType.PUBLIC); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setExecutor(clientEngine(true)); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + } + + @Parameters({"redirectUris"}) + @Test + public void registerWithHttp2(final String redirectUris) throws Exception { + showTitle("registerWithHttp2"); + + List redirectUriList = Lists.newArrayList(StringUtils.spaceSeparatedToList(redirectUris)); + redirectUriList.add("http://127.0.0.1/cb"); // URI with HTTP schema + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth web test app with HTTP schema in URI", + redirectUriList); + registerRequest.setSubjectType(SubjectType.PUBLIC); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setExecutor(clientEngine(true)); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + } + + @Parameters({"redirectUris"}) + @Test + public void registerWithHttpFail(final String redirectUris) throws Exception { + showTitle("registerWithHttpFail"); + + List redirectUriList = Lists.newArrayList(StringUtils.spaceSeparatedToList(redirectUris)); + redirectUriList.add("http://www.example.com/cb"); // URI with HTTP schema + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth web test app with HTTP schema in URI", + redirectUriList); + registerRequest.setSubjectType(SubjectType.PUBLIC); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setExecutor(clientEngine(true)); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 400); + assertNotNull(response.getEntity()); + assertNotNull(response.getErrorType()); + assertNotNull(response.getErrorDescription()); + } + + @Parameters({"redirectUris"}) + @Test + public void deleteClient(final String redirectUris) throws Exception { + showTitle("deleteClient"); + + List redirectUriList = Lists.newArrayList(StringUtils.spaceSeparatedToList(redirectUris)); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth web test app with HTTP schema in URI", redirectUriList); + registerRequest.setSubjectType(SubjectType.PUBLIC); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setExecutor(clientEngine(true)); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + registerRequest = new RegisterRequest(response.getRegistrationAccessToken()); + registerRequest.setHttpMethod(HttpMethod.DELETE); + + RegisterClient deleteClient = new RegisterClient(response.getRegistrationClientUri()); + deleteClient.setRequest(registerRequest); + deleteClient.setExecutor(clientEngine(true)); + RegisterResponse deleteResponse = deleteClient.exec(); + + showClient(deleteClient); + assertEquals(deleteResponse.getStatus(), 204, "Unexpected response code: " + response.getEntity()); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/RegistrationWithSoftwareStatement.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/RegistrationWithSoftwareStatement.java new file mode 100644 index 00000000..081151fd --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/RegistrationWithSoftwareStatement.java @@ -0,0 +1,490 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.gluu.oxauth.model.register.RegisterRequestParam.APPLICATION_TYPE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.CLIENT_NAME; +import static org.gluu.oxauth.model.register.RegisterRequestParam.CONTACTS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED; +import static org.gluu.oxauth.model.register.RegisterRequestParam.FRONT_CHANNEL_LOGOUT_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ID_TOKEN_ENCRYPTED_RESPONSE_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ID_TOKEN_ENCRYPTED_RESPONSE_ENC; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ID_TOKEN_SIGNED_RESPONSE_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.JWKS_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.LOGO_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.POLICY_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REDIRECT_URIS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REQUEST_OBJECT_ENCRYPTION_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REQUEST_OBJECT_ENCRYPTION_ENC; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REQUEST_OBJECT_SIGNING_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REQUEST_URIS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SCOPE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SECTOR_IDENTIFIER_URI; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SOFTWARE_ID; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SOFTWARE_STATEMENT; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SOFTWARE_VERSION; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SUBJECT_TYPE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.TOKEN_ENDPOINT_AUTH_METHOD; +import static org.gluu.oxauth.model.register.RegisterRequestParam.TOKEN_ENDPOINT_AUTH_SIGNING_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.USERINFO_ENCRYPTED_RESPONSE_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.USERINFO_ENCRYPTED_RESPONSE_ENC; +import static org.gluu.oxauth.model.register.RegisterRequestParam.USERINFO_SIGNED_RESPONSE_ALG; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.model.SoftwareStatement; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.SubjectType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.encryption.BlockEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.encryption.KeyEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.gluu.oxauth.model.util.Util; +import org.json.JSONArray; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +import com.google.common.collect.Lists; + +/** + * @author Javier Rojas Blum + * @version December 4, 2018 + */ +public class RegistrationWithSoftwareStatement extends BaseTest { + + private String registrationAccessToken1; + private String registrationClientUri1; + private String registrationAccessToken2; + private String registrationClientUri2; + + /** + * Verify signature with JWKS_URI + */ + @Parameters({"redirectUris", "sectorIdentifierUri", "logoutUri", "keyStoreFile", "keyStoreSecret", "dnName", + "RS256_keyId", "clientJwksUri"}) + @Test + public void requestClientAssociate1(final String redirectUris, final String sectorIdentifierUri, + final String logoutUri, final String keyStoreFile, final String keyStoreSecret, + final String dnName, final String keyId, final String clientJwksUri) throws Exception { + showTitle("requestClientAssociate1"); + + String softwareId = UUID.randomUUID().toString(); + String softwareVersion = "version_3.1.5"; + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + SoftwareStatement softwareStatement = new SoftwareStatement(SignatureAlgorithm.RS256, cryptoProvider); + softwareStatement.setKeyId(keyId); + softwareStatement.getClaims().put(APPLICATION_TYPE.toString(), ApplicationType.WEB); + softwareStatement.getClaims().put(CLIENT_NAME.toString(), "oxAuth test app"); + softwareStatement.getClaims().put(REDIRECT_URIS.toString(), StringUtils.spaceSeparatedToList(redirectUris)); + softwareStatement.getClaims().put(CONTACTS.toString(), Arrays.asList("javier@gluu.org", "javier.rojas.blum@gmail.com")); + softwareStatement.getClaims().put(SCOPE.toString(), Util.listAsString(Arrays.asList("openid", "address", "profile", "email", "phone", "clientinfo", "invalid_scope"))); + softwareStatement.getClaims().put(LOGO_URI.toString(), "http://www.gluu.org/wp-content/themes/gluursn/images/logo.png"); + softwareStatement.getClaims().put(TOKEN_ENDPOINT_AUTH_METHOD.toString(), AuthenticationMethod.CLIENT_SECRET_JWT); + softwareStatement.getClaims().put(POLICY_URI.toString(), "http://www.gluu.org/policy"); + softwareStatement.getClaims().put(JWKS_URI.toString(), clientJwksUri); + softwareStatement.getClaims().put(SECTOR_IDENTIFIER_URI.toString(), sectorIdentifierUri); + softwareStatement.getClaims().put(SUBJECT_TYPE.toString(), SubjectType.PAIRWISE); + softwareStatement.getClaims().put(REQUEST_URIS.toString(), Arrays.asList("http://www.gluu.org/request")); + softwareStatement.getClaims().put(FRONT_CHANNEL_LOGOUT_URI.toString(), Lists.newArrayList(logoutUri)); + softwareStatement.getClaims().put(FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED.toString(), true); + softwareStatement.getClaims().put(ID_TOKEN_SIGNED_RESPONSE_ALG.toString(), SignatureAlgorithm.RS512); + softwareStatement.getClaims().put(ID_TOKEN_ENCRYPTED_RESPONSE_ALG.toString(), KeyEncryptionAlgorithm.RSA1_5); + softwareStatement.getClaims().put(ID_TOKEN_ENCRYPTED_RESPONSE_ENC.toString(), BlockEncryptionAlgorithm.A128CBC_PLUS_HS256); + softwareStatement.getClaims().put(USERINFO_SIGNED_RESPONSE_ALG.toString(), SignatureAlgorithm.RS384); + softwareStatement.getClaims().put(USERINFO_ENCRYPTED_RESPONSE_ALG.toString(), KeyEncryptionAlgorithm.A128KW); + softwareStatement.getClaims().put(USERINFO_ENCRYPTED_RESPONSE_ENC.toString(), BlockEncryptionAlgorithm.A128GCM); + softwareStatement.getClaims().put(REQUEST_OBJECT_SIGNING_ALG.toString(), SignatureAlgorithm.RS256); + softwareStatement.getClaims().put(REQUEST_OBJECT_ENCRYPTION_ALG.toString(), KeyEncryptionAlgorithm.A256KW); + softwareStatement.getClaims().put(REQUEST_OBJECT_ENCRYPTION_ENC.toString(), BlockEncryptionAlgorithm.A256CBC_PLUS_HS512); + softwareStatement.getClaims().put(TOKEN_ENDPOINT_AUTH_METHOD.toString(), AuthenticationMethod.CLIENT_SECRET_JWT); + softwareStatement.getClaims().put(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString(), SignatureAlgorithm.ES256); + softwareStatement.getClaims().put(SOFTWARE_ID.toString(), softwareId); + softwareStatement.getClaims().put(SOFTWARE_VERSION.toString(), softwareVersion); + String encodedSoftwareStatement = softwareStatement.getEncodedJwt(); + + RegisterRequest registerRequest = new RegisterRequest(); + registerRequest.setSoftwareStatement(encodedSoftwareStatement); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + registerClient.setExecutor(clientEngine(true)); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + assertNotNull(response.getClaims().get(SCOPE.toString())); + assertNotNull(response.getClaims().get(FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED.toString())); + assertTrue(Boolean.parseBoolean(response.getClaims().get(FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED.toString()))); + assertNotNull(response.getClaims().get(FRONT_CHANNEL_LOGOUT_URI.toString())); + assertTrue(new JSONArray(response.getClaims().get(FRONT_CHANNEL_LOGOUT_URI.toString())).getString(0).equals(logoutUri)); + assertNotNull(response.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertEquals(SignatureAlgorithm.RS512, + SignatureAlgorithm.fromString(response.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString()))); + assertNotNull(response.getClaims().get(ID_TOKEN_ENCRYPTED_RESPONSE_ALG.toString())); + assertEquals(KeyEncryptionAlgorithm.RSA1_5, + KeyEncryptionAlgorithm.fromName(response.getClaims().get(ID_TOKEN_ENCRYPTED_RESPONSE_ALG.toString()))); + assertNotNull(response.getClaims().get(ID_TOKEN_ENCRYPTED_RESPONSE_ENC.toString())); + assertEquals(BlockEncryptionAlgorithm.A128CBC_PLUS_HS256, + BlockEncryptionAlgorithm.fromName(response.getClaims().get(ID_TOKEN_ENCRYPTED_RESPONSE_ENC.toString()))); + assertNotNull(response.getClaims().get(USERINFO_SIGNED_RESPONSE_ALG.toString())); + assertEquals(SignatureAlgorithm.RS384, + SignatureAlgorithm.fromString(response.getClaims().get(USERINFO_SIGNED_RESPONSE_ALG.toString()))); + assertNotNull(response.getClaims().get(USERINFO_ENCRYPTED_RESPONSE_ALG.toString())); + assertEquals(KeyEncryptionAlgorithm.A128KW, + KeyEncryptionAlgorithm.fromName(response.getClaims().get(USERINFO_ENCRYPTED_RESPONSE_ALG.toString()))); + assertNotNull(response.getClaims().get(USERINFO_ENCRYPTED_RESPONSE_ENC.toString())); + assertEquals(BlockEncryptionAlgorithm.A128GCM, + BlockEncryptionAlgorithm.fromName(response.getClaims().get(USERINFO_ENCRYPTED_RESPONSE_ENC.toString()))); + assertNotNull(response.getClaims().get(REQUEST_OBJECT_SIGNING_ALG.toString())); + assertEquals(SignatureAlgorithm.RS256, + SignatureAlgorithm.fromString(response.getClaims().get(REQUEST_OBJECT_SIGNING_ALG.toString()))); + assertNotNull(response.getClaims().get(REQUEST_OBJECT_ENCRYPTION_ALG.toString())); + assertEquals(KeyEncryptionAlgorithm.A256KW, + KeyEncryptionAlgorithm.fromName(response.getClaims().get(REQUEST_OBJECT_ENCRYPTION_ALG.toString()))); + assertNotNull(response.getClaims().get(REQUEST_OBJECT_ENCRYPTION_ENC.toString())); + assertEquals(BlockEncryptionAlgorithm.A256CBC_PLUS_HS512, + BlockEncryptionAlgorithm.fromName(response.getClaims().get(REQUEST_OBJECT_ENCRYPTION_ENC.toString()))); + assertNotNull(response.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(AuthenticationMethod.CLIENT_SECRET_JWT, + AuthenticationMethod.fromString(response.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()))); + assertNotNull(response.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(SignatureAlgorithm.ES256, + SignatureAlgorithm.fromString(response.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()))); + JSONArray scopesJsonArray = new JSONArray(StringUtils.spaceSeparatedToList(response.getClaims().get(SCOPE.toString()))); + List scopes = new ArrayList(); + for (int i = 0; i < scopesJsonArray.length(); i++) { + scopes.add(scopesJsonArray.get(i).toString()); + } + assertTrue(scopes.contains("openid")); + assertTrue(scopes.contains("address")); + assertTrue(scopes.contains("email")); + assertTrue(scopes.contains("profile")); + assertTrue(scopes.contains("phone")); + assertTrue(scopes.contains("clientinfo")); + assertTrue(response.getClaims().containsKey(SOFTWARE_ID.toString())); + assertEquals(response.getClaims().get(SOFTWARE_ID.toString()), softwareId); + assertTrue(response.getClaims().containsKey(SOFTWARE_VERSION.toString())); + assertEquals(response.getClaims().get(SOFTWARE_VERSION.toString()), softwareVersion); + assertTrue(response.getClaims().containsKey(SOFTWARE_STATEMENT.toString())); + + registrationAccessToken1 = response.getRegistrationAccessToken(); + registrationClientUri1 = response.getRegistrationClientUri(); + } + + /** + * Verify signature with JWKS + */ + @Parameters({"redirectUris", "sectorIdentifierUri", "logoutUri", "keyStoreFile", "keyStoreSecret", "dnName", + "RS256_keyId", "clientJwksUri"}) + @Test + public void requestClientAssociate2(final String redirectUris, final String sectorIdentifierUri, + final String logoutUri, final String keyStoreFile, final String keyStoreSecret, + final String dnName, final String keyId, final String clientJwksUri) throws Exception { + showTitle("requestClientAssociate2"); + + String softwareId = UUID.randomUUID().toString(); + String softwareVersion = "version_3.1.5"; + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + SoftwareStatement softwareStatement = new SoftwareStatement(SignatureAlgorithm.RS256, cryptoProvider); + softwareStatement.setKeyId(keyId); + softwareStatement.getClaims().put(APPLICATION_TYPE.toString(), ApplicationType.WEB); + softwareStatement.getClaims().put(CLIENT_NAME.toString(), "oxAuth test app"); + softwareStatement.getClaims().put(REDIRECT_URIS.toString(), StringUtils.spaceSeparatedToList(redirectUris)); + softwareStatement.getClaims().put(CONTACTS.toString(), Arrays.asList("javier@gluu.org", "javier.rojas.blum@gmail.com")); + softwareStatement.getClaims().put(SCOPE.toString(), Util.listAsString(Arrays.asList("openid", "address", "profile", "email", "phone", "clientinfo", "invalid_scope"))); + softwareStatement.getClaims().put(LOGO_URI.toString(), "http://www.gluu.org/wp-content/themes/gluursn/images/logo.png"); + softwareStatement.getClaims().put(TOKEN_ENDPOINT_AUTH_METHOD.toString(), AuthenticationMethod.CLIENT_SECRET_JWT); + softwareStatement.getClaims().put(POLICY_URI.toString(), "http://www.gluu.org/policy"); + softwareStatement.getClaims().put(JWKS_URI.toString(), clientJwksUri); + softwareStatement.getClaims().put(SECTOR_IDENTIFIER_URI.toString(), sectorIdentifierUri); + softwareStatement.getClaims().put(SUBJECT_TYPE.toString(), SubjectType.PAIRWISE); + softwareStatement.getClaims().put(REQUEST_URIS.toString(), Arrays.asList("http://www.gluu.org/request")); + softwareStatement.getClaims().put(FRONT_CHANNEL_LOGOUT_URI.toString(), Lists.newArrayList(logoutUri)); + softwareStatement.getClaims().put(FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED.toString(), true); + softwareStatement.getClaims().put(ID_TOKEN_SIGNED_RESPONSE_ALG.toString(), SignatureAlgorithm.RS512); + softwareStatement.getClaims().put(ID_TOKEN_ENCRYPTED_RESPONSE_ALG.toString(), KeyEncryptionAlgorithm.RSA1_5); + softwareStatement.getClaims().put(ID_TOKEN_ENCRYPTED_RESPONSE_ENC.toString(), BlockEncryptionAlgorithm.A128CBC_PLUS_HS256); + softwareStatement.getClaims().put(USERINFO_SIGNED_RESPONSE_ALG.toString(), SignatureAlgorithm.RS384); + softwareStatement.getClaims().put(USERINFO_ENCRYPTED_RESPONSE_ALG.toString(), KeyEncryptionAlgorithm.A128KW); + softwareStatement.getClaims().put(USERINFO_ENCRYPTED_RESPONSE_ENC.toString(), BlockEncryptionAlgorithm.A128GCM); + softwareStatement.getClaims().put(REQUEST_OBJECT_SIGNING_ALG.toString(), SignatureAlgorithm.RS256); + softwareStatement.getClaims().put(REQUEST_OBJECT_ENCRYPTION_ALG.toString(), KeyEncryptionAlgorithm.A256KW); + softwareStatement.getClaims().put(REQUEST_OBJECT_ENCRYPTION_ENC.toString(), BlockEncryptionAlgorithm.A256CBC_PLUS_HS512); + softwareStatement.getClaims().put(TOKEN_ENDPOINT_AUTH_METHOD.toString(), AuthenticationMethod.CLIENT_SECRET_JWT); + softwareStatement.getClaims().put(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString(), SignatureAlgorithm.ES256); + softwareStatement.getClaims().put(SOFTWARE_ID.toString(), softwareId); + softwareStatement.getClaims().put(SOFTWARE_VERSION.toString(), softwareVersion); + String encodedSoftwareStatement = softwareStatement.getEncodedJwt(); + + RegisterRequest registerRequest = new RegisterRequest(); + registerRequest.setSoftwareStatement(encodedSoftwareStatement); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + registerClient.setExecutor(clientEngine(true)); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + assertNotNull(response.getClaims().get(SCOPE.toString())); + assertNotNull(response.getClaims().get(FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED.toString())); + assertTrue(Boolean.parseBoolean(response.getClaims().get(FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED.toString()))); + assertNotNull(response.getClaims().get(FRONT_CHANNEL_LOGOUT_URI.toString())); + assertTrue(new JSONArray(response.getClaims().get(FRONT_CHANNEL_LOGOUT_URI.toString())).getString(0).equals(logoutUri)); + assertNotNull(response.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertEquals(SignatureAlgorithm.RS512, + SignatureAlgorithm.fromString(response.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString()))); + assertNotNull(response.getClaims().get(ID_TOKEN_ENCRYPTED_RESPONSE_ALG.toString())); + assertEquals(KeyEncryptionAlgorithm.RSA1_5, + KeyEncryptionAlgorithm.fromName(response.getClaims().get(ID_TOKEN_ENCRYPTED_RESPONSE_ALG.toString()))); + assertNotNull(response.getClaims().get(ID_TOKEN_ENCRYPTED_RESPONSE_ENC.toString())); + assertEquals(BlockEncryptionAlgorithm.A128CBC_PLUS_HS256, + BlockEncryptionAlgorithm.fromName(response.getClaims().get(ID_TOKEN_ENCRYPTED_RESPONSE_ENC.toString()))); + assertNotNull(response.getClaims().get(USERINFO_SIGNED_RESPONSE_ALG.toString())); + assertEquals(SignatureAlgorithm.RS384, + SignatureAlgorithm.fromString(response.getClaims().get(USERINFO_SIGNED_RESPONSE_ALG.toString()))); + assertNotNull(response.getClaims().get(USERINFO_ENCRYPTED_RESPONSE_ALG.toString())); + assertEquals(KeyEncryptionAlgorithm.A128KW, + KeyEncryptionAlgorithm.fromName(response.getClaims().get(USERINFO_ENCRYPTED_RESPONSE_ALG.toString()))); + assertNotNull(response.getClaims().get(USERINFO_ENCRYPTED_RESPONSE_ENC.toString())); + assertEquals(BlockEncryptionAlgorithm.A128GCM, + BlockEncryptionAlgorithm.fromName(response.getClaims().get(USERINFO_ENCRYPTED_RESPONSE_ENC.toString()))); + assertNotNull(response.getClaims().get(REQUEST_OBJECT_SIGNING_ALG.toString())); + assertEquals(SignatureAlgorithm.RS256, + SignatureAlgorithm.fromString(response.getClaims().get(REQUEST_OBJECT_SIGNING_ALG.toString()))); + assertNotNull(response.getClaims().get(REQUEST_OBJECT_ENCRYPTION_ALG.toString())); + assertEquals(KeyEncryptionAlgorithm.A256KW, + KeyEncryptionAlgorithm.fromName(response.getClaims().get(REQUEST_OBJECT_ENCRYPTION_ALG.toString()))); + assertNotNull(response.getClaims().get(REQUEST_OBJECT_ENCRYPTION_ENC.toString())); + assertEquals(BlockEncryptionAlgorithm.A256CBC_PLUS_HS512, + BlockEncryptionAlgorithm.fromName(response.getClaims().get(REQUEST_OBJECT_ENCRYPTION_ENC.toString()))); + assertNotNull(response.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(AuthenticationMethod.CLIENT_SECRET_JWT, + AuthenticationMethod.fromString(response.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()))); + assertNotNull(response.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(SignatureAlgorithm.ES256, + SignatureAlgorithm.fromString(response.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()))); + JSONArray scopesJsonArray = new JSONArray(StringUtils.spaceSeparatedToList(response.getClaims().get(SCOPE.toString()))); + List scopes = new ArrayList(); + for (int i = 0; i < scopesJsonArray.length(); i++) { + scopes.add(scopesJsonArray.get(i).toString()); + } + assertTrue(scopes.contains("openid")); + assertTrue(scopes.contains("address")); + assertTrue(scopes.contains("email")); + assertTrue(scopes.contains("profile")); + assertTrue(scopes.contains("phone")); + assertTrue(scopes.contains("clientinfo")); + assertTrue(response.getClaims().containsKey(SOFTWARE_ID.toString())); + assertEquals(response.getClaims().get(SOFTWARE_ID.toString()), softwareId); + assertTrue(response.getClaims().containsKey(SOFTWARE_VERSION.toString())); + assertEquals(response.getClaims().get(SOFTWARE_VERSION.toString()), softwareVersion); + assertTrue(response.getClaims().containsKey(SOFTWARE_STATEMENT.toString())); + + registrationAccessToken2 = response.getRegistrationAccessToken(); + registrationClientUri2 = response.getRegistrationClientUri(); + } + + @Test(dependsOnMethods = "requestClientAssociate1") + public void requestClientRead1() throws Exception { + showTitle("requestClientRead1"); + + RegisterRequest registerRequest = new RegisterRequest(registrationAccessToken1); + + RegisterClient registerClient = new RegisterClient(registrationClientUri1); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + assertNotNull(response.getClaims().get(SCOPE.toString())); + assertNotNull(response.getClaims().get(FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED.toString())); + assertTrue(Boolean.parseBoolean(response.getClaims().get(FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED.toString()))); + assertNotNull(response.getClaims().get(FRONT_CHANNEL_LOGOUT_URI.toString())); + assertNotNull(response.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertEquals(SignatureAlgorithm.RS512, + SignatureAlgorithm.fromString(response.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString()))); + assertNotNull(response.getClaims().get(ID_TOKEN_ENCRYPTED_RESPONSE_ALG.toString())); + assertEquals(KeyEncryptionAlgorithm.RSA1_5, + KeyEncryptionAlgorithm.fromName(response.getClaims().get(ID_TOKEN_ENCRYPTED_RESPONSE_ALG.toString()))); + assertNotNull(response.getClaims().get(ID_TOKEN_ENCRYPTED_RESPONSE_ENC.toString())); + assertEquals(BlockEncryptionAlgorithm.A128CBC_PLUS_HS256, + BlockEncryptionAlgorithm.fromName(response.getClaims().get(ID_TOKEN_ENCRYPTED_RESPONSE_ENC.toString()))); + assertNotNull(response.getClaims().get(USERINFO_SIGNED_RESPONSE_ALG.toString())); + assertEquals(SignatureAlgorithm.RS384, + SignatureAlgorithm.fromString(response.getClaims().get(USERINFO_SIGNED_RESPONSE_ALG.toString()))); + assertNotNull(response.getClaims().get(USERINFO_ENCRYPTED_RESPONSE_ALG.toString())); + assertEquals(KeyEncryptionAlgorithm.A128KW, + KeyEncryptionAlgorithm.fromName(response.getClaims().get(USERINFO_ENCRYPTED_RESPONSE_ALG.toString()))); + assertNotNull(response.getClaims().get(USERINFO_ENCRYPTED_RESPONSE_ENC.toString())); + assertEquals(BlockEncryptionAlgorithm.A128GCM, + BlockEncryptionAlgorithm.fromName(response.getClaims().get(USERINFO_ENCRYPTED_RESPONSE_ENC.toString()))); + assertNotNull(response.getClaims().get(REQUEST_OBJECT_SIGNING_ALG.toString())); + assertEquals(SignatureAlgorithm.RS256, + SignatureAlgorithm.fromString(response.getClaims().get(REQUEST_OBJECT_SIGNING_ALG.toString()))); + assertNotNull(response.getClaims().get(REQUEST_OBJECT_ENCRYPTION_ALG.toString())); + assertEquals(KeyEncryptionAlgorithm.A256KW, + KeyEncryptionAlgorithm.fromName(response.getClaims().get(REQUEST_OBJECT_ENCRYPTION_ALG.toString()))); + assertNotNull(response.getClaims().get(REQUEST_OBJECT_ENCRYPTION_ENC.toString())); + assertEquals(BlockEncryptionAlgorithm.A256CBC_PLUS_HS512, + BlockEncryptionAlgorithm.fromName(response.getClaims().get(REQUEST_OBJECT_ENCRYPTION_ENC.toString()))); + assertNotNull(response.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(AuthenticationMethod.CLIENT_SECRET_JWT, + AuthenticationMethod.fromString(response.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()))); + assertNotNull(response.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(SignatureAlgorithm.ES256, + SignatureAlgorithm.fromString(response.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()))); + JSONArray scopesJsonArray = new JSONArray(StringUtils.spaceSeparatedToList(response.getClaims().get(SCOPE.toString()))); + List scopes = new ArrayList(); + for (int i = 0; i < scopesJsonArray.length(); i++) { + scopes.add(scopesJsonArray.get(i).toString()); + } + assertTrue(scopes.contains("openid")); + assertTrue(scopes.contains("address")); + assertTrue(scopes.contains("email")); + assertTrue(scopes.contains("profile")); + assertTrue(scopes.contains("phone")); + assertTrue(scopes.contains("clientinfo")); + assertTrue(response.getClaims().containsKey(SOFTWARE_ID.toString())); + assertTrue(response.getClaims().containsKey(SOFTWARE_VERSION.toString())); + assertTrue(response.getClaims().containsKey(SOFTWARE_STATEMENT.toString())); + } + + @Test(dependsOnMethods = "requestClientAssociate2") + public void requestClientRead2() throws Exception { + showTitle("requestClientRead2"); + + RegisterRequest registerRequest = new RegisterRequest(registrationAccessToken2); + + RegisterClient registerClient = new RegisterClient(registrationClientUri2); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + assertNotNull(response.getClaims().get(SCOPE.toString())); + assertNotNull(response.getClaims().get(FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED.toString())); + assertTrue(Boolean.parseBoolean(response.getClaims().get(FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED.toString()))); + assertNotNull(response.getClaims().get(FRONT_CHANNEL_LOGOUT_URI.toString())); + assertNotNull(response.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertEquals(SignatureAlgorithm.RS512, + SignatureAlgorithm.fromString(response.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString()))); + assertNotNull(response.getClaims().get(ID_TOKEN_ENCRYPTED_RESPONSE_ALG.toString())); + assertEquals(KeyEncryptionAlgorithm.RSA1_5, + KeyEncryptionAlgorithm.fromName(response.getClaims().get(ID_TOKEN_ENCRYPTED_RESPONSE_ALG.toString()))); + assertNotNull(response.getClaims().get(ID_TOKEN_ENCRYPTED_RESPONSE_ENC.toString())); + assertEquals(BlockEncryptionAlgorithm.A128CBC_PLUS_HS256, + BlockEncryptionAlgorithm.fromName(response.getClaims().get(ID_TOKEN_ENCRYPTED_RESPONSE_ENC.toString()))); + assertNotNull(response.getClaims().get(USERINFO_SIGNED_RESPONSE_ALG.toString())); + assertEquals(SignatureAlgorithm.RS384, + SignatureAlgorithm.fromString(response.getClaims().get(USERINFO_SIGNED_RESPONSE_ALG.toString()))); + assertNotNull(response.getClaims().get(USERINFO_ENCRYPTED_RESPONSE_ALG.toString())); + assertEquals(KeyEncryptionAlgorithm.A128KW, + KeyEncryptionAlgorithm.fromName(response.getClaims().get(USERINFO_ENCRYPTED_RESPONSE_ALG.toString()))); + assertNotNull(response.getClaims().get(USERINFO_ENCRYPTED_RESPONSE_ENC.toString())); + assertEquals(BlockEncryptionAlgorithm.A128GCM, + BlockEncryptionAlgorithm.fromName(response.getClaims().get(USERINFO_ENCRYPTED_RESPONSE_ENC.toString()))); + assertNotNull(response.getClaims().get(REQUEST_OBJECT_SIGNING_ALG.toString())); + assertEquals(SignatureAlgorithm.RS256, + SignatureAlgorithm.fromString(response.getClaims().get(REQUEST_OBJECT_SIGNING_ALG.toString()))); + assertNotNull(response.getClaims().get(REQUEST_OBJECT_ENCRYPTION_ALG.toString())); + assertEquals(KeyEncryptionAlgorithm.A256KW, + KeyEncryptionAlgorithm.fromName(response.getClaims().get(REQUEST_OBJECT_ENCRYPTION_ALG.toString()))); + assertNotNull(response.getClaims().get(REQUEST_OBJECT_ENCRYPTION_ENC.toString())); + assertEquals(BlockEncryptionAlgorithm.A256CBC_PLUS_HS512, + BlockEncryptionAlgorithm.fromName(response.getClaims().get(REQUEST_OBJECT_ENCRYPTION_ENC.toString()))); + assertNotNull(response.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(AuthenticationMethod.CLIENT_SECRET_JWT, + AuthenticationMethod.fromString(response.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()))); + assertNotNull(response.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(SignatureAlgorithm.ES256, + SignatureAlgorithm.fromString(response.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()))); + JSONArray scopesJsonArray = new JSONArray(StringUtils.spaceSeparatedToList(response.getClaims().get(SCOPE.toString()))); + List scopes = new ArrayList(); + for (int i = 0; i < scopesJsonArray.length(); i++) { + scopes.add(scopesJsonArray.get(i).toString()); + } + assertTrue(scopes.contains("openid")); + assertTrue(scopes.contains("address")); + assertTrue(scopes.contains("email")); + assertTrue(scopes.contains("profile")); + assertTrue(scopes.contains("phone")); + assertTrue(scopes.contains("clientinfo")); + assertTrue(response.getClaims().containsKey(SOFTWARE_ID.toString())); + assertTrue(response.getClaims().containsKey(SOFTWARE_VERSION.toString())); + assertTrue(response.getClaims().containsKey(SOFTWARE_STATEMENT.toString())); + } + + @Test + public void requestClientRegistrationFail1() throws Exception { + showTitle("requestClientRegistrationFail1"); + + RegisterRequest registerRequest = new RegisterRequest(); + registerRequest.setSoftwareStatement("INVALID_SOFTWARE_STATEMENT"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + registerClient.setExecutor(clientEngine(true)); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 400); + assertNotNull(response.getEntity()); + assertNotNull(response.getErrorType()); + assertNotNull(response.getErrorDescription()); + } + + @Test + public void requestClientRegistrationFail2() throws Exception { + showTitle("requestClientRegistrationFail2"); + + RegisterRequest registerRequest = new RegisterRequest(); + // Test with invalid signature + registerRequest.setSoftwareStatement("eyJhbGciOiJSUzI1NiIsImtpZCI6IjQ4YmZhOGE0LWM4YTctNGEwOS1hZTk4LWJmMzI1ZDc0OTExOSJ9.eyJhcHBsaWNhdGlvbl90eXBlIjoid2ViIiwiY2xpZW50X25hbWUiOiJveEF1dGggdGVzdCBhcHAiLCJyZWRpcmVjdF91cmlzIjpbImh0dHBzOlwvXC9jZS5nbHV1LmluZm86ODQ0M1wvb3hhdXRoLXJwXC9ob21lLmh0bSIsImh0dHBzOlwvXC9jbGllbnQuZXhhbXBsZS5jb21cL2NiIiwiaHR0cHM6XC9cL2NsaWVudC5leGFtcGxlLmNvbVwvY2IxIiwiaHR0cHM6XC9cL2NsaWVudC5leGFtcGxlLmNvbVwvY2IyIl0sImNvbnRhY3RzIjpbImphdmllckBnbHV1Lm9yZyIsImphdmllci5yb2phcy5ibHVtQGdtYWlsLmNvbSJdLCJzY29wZSI6Im9wZW5pZCBhZGRyZXNzIHByb2ZpbGUgZW1haWwgcGhvbmUgY2xpZW50aW5mbyBpbnZhbGlkX3Njb3BlIiwibG9nb191cmkiOiJodHRwOlwvXC93d3cuZ2x1dS5vcmdcL3dwLWNvbnRlbnRcL3RoZW1lc1wvZ2x1dXJzblwvaW1hZ2VzXC9sb2dvLnBuZyIsInRva2VuX2VuZHBvaW50X2F1dGhfbWV0aG9kIjoiY2xpZW50X3NlY3JldF9qd3QiLCJwb2xpY3lfdXJpIjoiaHR0cDpcL1wvd3d3LmdsdXUub3JnXC9wb2xpY3kiLCJqd2tzX3VyaSI6Imh0dHA6XC9cL2xvY2FsaG9zdFwvb3hhdXRoLWNsaWVudFwvdGVzdFwvcmVzb3VyY2VzXC9qd2tzLmpzb24iLCJzZWN0b3JfaWRlbnRpZmllcl91cmkiOiJodHRwczpcL1wvY2UuZ2x1dS5pbmZvOjg0NDNcL3NlY3RvcmlkZW50aWZpZXJcL2E1NWVkZTI5LThmNWEtNDYxZC1iMDZlLTc2Y2FlZThkNDBiNSIsInN1YmplY3RfdHlwZSI6InBhaXJ3aXNlIiwicmVxdWVzdF91cmlzIjpbImh0dHA6XC9cL3d3dy5nbHV1Lm9yZ1wvcmVxdWVzdCJdLCJmcm9udGNoYW5uZWxfbG9nb3V0X3VyaSI6WyJodHRwczpcL1wvY2UuZ2x1dS5pbmZvOjg0NDNcL294YXV0aC1ycFwvaG9tZS5odG0iXSwiZnJvbnRjaGFubmVsX2xvZ291dF9zZXNzaW9uX3JlcXVpcmVkIjp0cnVlLCJpZF90b2tlbl9zaWduZWRfcmVzcG9uc2VfYWxnIjoiUlM1MTIiLCJpZF90b2tlbl9lbmNyeXB0ZWRfcmVzcG9uc2VfYWxnIjoiUlNBMV81IiwiaWRfdG9rZW5fZW5jcnlwdGVkX3Jlc3BvbnNlX2VuYyI6IkExMjhDQkMrSFMyNTYiLCJ1c2VyaW5mb19zaWduZWRfcmVzcG9uc2VfYWxnIjoiUlMzODQiLCJ1c2VyaW5mb19lbmNyeXB0ZWRfcmVzcG9uc2VfYWxnIjoiQTEyOEtXIiwidXNlcmluZm9fZW5jcnlwdGVkX3Jlc3BvbnNlX2VuYyI6IkExMjhHQ00iLCJyZXF1ZXN0X29iamVjdF9zaWduaW5nX2FsZyI6IlJTMjU2IiwicmVxdWVzdF9vYmplY3RfZW5jcnlwdGlvbl9hbGciOiJBMjU2S1ciLCJyZXF1ZXN0X29iamVjdF9lbmNyeXB0aW9uX2VuYyI6IkEyNTZDQkMrSFM1MTIiLCJ0b2tlbl9lbmRwb2ludF9hdXRoX3NpZ25pbmdfYWxnIjoiRVMyNTYiLCJzb2Z0d2FyZV9pZCI6Ijk0MDdlMWY2LTdkMmUtNDg0YS05NTg1LTY2NWI5MmY1NzgyNSIsInNvZnR3YXJlX3ZlcnNpb24iOiJ2ZXJzaW9uXzMuMS41In0.LtrjFNPRXHArcZbAv0vcYMcOdsQG8jZ0qkNPkmAQlHwyoJN1F3jv6OI8-rdu-55osStX39_NPYjpjHwzakhi3XN0pO_b1HL6sXAkhJ-UfQ7jNgtElfJ39b0maONdEJl4nblNhEho2-SbfO_OIOIFJha-OcsTS9-DUJ6umRNfaIoNhioFzrVj8rDK-MWNcXQNCKvj4IPgH2hW7adAuj6Du1k7BdtH-IeIVb1ZCjnOl9IETbq7wyc4xL6tILw40oelgVyyHCFbIWZOJJI8n59U8DlqIBqYx0lCOjIY-BH6DLxZ1PxGrXxqMRJx1h64Oh9QxuzK-GzUY4bFInnvv3Gf3g"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + registerClient.setExecutor(clientEngine(true)); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 400); + assertNotNull(response.getEntity()); + assertNotNull(response.getErrorType()); + assertNotNull(response.getErrorDescription()); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ResponseTypesRestrictionHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ResponseTypesRestrictionHttpTest.java new file mode 100644 index 00000000..57178bf0 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ResponseTypesRestrictionHttpTest.java @@ -0,0 +1,663 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.gluu.oxauth.model.register.RegisterRequestParam.APPLICATION_TYPE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.CLIENT_NAME; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ID_TOKEN_SIGNED_RESPONSE_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REDIRECT_URIS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.RESPONSE_TYPES; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SCOPE; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.Prompt; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.ITestContext; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Javier Rojas Blum + * @version November 29, 2017 + */ +public class ResponseTypesRestrictionHttpTest extends BaseTest { + + /** + * Registering without provide the response_types param, should register the Client using only + * the code response type. + */ + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void omittedResponseTypes( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("omittedResponseTypes"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_POST); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_POST); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + @DataProvider(name = "omittedResponseTypesFailDataProvider") + public Object[][] omittedResponseTypesFailDataProvider(ITestContext context) { + String redirectUris = context.getCurrentXmlTest().getParameter("redirectUris"); + String userId = context.getCurrentXmlTest().getParameter("userId"); + String userSecret = context.getCurrentXmlTest().getParameter("userSecret"); + String redirectUri = context.getCurrentXmlTest().getParameter("redirectUri"); + String sectorIdentifierUri = context.getCurrentXmlTest().getParameter("sectorIdentifierUri"); + + return new Object[][]{ + {redirectUris, redirectUri, userId, userSecret, Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN), sectorIdentifierUri}, + {redirectUris, redirectUri, userId, userSecret, Arrays.asList(ResponseType.TOKEN), sectorIdentifierUri}, + {redirectUris, redirectUri, userId, userSecret, Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN), sectorIdentifierUri}, + {redirectUris, redirectUri, userId, userSecret, Arrays.asList(ResponseType.CODE, ResponseType.TOKEN), sectorIdentifierUri}, + {redirectUris, redirectUri, userId, userSecret, Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), sectorIdentifierUri}, + {redirectUris, redirectUri, userId, userSecret, Arrays.asList(ResponseType.ID_TOKEN), sectorIdentifierUri}, + }; + } + + /** + * Authorization request with the other Response types combination should fail. + */ + @Test(dataProvider = "omittedResponseTypesFailDataProvider") + public void omittedResponseTypesFail( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final List responseTypes, final String sectorIdentifierUri) throws Exception { + showTitle("omittedResponseTypesFail"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + authorizationRequest.setAuthUsername(userId); + authorizationRequest.setAuthPassword(userSecret); + authorizationRequest.getPrompts().add(Prompt.NONE); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + + showClient(authorizeClient); + assertTrue(authorizationResponse.getStatus() == 302 + || authorizationResponse.getStatus() == 400, "Unexpected response code: " + authorizationResponse.getStatus()); + assertNotNull(authorizationResponse.getErrorType(), "The error type is null"); + assertNotNull(authorizationResponse.getErrorDescription(), "The error description is null"); + } + + /** + * Registering with the response_types param code, id_token. + */ + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void responseTypesCodeIdToken( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("responseTypesCodeIdToken"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_POST); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + String nonce = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getIdToken(), "The id token is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + + String authorizationCode = authorizationResponse.getCode(); + String idToken = authorizationResponse.getIdToken(); + + // 4. Validate code and id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.CODE_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + assertTrue(rsaSigner.validateAuthorizationCode(authorizationCode, jwt)); + + // 5. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_POST); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse1 = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse1.getStatus(), 200, "Unexpected response code: " + tokenResponse1.getStatus()); + assertNotNull(tokenResponse1.getEntity(), "The entity is null"); + assertNotNull(tokenResponse1.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse1.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse1.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse1.getRefreshToken(), "The refresh token is null"); + } + + @DataProvider(name = "responseTypesCodeIdTokenFailDataProvider") + public Object[][] responseTypesCodeIdTokenFailDataProvider(ITestContext context) { + String redirectUris = context.getCurrentXmlTest().getParameter("redirectUris"); + String redirectUri = context.getCurrentXmlTest().getParameter("redirectUri"); + String userId = context.getCurrentXmlTest().getParameter("userId"); + String userSecret = context.getCurrentXmlTest().getParameter("userSecret"); + String sectorIdentifierUri = context.getCurrentXmlTest().getParameter("sectorIdentifierUri"); + + return new Object[][]{ + {redirectUris, redirectUri, userId, userSecret, Arrays.asList(ResponseType.TOKEN), sectorIdentifierUri}, + {redirectUris, redirectUri, userId, userSecret, Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN), sectorIdentifierUri}, + {redirectUris, redirectUri, userId, userSecret, Arrays.asList(ResponseType.CODE, ResponseType.TOKEN), sectorIdentifierUri}, + {redirectUris, redirectUri, userId, userSecret, Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), sectorIdentifierUri}, + }; + } + + /** + * Authorization request with the other Response types combination should fail. + */ + @Test(dataProvider = "responseTypesCodeIdTokenFailDataProvider") + public void responseTypesCodeIdTokenFail( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final List responseTypes, final String sectorIdentifierUri) throws Exception { + showTitle("responseTypesCodeIdTokenFail"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN)); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + authorizationRequest.setAuthUsername(userId); + authorizationRequest.setAuthPassword(userSecret); + authorizationRequest.getPrompts().add(Prompt.NONE); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + + showClient(authorizeClient); + assertTrue(authorizationResponse.getStatus() == 302 + || authorizationResponse.getStatus() == 400, "Unexpected response code: " + authorizationResponse.getStatus()); + assertNotNull(authorizationResponse.getErrorType(), "The error type is null"); + assertNotNull(authorizationResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void responseTypesTokenIdToken( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("responseTypesTokenIdToken"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest( + responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The access token is null"); + assertNotNull(authorizationResponse.getIdToken(), "The id token is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + + String accessToken = authorizationResponse.getAccessToken(); + String idToken = authorizationResponse.getIdToken(); + + // 4. Validate code and id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + assertTrue(rsaSigner.validateAccessToken(accessToken, jwt)); + } + + @DataProvider(name = "responseTypesTokenIdTokenFailDataProvider") + public Object[][] responseTypesTokenIdTokenFailDataProvider(ITestContext context) { + String redirectUris = context.getCurrentXmlTest().getParameter("redirectUris"); + String redirectUri = context.getCurrentXmlTest().getParameter("redirectUri"); + String userId = context.getCurrentXmlTest().getParameter("userId"); + String userSecret = context.getCurrentXmlTest().getParameter("userSecret"); + String sectorIdentifierUri = context.getCurrentXmlTest().getParameter("sectorIdentifierUri"); + + return new Object[][]{ + {redirectUris, redirectUri, userId, userSecret, Arrays.asList(ResponseType.CODE), sectorIdentifierUri}, + {redirectUris, redirectUri, userId, userSecret, Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN), sectorIdentifierUri}, + {redirectUris, redirectUri, userId, userSecret, Arrays.asList(ResponseType.CODE, ResponseType.TOKEN), sectorIdentifierUri}, + {redirectUris, redirectUri, userId, userSecret, Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), sectorIdentifierUri}, + }; + } + + @Test(dataProvider = "responseTypesTokenIdTokenFailDataProvider") + public void responseTypesTokenIdTokenFail( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final List responseTypes, final String sectorIdentifierUri) throws Exception { + showTitle("responseTypesTokenIdTokenFail"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN)); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest( + responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.setAuthUsername(userId); + authorizationRequest.setAuthPassword(userSecret); + authorizationRequest.getPrompts().add(Prompt.NONE); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + + showClient(authorizeClient); + assertTrue(authorizationResponse.getStatus() == 302 + || authorizationResponse.getStatus() == 400, "Unexpected response code: " + authorizationResponse.getStatus()); + assertNotNull(authorizationResponse.getErrorType(), "The error type is null"); + assertNotNull(authorizationResponse.getErrorDescription(), "The error description is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/RevokeSessionHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/RevokeSessionHttpTest.java new file mode 100644 index 00000000..858d2537 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/RevokeSessionHttpTest.java @@ -0,0 +1,91 @@ +package org.gluu.oxauth.ws.rs; + +import static org.gluu.oxauth.client.Asserter.assertOk; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.RevokeSessionClient; +import org.gluu.oxauth.client.RevokeSessionRequest; +import org.gluu.oxauth.client.RevokeSessionResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Yuriy Zabrovarnyy + */ +public class RevokeSessionHttpTest extends BaseTest { + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri", "umaPatClientId", "umaPatClientSecret"}) + @Test + public void revokeSession( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri, String umaPatClientId, String umaPatClientSecret) throws Exception { + showTitle("revokeSession"); + + final AuthenticationMethod authnMethod = AuthenticationMethod.CLIENT_SECRET_BASIC; + + // 1. Register client + List responseTypes = Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + + registerRequest.setTokenEndpointAuthMethod(authnMethod); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setResponseTypes(responseTypes); + + RegisterClient registerClient = newRegisterClient(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertOk(registerResponse); + assertNotNull(registerResponse.getRegistrationAccessToken()); + + // 3. Request authorization + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + String nonce = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, registerResponse.getClientId(), scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getIdToken(), "The ID Token is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + + RevokeSessionRequest revokeSessionRequest = new RevokeSessionRequest("uid", "test"); + revokeSessionRequest.setAuthenticationMethod(authnMethod); + revokeSessionRequest.setAuthUsername(umaPatClientId); // it must be client with revoke_session scope + revokeSessionRequest.setAuthPassword(umaPatClientSecret); + + RevokeSessionClient revokeSessionClient = newRevokeSessionClient(revokeSessionRequest); + final RevokeSessionResponse revokeSessionResponse = revokeSessionClient.exec(); + + showClient(revokeSessionClient); + + assertEquals(revokeSessionResponse.getStatus(), 200); + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/SSOWithMultipleBackendServicesHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/SSOWithMultipleBackendServicesHttpTest.java new file mode 100644 index 00000000..f68b1153 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/SSOWithMultipleBackendServicesHttpTest.java @@ -0,0 +1,372 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.AuthorizationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.Prompt; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * Functional tests for SSO with Multiple Backend Services (HTTP) + * + * @author Javier Rojas Blum + * @version August 9, 2017 + */ +public class SSOWithMultipleBackendServicesHttpTest extends BaseTest { + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void sessionWorkFlow1( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("sessionWorkFlow1"); + + // Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + String state1 = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest1 = new AuthorizationRequest( + Arrays.asList(ResponseType.CODE), + clientId, + Arrays.asList("openid", "profile", "email"), + redirectUri, + null); + + authorizationRequest1.setState(state1); + authorizationRequest1.setRequestSessionId(true); + + AuthorizationResponse authorizationResponse1 = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest1, userId, userSecret); + + assertNotNull(authorizationResponse1.getLocation(), "The location is null"); + assertNotNull(authorizationResponse1.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse1.getSessionId(), "The session id is null"); + assertNotNull(authorizationResponse1.getScope(), "The scope is null"); + assertNotNull(authorizationResponse1.getState(), "The state is null"); + assertEquals(authorizationResponse1.getState(), state1); + + String code1 = authorizationResponse1.getCode(); + String sessionId = authorizationResponse1.getSessionId(); + + // TV sends the code to the Backend + // We don't use httpClient and cookieStore during this call + + //////////////////////////////////////////////// + // Backend 1 side. Code 1 // + //////////////////////////////////////////////// + + // Get the access token + TokenClient tokenClient1 = new TokenClient(tokenEndpoint); + TokenResponse tokenResponse1 = tokenClient1.execAuthorizationCode(code1, redirectUri, clientId, clientSecret); + + showClient(tokenClient1); + assertEquals(tokenResponse1.getStatus(), 200, "Unexpected response code: " + tokenResponse1.getStatus()); + assertNotNull(tokenResponse1.getEntity(), "The entity is null"); + assertNotNull(tokenResponse1.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse1.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse1.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse1.getRefreshToken(), "The refresh token is null"); + + String accessToken1 = tokenResponse1.getAccessToken(); + + // Get the user's claims + UserInfoClient userInfoClient1 = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse1 = userInfoClient1.execUserInfo(accessToken1); + + showClient(userInfoClient1); + assertEquals(userInfoResponse1.getStatus(), 200, "Unexpected response code: " + userInfoResponse1.getStatus()); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.SUBJECT_IDENTIFIER), "Unexpected result: subject not found"); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.NAME), "Unexpected result: name not found"); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.GIVEN_NAME), "Unexpected result: given_name not found"); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.FAMILY_NAME), "Unexpected result: family_name not found"); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.EMAIL), "Unexpected result: email not found"); + + + //////////////////////////////////////////////// + // TV side. Code 2 // + //////////////////////////////////////////////// + + String state2 = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest2 = new AuthorizationRequest( + Arrays.asList(ResponseType.CODE), + clientId, + Arrays.asList("openid", "profile", "email"), + redirectUri, + null); + + authorizationRequest2.getPrompts().add(Prompt.NONE); + authorizationRequest2.setState(state2); + authorizationRequest2.setSessionId(sessionId); + + AuthorizeClient authorizeClient2 = new AuthorizeClient(authorizationEndpoint); + authorizeClient2.setRequest(authorizationRequest2); + AuthorizationResponse authorizationResponse2 = authorizeClient2.exec(); + + showClient(authorizeClient2); + assertEquals(authorizationResponse2.getStatus(), 302, "Unexpected response code: " + authorizationResponse2.getStatus()); + assertNotNull(authorizationResponse2.getLocation(), "The location is null"); + assertNotNull(authorizationResponse2.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse2.getScope(), "The scope is null"); + assertNotNull(authorizationResponse2.getState(), "The state is null"); + assertEquals(authorizationResponse2.getState(), state2); + + String code2 = authorizationResponse2.getCode(); + + // TV sends the code to the Backend + // We don't use httpClient and cookieStore during this call + + //////////////////////////////////////////////// + // Backend 2 side. Code 2 // + //////////////////////////////////////////////// + + // Get the access token + TokenClient tokenClient2 = new TokenClient(tokenEndpoint); + TokenResponse tokenResponse2 = tokenClient2.execAuthorizationCode(code2, redirectUri, clientId, clientSecret); + + showClient(tokenClient2); + assertEquals(tokenResponse2.getStatus(), 200, "Unexpected response code: " + tokenResponse2.getStatus()); + assertNotNull(tokenResponse2.getEntity(), "The entity is null"); + assertNotNull(tokenResponse2.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse2.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse2.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse2.getRefreshToken(), "The refresh token is null"); + + String accessToken2 = tokenResponse2.getAccessToken(); + + // Get the user's claims + UserInfoClient userInfoClient2 = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse2 = userInfoClient2.execUserInfo(accessToken2); + + showClient(userInfoClient2); + assertEquals(userInfoResponse2.getStatus(), 200, "Unexpected response code: " + userInfoResponse2.getStatus()); + assertNotNull(userInfoResponse2.getClaim(JwtClaimName.SUBJECT_IDENTIFIER), "Unexpected result: subject not found"); + assertNotNull(userInfoResponse2.getClaim(JwtClaimName.NAME), "Unexpected result: name not found"); + assertNotNull(userInfoResponse2.getClaim(JwtClaimName.GIVEN_NAME), "Unexpected result: given_name not found"); + assertNotNull(userInfoResponse2.getClaim(JwtClaimName.FAMILY_NAME), "Unexpected result: family_name not found"); + assertNotNull(userInfoResponse2.getClaim(JwtClaimName.EMAIL), "Unexpected result: email not found"); + } + + @Parameters({"redirectUris", "redirectUri", "userInum", "userEmail", "sectorIdentifierUri"}) + @Test + public void sessionWorkFlow2( + final String redirectUris, final String redirectUri, final String userInum, final String userEmail, + final String sectorIdentifierUri) throws Exception { + showTitle("sessionWorkFlow2"); + + // Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_POST); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // Authorization code flow to authenticate on B1 + + String state1 = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest1 = new AuthorizationRequest( + Arrays.asList(ResponseType.CODE), + clientId, + Arrays.asList("openid", "profile", "email"), + redirectUri, + null); + + authorizationRequest1.addCustomParameter("mail", userEmail); + authorizationRequest1.addCustomParameter("inum", userInum); + authorizationRequest1.getPrompts().add(Prompt.NONE); + authorizationRequest1.setState(state1); + authorizationRequest1.setAuthorizationMethod(AuthorizationMethod.FORM_ENCODED_BODY_PARAMETER); + authorizationRequest1.setRequestSessionId(true); + + AuthorizationResponse authorizationResponse1 = authorizationRequestAndGrantAccess( + authorizationEndpoint, authorizationRequest1); + + assertNotNull(authorizationResponse1.getLocation(), "The location is null"); + assertNotNull(authorizationResponse1.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse1.getSessionId(), "The session id is null"); + assertNotNull(authorizationResponse1.getScope(), "The scope is null"); + assertNotNull(authorizationResponse1.getState(), "The state is null"); + assertEquals(authorizationRequest1.getState(), state1); + + String authorizationCode1 = authorizationResponse1.getCode(); + String sessionId = authorizationResponse1.getSessionId(); + + TokenRequest tokenRequest1 = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest1.setCode(authorizationCode1); + tokenRequest1.setRedirectUri(redirectUri); + tokenRequest1.setAuthUsername(clientId); + tokenRequest1.setAuthPassword(clientSecret); + tokenRequest1.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_POST); + + TokenClient tokenClient1 = new TokenClient(tokenEndpoint); + tokenClient1.setRequest(tokenRequest1); + TokenResponse tokenResponse1 = tokenClient1.exec(); + + showClient(tokenClient1); + assertEquals(tokenResponse1.getStatus(), 200, "Unexpected response code: " + tokenResponse1.getStatus()); + assertNotNull(tokenResponse1.getEntity(), "The entity is null"); + assertNotNull(tokenResponse1.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse1.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse1.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse1.getRefreshToken(), "The refresh token is null"); + + // User wants to authenticate on B2 (without sending its credentials) + + String state2 = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest2 = new AuthorizationRequest( + Arrays.asList(ResponseType.CODE), + clientId, + Arrays.asList("openid", "profile", "email"), + redirectUri, + null); + + authorizationRequest2.getPrompts().add(Prompt.NONE); + authorizationRequest2.setState(state2); + authorizationRequest2.setSessionId(sessionId); + + AuthorizeClient authorizeClient2 = new AuthorizeClient(authorizationEndpoint); + authorizeClient2.setRequest(authorizationRequest2); + AuthorizationResponse authorizationResponse2 = authorizeClient2.exec(); + + showClient(authorizeClient2); + assertEquals(authorizationResponse2.getStatus(), 302, "Unexpected response code: " + authorizationResponse2.getStatus()); + assertNotNull(authorizationResponse2.getLocation(), "The location is null"); + assertNotNull(authorizationResponse2.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse2.getScope(), "The scope is null"); + assertNotNull(authorizationResponse2.getState(), "The state is null"); + assertEquals(authorizationResponse2.getState(), state2); + + String authorizationCode2 = authorizationResponse2.getCode(); + + TokenRequest tokenRequest2 = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest2.setCode(authorizationCode2); + tokenRequest2.setRedirectUri(redirectUri); + tokenRequest2.setAuthUsername(clientId); + tokenRequest2.setAuthPassword(clientSecret); + tokenRequest2.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_POST); + + TokenClient tokenClient2 = new TokenClient(tokenEndpoint); + tokenClient2.setRequest(tokenRequest2); + TokenResponse tokenResponse2 = tokenClient2.exec(); + + showClient(tokenClient2); + assertEquals(tokenResponse2.getStatus(), 200, "Unexpected response code: " + tokenResponse2.getStatus()); + assertNotNull(tokenResponse2.getEntity(), "The entity is null"); + assertNotNull(tokenResponse2.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse2.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse2.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse2.getRefreshToken(), "The refresh token is null"); + + // User wants to authenticate on B3 (without sending its credentials) + + String state3 = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest3 = new AuthorizationRequest( + Arrays.asList(ResponseType.CODE), + clientId, + Arrays.asList("openid", "profile", "email"), + redirectUri, + null); + + authorizationRequest3.getPrompts().add(Prompt.NONE); + authorizationRequest3.setState(state3); + authorizationRequest3.setSessionId(sessionId); + + AuthorizeClient authorizeClient3 = new AuthorizeClient(authorizationEndpoint); + authorizeClient3.setRequest(authorizationRequest3); + AuthorizationResponse authorizationResponse3 = authorizeClient3.exec(); + + showClient(authorizeClient3); + assertEquals(authorizationResponse3.getStatus(), 302, "Unexpected response code: " + authorizationResponse3.getStatus()); + assertNotNull(authorizationResponse3.getLocation(), "The location is null"); + assertNotNull(authorizationResponse3.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse3.getScope(), "The scope is null"); + assertNotNull(authorizationResponse3.getState(), "The state is null"); + assertEquals(authorizationResponse3.getState(), state3); + + String authorizationCode3 = authorizationResponse3.getCode(); + + TokenRequest tokenRequest3 = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest3.setCode(authorizationCode3); + tokenRequest3.setRedirectUri(redirectUri); + tokenRequest3.setAuthUsername(clientId); + tokenRequest3.setAuthPassword(clientSecret); + tokenRequest3.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_POST); + + TokenClient tokenClient3 = new TokenClient(tokenEndpoint); + tokenClient3.setRequest(tokenRequest3); + TokenResponse tokenResponse3 = tokenClient3.exec(); + + showClient(tokenClient3); + assertEquals(tokenResponse3.getStatus(), 200, "Unexpected response code: " + tokenResponse3.getStatus()); + assertNotNull(tokenResponse3.getEntity(), "The entity is null"); + assertNotNull(tokenResponse3.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse3.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse3.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse3.getRefreshToken(), "The refresh token is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/SectorIdentifierUrlVerificationHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/SectorIdentifierUrlVerificationHttpTest.java new file mode 100644 index 00000000..37af5fa0 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/SectorIdentifierUrlVerificationHttpTest.java @@ -0,0 +1,450 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.Prompt; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.common.SubjectType; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * Functional tests for Sector Identifier URI Verification (HTTP) + * + * @author Javier Rojas Blum + * @version May 7, 2019 + */ +public class SectorIdentifierUrlVerificationHttpTest extends BaseTest { + + // Run this test with both pairwiseIdType persistent and algorithmic + // And ensure shareSubjectIdBetweenClientsWithSameSectorId is set to false + @Parameters({"redirectUris", "sectorIdentifierUri", "redirectUri", "userId", "userSecret"}) + @Test(enabled = false) + public void pairwiseSectorIdentifierTypeToPreventSubjectIdentifierCorrelation( + final String redirectUris, final String sectorIdentifierUri, final String redirectUri, + final String userId, final String userSecret) throws Exception { + showTitle("pairwiseSectorIdentifierTypeToPreventSubjectIdentifierCorrelation"); + + RegisterResponse registerResponse1 = requestClientRegistration(redirectUris, sectorIdentifierUri); + RegisterResponse registerResponse2 = requestClientRegistration(redirectUris, sectorIdentifierUri); + + String sub1 = requestAuthorizationCodeWithPairwiseSectorIdentifierType(redirectUri, userId, userSecret, + registerResponse1.getClientId(), + registerResponse1.getClientSecret(), + registerResponse1.getResponseTypes()); + String sub2 = requestAuthorizationCodeWithPairwiseSectorIdentifierType(redirectUri, userId, userSecret, + registerResponse2.getClientId(), + registerResponse2.getClientSecret(), + registerResponse2.getResponseTypes()); + + assertNotEquals(sub1, sub2, "Each client must receive a different sub value"); + + String sub3 = requestAuthorizationCodeWithPairwiseSectorIdentifierType(redirectUri, userId, userSecret, + registerResponse1.getClientId(), + registerResponse1.getClientSecret(), + registerResponse1.getResponseTypes()); + String sub4 = requestAuthorizationCodeWithPairwiseSectorIdentifierType(redirectUri, userId, userSecret, + registerResponse2.getClientId(), + registerResponse2.getClientSecret(), + registerResponse2.getResponseTypes()); + + assertEquals(sub1, sub3, "Same client must receive the same sub value"); + assertEquals(sub2, sub4, "Same client must receive the same sub value"); + } + + // Run this test with both pairwiseIdType persistent and algorithmic + // And ensure shareSubjectIdBetweenClientsWithSameSectorId is set to true + @Parameters({"redirectUris", "sectorIdentifierUri", "redirectUri", "userId", "userSecret"}) + @Test(enabled = true) + public void shareSubjectIdBetweenClientsWithSameSectorId( + final String redirectUris, final String sectorIdentifierUri, final String redirectUri, + final String userId, final String userSecret) throws Exception { + showTitle("shareSubjectIdBetweenClientsWithSameSectorId"); + + RegisterResponse registerResponse1 = requestClientRegistration(redirectUris, sectorIdentifierUri); + RegisterResponse registerResponse2 = requestClientRegistration(redirectUris, sectorIdentifierUri); + + String sub1 = requestAuthorizationCodeWithPairwiseSectorIdentifierType(redirectUri, userId, userSecret, + registerResponse1.getClientId(), + registerResponse1.getClientSecret(), + registerResponse1.getResponseTypes()); + String sub2 = requestAuthorizationCodeWithPairwiseSectorIdentifierType(redirectUri, userId, userSecret, + registerResponse2.getClientId(), + registerResponse2.getClientSecret(), + registerResponse2.getResponseTypes()); + + assertEquals(sub1, sub2, "Each client must share the same sub value"); + + String sub3 = requestAuthorizationCodeWithPairwiseSectorIdentifierType(redirectUri, userId, userSecret, + registerResponse1.getClientId(), + registerResponse1.getClientSecret(), + registerResponse1.getResponseTypes()); + String sub4 = requestAuthorizationCodeWithPairwiseSectorIdentifierType(redirectUri, userId, userSecret, + registerResponse2.getClientId(), + registerResponse2.getClientSecret(), + registerResponse2.getResponseTypes()); + + assertEquals(sub1, sub3, "Same client must receive the same sub value"); + assertEquals(sub2, sub4, "Same client must receive the same sub value"); + } + + public RegisterResponse requestClientRegistration( + final String redirectUris, final String sectorIdentifierUri) { + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + + // Register client with Sector Identifier URL + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_POST); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + return registerResponse; + } + + public String requestAuthorizationCodeWithPairwiseSectorIdentifierType( + final String redirectUri, final String userId, final String userSecret, + final String clientId, final String clientSecret, final List responseTypes) throws Exception { + + // 1. Request authorization and receive the authorization code. + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + String nonce = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.setAuthUsername(userId); + authorizationRequest.setAuthPassword(userSecret); + authorizationRequest.getPrompts().add(Prompt.NONE); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(authorizationResponse.getStatus(), 302, "Unexpected response code: " + authorizationResponse.getStatus()); + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertEquals(authorizationResponse.getState(), state); + + String authorizationCode = authorizationResponse.getCode(); + String idToken = authorizationResponse.getIdToken(); + + // 2. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.CODE_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + String sub = jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER); + + // 3. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_POST); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + + String accessToken = tokenResponse.getAccessToken(); + + // 4. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + + return sub; + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret"}) + @Test + public void publicSectorIdentifierType( + final String redirectUris, final String redirectUri, final String userId, final String userSecret) throws Exception { + showTitle("publicSectorIdentifierType"); + + String sub1 = requestAuthorizationCodeWithPublicSectorIdentifierType(redirectUris, redirectUri, userId, userSecret); + String sub2 = requestAuthorizationCodeWithPublicSectorIdentifierType(redirectUris, redirectUri, userId, userSecret); + + assertEquals(sub1, sub2, "Each client must receive the same sub value"); + } + + public String requestAuthorizationCodeWithPublicSectorIdentifierType( + final String redirectUris, final String redirectUri, final String userId, final String userSecret) throws Exception { + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + + // 1. Register client with Sector Identifier URL + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSubjectType(SubjectType.PUBLIC); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_POST); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization and receive the authorization code. + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + String nonce = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.setAuthUsername(userId); + authorizationRequest.setAuthPassword(userSecret); + authorizationRequest.getPrompts().add(Prompt.NONE); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(authorizationResponse.getStatus(), 302, "Unexpected response code: " + authorizationResponse.getStatus()); + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertEquals(authorizationResponse.getState(), state); + + String authorizationCode = authorizationResponse.getCode(); + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.CODE_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + String sub = jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER); + + // 4. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_POST); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + + String accessToken = tokenResponse.getAccessToken(); + + // 5. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + + return sub; + } + + @Parameters({"redirectUris"}) + @Test + public void sectorIdentifierUrlVerificationFail1(final String redirectUris) throws Exception { + showTitle("sectorIdentifierUrlVerificationFail1"); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri("https://INVALID_SECTOR_IDENTIFIER_URL"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 400, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getEntity(), "The entity is null"); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + } + + @Parameters({"sectorIdentifierUri"}) + @Test + public void sectorIdentifierUrlVerificationFail2(final String sectorIdentifierUri) throws Exception { + showTitle("sectorIdentifierUrlVerificationFail2"); + + String redirectUris = "https://INVALID_REDIRECT_URI https://client.example.com/cb https://client.example.com/cb1 https://client.example.com/cb2"; + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 400, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getEntity(), "The entity is null"); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + } + + /** + * Register with pairwise Subject Type and without Sector Identifier URI must fail because there are multiple + * hostnames in the Redirect URI list. + */ + @Parameters({"redirectUris"}) + @Test + public void sectorIdentifierUrlVerificationFail3(final String redirectUris) throws Exception { + showTitle("sectorIdentifierUrlVerificationFail3"); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(null); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 400, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getEntity(), "The entity is null"); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/SelectAccountHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/SelectAccountHttpTest.java new file mode 100644 index 00000000..3fbee6d2 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/SelectAccountHttpTest.java @@ -0,0 +1,210 @@ +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.apache.logging.log4j.util.Strings; +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.Prompt; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.common.SubjectType; +import org.gluu.oxauth.model.exception.InvalidJwtException; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.gluu.oxauth.page.LoginPage; +import org.gluu.oxauth.page.PageConfig; +import org.gluu.oxauth.page.SelectPage; +import org.json.JSONArray; +import org.openqa.selenium.htmlunit.HtmlUnitDriver; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +import com.google.common.collect.Lists; + +/** + * @author Yuriy Zabrovarnyy + */ +public class SelectAccountHttpTest extends BaseTest { + + private PageConfig pageConfig; + + @BeforeTest + public void setUp() { + driver = new HtmlUnitDriver(true); + pageConfig = newPageConfig(driver); + } + + @AfterTest + public void tearDown() { + driver.quit(); + driver = null; + pageConfig = null; + } + + @Parameters({"userId", "userSecret", "userId2", "userSecret2", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void selectAccountTest(final String userId, final String userSecret, + final String userId2, final String userSecret2, + final String redirectUris, final String redirectUri, String sectorIdentifierUri) throws Exception { + showTitle("authorizationCodeFlow"); + + List responseTypes = Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN); + List scopes = Arrays.asList("openid", "profile", "address", "email", "phone", "user_name"); + + RegisterResponse registerResponse = registerClient(redirectUris, responseTypes, scopes, sectorIdentifierUri); + + output("1. Account1 : Request authorization and receive the code and id_token"); + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, scopes, registerResponse.getClientId(), randomUUID()); + + assertNotNull(authorizationResponse, "The authorization response is null"); + assertNotNull(authorizationResponse.getCode(), "The code is null"); + assertIdToken(authorizationResponse.getIdToken()); + String account1SessionId = assertSessionIdCookie(); + + output("2. Account2 : Request authorization with prompt=select_account and receive the code and id_token"); + AuthorizationResponse responseFromSelectAccount = selectAccount(userId2, userSecret2, redirectUri, responseTypes, scopes, registerResponse.getClientId(), randomUUID()); + + assertNotNull(responseFromSelectAccount, "The authorization response is null"); + assertNotNull(responseFromSelectAccount.getCode(), "The code is null"); + assertIdToken(responseFromSelectAccount.getIdToken()); + String account2SessionId = assertSessionIdCookie(); + assertNotEquals(account1SessionId, account2SessionId); + + output("3. Go again to Select Accounts : we should have 2 accounts"); + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, registerResponse.getClientId(), scopes, redirectUri, randomUUID()); + authorizationRequest.setState(randomUUID()); + authorizationRequest.setPrompts(Lists.newArrayList(Prompt.SELECT_ACCOUNT)); + + output("4. both Account 1 and Account 2 sessions must be in current_sessions cookie"); + assertEquals(account2SessionId, assertSessionIdCookie()); + List currentSessions = new JSONArray(driver.manage().getCookieNamed("current_sessions").getValue()).toList(); + assertTrue(currentSessions.contains(account1SessionId)); + assertTrue(currentSessions.contains(account2SessionId)); + + output("5. Check that we have 2 buttons for Account 1 and Account 2"); + final SelectPage selectPage = SelectPage.navigate(pageConfig, authorizationEndpoint + "?" + authorizationRequest.getQueryString()); + assertNotNull(selectPage.getAccountButton("oxAuth Test User")); + assertNotNull(selectPage.getAccountButton("oxAuth Test User2")); + + output("6. Switch back to Account 1"); + selectPage.switchAccount(selectPage.getAccountButton("oxAuth Test User")); + assertEquals(account1SessionId, assertSessionIdCookie()); // check session_id really corresponds to Account 1 + } + + private String assertSessionIdCookie() { + final String value = driver.manage().getCookieNamed("session_id").getValue(); + assertTrue(Strings.isNotBlank(value), "The session_id is blank"); + output("Cookie session_id: " + value); + return value; + } + + private void assertIdToken(String idToken) throws InvalidJwtException { + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + } + + private AuthorizationResponse selectAccount(final String userId, final String userSecret, final String redirectUri, + List responseTypes, List scopes, String clientId, String nonce) { + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.setPrompts(Lists.newArrayList(Prompt.SELECT_ACCOUNT)); + + String authorizationRequestUrl = authorizationEndpoint + "?" + authorizationRequest.getQueryString(); + + final SelectPage selectPage = SelectPage.navigate(pageConfig, authorizationRequestUrl); + + final String currentUrl = driver.getCurrentUrl(); + final LoginPage loginPage = selectPage.clickOnLoginAsAnotherUser(); + + loginPage.enterUsername(userId); + loginPage.enterPassword(userSecret); + loginPage.getLoginButton().click(); + if (ENABLE_REDIRECT_TO_LOGIN_PAGE) { + loginPage.waitForPageSwitch(currentUrl); + } + + String authorizationResponseStr = acceptAuthorization(driver, authorizationRequest.getRedirectUri()); + + + AuthorizationResponse authorizationResponse = buildAuthorizationResponse(authorizationRequest, driver, authorizationResponseStr); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + return authorizationResponse; + } + + public AuthorizationResponse authorize(AuthorizationRequest authorizationRequest, String userId, String userSecret, int authzSteps) { + AuthorizeClient authorizeClient = processAuthentication(driver, authorizationEndpoint, authorizationRequest, + userId, userSecret); + + int remainAuthzSteps = authzSteps; + + String authorizationResponseStr = null; + do { + authorizationResponseStr = acceptAuthorization(driver, authorizationRequest.getRedirectUri()); + remainAuthzSteps--; + } while (remainAuthzSteps >= 1); + + return buildAuthorizationResponse(authorizationRequest, driver, authorizeClient, authorizationResponseStr); + } + + private AuthorizationResponse requestAuthorization(final String userId, final String userSecret, final String redirectUri, + List responseTypes, List scopes, String clientId, String nonce) { + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(randomUUID()); + + AuthorizationResponse authorizationResponse = authorize(authorizationRequest, userId, userSecret, 1); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + return authorizationResponse; + } + + private RegisterResponse registerClient(String redirectUris, List responseTypes, List scopes, String sectorIdentifierUri) { + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth select accounts test app", StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setScope(scopes); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = newRegisterClient(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + return registerResponse; + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/SpontaneousScopeHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/SpontaneousScopeHttpTest.java new file mode 100644 index 00000000..b2e76108 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/SpontaneousScopeHttpTest.java @@ -0,0 +1,109 @@ +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +import com.google.common.collect.Lists; + +/** + * @author Yuriy Zabrovarnyy + */ +public class SpontaneousScopeHttpTest extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUri"}) + @Test + public void spontaneousScope(final String userId, final String userSecret, final String redirectUri) throws Exception { + showTitle("spontaneousScope"); + + List responseTypes = Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN, ResponseType.TOKEN); + + RegisterResponse registerResponse = registerClient(redirectUri, responseTypes); + + String clientId = registerResponse.getClientId(); + + // Request authorization and receive the authorization code. + List scopes = Lists.newArrayList("openid", "profile", "address", "email", "phone", "user_name", + "transaction:245", "transaction:8645"); + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, scopes, clientId); + + final String[] responseScopes = authorizationResponse.getScope().split(" "); + + // Validate spontaneous scopes are present + assertTrue(Arrays.asList(responseScopes).contains("transaction:245")); + assertTrue(Arrays.asList(responseScopes).contains("transaction:8645")); + assertFalse(Arrays.asList(responseScopes).contains("transaction:not_requested")); + } + + private RegisterResponse registerClient(String redirectUris, List responseTypes) throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "Spontaneous scope test", StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setScope(Lists.newArrayList("openid", "profile", "address", "email", "phone", "user_name")); + + // 1. allow spontaneous scopes (off by default) + // 2. set spontaneous scope regular expression. In this example `transaction:345236456` + registerRequest.setAllowSpontaneousScopes(true); + registerRequest.setSpontaneousScopes(Lists.newArrayList("^transaction:.+$")); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setExecutor(clientEngine(true)); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + return registerResponse; + } + + private AuthorizationResponse requestAuthorization(final String userId, final String userSecret, final String redirectUri, + List responseTypes, List scopes, String clientId) { + String state = UUID.randomUUID().toString(); + String nonce = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + return authorizationResponse; + } + + /* + public static void main(String[] args) throws JsonProcessingException { + System.out.println(Pattern.matches("^transaction:.+$", "openid")); + System.out.println(Pattern.matches("^transaction:.+$", "transaction")); + System.out.println(Pattern.matches("^transaction:.+$", "transaction:")); + System.out.println(Pattern.matches("^transaction:.+$", "transaction:bla")); + }*/ +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/TokenBindingHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/TokenBindingHttpTest.java new file mode 100644 index 00000000..ec35794a --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/TokenBindingHttpTest.java @@ -0,0 +1,130 @@ +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.Prompt; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient43Engine; +import org.testng.Assert; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Yuriy Zabrovarnyy + */ +public class TokenBindingHttpTest extends BaseTest { + + public static final String ENCODED_TOKEN_BINDING_MESSAGE = "ARIAAgBBQCfsI1D1sTq5mvT_2H_dihNIvuHJCHGjHPJchPavNbGrOo26-2JgT_IsbvZd4daDFbirYBIwJ-TK1rh8FzrC-psAQO4Au9xPupLSkhwT9Y" + + "n9aSvHXFsMLh4d4cEBKGP1clJtsfUFGDw-8HQSKwgKFN3WfZGq27y8NB3NAM1oNzvqVOIAAAECAEFArPIiuZxj9gK0dWhIcG63r2-sZ8V3LX9gpNl8Um_oGOtmwoP1v0VHNI" + + "HEOzW3BOqcBLvUzVEG6a6KGEj3GrFcqQBA9YxqHPBIuDui_aQ1SoRGKyBEhaG2i-Wke3erRb1YwC7nTgrpqqJG3z1P8bt7cjZN6TpOyktdSSK7OJgiApwG7AAA"; + public static final String EXPECTED_ID_HASH = "suMuxh_IlrP-Zrj33LuQOQ5rX039cmBe-wt2df3BrUQ"; + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void tokenBindingWithImplicitFlow(final String userId, final String userSecret, + final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("tokenBindingWithImplicitFlow"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN + ); + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // 1. Register client + RegisterResponse registerResponse = registerClient(redirectUri, responseTypes, grantTypes, sectorIdentifierUri); + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, clientId); + + Jwt jwt = Jwt.parse(authorizationResponse.getIdToken()); + Assert.assertEquals(EXPECTED_ID_HASH, jwt.getClaims().getClaimAsJSON(JwtClaimName.CNF).optString(JwtClaimName.TOKEN_BINDING_HASH)); + } + + private AuthorizationResponse requestAuthorization(final String userId, final String userSecret, final String redirectUri, + List responseTypes, String clientId) throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + List scopes = Arrays.asList("openid", "profile", "address", "email"); + return requestAuthorization(userId, userSecret, redirectUri, responseTypes, clientId, scopes); + } + + private AuthorizationResponse requestAuthorization( + final String userId, final String userSecret, final String redirectUri, List responseTypes, + String clientId, List scopes) throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest( + responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.setAuthUsername(userId); + authorizationRequest.setAuthPassword(userSecret); + authorizationRequest.getPrompts().add(Prompt.NONE); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setExecutor(new ApacheHttpClient43Engine(createHttpClientTrustAll())); + authorizeClient.setRequest(authorizationRequest); + authorizeClient.getHeaders().put("Sec-Token-Binding", ENCODED_TOKEN_BINDING_MESSAGE); + + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + showClient(authorizeClient); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The access token is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getTokenType(), "The token type is null"); + assertNotNull(authorizationResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(authorizationResponse.getScope(), "The scope must be null"); + assertNotNull(authorizationResponse.getIdToken(), "The id token must be null"); + return authorizationResponse; + } + + private RegisterResponse registerClient(final String redirectUris, final List responseTypes, + final List grantTypes, final String sectorIdentifierUri) throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setGrantTypes(grantTypes); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setIdTokenTokenBindingCnf(JwtClaimName.TOKEN_BINDING_HASH); // token binding hash for cnf + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setExecutor(new ApacheHttpClient43Engine(createHttpClientTrustAll())); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + return registerResponse; + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/TokenEncryptionHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/TokenEncryptionHttpTest.java new file mode 100644 index 00000000..a0a706ea --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/TokenEncryptionHttpTest.java @@ -0,0 +1,425 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.fail; + +import java.security.PrivateKey; +import java.util.Arrays; +import java.util.List; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.encryption.BlockEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.encryption.KeyEncryptionAlgorithm; +import org.gluu.oxauth.model.jwe.Jwe; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.gluu.oxauth.model.util.Util; +import org.testng.annotations.Parameters; + +/** + * @author Javier Rojas Blum + * @version September 3, 2018 + */ +@Deprecated +public class TokenEncryptionHttpTest extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "clientJwksUri", + "RS256_enc_keyId", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + //@Test // Before run this test, set openidScopeBackwardCompatibility to true + @Deprecated + public void requestIdTokenAlgRSAOAEPEncA256GCM( + final String userId, final String userSecret, final String redirectUris, final String jwksUri, + final String keyId, final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) { + try { + showTitle("requestIdTokenAlgRSAOAEPEncA256GCM"); + + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setJwksUri(jwksUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA_OAEP); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A256GCM); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(grantTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + String clientId = response.getClientId(); + String clientSecret = response.getClientSecret(); + + // 2. Request authorization + TokenRequest tokenRequest = new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + tokenRequest.setUsername(userId); + tokenRequest.setPassword(userSecret); + tokenRequest.setScope("openid"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + assertNotNull(tokenResponse.getScope(), "The scope is null"); + assertNotNull(tokenResponse.getIdToken(), "The id token is null"); + + String idToken = tokenResponse.getIdToken(); + + // 3. Read Encrypted ID Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, null); + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + + Jwe jwe = Jwe.parse(idToken, privateKey, null); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.OX_OPENID_CONNECT_VERSION)); + } catch (Exception ex) { + fail(ex.getMessage(), ex); + } + } + + @Parameters({"userId", "userSecret", "redirectUris", "clientJwksUri", + "RS256_enc_keyId", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + //@Test // Before run this test, set openidScopeBackwardCompatibility to true + @Deprecated + public void requestIdTokenAlgRSA15EncA128CBCPLUSHS256( + final String userId, final String userSecret, final String redirectUris, final String jwksUri, + final String keyId, final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) { + try { + showTitle("requestIdTokenAlgRSA15EncA128CBCPLUSHS256"); + + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setJwksUri(jwksUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A128CBC_PLUS_HS256); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(grantTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + String clientId = response.getClientId(); + String clientSecret = response.getClientSecret(); + + // 2. Request authorization + TokenRequest tokenRequest = new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + tokenRequest.setUsername(userId); + tokenRequest.setPassword(userSecret); + tokenRequest.setScope("openid"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + assertNotNull(tokenResponse.getScope(), "The scope is null"); + assertNotNull(tokenResponse.getIdToken(), "The id token is null"); + + String idToken = tokenResponse.getIdToken(); + + // 3. Read Encrypted ID Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, null); + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + + Jwe jwe = Jwe.parse(idToken, privateKey, null); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.OX_OPENID_CONNECT_VERSION)); + } catch (Exception ex) { + fail(ex.getMessage(), ex); + } + } + + @Parameters({"userId", "userSecret", "redirectUris", "clientJwksUri", + "RS256_enc_keyId", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + //@Test // Before run this test, set openidScopeBackwardCompatibility to true + @Deprecated + public void requestIdTokenAlgRSA15EncA256CBCPLUSHS512( + final String userId, final String userSecret, final String redirectUris, final String jwksUri, + final String keyId, final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) { + try { + showTitle("requestIdTokenAlgRSA15EncA256CBCPLUSHS512"); + + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setJwksUri(jwksUri); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A256CBC_PLUS_HS512); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(grantTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + String clientId = response.getClientId(); + String clientSecret = response.getClientSecret(); + + // 2. Request authorization + TokenRequest tokenRequest = new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + tokenRequest.setUsername(userId); + tokenRequest.setPassword(userSecret); + tokenRequest.setScope("openid"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + assertNotNull(tokenResponse.getScope(), "The scope is null"); + assertNotNull(tokenResponse.getIdToken(), "The id token is null"); + + String idToken = tokenResponse.getIdToken(); + + // 3. Read Encrypted ID Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, null); + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + + Jwe jwe = Jwe.parse(idToken, privateKey, null); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.OX_OPENID_CONNECT_VERSION)); + } catch (Exception ex) { + fail(ex.getMessage(), ex); + } + } + + @Parameters({"userId", "userSecret", "redirectUris", "sectorIdentifierUri"}) + //@Test // Before run this test, set openidScopeBackwardCompatibility to true + @Deprecated + public void requestIdTokenAlgA128KWEncA128GCM( + final String userId, final String userSecret, final String redirectUris, final String sectorIdentifierUri) { + try { + showTitle("requestIdTokenAlgA128KWEncA128GCM"); + + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.A128KW); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A128GCM); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(grantTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + String clientId = response.getClientId(); + String clientSecret = response.getClientSecret(); + + // 2. Request authorization + TokenRequest tokenRequest = new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + tokenRequest.setUsername(userId); + tokenRequest.setPassword(userSecret); + tokenRequest.setScope("openid"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + assertNotNull(tokenResponse.getScope(), "The scope is null"); + assertNotNull(tokenResponse.getIdToken(), "The id token is null"); + + String idToken = tokenResponse.getIdToken(); + + // 3. Read Encrypted ID Token + Jwe jwe = Jwe.parse(idToken, null, clientSecret.getBytes(Util.UTF8_STRING_ENCODING)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.OX_OPENID_CONNECT_VERSION)); + } catch (Exception ex) { + fail(ex.getMessage(), ex); + } + } + + @Parameters({"userId", "userSecret", "redirectUris", "sectorIdentifierUri"}) + //@Test // Before run this test, set openidScopeBackwardCompatibility to true + @Deprecated + public void requestIdTokenAlgA256KWEncA256GCM( + final String userId, final String userSecret, final String redirectUris, final String sectorIdentifierUri) { + try { + showTitle("requestIdTokenAlgA256KWEncA256GCM"); + + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setIdTokenEncryptedResponseAlg(KeyEncryptionAlgorithm.A256KW); + registerRequest.setIdTokenEncryptedResponseEnc(BlockEncryptionAlgorithm.A256GCM); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(grantTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + String clientId = response.getClientId(); + String clientSecret = response.getClientSecret(); + + // 2. Request authorization + TokenRequest tokenRequest = new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + tokenRequest.setUsername(userId); + tokenRequest.setPassword(userSecret); + tokenRequest.setScope("openid"); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + assertNotNull(tokenResponse.getScope(), "The scope is null"); + assertNotNull(tokenResponse.getIdToken(), "The id token is null"); + + String idToken = tokenResponse.getIdToken(); + + // 3. Read Encrypted ID Token + Jwe jwe = Jwe.parse(idToken, null, clientSecret.getBytes(Util.UTF8_STRING_ENCODING)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwe.getClaims().getClaimAsString(JwtClaimName.OX_OPENID_CONNECT_VERSION)); + } catch (Exception ex) { + fail(ex.getMessage(), ex); + } + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/TokenEndpointAuthMethodRestrictionHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/TokenEndpointAuthMethodRestrictionHttpTest.java new file mode 100644 index 00000000..275559a9 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/TokenEndpointAuthMethodRestrictionHttpTest.java @@ -0,0 +1,10674 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.gluu.oxauth.model.register.RegisterRequestParam.APPLICATION_TYPE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.CLIENT_NAME; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ID_TOKEN_SIGNED_RESPONSE_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REDIRECT_URIS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.RESPONSE_TYPES; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SCOPE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.TOKEN_ENDPOINT_AUTH_METHOD; +import static org.gluu.oxauth.model.register.RegisterRequestParam.TOKEN_ENDPOINT_AUTH_SIGNING_ALG; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Javier Rojas Blum + * @version February 8, 2019 + */ +public class TokenEndpointAuthMethodRestrictionHttpTest extends BaseTest { + + /** + * Register a client without specify a Token Endpoint Auth Method. + * Read client to check whether it is using the default Token Endpoint Auth Method client_secret_basic. + */ + @Parameters({"redirectUris", "sectorIdentifierUri"}) + @Test + public void omittedTokenEndpointAuthMethod(final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("omittedTokenEndpointAuthMethod"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_BASIC.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + } + + /** + * Register a client with Token Endpoint Auth Method client_secret_basic. + * Read client to check whether it is using the Token Endpoint Auth Method client_secret_basic. + * Request authorization code. + * Call to Token Endpoint with Auth Method client_secret_basic. + */ + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretBasic( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretBasic"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_BASIC.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + /** + * Fail 1: Call to Token Endpoint with Auth Method client_secret_post should fail. + */ + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretBasicFail1( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretBasicFail1"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_BASIC.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_POST); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + /** + * Fail 2: Call to Token Endpoint with Auth Method client_secret_jwt should fail. + */ + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretBasicFail2( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretBasicFail2"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_BASIC.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + /** + * Fail 3: Call to Token Endpoint with Auth Method private_key_jwt should fail. + */ + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "RS256_keyId", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretBasicFail3( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String keyId, final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretBasicFail3"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_BASIC.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, null); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS256); + tokenRequest.setKeyId(keyId); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + /** + * Register a client with Token Endpoint Auth Method client_secret_post. + * Read client to check whether it is using the Token Endpoint Auth Method client_secret_post. + * Request authorization code. + * Call to Token Endpoint with Auth Method client_secret_post. + */ + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretPost( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretPost"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_POST); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_POST.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_POST); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + /** + * Fail 1: Call to Token Endpoint with Auth Method client_secret_basic should fail. + */ + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretPostFail1( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretPostFail1"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_POST); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_POST.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + /** + * Fail 2: Call to Token Endpoint with Auth Method client_secret_jwt should fail. + */ + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretPostFail2( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretPostFail2"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_POST); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_POST.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + /** + * Fail 3: Call to Token Endpoint with Auth Method private_key_jwt should fail. + */ + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "RS256_keyId", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretPostFail3( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String keyId, final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretPostFail3"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_POST); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_POST.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, null); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS256); + tokenRequest.setKeyId(keyId); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + /** + * Register a client with Token Endpoint Auth Method client_secret_jwt. + * Read client to check whether it is using the Token Endpoint Auth Method client_secret_jwt. + * Request authorization code. + * Call to Token Endpoint with Auth Method client_secret_Jwt. + */ + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretJwt( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretJwt"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_JWT.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setCryptoProvider(cryptoProvider); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + /** + * If token_endpoint_auth_signing_alg is omitted in client registration, + * only symmetric algorithm supported by the OP and the RP can be used. + */ + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretJwtHS256( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretJwtHS256"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_JWT.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.HS256); + tokenRequest.setCryptoProvider(cryptoProvider); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + /** + * If token_endpoint_auth_signing_alg is omitted in client registration, + * only symmetric algorithm supported by the OP and the RP can be used. + */ + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretJwtHS384( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretJwtHS384"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_JWT.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.HS384); + tokenRequest.setCryptoProvider(cryptoProvider); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + /** + * If token_endpoint_auth_signing_alg is omitted in client registration, + * any algorithm supported by the OP and the RP can be used. + */ + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretJwtHS512( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretJwtHS512"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_JWT.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.HS512); + tokenRequest.setCryptoProvider(cryptoProvider); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + /** + * If token_endpoint_auth_signing_alg is omitted in client registration, + * only symmetric algorithm supported by the OP and the RP can be used. + */ + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "dnName", "keyStoreFile", "keyStoreSecret", + "RS256_keyId", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretJwtRS256Fail( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String keyId, + final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretJwtRS256Fail"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_JWT.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS256); + tokenRequest.setKeyId(keyId); + tokenRequest.setCryptoProvider(cryptoProvider); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + /** + * If token_endpoint_auth_signing_alg is omitted in client registration, + * only symmetric algorithm supported by the OP and the RP can be used. + */ + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "dnName", "keyStoreFile", "keyStoreSecret", + "RS384_keyId", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretJwtRS384Fail( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String keyId, + final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretJwtRS384Fail"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_JWT.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS384); + tokenRequest.setKeyId(keyId); + tokenRequest.setCryptoProvider(cryptoProvider); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + /** + * If token_endpoint_auth_signing_alg is omitted in client registration, + * only symmetric algorithm supported by the OP and the RP can be used. + */ + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "dnName", "keyStoreFile", "keyStoreSecret", + "RS512_keyId", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretJwtRS512Fail( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String keyId, + final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretJwtRS512Fail"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_JWT.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS512); + tokenRequest.setKeyId(keyId); + tokenRequest.setCryptoProvider(cryptoProvider); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + /** + * If token_endpoint_auth_signing_alg is omitted in client registration, + * only symmetric algorithm supported by the OP and the RP can be used. + */ + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "dnName", "keyStoreFile", "keyStoreSecret", + "ES256_keyId", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretJwtES256Fail( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String keyId, + final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretJwtES256Fail"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_JWT.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES256); + tokenRequest.setKeyId(keyId); + tokenRequest.setCryptoProvider(cryptoProvider); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + /** + * If token_endpoint_auth_signing_alg is omitted in client registration, + * only symmetric algorithm supported by the OP and the RP can be used. + */ + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "dnName", "keyStoreFile", "keyStoreSecret", + "ES384_keyId", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretJwtES384Fail( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String keyId, + final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretJwtES384Fail"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_JWT.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES384); + tokenRequest.setKeyId(keyId); + tokenRequest.setCryptoProvider(cryptoProvider); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + /** + * If token_endpoint_auth_signing_alg is omitted in client registration, + * only symmetric algorithm supported by the OP and the RP can be used. + */ + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "dnName", "keyStoreFile", "keyStoreSecret", + "ES512_keyId", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretJwtES512Fail( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String keyId, + final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretJwtES512Fail"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_JWT.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES512); + tokenRequest.setKeyId(keyId); + tokenRequest.setCryptoProvider(cryptoProvider); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + /** + * If token_endpoint_auth_signing_alg is omitted in client registration, + * only symmetric algorithm supported by the OP and the RP can be used. + */ + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "dnName", "keyStoreFile", "keyStoreSecret", + "PS256_keyId", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretJwtPS256Fail( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String keyId, + final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretJwtPS256Fail"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_JWT.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.PS256); + tokenRequest.setKeyId(keyId); + tokenRequest.setCryptoProvider(cryptoProvider); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + /** + * If token_endpoint_auth_signing_alg is omitted in client registration, + * only symmetric algorithm supported by the OP and the RP can be used. + */ + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "dnName", "keyStoreFile", "keyStoreSecret", + "PS384_keyId", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretJwtPS384Fail( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String keyId, + final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretJwtPS384Fail"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_JWT.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.PS384); + tokenRequest.setKeyId(keyId); + tokenRequest.setCryptoProvider(cryptoProvider); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + /** + * If token_endpoint_auth_signing_alg is omitted in client registration, + * only symmetric algorithm supported by the OP and the RP can be used. + */ + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "dnName", "keyStoreFile", "keyStoreSecret", + "PS512_keyId", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretJwtPS512Fail( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String keyId, + final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretJwtPS512Fail"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_JWT.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.PS512); + tokenRequest.setKeyId(keyId); + tokenRequest.setCryptoProvider(cryptoProvider); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretJwtSigningAlgHS256( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretJwtSigningAlgHS256"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.HS256); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.HS256.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.HS256); + tokenRequest.setCryptoProvider(cryptoProvider); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretJwtSigningAlgHS256Fail1( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretJwtSigningAlgHS256Fail1"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.HS256); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.HS256.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.HS384); + tokenRequest.setCryptoProvider(cryptoProvider); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretJwtSigningAlgHS256Fail2( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretJwtSigningAlgHS256Fail2"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.HS256); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.HS256.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.HS512); + tokenRequest.setCryptoProvider(cryptoProvider); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretJwtSigningAlgHS384( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretJwtSigningAlgHS384"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.HS384); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.HS384.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.HS384); + tokenRequest.setCryptoProvider(cryptoProvider); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretJwtSigningAlgHS384Fail1( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretJwtSigningAlgHS384Fail1"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.HS384); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.HS384.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.HS256); + tokenRequest.setCryptoProvider(cryptoProvider); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretJwtSigningAlgHS384Fail2( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretJwtSigningAlgHS384Fail2"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.HS384); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.HS384.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.HS512); + tokenRequest.setCryptoProvider(cryptoProvider); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretJwtSigningAlgHS512( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretJwtSigningAlgHS512"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.HS512); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.HS512.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.HS512); + tokenRequest.setCryptoProvider(cryptoProvider); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretJwtSigningAlgHS512Fail1( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretJwtSigningAlgHS512Fail1"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.HS512); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.HS512.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.HS256); + tokenRequest.setCryptoProvider(cryptoProvider); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "dnName", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretJwtSigningAlgHS512Fail2( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String dnName, final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretJwtSigningAlgHS512Fail2"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.HS512); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.HS512.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.HS384); + tokenRequest.setCryptoProvider(cryptoProvider); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + /** + * Fail 1: Call to Token Endpoint with Auth Method client_secret_basic should fail. + */ + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretJwtFail1( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretJwtFail1"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_JWT.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + /** + * Fail 2: Call to Token Endpoint with Auth Method client_secret_post should fail. + */ + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretJwtFail2( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretJwtFail2"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_JWT.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_POST); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + /** + * Fail 3: Call to Token Endpoint with Auth Method private_key_jwt should fail. + */ + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "RS256_keyId", "keyStoreFile", "keyStoreSecret", + "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodClientSecretJwtFail3( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String keyId, final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodClientSecretJwtFail3"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.CLIENT_SECRET_JWT.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, null); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS256); + tokenRequest.setKeyId(keyId); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + /** + * Register a client with Token Endpoint Auth Method private_key_jwt. + * Read client to check whether it is using the Token Endpoint Auth Method private_key_jwt. + * Request authorization code. + * Call to Token Endpoint with Auth Method private_key_jwt. + */ + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "RS256_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwt( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwt"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + /** + * Fail 1: Call to Token Endpoint with Auth Method client_secret_basic should fail. + */ + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtFail1( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtFail1"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setJwksUri(jwksUri); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + /** + * Fail 2: Call to Token Endpoint with Auth Method client_secret_post should fail. + */ + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtFail2( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtFail2"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setJwksUri(jwksUri); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_POST); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + /** + * Fail 3: Call to Token Endpoint with Auth Method client_secret_jwt should fail. + */ + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtFail3( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtFail3"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setJwksUri(jwksUri); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "RS256_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtRS256( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtRS256"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "RS384_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtRS384( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtRS384"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "RS512_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtRS512( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtRS512"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "ES256_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtES256( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtES256"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "ES384_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtES384( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtES384"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "ES512_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtES512( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtES512"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "PS256_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtPS256( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtPS256"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.PS256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "PS384_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtPS384( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtPS384"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.PS384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "PS512_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtPS512( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtPS512"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.PS512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "RS256_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS256( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS256"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.RS256); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.RS256.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "RS384_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS256Fail1( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS256Fail1"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.RS256); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.RS256.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "RS512_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS256Fail2( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS256Fail2"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.RS256); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.RS256.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "ES256_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS256Fail3( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS256Fail3"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.RS256); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.RS256.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "ES384_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS256Fail4( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS256Fail4"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.RS256); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.RS256.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "ES512_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS256Fail5( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS256Fail5"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.RS256); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.RS256.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "RS384_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS384( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS384"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.RS384); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.RS384.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "RS256_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS384Fail1( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS384Fail1"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.RS384); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.RS384.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "RS512_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS384Fail2( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS384Fail2"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.RS384); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.RS384.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "ES256_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS384Fail3( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS384Fail3"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.RS384); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.RS384.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "ES384_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS384Fail4( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS384Fail4"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.RS384); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.RS384.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "ES512_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS384Fail5( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS384Fail5"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.RS384); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.RS384.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "RS512_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS512( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS512"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.RS512); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.RS512.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "RS256_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS512Fail1( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS512Fail1"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.RS512); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.RS512.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "RS384_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS512Fail2( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS512Fail2"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.RS512); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.RS512.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "ES256_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS512Fail3( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS512Fail3"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.RS512); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.RS512.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "ES384_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS512Fail4( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS512Fail4"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.RS512); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.RS512.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "ES512_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS512Fail5( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgRS512Fail5"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.RS512); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.RS512.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "ES256_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES256( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES256"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.ES256); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.ES256.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "RS256_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES256Fail1( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES256Fail1"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.ES256); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.ES256.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "RS384_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES256Fail2( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES256Fail2"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.ES256); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.ES256.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "RS512_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES256Fail3( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES256Fail3"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.ES256); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.ES256.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "ES384_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES256Fail4( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES256Fail4"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.ES256); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.ES256.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "ES512_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES256Fail5( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES256Fail5"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.ES256); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.ES256.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "ES384_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES384( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES384"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.ES384); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.ES384.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "RS256_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES384Fail1( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES384Fail1"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.ES384); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.ES384.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "RS384_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES384Fail2( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES384Fail2"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.ES384); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.ES384.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "RS512_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES384Fail3( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES384Fail3"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.ES384); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.ES384.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "ES256_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES384Fail4( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES384Fail4"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.ES384); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.ES384.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "ES512_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES384Fail5( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES384Fail5"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.ES384); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.ES384.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "ES512_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES512( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES512"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.ES512); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.ES512.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "RS256_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES512Fail1( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES512Fail1"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.ES512); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.ES512.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "RS384_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES512Fail2( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES512Fail2"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.ES512); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.ES512.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "RS512_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES512Fail3( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES512Fail3"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.ES512); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.ES512.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "ES256_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES512Fail4( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES512Fail4"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.ES512); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.ES512.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "ES384_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES512Fail5( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgES512Fail5"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.ES512); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.ES512.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "PS256_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS256( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS256"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.PS256); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.PS256.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.PS256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "PS384_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS256Fail1( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS256Fail1"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.PS256); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.PS256.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.PS384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "PS512_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS256Fail2( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS256Fail2"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.PS256); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.PS256.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.PS512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "ES256_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS256Fail3( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS256Fail3"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.PS256); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.PS256.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "PS384_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS256Fail4( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS256Fail4"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.PS256); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.PS256.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.PS384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "ES512_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS256Fail5( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS256Fail5"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.PS256); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.PS256.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "PS384_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS384( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS384"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.PS384); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.PS384.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.PS384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "RS256_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS384Fail1( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS384Fail1"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.PS384); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = newRegisterClient(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + readClient.setExecutor(clientEngine(true)); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.PS384.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = newTokenClient(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "RS512_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS384Fail2( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS384Fail2"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.PS384); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.PS384.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "ES256_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS384Fail3( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS384Fail3"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.PS384); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.PS384.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "ES384_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS384Fail4( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS384Fail4"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.PS384); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.PS384.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "ES512_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS384Fail5( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS384Fail5"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.PS384); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.PS384.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "PS512_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS512( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS512"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.PS512); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.PS512.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.PS512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "PS256_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS512Fail1( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS512Fail1"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.PS512); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.PS512.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "RS384_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS512Fail2( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS512Fail2"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.PS512); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.PS512.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "ES256_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS512Fail3( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS512Fail3"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.PS512); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.PS512.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "ES384_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS512Fail4( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS512Fail4"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.PS512); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.PS512.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "clientJwksUri", "ES512_keyId", "dnName", + "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS512Fail5( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String clientJwksUri, final String keyId, final String dnName, final String keyStoreFile, + final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("tokenEndpointAuthMethodPrivateKeyJwtSigningAlgPS512Fail5"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.setTokenEndpointAuthSigningAlg(SignatureAlgorithm.PS512); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_METHOD.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_METHOD.toString()), + AuthenticationMethod.PRIVATE_KEY_JWT.toString()); + assertTrue(readClientResponse.getClaims().containsKey(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString())); + assertEquals(readClientResponse.getClaims().get(TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString()), + SignatureAlgorithm.PS512.toString()); + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List responseTypes = Arrays.asList(ResponseType.CODE); + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, null); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNull(authorizationResponse.getIdToken(), "The id token is not null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 4. Get Access Token + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/TokenRestWebServiceHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/TokenRestWebServiceHttpTest.java new file mode 100644 index 00000000..eb40280e --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/TokenRestWebServiceHttpTest.java @@ -0,0 +1,1460 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * Functional tests for Token Web Services (HTTP) + * + * @author Javier Rojas Blum + * @version March 9, 2019 + */ +public class TokenRestWebServiceHttpTest extends BaseTest { + + @Parameters({"redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAccessTokenFail(final String redirectUris, final String redirectUri, final String sectorIdentifierUri) throws Exception { + showTitle("requestAccessTokenFail"); + + List responseTypes = new ArrayList(); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request with invalid Authorization Code + String code = "INVALID_AUTHORIZATION_CODE"; + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + TokenResponse response = tokenClient.execAuthorizationCode(code, redirectUri, clientId, clientSecret); + + showClient(tokenClient); + assertEquals(response.getStatus(), 400, "Unexpected response code: " + response.getStatus()); + assertNotNull(response.getEntity(), "The entity is null"); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "sectorIdentifierUri"}) + @Test + public void requestAccessTokenPassword( + final String userId, final String userSecret, final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("requestAccessTokenPassword"); + + List responseTypes = new ArrayList(); + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setGrantTypes(grantTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request Resource Owner Credentials Grant + String username = userId; + String password = userSecret; + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + TokenResponse tokenResponse = tokenClient.execResourceOwnerPasswordCredentialsGrant(username, password, null, + clientId, clientSecret); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "sectorIdentifierUri"}) + @Test + public void requestAccessTokenPasswordFail( + final String userId, final String userSecret, final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("requestAccessTokenPasswordFail"); + + List responseTypes = new ArrayList(); + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setGrantTypes(grantTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request Resource Owner Credentials Grant + String username = userId; + String password = "BAD_PASSWORD"; + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + TokenResponse tokenResponse = tokenClient.execResourceOwnerPasswordCredentialsGrant(username, password, null, + clientId, clientSecret); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 401, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void requestAccessTokenWithClientSecretPost( + final String redirectUris, final String userId, final String userSecret, final String sectorIdentifierUri) throws Exception { + showTitle("requestAccessTokenWithClientSecretPost"); + + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_POST); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(grantTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + TokenRequest request = new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + request.setUsername(userId); + request.setPassword(userSecret); + request.setAuthUsername(clientId); + request.setAuthPassword(clientSecret); + request.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_POST); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(request); + TokenResponse response1 = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(response1.getStatus(), 200, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getEntity(), "The entity is null"); + assertNotNull(response1.getAccessToken(), "The access token is null"); + assertNotNull(response1.getTokenType(), "The token type is null"); + } + + @Parameters({"redirectUris", "userId", "userSecret", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestAccessTokenWithClientSecretJwtHS256( + final String redirectUris, final String userId, final String userSecret, final String dnName, + final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("requestAccessTokenWithClientSecretJwtHS256"); + + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(grantTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + tokenRequest.setUsername(userId); + tokenRequest.setPassword(userSecret); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse response1 = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(response1.getStatus(), 200, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getEntity(), "The entity is null"); + assertNotNull(response1.getAccessToken(), "The access token is null"); + assertNotNull(response1.getTokenType(), "The token type is null"); + } + + @Parameters({"redirectUris", "userId", "userSecret", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestAccessTokenWithClientSecretJwtHS384( + final String redirectUris, final String userId, final String userSecret, final String dnName, + final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("requestAccessTokenWithClientSecretJwtHS384"); + + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(grantTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + tokenRequest.setUsername(userId); + tokenRequest.setPassword(userSecret); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setAlgorithm(SignatureAlgorithm.HS384); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse response1 = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(response1.getStatus(), 200, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getEntity(), "The entity is null"); + assertNotNull(response1.getAccessToken(), "The access token is null"); + assertNotNull(response1.getTokenType(), "The token type is null"); + } + + @Parameters({"redirectUris", "userId", "userSecret", "dnName", "keyStoreFile", "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestAccessTokenWithClientSecretJwtHS512( + final String redirectUris, final String userId, final String userSecret, final String dnName, + final String keyStoreFile, final String keyStoreSecret, final String sectorIdentifierUri) throws Exception { + showTitle("requestAccessTokenWithClientSecretJwtHS512"); + + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(grantTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + tokenRequest.setUsername(userId); + tokenRequest.setPassword(userSecret); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setAlgorithm(SignatureAlgorithm.HS512); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse response1 = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(response1.getStatus(), 200, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getEntity(), "The entity is null"); + assertNotNull(response1.getAccessToken(), "The access token is null"); + assertNotNull(response1.getTokenType(), "The token type is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "clientJwksUri", "RS256_keyId", "dnName", "keyStoreFile", + "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestAccessTokenWithClientSecretJwtRS256( + final String userId, final String userSecret, final String redirectUris, final String jwksUri, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAccessTokenWithClientSecretJwtRS256"); + + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setJwksUri(jwksUri); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(grantTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + tokenRequest.setUsername(userId); + tokenRequest.setPassword(userSecret); + + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "clientJwksUri", "RS384_keyId", "dnName", "keyStoreFile", + "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestAccessTokenWithClientSecretJwtRS384( + final String userId, final String userSecret, final String redirectUris, final String jwksUri, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAccessTokenWithClientSecretJwtRS384"); + + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setJwksUri(jwksUri); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(grantTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + tokenRequest.setUsername(userId); + tokenRequest.setPassword(userSecret); + + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "clientJwksUri", "RS512_keyId", "dnName", "keyStoreFile", + "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestAccessTokenWithClientSecretJwtRS512( + final String userId, final String userSecret, final String redirectUris, final String jwksUri, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAccessTokenWithClientSecretJwtRS512"); + + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setJwksUri(jwksUri); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(grantTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + tokenRequest.setUsername(userId); + tokenRequest.setPassword(userSecret); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "clientJwksUri", "ES256_keyId", "dnName", "keyStoreFile", + "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestAccessTokenWithClientSecretJwtES256( + final String userId, final String userSecret, final String redirectUris, final String jwksUri, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAccessTokenWithClientSecretJwtES256"); + + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setJwksUri(jwksUri); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(grantTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + tokenRequest.setUsername(userId); + tokenRequest.setPassword(userSecret); + + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse response1 = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(response1.getStatus(), 200, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getEntity(), "The entity is null"); + assertNotNull(response1.getAccessToken(), "The access token is null"); + assertNotNull(response1.getTokenType(), "The token type is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "clientJwksUri", "ES384_keyId", "dnName", "keyStoreFile", + "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestAccessTokenWithClientSecretJwtES384( + final String userId, final String userSecret, final String redirectUris, final String jwksUri, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAccessTokenWithClientSecretJwtES384"); + + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setJwksUri(jwksUri); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(grantTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + tokenRequest.setUsername(userId); + tokenRequest.setPassword(userSecret); + + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "clientJwksUri", "ES512_keyId", "dnName", "keyStoreFile", + "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestAccessTokenWithClientSecretJwtES512( + final String userId, final String userSecret, final String redirectUris, final String jwksUri, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAccessTokenWithClientSecretJwtES512"); + + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setJwksUri(jwksUri); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(grantTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + tokenRequest.setUsername(userId); + tokenRequest.setPassword(userSecret); + + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "clientJwksUri", "PS256_keyId", "dnName", "keyStoreFile", + "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestAccessTokenWithClientSecretJwtPS256( + final String userId, final String userSecret, final String redirectUris, final String jwksUri, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAccessTokenWithClientSecretJwtPS256"); + + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setJwksUri(jwksUri); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(grantTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + tokenRequest.setUsername(userId); + tokenRequest.setPassword(userSecret); + + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.PS256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "clientJwksUri", "PS384_keyId", "dnName", "keyStoreFile", + "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestAccessTokenWithClientSecretJwtPS384( + final String userId, final String userSecret, final String redirectUris, final String jwksUri, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAccessTokenWithClientSecretJwtPS384"); + + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setJwksUri(jwksUri); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(grantTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + tokenRequest.setUsername(userId); + tokenRequest.setPassword(userSecret); + + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.PS384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "clientJwksUri", "PS512_keyId", "dnName", "keyStoreFile", + "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestAccessTokenWithClientSecretJwtPS512( + final String userId, final String userSecret, final String redirectUris, final String jwksUri, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAccessTokenWithClientSecretJwtPS512"); + + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setJwksUri(jwksUri); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(grantTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + tokenRequest.setUsername(userId); + tokenRequest.setPassword(userSecret); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.PS512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "clientJwksUri", "RS256_keyId", "dnName", "keyStoreFile", + "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestAccessTokenWithClientSecretJwtRS256X509Cert( + final String userId, final String userSecret, final String redirectUris, final String jwksUri, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAccessTokenWithClientSecretJwtRS256X509Cert"); + + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setJwksUri(jwksUri); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(grantTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + tokenRequest.setUsername(userId); + tokenRequest.setPassword(userSecret); + + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "clientJwksUri", "RS384_keyId", "dnName", "keyStoreFile", + "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestAccessTokenWithClientSecretJwtRS384X509Cert( + final String userId, final String userSecret, final String redirectUris, final String jwksUri, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAccessTokenWithClientSecretJwtRS384X509Cert"); + + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setJwksUri(jwksUri); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(grantTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + tokenRequest.setUsername(userId); + tokenRequest.setPassword(userSecret); + + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "clientJwksUri", "RS512_keyId", "dnName", "keyStoreFile", + "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestAccessTokenWithClientSecretJwtRS512X509Cert( + final String userId, final String userSecret, final String redirectUris, final String jwksUri, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAccessTokenWithClientSecretJwtRS512X509Cert"); + + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setJwksUri(jwksUri); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(grantTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + tokenRequest.setUsername(userId); + tokenRequest.setPassword(userSecret); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.RS512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "clientJwksUri", "ES256_keyId", "dnName", "keyStoreFile", + "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestAccessTokenWithClientSecretJwtES256X509Cert( + final String userId, final String userSecret, final String redirectUris, final String jwksUri, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAccessTokenWithClientSecretJwtES256X509Cert"); + + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setJwksUri(jwksUri); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(grantTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + tokenRequest.setUsername(userId); + tokenRequest.setPassword(userSecret); + + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES256); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "clientJwksUri", "ES384_keyId", "dnName", "keyStoreFile", + "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestAccessTokenWithClientSecretJwtES384X509Cert( + final String userId, final String userSecret, final String redirectUris, final String jwksUri, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAccessTokenWithClientSecretJwtES384X509Cert"); + + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setJwksUri(jwksUri); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(grantTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + tokenRequest.setUsername(userId); + tokenRequest.setPassword(userSecret); + + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES384); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "clientJwksUri", "ES512_keyId", "dnName", "keyStoreFile", + "keyStoreSecret", "sectorIdentifierUri"}) + @Test + public void requestAccessTokenWithClientSecretJwtES512X509Cert( + final String userId, final String userSecret, final String redirectUris, final String jwksUri, + final String keyId, final String dnName, final String keyStoreFile, final String keyStoreSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAccessTokenWithClientSecretJwtES512X509Cert"); + + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // 1. Dynamic Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setJwksUri(jwksUri); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(grantTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + + TokenRequest tokenRequest = new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + tokenRequest.setUsername(userId); + tokenRequest.setPassword(userSecret); + + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.PRIVATE_KEY_JWT); + tokenRequest.setAlgorithm(SignatureAlgorithm.ES512); + tokenRequest.setCryptoProvider(cryptoProvider); + tokenRequest.setKeyId(keyId); + tokenRequest.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "sectorIdentifierUri"}) + @Test + public void requestAccessTokenWithClientSecretJwtFail( + final String userId, final String userSecret, final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("requestAccessTokenWithClientSecretJwtFail"); + + List responseTypes = new ArrayList(); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request with invalid Client Secret + String username = userId; + String password = userSecret; + + TokenRequest request = new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + request.setUsername(username); + request.setPassword(password); + request.setAuthUsername(clientId); + request.setAuthPassword("INVALID_CLIENT_SECRET"); + request.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + request.setAudience(tokenEndpoint); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(request); + TokenResponse response = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(response.getStatus(), 401, "Unexpected response code: " + response.getStatus()); + assertNotNull(response.getEntity(), "The entity is null"); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + } + + @Parameters({"redirectUris", "sectorIdentifierUri"}) + @Test + public void requestAccessTokenClientCredentials(final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("requestAccessTokenClientCredentials"); + + List responseTypes = new ArrayList(); + List grantTypes = Arrays.asList( + GrantType.CLIENT_CREDENTIALS + ); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setGrantTypes(grantTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request Client Credentials Grant + String scope = "storage"; + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + TokenResponse response = tokenClient.execClientCredentialsGrant(scope, clientId, clientSecret); + + showClient(tokenClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getStatus()); + assertNotNull(response.getEntity(), "The entity is null"); + assertNotNull(response.getAccessToken(), "The access token is null"); + assertNotNull(response.getTokenType(), "The token type is null"); + assertNotNull(response.getScope(), "The scope is null"); + } + + @Parameters({"redirectUris", "sectorIdentifierUri"}) + @Test + public void refreshingAccessTokenFail(final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("refreshingAccessTokenFail"); + + List responseTypes = new ArrayList(); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request Refresh Token + String scope = "email read_stream manage_pages"; + String refreshToken = "tGzv3JOkF0XG5Qx2TlKWIA"; + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + TokenResponse response = tokenClient.execRefreshToken(scope, refreshToken, clientId, clientSecret); + + showClient(tokenClient); + assertEquals(response.getStatus(), 400, "Unexpected response code: " + response.getStatus()); + assertNotNull(response.getEntity(), "The entity is null"); + assertNotNull(response.getErrorType(), "The error type is null"); + assertNotNull(response.getErrorDescription(), "The error description is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/TokenRevocationTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/TokenRevocationTest.java new file mode 100644 index 00000000..2efb8301 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/TokenRevocationTest.java @@ -0,0 +1,981 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.client.TokenRevocationClient; +import org.gluu.oxauth.client.TokenRevocationRequest; +import org.gluu.oxauth.client.TokenRevocationResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.common.SubjectType; +import org.gluu.oxauth.model.common.TokenTypeHint; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * The oxAuth authorization server's revocation policy acts as follows: + * The revocation of a particular token cause the revocation of related + * tokens and the underlying authorization grant. If the particular + * token is a refresh token, then the authorization server will also + * invalidate all access tokens based on the same authorization grant. + * If the token passed to the request is an access token, the server will + * revoke the respective refresh token as well. + * + * @author Javier Rojas Blum + * @version May 14, 2019 + */ +public class TokenRevocationTest extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestTokenRevocation_withPublicClient( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, final String sectorIdentifierUri) { + showTitle("requestTokenRevocation_withPublicClient"); + + List responseTypes = Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN, ResponseType.TOKEN); + List scopes = Arrays.asList("openid", "profile", "address", "email", "phone", "user_name"); + + // 1. Register client + RegisterResponse registerResponse = registerPublicClient(redirectUris, responseTypes, scopes, sectorIdentifierUri); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization and receive the authorization code. + String nonce = UUID.randomUUID().toString(); + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, scopes, clientId, nonce); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient1 = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse1 = userInfoClient1.execUserInfo(accessToken); + + showClient(userInfoClient1); + assertEquals(userInfoResponse1.getStatus(), 200, "Unexpected response code: " + userInfoResponse1.getStatus()); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.NAME)); + + // 4. Request token revocation + TokenRevocationRequest revocationRequest = new TokenRevocationRequest(); + revocationRequest.setToken(accessToken); + revocationRequest.setTokenTypeHint(TokenTypeHint.ACCESS_TOKEN); + revocationRequest.setAuthUsername(clientId); + + TokenRevocationClient revocationClient = new TokenRevocationClient(tokenRevocationEndpoint); + revocationClient.setRequest(revocationRequest); + + TokenRevocationResponse revocationResponse = revocationClient.exec(); + + showClient(revocationClient); + assertEquals(revocationResponse.getStatus(), 200, "Unexpected response code: " + revocationResponse.getStatus()); + + // 5. Request user info with the revoked access token should fail + UserInfoClient userInfoClient2 = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse2 = userInfoClient2.execUserInfo(accessToken); + + showClient(userInfoClient2); + assertEquals(userInfoResponse2.getStatus(), 401, "Unexpected response code: " + userInfoResponse2.getStatus()); + assertNotNull(userInfoResponse2.getErrorType(), "Unexpected result: errorType not found"); + assertNotNull(userInfoResponse2.getErrorDescription(), "Unexpected result: errorDescription not found"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestTokenRevocation1( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestTokenRevocation1"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + List scopes = Arrays.asList("openid", "profile", "address", "email", "phone", "user_name"); + + // 1. Register client + RegisterResponse registerResponse = registerClient(redirectUris, responseTypes, scopes, sectorIdentifierUri); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization and receive the authorization code. + String nonce = UUID.randomUUID().toString(); + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, scopes, clientId, nonce); + + String scope = authorizationResponse.getScope(); + String authorizationCode = authorizationResponse.getCode(); + String idToken = authorizationResponse.getIdToken(); + + // 3. Request access token using the authorization code. + TokenRequest tokenRequest1 = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest1.setCode(authorizationCode); + tokenRequest1.setRedirectUri(redirectUri); + tokenRequest1.setAuthUsername(clientId); + tokenRequest1.setAuthPassword(clientSecret); + tokenRequest1.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient1 = new TokenClient(tokenEndpoint); + tokenClient1.setRequest(tokenRequest1); + TokenResponse tokenResponse1 = tokenClient1.exec(); + + showClient(tokenClient1); + assertEquals(tokenResponse1.getStatus(), 200, "Unexpected response code: " + tokenResponse1.getStatus()); + assertNotNull(tokenResponse1.getEntity(), "The entity is null"); + assertNotNull(tokenResponse1.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse1.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse1.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse1.getRefreshToken(), "The refresh token is null"); + + String refreshToken = tokenResponse1.getRefreshToken(); + + // 4. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.CODE_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.OX_OPENID_CONNECT_VERSION)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 5. Request new access token using the refresh token. + TokenClient tokenClient2 = new TokenClient(tokenEndpoint); + TokenResponse tokenResponse2 = tokenClient2.execRefreshToken(scope, refreshToken, clientId, clientSecret); + + showClient(tokenClient2); + assertEquals(tokenResponse2.getStatus(), 200, "Unexpected response code: " + tokenResponse2.getStatus()); + assertNotNull(tokenResponse2.getEntity(), "The entity is null"); + assertNotNull(tokenResponse2.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse2.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse2.getRefreshToken(), "The refresh token is null"); + assertNotNull(tokenResponse2.getScope(), "The scope is null"); + + String accessToken2 = tokenResponse2.getAccessToken(); + String refreshToken2 = tokenResponse2.getRefreshToken(); + + // 6. Request user info + UserInfoClient userInfoClient1 = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse1 = userInfoClient1.execUserInfo(accessToken2); + + showClient(userInfoClient1); + assertEquals(userInfoResponse1.getStatus(), 200, "Unexpected response code: " + userInfoResponse1.getStatus()); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.BIRTHDATE)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.GENDER)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.MIDDLE_NAME)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.NICKNAME)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.PREFERRED_USERNAME)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.PROFILE)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.WEBSITE)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.PHONE_NUMBER)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.PHONE_NUMBER_VERIFIED)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.USER_NAME)); + assertNull(userInfoResponse1.getClaim("org_name")); + assertNull(userInfoResponse1.getClaim("work_phone")); + + // 7. Request refresh token revocation + TokenRevocationRequest tokenRevocationRequest1 = new TokenRevocationRequest(); + tokenRevocationRequest1.setToken(refreshToken2); + tokenRevocationRequest1.setTokenTypeHint(TokenTypeHint.REFRESH_TOKEN); + tokenRevocationRequest1.setAuthUsername(clientId); + tokenRevocationRequest1.setAuthPassword(clientSecret); + + TokenRevocationClient tokenRevocationClient1 = new TokenRevocationClient(tokenRevocationEndpoint); + tokenRevocationClient1.setRequest(tokenRevocationRequest1); + + TokenRevocationResponse tokenRevocationResponse1 = tokenRevocationClient1.exec(); + + showClient(tokenRevocationClient1); + assertEquals(tokenRevocationResponse1.getStatus(), 200, "Unexpected response code: " + tokenRevocationResponse1.getStatus()); + + // 8. Request new access token using the revoked refresh token should fail. + TokenClient tokenClient3 = new TokenClient(tokenEndpoint); + TokenResponse tokenResponse3 = tokenClient3.execRefreshToken(scope, refreshToken2, clientId, clientSecret); + + showClient(tokenClient3); + assertEquals(tokenResponse3.getStatus(), 400, "Unexpected response code: " + tokenResponse3.getStatus()); + assertNotNull(tokenResponse3.getEntity(), "The entity is null"); + assertNotNull(tokenResponse3.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse3.getErrorDescription(), "The error description is null"); + + // 9. Request token revocation + TokenRevocationRequest tokenRevocationRequest2 = new TokenRevocationRequest(); + tokenRevocationRequest2.setToken(accessToken2); + tokenRevocationRequest2.setTokenTypeHint(TokenTypeHint.ACCESS_TOKEN); + tokenRevocationRequest2.setAuthUsername(clientId); + tokenRevocationRequest2.setAuthPassword(clientSecret); + + TokenRevocationClient tokenRevocationClient2 = new TokenRevocationClient(tokenRevocationEndpoint); + tokenRevocationClient2.setRequest(tokenRevocationRequest2); + + TokenRevocationResponse tokenRevocationResponse2 = tokenRevocationClient2.exec(); + + showClient(tokenRevocationClient2); + assertEquals(tokenRevocationResponse2.getStatus(), 200, "Unexpected response code: " + tokenRevocationResponse2.getStatus()); + + // 10. Request user info with the revoked access token should fail + UserInfoClient userInfoClient2 = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse2 = userInfoClient2.execUserInfo(accessToken2); + + showClient(userInfoClient2); + assertEquals(userInfoResponse2.getStatus(), 401, "Unexpected response code: " + userInfoResponse2.getStatus()); + assertNotNull(userInfoResponse2.getErrorType(), "Unexpected result: errorType not found"); + assertNotNull(userInfoResponse2.getErrorDescription(), "Unexpected result: errorDescription not found"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestTokenRevocation2( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestTokenRevocation2"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + List scopes = Arrays.asList("openid", "profile", "address", "email", "phone", "user_name"); + + // 1. Register client + RegisterResponse registerResponse = registerClient(redirectUris, responseTypes, scopes, sectorIdentifierUri); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization and receive the authorization code. + String nonce = UUID.randomUUID().toString(); + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, scopes, clientId, nonce); + + String scope = authorizationResponse.getScope(); + String authorizationCode = authorizationResponse.getCode(); + String idToken = authorizationResponse.getIdToken(); + + // 3. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient1 = new TokenClient(tokenEndpoint); + tokenClient1.setRequest(tokenRequest); + TokenResponse tokenResponse1 = tokenClient1.exec(); + + showClient(tokenClient1); + assertEquals(tokenResponse1.getStatus(), 200, "Unexpected response code: " + tokenResponse1.getStatus()); + assertNotNull(tokenResponse1.getEntity(), "The entity is null"); + assertNotNull(tokenResponse1.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse1.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse1.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse1.getRefreshToken(), "The refresh token is null"); + + String accessToken = tokenResponse1.getAccessToken(); + String refreshToken = tokenResponse1.getRefreshToken(); + + // 4. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.CODE_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.OX_OPENID_CONNECT_VERSION)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 6. Request user info + UserInfoClient userInfoClient1 = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse1 = userInfoClient1.execUserInfo(accessToken); + + showClient(userInfoClient1); + assertEquals(userInfoResponse1.getStatus(), 200, "Unexpected response code: " + userInfoResponse1.getStatus()); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.BIRTHDATE)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.GENDER)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.MIDDLE_NAME)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.NICKNAME)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.PREFERRED_USERNAME)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.PROFILE)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.WEBSITE)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.PHONE_NUMBER)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.PHONE_NUMBER_VERIFIED)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.USER_NAME)); + assertNull(userInfoResponse1.getClaim("org_name")); + assertNull(userInfoResponse1.getClaim("work_phone")); + + // 7. Request access token revocation + TokenRevocationRequest tokenRevocationRequest2 = new TokenRevocationRequest(); + tokenRevocationRequest2.setToken(accessToken); + tokenRevocationRequest2.setTokenTypeHint(TokenTypeHint.ACCESS_TOKEN); + tokenRevocationRequest2.setAuthUsername(clientId); + tokenRevocationRequest2.setAuthPassword(clientSecret); + + TokenRevocationClient tokenRevocationClient2 = new TokenRevocationClient(tokenRevocationEndpoint); + tokenRevocationClient2.setRequest(tokenRevocationRequest2); + + TokenRevocationResponse tokenRevocationResponse2 = tokenRevocationClient2.exec(); + + showClient(tokenRevocationClient2); + assertEquals(tokenRevocationResponse2.getStatus(), 200, "Unexpected response code: " + tokenRevocationResponse2.getStatus()); + + // 8. Request user info with the revoked access token must fail + UserInfoClient userInfoClient2 = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse2 = userInfoClient2.execUserInfo(accessToken); + + showClient(userInfoClient2); + assertEquals(userInfoResponse2.getStatus(), 401, "Unexpected response code: " + userInfoResponse2.getStatus()); + assertNotNull(userInfoResponse2.getErrorType(), "Unexpected result: errorType not found"); + assertNotNull(userInfoResponse2.getErrorDescription(), "Unexpected result: errorDescription not found"); + + // 9. Request new access token using the refresh token must fail. + TokenClient tokenClient2 = new TokenClient(tokenEndpoint); + TokenResponse tokenResponse2 = tokenClient2.execRefreshToken(scope, refreshToken, clientId, clientSecret); + + showClient(tokenClient2); + assertEquals(tokenResponse2.getStatus(), 400, "Unexpected response code: " + tokenResponse2.getStatus()); + assertNotNull(tokenResponse2.getEntity(), "The entity is null"); + assertNotNull(tokenResponse2.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse2.getErrorDescription(), "The error description is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestTokenRevocation3( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestTokenRevocation3"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + List scopes = Arrays.asList("openid", "profile", "address", "email", "phone", "user_name"); + + // 1. Register client + RegisterResponse registerResponse = registerClient(redirectUris, responseTypes, scopes, sectorIdentifierUri); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization and receive the authorization code. + String nonce = UUID.randomUUID().toString(); + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, scopes, clientId, nonce); + + String scope = authorizationResponse.getScope(); + String authorizationCode = authorizationResponse.getCode(); + String idToken = authorizationResponse.getIdToken(); + + // 3. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient1 = new TokenClient(tokenEndpoint); + tokenClient1.setRequest(tokenRequest); + TokenResponse tokenResponse1 = tokenClient1.exec(); + + showClient(tokenClient1); + assertEquals(tokenResponse1.getStatus(), 200, "Unexpected response code: " + tokenResponse1.getStatus()); + assertNotNull(tokenResponse1.getEntity(), "The entity is null"); + assertNotNull(tokenResponse1.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse1.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse1.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse1.getRefreshToken(), "The refresh token is null"); + + String accessToken = tokenResponse1.getAccessToken(); + String refreshToken = tokenResponse1.getRefreshToken(); + + // 4. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.CODE_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.OX_OPENID_CONNECT_VERSION)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 5. Request refresh token revocation + TokenRevocationRequest tokenRevocationRequest1 = new TokenRevocationRequest(); + tokenRevocationRequest1.setToken(refreshToken); + tokenRevocationRequest1.setTokenTypeHint(TokenTypeHint.REFRESH_TOKEN); + tokenRevocationRequest1.setAuthUsername(clientId); + tokenRevocationRequest1.setAuthPassword(clientSecret); + + TokenRevocationClient tokenRevocationClient1 = new TokenRevocationClient(tokenRevocationEndpoint); + tokenRevocationClient1.setRequest(tokenRevocationRequest1); + + TokenRevocationResponse tokenRevocationResponse1 = tokenRevocationClient1.exec(); + + showClient(tokenRevocationClient1); + assertEquals(tokenRevocationResponse1.getStatus(), 200, "Unexpected response code: " + tokenRevocationResponse1.getStatus()); + + // 6. Request new access token using revoked refresh token. + TokenClient tokenClient2 = new TokenClient(tokenEndpoint); + TokenResponse tokenResponse2 = tokenClient2.execRefreshToken(scope, refreshToken, clientId, clientSecret); + + showClient(tokenClient2); + assertEquals(tokenResponse2.getStatus(), 400, "Unexpected response code: " + tokenResponse2.getStatus()); + assertNotNull(tokenResponse2.getEntity(), "The entity is null"); + assertNotNull(tokenResponse2.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse2.getErrorDescription(), "The error description is null"); + + // 7. Request user info must fail + UserInfoClient userInfoClient1 = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse1 = userInfoClient1.execUserInfo(accessToken); + + showClient(userInfoClient1); + assertEquals(userInfoResponse1.getStatus(), 401, "Unexpected response code: " + userInfoResponse1.getStatus()); + assertNotNull(userInfoResponse1.getErrorType(), "Unexpected result: errorType not found"); + assertNotNull(userInfoResponse1.getErrorDescription(), "Unexpected result: errorDescription not found"); + + // 8. Request access token revocation + TokenRevocationRequest tokenRevocationRequest2 = new TokenRevocationRequest(); + tokenRevocationRequest2.setToken(accessToken); + tokenRevocationRequest2.setTokenTypeHint(TokenTypeHint.ACCESS_TOKEN); + tokenRevocationRequest2.setAuthUsername(clientId); + tokenRevocationRequest2.setAuthPassword(clientSecret); + + TokenRevocationClient tokenRevocationClient2 = new TokenRevocationClient(tokenRevocationEndpoint); + tokenRevocationClient2.setRequest(tokenRevocationRequest2); + + TokenRevocationResponse tokenRevocationResponse2 = tokenRevocationClient2.exec(); + + showClient(tokenRevocationClient2); + assertEquals(tokenRevocationResponse2.getStatus(), 200, "Unexpected response code: " + tokenRevocationResponse2.getStatus()); + + // 9. Request user info with the revoked access token should fail + UserInfoClient userInfoClient2 = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse2 = userInfoClient2.execUserInfo(accessToken); + + showClient(userInfoClient2); + assertEquals(userInfoResponse2.getStatus(), 401, "Unexpected response code: " + userInfoResponse2.getStatus()); + assertNotNull(userInfoResponse2.getErrorType(), "Unexpected result: errorType not found"); + assertNotNull(userInfoResponse2.getErrorDescription(), "Unexpected result: errorDescription not found"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestTokenRevocationOptionalTokenTypeHint( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestTokenRevocationOptionalTokenTypeHint"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + List scopes = Arrays.asList("openid", "profile", "address", "email", "phone", "user_name"); + + // 1. Register client + RegisterResponse registerResponse = registerClient(redirectUris, responseTypes, scopes, sectorIdentifierUri); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization and receive the authorization code. + String nonce = UUID.randomUUID().toString(); + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, scopes, clientId, nonce); + + String scope = authorizationResponse.getScope(); + String authorizationCode = authorizationResponse.getCode(); + String idToken = authorizationResponse.getIdToken(); + + // 3. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient1 = new TokenClient(tokenEndpoint); + tokenClient1.setRequest(tokenRequest); + TokenResponse tokenResponse1 = tokenClient1.exec(); + + showClient(tokenClient1); + assertEquals(tokenResponse1.getStatus(), 200, "Unexpected response code: " + tokenResponse1.getStatus()); + assertNotNull(tokenResponse1.getEntity(), "The entity is null"); + assertNotNull(tokenResponse1.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse1.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse1.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse1.getRefreshToken(), "The refresh token is null"); + + String refreshToken = tokenResponse1.getRefreshToken(); + + // 4. Validate id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.CODE_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.OX_OPENID_CONNECT_VERSION)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + // 5. Request new access token using the refresh token. + TokenClient tokenClient2 = new TokenClient(tokenEndpoint); + TokenResponse tokenResponse2 = tokenClient2.execRefreshToken(scope, refreshToken, clientId, clientSecret); + + showClient(tokenClient2); + assertEquals(tokenResponse2.getStatus(), 200, "Unexpected response code: " + tokenResponse2.getStatus()); + assertNotNull(tokenResponse2.getEntity(), "The entity is null"); + assertNotNull(tokenResponse2.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse2.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse2.getRefreshToken(), "The refresh token is null"); + assertNotNull(tokenResponse2.getScope(), "The scope is null"); + + String accessToken = tokenResponse2.getAccessToken(); + String refreshToken2 = tokenResponse2.getRefreshToken(); + + // 6. Request user info + UserInfoClient userInfoClient1 = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse1 = userInfoClient1.execUserInfo(accessToken); + + showClient(userInfoClient1); + assertEquals(userInfoResponse1.getStatus(), 200, "Unexpected response code: " + userInfoResponse1.getStatus()); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.BIRTHDATE)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.GENDER)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.MIDDLE_NAME)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.NICKNAME)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.PREFERRED_USERNAME)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.PROFILE)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.WEBSITE)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.PHONE_NUMBER)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.PHONE_NUMBER_VERIFIED)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse1.getClaim(JwtClaimName.USER_NAME)); + assertNull(userInfoResponse1.getClaim("org_name")); + assertNull(userInfoResponse1.getClaim("work_phone")); + + // 7. Request refresh token revocation + TokenRevocationRequest tokenRevocationRequest1 = new TokenRevocationRequest(); + tokenRevocationRequest1.setToken(refreshToken2); + tokenRevocationRequest1.setAuthUsername(clientId); + tokenRevocationRequest1.setAuthPassword(clientSecret); + + TokenRevocationClient tokenRevocationClient1 = new TokenRevocationClient(tokenRevocationEndpoint); + tokenRevocationClient1.setRequest(tokenRevocationRequest1); + + TokenRevocationResponse tokenRevocationResponse1 = tokenRevocationClient1.exec(); + + showClient(tokenRevocationClient1); + assertEquals(tokenRevocationResponse1.getStatus(), 200, "Unexpected response code: " + tokenRevocationResponse1.getStatus()); + + // 8. Request new access token using the revoked refresh token should fail. + TokenClient tokenClient3 = new TokenClient(tokenEndpoint); + TokenResponse tokenResponse3 = tokenClient3.execRefreshToken(scope, refreshToken2, clientId, clientSecret); + + showClient(tokenClient3); + assertEquals(tokenResponse3.getStatus(), 400, "Unexpected response code: " + tokenResponse2.getStatus()); + assertNotNull(tokenResponse3.getEntity(), "The entity is null"); + assertNotNull(tokenResponse3.getErrorType(), "The error type is null"); + assertNotNull(tokenResponse3.getErrorDescription(), "The error description is null"); + + // 9. Request token revocation + TokenRevocationRequest tokenRevocationRequest2 = new TokenRevocationRequest(); + tokenRevocationRequest2.setToken(accessToken); + tokenRevocationRequest2.setAuthUsername(clientId); + tokenRevocationRequest2.setAuthPassword(clientSecret); + + TokenRevocationClient tokenRevocationClient2 = new TokenRevocationClient(tokenRevocationEndpoint); + tokenRevocationClient2.setRequest(tokenRevocationRequest2); + + TokenRevocationResponse tokenRevocationResponse2 = tokenRevocationClient2.exec(); + + showClient(tokenRevocationClient2); + assertEquals(tokenRevocationResponse2.getStatus(), 200, "Unexpected response code: " + tokenRevocationResponse2.getStatus()); + + // 10. Request user info with the revoked access token should fail + UserInfoClient userInfoClient2 = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse2 = userInfoClient2.execUserInfo(accessToken); + + showClient(userInfoClient2); + assertEquals(userInfoResponse2.getStatus(), 401, "Unexpected response code: " + userInfoResponse2.getStatus()); + assertNotNull(userInfoResponse2.getErrorType(), "Unexpected result: errorType not found"); + assertNotNull(userInfoResponse2.getErrorDescription(), "Unexpected result: errorDescription not found"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestTokenRevocationFail1( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) { + showTitle("requestTokenRevocationFail1"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + List scopes = Arrays.asList("openid", "profile", "address", "email", "phone", "user_name"); + + // 1. Register client + RegisterResponse registerResponse = registerClient(redirectUris, responseTypes, scopes, sectorIdentifierUri); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization and receive the authorization code. + String nonce = UUID.randomUUID().toString(); + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, scopes, clientId, nonce); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + + String refreshToken = tokenResponse.getRefreshToken(); + + // 4. Request refresh token revocation + TokenRevocationRequest tokenRevocationRequest = new TokenRevocationRequest(); + tokenRevocationRequest.setToken(refreshToken); + tokenRevocationRequest.setTokenTypeHint(TokenTypeHint.REFRESH_TOKEN); + tokenRevocationRequest.setAuthUsername(clientId); + tokenRevocationRequest.setAuthPassword("INVALID_CLIENT_SECRET"); + + TokenRevocationClient tokenRevocationClient = new TokenRevocationClient(tokenRevocationEndpoint); + tokenRevocationClient.setRequest(tokenRevocationRequest); + + TokenRevocationResponse tokenRevocationResponse = tokenRevocationClient.exec(); + + showClient(tokenRevocationClient); + assertEquals(tokenRevocationResponse.getStatus(), 401, "Unexpected response code: " + tokenRevocationResponse.getStatus()); + assertNotNull(tokenRevocationResponse.getEntity(), "The entity is null"); + assertNotNull(tokenRevocationResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenRevocationResponse.getErrorDescription(), "The error description is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestTokenRevocationFail2( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) { + showTitle("requestTokenRevocationFail2"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + List scopes = Arrays.asList("openid", "profile", "address", "email", "phone", "user_name"); + + // 1. Register client + RegisterResponse registerResponse = registerClient(redirectUris, responseTypes, scopes, sectorIdentifierUri); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization and receive the authorization code. + String nonce = UUID.randomUUID().toString(); + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, scopes, clientId, nonce); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + + String accessToken = tokenResponse.getAccessToken(); + + // 4. Request refresh token revocation: Invalid tokens do not cause an error. + TokenRevocationRequest tokenRevocationRequest = new TokenRevocationRequest(); + tokenRevocationRequest.setToken("INVALID_ACCESS_TOKEN"); + tokenRevocationRequest.setTokenTypeHint(TokenTypeHint.ACCESS_TOKEN); + tokenRevocationRequest.setAuthUsername(clientId); + tokenRevocationRequest.setAuthPassword(clientSecret); + + TokenRevocationClient tokenRevocationClient = new TokenRevocationClient(tokenRevocationEndpoint); + tokenRevocationClient.setRequest(tokenRevocationRequest); + + TokenRevocationResponse tokenRevocationResponse = tokenRevocationClient.exec(); + + showClient(tokenRevocationClient); + assertEquals(tokenRevocationResponse.getStatus(), 200, "Unexpected response code: " + tokenRevocationResponse.getStatus()); + + // 5. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.BIRTHDATE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GENDER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.MIDDLE_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NICKNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PREFERRED_USERNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PROFILE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.WEBSITE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PHONE_NUMBER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PHONE_NUMBER_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.USER_NAME)); + assertNull(userInfoResponse.getClaim("org_name")); + assertNull(userInfoResponse.getClaim("work_phone")); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestTokenRevocationFail3( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) { + showTitle("requestTokenRevocationFail3"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + List scopes = Arrays.asList("openid", "profile", "address", "email", "phone", "user_name"); + + // 1. Register client + RegisterResponse registerResponse = registerClient(redirectUris, responseTypes, scopes, sectorIdentifierUri); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization and receive the authorization code. + String nonce = UUID.randomUUID().toString(); + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, scopes, clientId, nonce); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + + String refreshToken = tokenResponse.getRefreshToken(); + + // 4. Request token revocation without required parameter token + TokenRevocationRequest tokenRevocationRequest = new TokenRevocationRequest(); + tokenRevocationRequest.setToken(null); + tokenRevocationRequest.setAuthUsername(clientId); + tokenRevocationRequest.setAuthPassword(clientSecret); + + TokenRevocationClient tokenRevocationClient = new TokenRevocationClient(tokenRevocationEndpoint); + tokenRevocationClient.setRequest(tokenRevocationRequest); + + TokenRevocationResponse tokenRevocationResponse = tokenRevocationClient.exec(); + + showClient(tokenRevocationClient); + assertEquals(tokenRevocationResponse.getStatus(), 400, "Unexpected response code: " + tokenRevocationResponse.getStatus()); + assertNotNull(tokenRevocationResponse.getEntity(), "The entity is null"); + assertNotNull(tokenRevocationResponse.getErrorType(), "The error type is null"); + assertNotNull(tokenRevocationResponse.getErrorDescription(), "The error description is null"); + } + + private AuthorizationResponse requestAuthorization(final String userId, final String userSecret, final String redirectUri, + List responseTypes, List scopes, String clientId, String nonce) { + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + return authorizationResponse; + } + + private RegisterResponse registerClient( + final String redirectUris, List responseTypes, List scopes, String sectorIdentifierUri) { + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setScope(scopes); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + return registerResponse; + } + + private RegisterResponse registerPublicClient(String redirectUris, List responseTypes, List scopes, String sectorIdentifierUri) { + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setScope(scopes); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setAuthenticationMethod(AuthenticationMethod.NONE); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.NONE); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); +// assertNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + return registerResponse; + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/TokenSignaturesHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/TokenSignaturesHttpTest.java new file mode 100644 index 00000000..c2675f10 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/TokenSignaturesHttpTest.java @@ -0,0 +1,1258 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.JwkResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.Prompt; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.AbstractCryptoProvider; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.JwtUtil; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Javier Rojas Blum + * @version February 8, 2019 + */ +public class TokenSignaturesHttpTest extends BaseTest { + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationIdTokenNone( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationIdTokenNone"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + + // 1. Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setContacts(Arrays.asList("javier@gluu.org", "javier.rojas.blum@gmail.com")); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.NONE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization and receive the authorization code. + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + assertNotNull(authorizationResponse.getScope()); + assertNull(authorizationResponse.getIdToken()); + + String scope = authorizationResponse.getScope(); + String authorizationCode = authorizationResponse.getCode(); + + // 3. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200); + assertNotNull(tokenResponse.getEntity()); + assertNotNull(tokenResponse.getAccessToken()); + assertNotNull(tokenResponse.getExpiresIn()); + assertNotNull(tokenResponse.getTokenType()); + assertNotNull(tokenResponse.getRefreshToken()); + + String idToken = tokenResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + + AbstractCryptoProvider cryptoProvider = createCryptoProviderWithAllowedNone(); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), null, + null, null, SignatureAlgorithm.NONE); + assertTrue(validJwt); + } + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationIdTokenHS256( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationIdTokenHS256"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setContacts(Arrays.asList("javier@gluu.org", "javier.rojas.blum@gmail.com")); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.HS256); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request Authorization + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.setAuthUsername(userId); + authorizationRequest.setAuthPassword(userSecret); + authorizationRequest.getPrompts().add(Prompt.NONE); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(authorizationResponse.getStatus(), 302, "Unexpected response code: " + authorizationResponse.getStatus()); + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), null, + null, clientSecret, SignatureAlgorithm.HS256); + assertTrue(validJwt); + } + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationIdTokenHS384( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationIdTokenHS384"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setContacts(Arrays.asList("javier@gluu.org", "javier.rojas.blum@gmail.com")); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.HS384); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request Authorization + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.setAuthUsername(userId); + authorizationRequest.setAuthPassword(userSecret); + authorizationRequest.getPrompts().add(Prompt.NONE); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(authorizationResponse.getStatus(), 302, "Unexpected response code: " + authorizationResponse.getStatus()); + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), null, + null, clientSecret, SignatureAlgorithm.HS384); + assertTrue(validJwt); + } + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationIdTokenHS512( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationIdTokenHS512"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setContacts(Arrays.asList("javier@gluu.org", "javier.rojas.blum@gmail.com")); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.HS512); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request Authorization + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest request = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + request.setState(state); + request.setAuthUsername(userId); + request.setAuthPassword(userSecret); + request.getPrompts().add(Prompt.NONE); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(request); + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(authorizationResponse.getStatus(), 302, "Unexpected response code: " + authorizationResponse.getStatus()); + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), null, + null, clientSecret, SignatureAlgorithm.HS512); + assertTrue(validJwt); + } + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationIdTokenRS256( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationIdTokenRS256"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setContacts(Arrays.asList("javier@gluu.org", "javier.rojas.blum@gmail.com")); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS256); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Authorization + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.setAuthUsername(userId); + authorizationRequest.setAuthPassword(userSecret); + authorizationRequest.getPrompts().add(Prompt.NONE); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(authorizationResponse.getStatus(), 302, "Unexpected response code: " + authorizationResponse.getStatus()); + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + String keyId = jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID); + JwkClient jwkClient = new JwkClient(jwksUri); + JwkResponse jwkResponse = jwkClient.exec(); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), keyId, + jwkResponse.getJwks().toJSONObject(), null, SignatureAlgorithm.RS256); + assertTrue(validJwt); + } + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationIdTokenRS384( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationIdTokenRS384"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setContacts(Arrays.asList("javier@gluu.org", "javier.rojas.blum@gmail.com")); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS384); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Authorization + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.setAuthUsername(userId); + authorizationRequest.setAuthPassword(userSecret); + authorizationRequest.getPrompts().add(Prompt.NONE); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(authorizationResponse.getStatus(), 302, "Unexpected response code: " + authorizationResponse.getStatus()); + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + String keyId = jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID); + JwkClient jwkClient = new JwkClient(jwksUri); + JwkResponse jwkResponse = jwkClient.exec(); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), keyId, + jwkResponse.getJwks().toJSONObject(), null, SignatureAlgorithm.RS384); + assertTrue(validJwt); + } + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationIdTokenRS512( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationIdTokenRS512"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setContacts(Arrays.asList("javier@gluu.org", "javier.rojas.blum@gmail.com")); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.RS512); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Authorization + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.setAuthUsername(userId); + authorizationRequest.setAuthPassword(userSecret); + authorizationRequest.getPrompts().add(Prompt.NONE); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(authorizationResponse.getStatus(), 302, "Unexpected response code: " + authorizationResponse.getStatus()); + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + String keyId = jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID); + JwkClient jwkClient = new JwkClient(jwksUri); + JwkResponse jwkResponse = jwkClient.exec(); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), keyId, + jwkResponse.getJwks().toJSONObject(), null, SignatureAlgorithm.RS512); + assertTrue(validJwt); + } + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationIdTokenES256( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationIdTokenES256"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setContacts(Arrays.asList("javier@gluu.org", "javier.rojas.blum@gmail.com")); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.ES256); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Authorization + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + String keyId = jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID); + JwkClient jwkClient = new JwkClient(jwksUri); + JwkResponse jwkResponse = jwkClient.exec(); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), keyId, + jwkResponse.getJwks().toJSONObject(), null, SignatureAlgorithm.ES256); + assertTrue(validJwt); + } + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationIdTokenES384( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationIdTokenES384"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setContacts(Arrays.asList("javier@gluu.org", "javier.rojas.blum@gmail.com")); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.ES384); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Authorization + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + String keyId = jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID); + JwkClient jwkClient = new JwkClient(jwksUri); + JwkResponse jwkResponse = jwkClient.exec(); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), keyId, + jwkResponse.getJwks().toJSONObject(), null, SignatureAlgorithm.ES384); + assertTrue(validJwt); + } + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationIdTokenES512( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationIdTokenES512"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setContacts(Arrays.asList("javier@gluu.org", "javier.rojas.blum@gmail.com")); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.ES512); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Authorization + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + String keyId = jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID); + JwkClient jwkClient = new JwkClient(jwksUri); + JwkResponse jwkResponse = jwkClient.exec(); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), keyId, + jwkResponse.getJwks().toJSONObject(), null, SignatureAlgorithm.ES512); + assertTrue(validJwt); + } + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationIdTokenPS256( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationIdTokenPS256"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setContacts(Arrays.asList("javier@gluu.org", "javier.rojas.blum@gmail.com")); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.PS256); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Authorization + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.setAuthUsername(userId); + authorizationRequest.setAuthPassword(userSecret); + authorizationRequest.getPrompts().add(Prompt.NONE); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(authorizationResponse.getStatus(), 302, "Unexpected response code: " + authorizationResponse.getStatus()); + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + String keyId = jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID); + JwkClient jwkClient = new JwkClient(jwksUri); + JwkResponse jwkResponse = jwkClient.exec(); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), keyId, + jwkResponse.getJwks().toJSONObject(), null, SignatureAlgorithm.PS256); + assertTrue(validJwt); + } + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationIdTokenPRS384( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationIdTokenPS384"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setContacts(Arrays.asList("javier@gluu.org", "javier.rojas.blum@gmail.com")); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.PS384); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Authorization + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.setAuthUsername(userId); + authorizationRequest.setAuthPassword(userSecret); + authorizationRequest.getPrompts().add(Prompt.NONE); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(authorizationResponse.getStatus(), 302, "Unexpected response code: " + authorizationResponse.getStatus()); + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + String keyId = jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID); + JwkClient jwkClient = new JwkClient(jwksUri); + JwkResponse jwkResponse = jwkClient.exec(); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), keyId, + jwkResponse.getJwks().toJSONObject(), null, SignatureAlgorithm.PS384); + assertTrue(validJwt); + } + + @Parameters({"redirectUris", "userId", "userSecret", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestAuthorizationIdTokenPS512( + final String redirectUris, final String userId, final String userSecret, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAuthorizationIdTokenPS512"); + + List responseTypes = Arrays.asList(ResponseType.ID_TOKEN); + + // 1. Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setContacts(Arrays.asList("javier@gluu.org", "javier.rojas.blum@gmail.com")); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setIdTokenSignedResponseAlg(SignatureAlgorithm.PS512); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request Authorization + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.setAuthUsername(userId); + authorizationRequest.setAuthPassword(userSecret); + authorizationRequest.getPrompts().add(Prompt.NONE); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setRequest(authorizationRequest); + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(authorizationResponse.getStatus(), 302, "Unexpected response code: " + authorizationResponse.getStatus()); + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String idToken = authorizationResponse.getIdToken(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + String keyId = jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID); + JwkClient jwkClient = new JwkClient(jwksUri); + JwkResponse jwkResponse = jwkClient.exec(); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + boolean validJwt = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), keyId, + jwkResponse.getJwks().toJSONObject(), null, SignatureAlgorithm.PS512); + assertTrue(validJwt); + } + + @Test + public void printAlgorithmsAndProviders() { + showTitle("printAlgorithmsAndProviders"); + + JwtUtil.printAlgorithmsAndProviders(); + } + + @Test + public void hs256() { + try { + showTitle("hs256"); + + String signingInput = "eyJhbGciOiJIUzI1NiJ9.eyJub25jZSI6ICI2Qm9HN1QwR0RUZ2wiLCAiaWRfdG9rZW4iOiB7Im1heF9hZ2UiOiA4NjQwMH0sICJzdGF0ZSI6ICJTVEFURTAiLCAicmVkaXJlY3RfdXJpIjogImh0dHBzOi8vbG9jYWxob3N0L2NhbGxiYWNrMSIsICJ1c2VyaW5mbyI6IHsiY2xhaW1zIjogeyJuYW1lIjogbnVsbH19LCAiY2xpZW50X2lkIjogIkAhMTExMSEwMDA4IUU2NTQuQjQ2MCIsICJzY29wZSI6IFsib3BlbmlkIl0sICJyZXNwb25zZV90eXBlIjogWyJjb2RlIl19"; + String secret = "071d68a5-9eb0-47fb-8608-f54a0d9c8ede"; + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + String encodedSignature = cryptoProvider.sign(signingInput, null, secret, SignatureAlgorithm.HS256); + + System.out.println("Encoded Signature: " + encodedSignature); + assertEquals(encodedSignature, "BQwm1HCz0cjHYbulWMumkhZgyb2dD93uScXmC6Fv8Ik"); + } catch (Exception e) { + fail(e.getMessage(), e); + } + } + + @Test + public void hs384() { + try { + showTitle("hs384"); + + String signingInput = "eyJhbGciOiJIUzI1NiJ9.eyJub25jZSI6ICI2Qm9HN1QwR0RUZ2wiLCAiaWRfdG9rZW4iOiB7Im1heF9hZ2UiOiA4NjQwMH0sICJzdGF0ZSI6ICJTVEFURTAiLCAicmVkaXJlY3RfdXJpIjogImh0dHBzOi8vbG9jYWxob3N0L2NhbGxiYWNrMSIsICJ1c2VyaW5mbyI6IHsiY2xhaW1zIjogeyJuYW1lIjogbnVsbH19LCAiY2xpZW50X2lkIjogIkAhMTExMSEwMDA4IUU2NTQuQjQ2MCIsICJzY29wZSI6IFsib3BlbmlkIl0sICJyZXNwb25zZV90eXBlIjogWyJjb2RlIl19"; + String secret = "071d68a5-9eb0-47fb-8608-f54a0d9c8ede"; + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + String encodedSignature = cryptoProvider.sign(signingInput, null, secret, SignatureAlgorithm.HS384); + + System.out.println("Encoded Signature: " + encodedSignature); + assertEquals(encodedSignature, "pe7gU1XxroqizSzucuHOor36L-M9_XPZ7KZcR6JW6xQAa2fmTLSDCc02fNER9atB"); + } catch (Exception e) { + fail(e.getMessage(), e); + } + } + + @Test + public void hs512() { + try { + showTitle("hs512"); + + String signingInput = "eyJhbGciOiJIUzI1NiJ9.eyJub25jZSI6ICI2Qm9HN1QwR0RUZ2wiLCAiaWRfdG9rZW4iOiB7Im1heF9hZ2UiOiA4NjQwMH0sICJzdGF0ZSI6ICJTVEFURTAiLCAicmVkaXJlY3RfdXJpIjogImh0dHBzOi8vbG9jYWxob3N0L2NhbGxiYWNrMSIsICJ1c2VyaW5mbyI6IHsiY2xhaW1zIjogeyJuYW1lIjogbnVsbH19LCAiY2xpZW50X2lkIjogIkAhMTExMSEwMDA4IUU2NTQuQjQ2MCIsICJzY29wZSI6IFsib3BlbmlkIl0sICJyZXNwb25zZV90eXBlIjogWyJjb2RlIl19"; + String secret = "071d68a5-9eb0-47fb-8608-f54a0d9c8ede"; + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + String encodedSignature = cryptoProvider.sign(signingInput, null, secret, SignatureAlgorithm.HS512); + + System.out.println("Encoded Signature: " + encodedSignature); + assertEquals(encodedSignature, "IZsXiRrRfP9eNFj6snm_MGEnrtfvX8vOF43Z-FuFkRj29y0WUaPR50IXRDI5uGatJvVdr_i7eJCJ4N_EwwrIhQ"); + } catch (Exception e) { + fail(e.getMessage(), e); + } + } + + @Parameters({"clientJwksUri", "RS256_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void testRS256(final String clientJwksUri, final String keyId, final String dnName, + final String keyStoreFile, final String keyStoreSecret) { + try { + showTitle("Test RS256"); + + JwkClient jwkClient = new JwkClient(clientJwksUri); + JwkResponse jwkResponse = jwkClient.exec(); + + String signingInput = "eyJhbGciOiJIUzI1NiJ9.eyJub25jZSI6ICI2Qm9HN1QwR0RUZ2wiLCAiaWRfdG9rZW4iOiB7Im1heF9hZ2UiOiA4NjQwMH0sICJzdGF0ZSI6ICJTVEFURTAiLCAicmVkaXJlY3RfdXJpIjogImh0dHBzOi8vbG9jYWxob3N0L2NhbGxiYWNrMSIsICJ1c2VyaW5mbyI6IHsiY2xhaW1zIjogeyJuYW1lIjogbnVsbH19LCAiY2xpZW50X2lkIjogIkAhMTExMSEwMDA4IUU2NTQuQjQ2MCIsICJzY29wZSI6IFsib3BlbmlkIl0sICJyZXNwb25zZV90eXBlIjogWyJjb2RlIl19"; + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(signingInput, keyId, null, SignatureAlgorithm.RS256); + + System.out.println("Encoded Signature: " + encodedSignature); + + boolean signatureVerified = cryptoProvider.verifySignature( + signingInput, encodedSignature, keyId, jwkResponse.getJwks().toJSONObject(), null, + SignatureAlgorithm.RS256); + assertTrue(signatureVerified, "Invalid signature"); + } catch (Exception e) { + fail(e.getMessage(), e); + } + } + + @Parameters({"clientJwksUri", "RS384_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void testRS384(final String clientJwksUri, final String keyId, final String dnName, + final String keyStoreFile, final String keyStoreSecret) { + try { + showTitle("Test RS384"); + + JwkClient jwkClient = new JwkClient(clientJwksUri); + JwkResponse jwkResponse = jwkClient.exec(); + + String signingInput = "eyJhbGciOiJIUzI1NiJ9.eyJub25jZSI6ICI2Qm9HN1QwR0RUZ2wiLCAiaWRfdG9rZW4iOiB7Im1heF9hZ2UiOiA4NjQwMH0sICJzdGF0ZSI6ICJTVEFURTAiLCAicmVkaXJlY3RfdXJpIjogImh0dHBzOi8vbG9jYWxob3N0L2NhbGxiYWNrMSIsICJ1c2VyaW5mbyI6IHsiY2xhaW1zIjogeyJuYW1lIjogbnVsbH19LCAiY2xpZW50X2lkIjogIkAhMTExMSEwMDA4IUU2NTQuQjQ2MCIsICJzY29wZSI6IFsib3BlbmlkIl0sICJyZXNwb25zZV90eXBlIjogWyJjb2RlIl19"; + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(signingInput, keyId, null, SignatureAlgorithm.RS384); + + System.out.println("Encoded Signature: " + encodedSignature); + + boolean signatureVerified = cryptoProvider.verifySignature( + signingInput, encodedSignature, keyId, jwkResponse.getJwks().toJSONObject(), null, + SignatureAlgorithm.RS384); + assertTrue(signatureVerified, "Invalid signature"); + } catch (Exception e) { + fail(e.getMessage(), e); + } + } + + @Parameters({"clientJwksUri", "RS512_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void testRS512(final String clientJwksUri, final String keyId, final String dnName, + final String keyStoreFile, final String keyStoreSecret) { + try { + showTitle("Test RS512"); + + JwkClient jwkClient = new JwkClient(clientJwksUri); + JwkResponse jwkResponse = jwkClient.exec(); + + String signingInput = "eyJhbGciOiJIUzI1NiJ9.eyJub25jZSI6ICI2Qm9HN1QwR0RUZ2wiLCAiaWRfdG9rZW4iOiB7Im1heF9hZ2UiOiA4NjQwMH0sICJzdGF0ZSI6ICJTVEFURTAiLCAicmVkaXJlY3RfdXJpIjogImh0dHBzOi8vbG9jYWxob3N0L2NhbGxiYWNrMSIsICJ1c2VyaW5mbyI6IHsiY2xhaW1zIjogeyJuYW1lIjogbnVsbH19LCAiY2xpZW50X2lkIjogIkAhMTExMSEwMDA4IUU2NTQuQjQ2MCIsICJzY29wZSI6IFsib3BlbmlkIl0sICJyZXNwb25zZV90eXBlIjogWyJjb2RlIl19"; + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(signingInput, keyId, null, SignatureAlgorithm.RS512); + + System.out.println("Encoded Signature: " + encodedSignature); + + boolean signatureVerified = cryptoProvider.verifySignature( + signingInput, encodedSignature, keyId, jwkResponse.getJwks().toJSONObject(), null, + SignatureAlgorithm.RS512); + assertTrue(signatureVerified, "Invalid signature"); + } catch (Exception e) { + fail(e.getMessage(), e); + } + } + + @Parameters({"clientJwksUri", "ES256_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void testES256(final String clientJwksUri, final String keyId, final String dnName, + final String keyStoreFile, final String keyStoreSecret) { + try { + showTitle("Test ES256"); + + JwkClient jwkClient = new JwkClient(clientJwksUri); + JwkResponse jwkResponse = jwkClient.exec(); + + String signingInput = "eyJhbGciOiJIUzI1NiJ9.eyJub25jZSI6ICI2Qm9HN1QwR0RUZ2wiLCAiaWRfdG9rZW4iOiB7Im1heF9hZ2UiOiA4NjQwMH0sICJzdGF0ZSI6ICJTVEFURTAiLCAicmVkaXJlY3RfdXJpIjogImh0dHBzOi8vbG9jYWxob3N0L2NhbGxiYWNrMSIsICJ1c2VyaW5mbyI6IHsiY2xhaW1zIjogeyJuYW1lIjogbnVsbH19LCAiY2xpZW50X2lkIjogIkAhMTExMSEwMDA4IUU2NTQuQjQ2MCIsICJzY29wZSI6IFsib3BlbmlkIl0sICJyZXNwb25zZV90eXBlIjogWyJjb2RlIl19"; + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(signingInput, keyId, null, SignatureAlgorithm.ES256); + + System.out.println("Encoded Signature: " + encodedSignature); + + boolean signatureVerified = cryptoProvider.verifySignature( + signingInput, encodedSignature, keyId, jwkResponse.getJwks().toJSONObject(), null, + SignatureAlgorithm.ES256); + assertTrue(signatureVerified, "Invalid signature"); + } catch (Exception e) { + fail(e.getMessage(), e); + } + } + + @Parameters({"clientJwksUri", "ES384_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void testES384(final String clientJwksUri, final String keyId, final String dnName, + final String keyStoreFile, final String keyStoreSecret) { + try { + showTitle("Test ES384"); + + JwkClient jwkClient = new JwkClient(clientJwksUri); + JwkResponse jwkResponse = jwkClient.exec(); + + String signingInput = "eyJhbGciOiJIUzI1NiJ9.eyJub25jZSI6ICI2Qm9HN1QwR0RUZ2wiLCAiaWRfdG9rZW4iOiB7Im1heF9hZ2UiOiA4NjQwMH0sICJzdGF0ZSI6ICJTVEFURTAiLCAicmVkaXJlY3RfdXJpIjogImh0dHBzOi8vbG9jYWxob3N0L2NhbGxiYWNrMSIsICJ1c2VyaW5mbyI6IHsiY2xhaW1zIjogeyJuYW1lIjogbnVsbH19LCAiY2xpZW50X2lkIjogIkAhMTExMSEwMDA4IUU2NTQuQjQ2MCIsICJzY29wZSI6IFsib3BlbmlkIl0sICJyZXNwb25zZV90eXBlIjogWyJjb2RlIl19"; + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(signingInput, keyId, null, SignatureAlgorithm.ES384); + + System.out.println("Encoded Signature: " + encodedSignature); + + boolean signatureVerified = cryptoProvider.verifySignature( + signingInput, encodedSignature, keyId, jwkResponse.getJwks().toJSONObject(), null, + SignatureAlgorithm.ES384); + assertTrue(signatureVerified, "Invalid signature"); + } catch (Exception e) { + fail(e.getMessage(), e); + } + } + + @Parameters({"clientJwksUri", "ES512_keyId", "dnName", "keyStoreFile", "keyStoreSecret"}) + @Test + public void testES512(final String clientJwksUri, final String keyId, final String dnName, + final String keyStoreFile, final String keyStoreSecret) { + try { + showTitle("Test ES512"); + + JwkClient jwkClient = new JwkClient(clientJwksUri); + JwkResponse jwkResponse = jwkClient.exec(); + + String signingInput = "eyJhbGciOiJIUzI1NiJ9.eyJub25jZSI6ICI2Qm9HN1QwR0RUZ2wiLCAiaWRfdG9rZW4iOiB7Im1heF9hZ2UiOiA4NjQwMH0sICJzdGF0ZSI6ICJTVEFURTAiLCAicmVkaXJlY3RfdXJpIjogImh0dHBzOi8vbG9jYWxob3N0L2NhbGxiYWNrMSIsICJ1c2VyaW5mbyI6IHsiY2xhaW1zIjogeyJuYW1lIjogbnVsbH19LCAiY2xpZW50X2lkIjogIkAhMTExMSEwMDA4IUU2NTQuQjQ2MCIsICJzY29wZSI6IFsib3BlbmlkIl0sICJyZXNwb25zZV90eXBlIjogWyJjb2RlIl19"; + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, dnName); + String encodedSignature = cryptoProvider.sign(signingInput, keyId, null, SignatureAlgorithm.ES512); + + System.out.println("Encoded Signature: " + encodedSignature); + + boolean signatureVerified = cryptoProvider.verifySignature( + signingInput, encodedSignature, keyId, jwkResponse.getJwks().toJSONObject(), null, + SignatureAlgorithm.ES512); + assertTrue(signatureVerified, "Invalid signature"); + } catch (Exception e) { + fail(e.getMessage(), e); + } + } + + @Test + public void getMessageDigestSHA256() { + showTitle("sha256"); + + try { + String input = "The quick brown fox jumps over the lazy dog"; + System.out.println("Input: " + input); + + byte[] digest = JwtUtil.getMessageDigestSHA256(input); + + BigInteger result = new BigInteger(1, digest); + BigInteger expectedResult = new BigInteger("d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592", 16); + + System.out.println("Result : " + result); + System.out.println("Expected: " + expectedResult); + + assertEquals(result, expectedResult); + } catch (NoSuchProviderException e) { + e.printStackTrace(); + fail(e.getMessage()); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + fail(e.getMessage()); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + fail(e.getMessage()); + } + } + + @Test + public void getMessageDigestSHA384() { + showTitle("sha384"); + + try { + String input = "The quick brown fox jumps over the lazy dog"; + System.out.println("Input: " + input); + + byte[] digest = JwtUtil.getMessageDigestSHA384(input); + + BigInteger result = new BigInteger(1, digest); + BigInteger expectedResult = new BigInteger("ca737f1014a48f4c0b6dd43cb177b0afd9e5169367544c494011e3317dbf9a509cb1e5dc1e85a941bbee3d7f2afbc9b1", 16); + + System.out.println("Result : " + result); + System.out.println("Expected : " + expectedResult); + + assertEquals(result, expectedResult); + } catch (NoSuchProviderException e) { + e.printStackTrace(); + fail(e.getMessage()); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + fail(e.getMessage()); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + fail(e.getMessage()); + } + } + + @Test + public void getMessageDigestSHA512() { + showTitle("sha512"); + + try { + String input = "The quick brown fox jumps over the lazy dog"; + System.out.println("Input: " + input); + + byte[] digest = JwtUtil.getMessageDigestSHA512(input); + + BigInteger result = new BigInteger(1, digest); + BigInteger expectedResult = new BigInteger("07e547d9586f6a73f73fbac0435ed76951218fb7d0c8d788a309d785436bbb642e93a252a954f23912547d1e8a3b5ed6e1bfd7097821233fa0538f3db854fee6", 16); + + System.out.println("Result : " + result); + System.out.println("Expected : " + expectedResult); + + assertEquals(result, expectedResult); + } catch (NoSuchProviderException e) { + e.printStackTrace(); + fail(e.getMessage()); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + fail(e.getMessage()); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + fail(e.getMessage()); + } + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/UILocales.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/UILocales.java new file mode 100644 index 00000000..1af4632f --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/UILocales.java @@ -0,0 +1,111 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.gluu.oxauth.model.register.RegisterRequestParam.APPLICATION_TYPE; +import static org.gluu.oxauth.model.register.RegisterRequestParam.CLIENT_NAME; +import static org.gluu.oxauth.model.register.RegisterRequestParam.ID_TOKEN_SIGNED_RESPONSE_ALG; +import static org.gluu.oxauth.model.register.RegisterRequestParam.REDIRECT_URIS; +import static org.gluu.oxauth.model.register.RegisterRequestParam.RESPONSE_TYPES; +import static org.gluu.oxauth.model.register.RegisterRequestParam.SCOPE; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Javier Rojas Blum + * @version November 29, 2017 + */ +public class UILocales extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void uiLocales( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("uiLocales"); + + //List responseTypes = Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN); + List responseTypes = Arrays.asList(ResponseType.TOKEN); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String registrationAccessToken = registerResponse.getRegistrationAccessToken(); + String registrationClientUri = registerResponse.getRegistrationClientUri(); + + // 2. Client read + RegisterRequest readClientRequest = new RegisterRequest(registrationAccessToken); + + RegisterClient readClient = new RegisterClient(registrationClientUri); + readClient.setRequest(readClientRequest); + RegisterResponse readClientResponse = readClient.exec(); + + showClient(readClient); + assertEquals(readClientResponse.getStatus(), 200, "Unexpected response code: " + readClientResponse.getEntity()); + assertNotNull(readClientResponse.getClientId()); + assertNotNull(readClientResponse.getClientSecret()); + assertNotNull(readClientResponse.getClientIdIssuedAt()); + assertNotNull(readClientResponse.getClientSecretExpiresAt()); + + assertNotNull(readClientResponse.getClaims().get(RESPONSE_TYPES.toString())); + assertNotNull(readClientResponse.getClaims().get(REDIRECT_URIS.toString())); + assertNotNull(readClientResponse.getClaims().get(APPLICATION_TYPE.toString())); + assertNotNull(readClientResponse.getClaims().get(CLIENT_NAME.toString())); + assertNotNull(readClientResponse.getClaims().get(ID_TOKEN_SIGNED_RESPONSE_ALG.toString())); + assertNotNull(readClientResponse.getClaims().get(SCOPE.toString())); + + // 3. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The access token is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getTokenType(), "The token type is null"); + assertNotNull(authorizationResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(authorizationResponse.getScope(), "The scope must be null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/UserAuthenticationFilterHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/UserAuthenticationFilterHttpTest.java new file mode 100644 index 00000000..4d102dd2 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/UserAuthenticationFilterHttpTest.java @@ -0,0 +1,320 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.AuthorizationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.Prompt; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Javier Rojas Blum + * @version March 9, 2019 + */ +public class UserAuthenticationFilterHttpTest extends BaseTest { + + @Parameters({"redirectUris", "userInum", "userEmail", "sectorIdentifierUri"}) + @Test + public void requestAccessTokenCustomAuth1( + final String redirectUris, final String userInum, final String userEmail, final String sectorIdentifierUri) throws Exception { + showTitle("requestAccessTokenCustomAuth1"); + + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_POST); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(grantTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + TokenRequest tokenRequest = new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.addCustomParameter("mail", userEmail); + tokenRequest.addCustomParameter("inum", userInum); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_POST); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse response1 = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(response1.getStatus(), 200, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getEntity(), "The entity is null"); + assertNotNull(response1.getAccessToken(), "The access token is null"); + assertNotNull(response1.getTokenType(), "The token type is null"); + } + + @Parameters({"redirectUris", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void requestAccessTokenCustomAuth2( + final String redirectUris, final String userId, final String userSecret, final String sectorIdentifierUri) throws Exception { + showTitle("requestAccessTokenCustomAuth2"); + + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_POST); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(grantTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + TokenRequest tokenRequest = new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.addCustomParameter("uid", userId); + tokenRequest.addCustomParameter("pwd", userSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_POST); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse response1 = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(response1.getStatus(), 200, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getEntity(), "The entity is null"); + assertNotNull(response1.getAccessToken(), "The access token is null"); + assertNotNull(response1.getTokenType(), "The token type is null"); + } + + @Parameters({"redirectUris", "userInum", "userEmail", "sectorIdentifierUri"}) + @Test + public void requestAccessTokenCustomAuth3( + final String redirectUris, final String userInum, final String userEmail, final String sectorIdentifierUri) throws Exception { + showTitle("requestAccessTokenCustomAuth3"); + + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(grantTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + TokenRequest tokenRequest = new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.addCustomParameter("mail", userEmail); + tokenRequest.addCustomParameter("inum", userInum); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse response1 = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(response1.getStatus(), 200, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getEntity(), "The entity is null"); + assertNotNull(response1.getAccessToken(), "The access token is null"); + assertNotNull(response1.getTokenType(), "The token type is null"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "sectorIdentifierUri"}) + @Test + public void requestAccessTokenCustomAuth4( + final String userId, final String userSecret, final String redirectUris, final String sectorIdentifierUri) throws Exception { + showTitle("requestAccessTokenCustomAuth4"); + + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setGrantTypes(grantTypes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + TokenRequest tokenRequest = new TokenRequest(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.addCustomParameter("uid", userId); + tokenRequest.addCustomParameter("pwd", userSecret); + tokenRequest.setAudience(tokenEndpoint); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_JWT); + tokenRequest.setCryptoProvider(cryptoProvider); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse response1 = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(response1.getStatus(), 200, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getEntity(), "The entity is null"); + assertNotNull(response1.getAccessToken(), "The access token is null"); + assertNotNull(response1.getTokenType(), "The token type is null"); + } + + @Parameters({"redirectUris", "redirectUri", "userInum", "userEmail", "sectorIdentifierUri"}) + @Test + public void requestAccessTokenCustomAuth5( + final String redirectUris, final String redirectUri, final String userInum, final String userEmail, + final String sectorIdentifierUri) throws Exception { + showTitle("requestAccessTokenCustomAuth5"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + + // 1. Register client. + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_POST); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization and receive the authorization code. + List scopes = Arrays.asList( + "openid", + "profile", + "address", + "email"); + String state = UUID.randomUUID().toString(); + String nonce = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.getPrompts().add(Prompt.NONE); + authorizationRequest.addCustomParameter("mail", userEmail); + authorizationRequest.addCustomParameter("inum", userInum); + authorizationRequest.setAuthorizationMethod(AuthorizationMethod.FORM_ENCODED_BODY_PARAMETER); + + AuthorizationResponse authorizationResponse = authorizationRequestAndGrantAccess( + authorizationEndpoint, authorizationRequest); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_POST); + + TokenClient tokenClient1 = new TokenClient(tokenEndpoint); + tokenClient1.setRequest(tokenRequest); + TokenResponse response2 = tokenClient1.exec(); + + showClient(tokenClient1); + assertEquals(response2.getStatus(), 200, "Unexpected response code: " + response2.getStatus()); + assertNotNull(response2.getEntity(), "The entity is null"); + assertNotNull(response2.getAccessToken(), "The access token is null"); + assertNotNull(response2.getExpiresIn(), "The expires in value is null"); + assertNotNull(response2.getTokenType(), "The token type is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/UserInfoRestWebServiceHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/UserInfoRestWebServiceHttpTest.java new file mode 100644 index 00000000..1a7311cc --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/UserInfoRestWebServiceHttpTest.java @@ -0,0 +1,1868 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.fail; + +import java.security.PrivateKey; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoRequest; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.client.model.authorize.Claim; +import org.gluu.oxauth.client.model.authorize.ClaimValue; +import org.gluu.oxauth.client.model.authorize.JwtAuthorizationRequest; +import org.gluu.oxauth.client.model.authorize.UserInfoMember; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.AuthorizationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.common.SubjectType; +import org.gluu.oxauth.model.crypto.OxAuthCryptoProvider; +import org.gluu.oxauth.model.crypto.encryption.BlockEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.encryption.KeyEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.json.JSONObject; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * Functional tests for User Info Web Services (HTTP) + * + * @author Javier Rojas Blum + * @version May 14, 2019 + */ +public class UserInfoRestWebServiceHttpTest extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestUserInfoImplicitFlow(final String userId, final String userSecret, + final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) { + showTitle("requestUserInfoImplicitFlow"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN + ); + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // 1. Register client + RegisterResponse registerResponse = registerClient(redirectUris, responseTypes, grantTypes, sectorIdentifierUri); + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + AuthorizationResponse response1 = requestAuthorization(userId, userSecret, redirectUri, responseTypes, clientId); + + String accessToken = response1.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response2 = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(response2.getStatus(), 200, "Unexpected response code: " + response2.getStatus()); + assertNotNull(response2.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response2.getClaim(JwtClaimName.NAME)); + assertNotNull(response2.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response2.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response2.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response2.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response2.getClaim(JwtClaimName.LOCALE)); + assertNotNull(response2.getClaim(JwtClaimName.ADDRESS)); + assertNull(response2.getClaim("org_name")); + assertNull(response2.getClaim("work_phone")); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestUserInfoWithNotAllowedScopeImplicitFlow(final String userId, final String userSecret, + final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) { + showTitle("requestUserInfoWithNotAllowedScopeImplicitFlow"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN + ); + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // 1. Register client + RegisterResponse registerResponse = registerClient(redirectUris, responseTypes, grantTypes, sectorIdentifierUri); + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("openid", "profile", "address", "email", "mobile_phone"); + AuthorizationResponse response1 = requestAuthorization(userId, userSecret, redirectUri, responseTypes, clientId, scopes); + + String accessToken = response1.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response2 = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(response2.getStatus(), 200, "Unexpected response code: " + response2.getStatus()); + assertNotNull(response2.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response2.getClaim(JwtClaimName.NAME)); + assertNotNull(response2.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response2.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response2.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response2.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response2.getClaim(JwtClaimName.LOCALE)); + assertNotNull(response2.getClaim(JwtClaimName.ADDRESS)); + assertNull(response2.getClaim("phone_mobile_number")); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestUserInfoDynamicScopesImplicitFlow(final String userId, final String userSecret, + final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) { + showTitle("requestUserInfoDynamicScopesImplicitFlow"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN + ); + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + List scopes = Arrays.asList("openid", "profile", "address", "email", "org_name", "work_phone"); + + // 1. Register client + RegisterResponse registerResponse = registerClient(redirectUris, responseTypes, grantTypes, sectorIdentifierUri); + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + AuthorizationResponse response1 = requestAuthorization(userId, userSecret, redirectUri, responseTypes, clientId, scopes); + + String accessToken = response1.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response2 = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(response2.getStatus(), 200, "Unexpected response code: " + response2.getStatus()); + assertNotNull(response2.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response2.getClaim(JwtClaimName.NAME)); + assertNotNull(response2.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response2.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response2.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response2.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response2.getClaim(JwtClaimName.LOCALE)); + assertNotNull(response2.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(response2.getClaim("org_name")); + assertNotNull(response2.getClaim("work_phone")); + } + + @Parameters({"userId", "userSecret", "redirectUris", "sectorIdentifierUri"}) + @Test + public void requestUserInfoPasswordFlow(final String userId, final String userSecret, + final String redirectUris, final String sectorIdentifierUri) { + showTitle("requestUserInfoPasswordFlow"); + + List responseTypes = new ArrayList(); + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + RegisterResponse registerResponse = registerClient(redirectUris, responseTypes, grantTypes, sectorIdentifierUri); + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + String username = userId; + String password = userSecret; + String scope = "openid profile address email"; + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + TokenResponse response1 = tokenClient.execResourceOwnerPasswordCredentialsGrant(username, password, scope, + clientId, clientSecret); + + showClient(tokenClient); + assertEquals(response1.getStatus(), 200, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getEntity(), "The entity is null"); + assertNotNull(response1.getAccessToken(), "The access token is null"); + assertNotNull(response1.getTokenType(), "The token type is null"); + assertNotNull(response1.getScope(), "The scope is null"); + + String accessToken = response1.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response2 = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(response2.getStatus(), 200, "Unexpected response code: " + response2.getStatus()); + assertNotNull(response2.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response2.getClaim(JwtClaimName.NAME)); + assertNotNull(response2.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response2.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response2.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response2.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response2.getClaim(JwtClaimName.LOCALE)); + assertNull(response2.getClaim("org_name")); + assertNull(response2.getClaim("work_phone")); + } + + @Parameters({"userId", "userSecret", "redirectUris", "sectorIdentifierUri"}) + @Test + public void requestUserInfoWithNotAllowedScopePasswordFlow(final String userId, final String userSecret, + final String redirectUris, final String sectorIdentifierUri) { + showTitle("requestUserInfoWithNotAllowedScopePasswordFlow"); + + List responseTypes = new ArrayList(); + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + RegisterResponse registerResponse = registerClient(redirectUris, responseTypes, grantTypes, sectorIdentifierUri); + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + String username = userId; + String password = userSecret; + String scope = "openid profile address email mobile_phone"; + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + TokenResponse response1 = tokenClient.execResourceOwnerPasswordCredentialsGrant(username, password, scope, + clientId, clientSecret); + + showClient(tokenClient); + assertEquals(response1.getStatus(), 200, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getEntity(), "The entity is null"); + assertNotNull(response1.getAccessToken(), "The access token is null"); + assertNotNull(response1.getTokenType(), "The token type is null"); + assertNotNull(response1.getScope(), "The scope is null"); + + String accessToken = response1.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response2 = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(response2.getStatus(), 200, "Unexpected response code: " + response2.getStatus()); + assertNotNull(response2.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response2.getClaim(JwtClaimName.NAME)); + assertNotNull(response2.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response2.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response2.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response2.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response2.getClaim(JwtClaimName.LOCALE)); + assertNull(response2.getClaim("phone_mobile_number")); + } + + @Parameters({"userId", "userSecret", "redirectUris", "sectorIdentifierUri"}) + @Test + public void requestUserInfoDynamicScopesPasswordFlow(final String userId, final String userSecret, + final String redirectUris, final String sectorIdentifierUri) { + showTitle("requestUserInfoDynamicScopesPasswordFlow"); + + List responseTypes = new ArrayList(); + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + RegisterResponse registerResponse = registerClient(redirectUris, responseTypes, grantTypes, sectorIdentifierUri); + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + String username = userId; + String password = userSecret; + String scope = "openid profile address email org_name work_phone"; + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + TokenResponse response1 = tokenClient.execResourceOwnerPasswordCredentialsGrant(username, password, scope, + clientId, clientSecret); + + showClient(tokenClient); + assertEquals(response1.getStatus(), 200, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getEntity(), "The entity is null"); + assertNotNull(response1.getAccessToken(), "The access token is null"); + assertNotNull(response1.getTokenType(), "The token type is null"); + assertNotNull(response1.getScope(), "The scope is null"); + + String accessToken = response1.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response2 = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(response2.getStatus(), 200, "Unexpected response code: " + response2.getStatus()); + assertNotNull(response2.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response2.getClaim(JwtClaimName.NAME)); + assertNotNull(response2.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response2.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response2.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response2.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response2.getClaim(JwtClaimName.LOCALE)); + assertNotNull(response2.getClaim("org_name")); + assertNotNull(response2.getClaim("work_phone")); + } + + @Test + public void requestUserInfoInvalidRequest() { + showTitle("requestUserInfoInvalidRequest"); + + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response = userInfoClient.execUserInfo(null); + + showClient(userInfoClient); + assertEquals(response.getStatus(), 400, "Unexpected response code: " + response.getStatus()); + assertNotNull(response.getErrorType(), "Unexpected result: errorType not found"); + assertNotNull(response.getErrorDescription(), "Unexpected result: errorDescription not found"); + } + + @Test + public void requestUserInfoInvalidToken() { + showTitle("requestUserInfoInvalidToken"); + + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse response = userInfoClient.execUserInfo("INVALID_ACCESS_TOKEN"); + + showClient(userInfoClient); + assertEquals(response.getStatus(), 401, "Unexpected response code: " + response.getStatus()); + assertNotNull(response.getErrorType(), "Unexpected result: errorType not found"); + assertNotNull(response.getErrorDescription(), "Unexpected result: errorDescription not found"); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestUserInfoInsufficientScope(final String userId, final String userSecret, + final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) { + showTitle("requestUserInfoInsufficientScope"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN + ); + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + RegisterResponse registerResponse = registerClient(redirectUris, responseTypes, grantTypes, sectorIdentifierUri); + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("picture"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The access token is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getTokenType(), "The token type is null"); + assertNotNull(authorizationResponse.getExpiresIn(), "The expires in value is null"); + assertNull(authorizationResponse.getScope(), "The scope must be null"); // null because picture scope is not sufficient + assertNotNull(authorizationResponse.getIdToken(), "The id token must be null"); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 403, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getErrorType(), "Unexpected result: errorType not found"); + assertNotNull(userInfoResponse.getErrorDescription(), "Unexpected result: errorDescription not found"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void requestUserInfoExpiredAccessToken(final String redirectUris, final String redirectUri, + final String userId, final String userSecret, + final String sectorIdentifierUri) throws Exception { + showTitle("requestUserInfoRS256"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Dynamic Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.RS256); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setAccessTokenLifetime(3); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, clientId); + + String accessToken = authorizationResponse.getAccessToken(); + + { + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + } + + Thread.sleep(4000); + + { + // 3. Request user info with expired access token + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 401, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getErrorType(), "Unexpected result: errorType not found"); + assertNotNull(userInfoResponse.getErrorDescription(), "Unexpected result: errorDescription not found"); + } + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestUserInfoAdditionalClaims(final String userId, final String userSecret, + final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("requestUserInfoAdditionalClaims"); + + List responseTypes = Arrays.asList(ResponseType.TOKEN); + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + // 1. Client Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setGrantTypes(grantTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setClaims(Arrays.asList( + "iname", + "o")); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest( + responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest( + authorizationRequest, SignatureAlgorithm.HS256, clientSecret, cryptoProvider); + jwtAuthorizationRequest.addUserInfoClaim(new Claim("invalid", ClaimValue.createEssential(false))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim("iname", ClaimValue.createNull())); + //jwtAuthorizationRequest.addUserInfoClaim(new Claim("gluuStatus", ClaimValue.createEssential(true))); + //jwtAuthorizationRequest.addUserInfoClaim(new Claim("gluuWhitePagesListed", ClaimValue.createEssential(true))); + jwtAuthorizationRequest.addUserInfoClaim(new Claim("o", ClaimValue.createEssential(true))); + String authJwt = jwtAuthorizationRequest.getEncodedJwt(); + authorizationRequest.setRequest(authJwt); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The access token is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getTokenType(), "The token type is null"); + assertNotNull(authorizationResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(authorizationResponse.getScope(), "The scope must be null"); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info (AUTHORIZATION_REQUEST_HEADER_FIELD) + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + userInfoRequest.setAuthorizationMethod(AuthorizationMethod.AUTHORIZATION_REQUEST_HEADER_FIELD); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + + // Custom Claims + assertNotNull(userInfoResponse.getClaim("iname"), "Unexpected result: iname not found"); + //assertNotNull(response2.getClaim("gluuStatus"), "Unexpected result: gluuStatus not found"); + //assertNotNull(response2.getClaim("gluuWhitePagesListed"), "Unexpected result: gluuWhitePagesListed not found"); + assertNotNull(userInfoResponse.getClaim("o"), "Unexpected result: organization not found"); + + // 4. Request user info (FORM_ENCODED_BODY_PARAMETER) + UserInfoRequest userInfoRequest2 = new UserInfoRequest(accessToken); + userInfoRequest2.setAuthorizationMethod(AuthorizationMethod.FORM_ENCODED_BODY_PARAMETER); + UserInfoClient userInfoClient2 = new UserInfoClient(userInfoEndpoint); + userInfoClient2.setRequest(userInfoRequest2); + UserInfoResponse response3 = userInfoClient2.exec(); + + showClient(userInfoClient2); + assertEquals(response3.getStatus(), 200, "Unexpected response code: " + response3.getStatus()); + assertNotNull(response3.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response3.getClaim(JwtClaimName.NAME)); + assertNotNull(response3.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response3.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response3.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response3.getClaim(JwtClaimName.LOCALE)); + + // 5. Request user info (URL_QUERY_PARAMETER) + UserInfoRequest userInfoRequest3 = new UserInfoRequest(accessToken); + userInfoRequest3.setAuthorizationMethod(AuthorizationMethod.URL_QUERY_PARAMETER); + UserInfoClient userInfoClient3 = new UserInfoClient(userInfoEndpoint); + userInfoClient3.setRequest(userInfoRequest3); + UserInfoResponse response4 = userInfoClient3.exec(); + + showClient(userInfoClient3); + assertEquals(response4.getStatus(), 200, "Unexpected response code: " + response4.getStatus()); + assertNotNull(response4.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(response4.getClaim(JwtClaimName.NAME)); + assertNotNull(response4.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(response4.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(response4.getClaim(JwtClaimName.EMAIL)); + assertNotNull(response4.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(response4.getClaim(JwtClaimName.LOCALE)); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri", "clientJwksUri", + "postLogoutRedirectUri"}) + @Test + public void claimsRequestWithEssentialNameClaim( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri, final String clientJwksUri, final String postLogoutRedirectUri) throws Exception { + showTitle("claimsRequestWithEssentialNameClaim"); + + List responseTypes = Arrays.asList(ResponseType.CODE); + List grantTypes = Arrays.asList( + GrantType.AUTHORIZATION_CODE + ); + + // 1. Dynamic Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setContacts(Arrays.asList("javier@gluu.org", "javier.rojas.blum@gmail.com")); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setGrantTypes(grantTypes); + registerRequest.setJwksUri(clientJwksUri); + registerRequest.setPostLogoutRedirectUris(Arrays.asList(postLogoutRedirectUri)); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(); + + List scopes = Arrays.asList("openid"); + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + JSONObject claimsObj = new JSONObject(); + UserInfoMember userInfoMember = new UserInfoMember(); + userInfoMember.getClaims().add(new Claim("name", ClaimValue.createEssential(true))); + claimsObj.put("userinfo", userInfoMember.toJSONObject()); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest( + responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.setClaims(claimsObj); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation()); + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + assertNotNull(authorizationResponse.getScope()); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + + String accessToken = tokenResponse.getAccessToken(); + + // 4. Request user info + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setRequest(userInfoRequest); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void requestUserInfoHS256(final String redirectUris, final String redirectUri, + final String userId, final String userSecret, + final String sectorIdentifierUri) { + showTitle("requestUserInfoHS256"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Dynamic Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.HS256); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, clientId); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void requestUserInfoHS384(final String redirectUris, final String redirectUri, + final String userId, final String userSecret, + final String sectorIdentifierUri) { + showTitle("requestUserInfoHS384"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Dynamic Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.HS384); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, clientId); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void requestUserInfoHS512(final String redirectUris, final String redirectUri, + final String userId, final String userSecret, + final String sectorIdentifierUri) { + showTitle("requestUserInfoHS512"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Dynamic Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.HS512); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, clientId); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setSharedKey(clientSecret); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void requestUserInfoRS256(final String redirectUris, final String redirectUri, + final String userId, final String userSecret, + final String sectorIdentifierUri) { + showTitle("requestUserInfoRS256"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Dynamic Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.RS256); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, clientId); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void requestUserInfoRS384(final String redirectUris, final String redirectUri, + final String userId, final String userSecret, + final String sectorIdentifierUri) { + showTitle("requestUserInfoRS384"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Dynamic Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.RS384); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, clientId); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void requestUserInfoRS512(final String redirectUris, final String redirectUri, + final String userId, final String userSecret, + final String sectorIdentifierUri) { + showTitle("requestUserInfoRS512"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Dynamic Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.RS512); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, clientId); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void requestUserInfoES256(final String redirectUris, final String redirectUri, + final String userId, final String userSecret, + final String sectorIdentifierUri) { + showTitle("requestUserInfoES256"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Dynamic Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.ES256); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, clientId); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void requestUserInfoES384(final String redirectUris, final String redirectUri, + final String userId, final String userSecret, + final String sectorIdentifierUri) { + showTitle("requestUserInfoES384"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Dynamic Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.ES384); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, clientId); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void requestUserInfoES512(final String redirectUris, final String redirectUri, + final String userId, final String userSecret, + final String sectorIdentifierUri) { + showTitle("requestUserInfoES512"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Dynamic Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.ES512); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, clientId); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void requestUserInfoPS256(final String redirectUris, final String redirectUri, + final String userId, final String userSecret, + final String sectorIdentifierUri) { + showTitle("requestUserInfoPS256"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Dynamic Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.PS256); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, clientId); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void requestUserInfoPS384(final String redirectUris, final String redirectUri, + final String userId, final String userSecret, + final String sectorIdentifierUri) { + showTitle("requestUserInfoRS384"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Dynamic Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.PS384); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, clientId); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void requestUserInfoPS512(final String redirectUris, final String redirectUri, + final String userId, final String userSecret, + final String sectorIdentifierUri) { + showTitle("requestUserInfoPS512"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Dynamic Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.PS512); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, clientId); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", + "clientJwksUri", "sectorIdentifierUri", "RSA_OAEP_keyId", "keyStoreFile", + "keyStoreSecret"}) + @Test + public void requestUserInfoAlgRSAOAEPEncA256GCM( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String jwksUri, final String sectorIdentifierUri, final String keyId, final String keyStoreFile, + final String keyStoreSecret) { + try { + showTitle("requestUserInfoAlgRSAOAEPEncA256GCM"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Dynamic Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setJwksUri(jwksUri); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA_OAEP); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A256GCM); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, clientId); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info (encrypted) + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, null); + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setPrivateKey(privateKey); + userInfoClient.setRequest(userInfoRequest); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + } catch (Exception ex) { + fail(ex.getMessage(), ex); + } + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", + "clientJwksUri", "sectorIdentifierUri", "RSA1_5_keyId", "keyStoreFile", + "keyStoreSecret"}) + @Test + public void requestUserInfoAlgRSA15EncA128CBCPLUSHS256( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String jwksUri, final String sectorIdentifierUri, final String keyId, final String keyStoreFile, + final String keyStoreSecret) { + try { + showTitle("requestUserInfoAlgRSA15EncA128CBCPLUSHS256"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Dynamic Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setJwksUri(jwksUri); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A128CBC_PLUS_HS256); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, clientId); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info (encrypted) + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, null); + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setPrivateKey(privateKey); + userInfoClient.setRequest(userInfoRequest); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + } catch (Exception ex) { + fail(ex.getMessage(), ex); + } + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", + "clientJwksUri", "sectorIdentifierUri", "RSA1_5_keyId", "keyStoreFile", + "keyStoreSecret"}) + @Test + public void requestUserInfoAlgRSA15EncA256CBCPLUSHS512( + final String redirectUris, final String redirectUri, final String userId, final String userSecret, + final String jwksUri, final String sectorIdentifierUri, final String keyId, final String keyStoreFile, + final String keyStoreSecret) { + try { + showTitle("requestUserInfoAlgRSA15EncA256CBCPLUSHS512"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Dynamic Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setJwksUri(jwksUri); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.RSA1_5); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A256CBC_PLUS_HS512); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, clientId); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info (encrypted) + OxAuthCryptoProvider cryptoProvider = new OxAuthCryptoProvider(keyStoreFile, keyStoreSecret, null); + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setPrivateKey(privateKey); + userInfoClient.setRequest(userInfoRequest); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + } catch (Exception ex) { + fail(ex.getMessage(), ex); + } + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void requestUserInfoAlgA128KWEncA128GCM(final String redirectUris, final String redirectUri, + final String userId, final String userSecret, + final String sectorIdentifierUri) { + showTitle("requestUserInfoAlgA128KWEncA128GCM"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Dynamic Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.A128KW); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A128GCM); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, clientId); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info (encrypted) + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setSharedKey(clientSecret); + userInfoClient.setRequest(userInfoRequest); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void requestUserInfoAlgA256KWEncA256GCM(final String redirectUris, final String redirectUri, + final String userId, final String userSecret, + final String sectorIdentifierUri) { + showTitle("requestUserInfoAlgA256KWEncA256GCM"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Dynamic Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoEncryptedResponseAlg(KeyEncryptionAlgorithm.A256KW); + registerRequest.setUserInfoEncryptedResponseEnc(BlockEncryptionAlgorithm.A256GCM); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, clientId); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info (encrypted) + UserInfoRequest userInfoRequest = new UserInfoRequest(accessToken); + + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setSharedKey(clientSecret); + userInfoClient.setRequest(userInfoRequest); + UserInfoResponse userInfoResponse = userInfoClient.exec(); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + } + + private RegisterResponse registerClient(final String redirectUris, final List responseTypes, + final List grantTypes, final String sectorIdentifierUri) { + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setGrantTypes(grantTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + return registerResponse; + } + + private AuthorizationResponse requestAuthorization(final String userId, final String userSecret, final String redirectUri, + List responseTypes, String clientId) { + List scopes = Arrays.asList("openid", "profile", "address", "email"); + return requestAuthorization(userId, userSecret, redirectUri, responseTypes, clientId, scopes); + } + + private AuthorizationResponse requestAuthorization( + final String userId, final String userSecret, final String redirectUri, List responseTypes, + String clientId, List scopes) { + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest( + responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The access token is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getTokenType(), "The token type is null"); + assertNotNull(authorizationResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(authorizationResponse.getScope(), "The scope must be null"); + assertNotNull(authorizationResponse.getIdToken(), "The id token must be null"); + return authorizationResponse; + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void requestUserInfoWithoutOpenidScope(final String userId, final String userSecret, + final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) { + showTitle("requestUserInfoWithoutOpenidScope"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN + ); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + // 2. Request authorization + List scopes = Arrays.asList("profile", "address", "email"); + + String nonce = UUID.randomUUID().toString(); + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest( + responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getAccessToken(), "The access token is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertNotNull(authorizationResponse.getTokenType(), "The token type is null"); + assertNotNull(authorizationResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(authorizationResponse.getScope(), "The scope must be null"); + assertNotNull(authorizationResponse.getIdToken(), "The id token must be null"); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 403, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getErrorType(), "Unexpected result: errorType not found"); + assertNotNull(userInfoResponse.getErrorDescription(), "Unexpected result: errorDescription not found"); + } + + @Parameters({"redirectUris", "redirectUri", "userId", "userSecret", "sectorIdentifierUri"}) + @Test + public void requestUserInfoSubjectTypePublic(final String redirectUris, final String redirectUri, + final String userId, final String userSecret, + final String sectorIdentifierUri) { + showTitle("requestUserInfoSubjectTypePublic"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + // 1. Dynamic Registration + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth Test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setUserInfoSignedResponseAlg(SignatureAlgorithm.PS512); + registerRequest.setSubjectType(SubjectType.PUBLIC); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, clientId); + + String accessToken = authorizationResponse.getAccessToken(); + + // 3. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setJwksUri(jwksUri); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ISSUER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.AUDIENCE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ValidateIdTokenHashesTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ValidateIdTokenHashesTest.java new file mode 100644 index 00000000..ec9b5a5a --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/ValidateIdTokenHashesTest.java @@ -0,0 +1,213 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.Asserter; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.common.SubjectType; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Javier Rojas Blum + * @version March 14, 2019 + */ +public class ValidateIdTokenHashesTest extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri", "sectorIdentifierUri"}) + @Test + public void validateIdTokenHashes( + final String userId, final String userSecret, final String redirectUris, final String redirectUri, + final String sectorIdentifierUri) throws Exception { + showTitle("authorizationCodeFlow"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + List scopes = Arrays.asList("openid", "profile", "address", "email", "phone", "user_name"); + + // 1. Register client + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setScope(scopes); + registerRequest.setSubjectType(SubjectType.PAIRWISE); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Request authorization and receive the authorization code. + String nonce = UUID.randomUUID().toString(); + String stateParam = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(stateParam); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + assertEquals(authorizationResponse.getState(), stateParam); + + String scope = authorizationResponse.getScope(); + String authorizationCode = authorizationResponse.getCode(); + String accessToken = authorizationResponse.getAccessToken(); + String idToken = authorizationResponse.getIdToken(); + String state = authorizationResponse.getState(); + + // 3. Validate id_token + Jwt jwt = Jwt.parse(idToken); + Asserter.assertIdToken(jwt); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.CODE_HASH)); + assertTrue(rsaSigner.validateAuthorizationCode(authorizationCode, jwt)); + + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertTrue(rsaSigner.validateAccessToken(accessToken, jwt)); + + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.STATE_HASH)); + assertTrue(rsaSigner.validateState(state, jwt)); + + // 4. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient1 = new TokenClient(tokenEndpoint); + tokenClient1.setRequest(tokenRequest); + TokenResponse tokenResponse1 = tokenClient1.exec(); + + showClient(tokenClient1); + assertEquals(tokenResponse1.getStatus(), 200, "Unexpected response code: " + tokenResponse1.getStatus()); + assertNotNull(tokenResponse1.getEntity(), "The entity is null"); + assertNotNull(tokenResponse1.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse1.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse1.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse1.getRefreshToken(), "The refresh token is null"); + + String refreshToken = tokenResponse1.getRefreshToken(); + String idToken2 = tokenResponse1.getIdToken(); + String accessToken2 = tokenResponse1.getAccessToken(); + + // 5. Validate id_token + Jwt jwt2 = Jwt.parse(idToken2); + Asserter.assertIdToken(jwt2); + + RSAPublicKey publicKey2 = JwkClient.getRSAPublicKey( + jwksUri, + jwt2.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner2 = new RSASigner(SignatureAlgorithm.RS256, publicKey2); + + assertTrue(rsaSigner2.validate(jwt2)); + + assertNotNull(jwt2.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + assertTrue(rsaSigner2.validateAccessToken(accessToken2, jwt2)); + + assertNull(jwt2.getClaims().getClaimAsString(JwtClaimName.STATE_HASH)); + + // 6. Request new access token using the refresh token. + TokenClient tokenClient2 = new TokenClient(tokenEndpoint); + TokenResponse tokenResponse2 = tokenClient2.execRefreshToken(scope, refreshToken, clientId, clientSecret); + + showClient(tokenClient2); + assertEquals(tokenResponse2.getStatus(), 200, "Unexpected response code: " + tokenResponse2.getStatus()); + assertNotNull(tokenResponse2.getEntity(), "The entity is null"); + assertNotNull(tokenResponse2.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse2.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse2.getRefreshToken(), "The refresh token is null"); + assertNotNull(tokenResponse2.getScope(), "The scope is null"); + + String accessToken3 = tokenResponse2.getAccessToken(); + + // 7. Request user info + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken3); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.BIRTHDATE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GENDER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.MIDDLE_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NICKNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PREFERRED_USERNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PROFILE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.WEBSITE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PHONE_NUMBER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PHONE_NUMBER_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.USER_NAME)); + assertNull(userInfoResponse.getClaim("org_name")); + assertNull(userInfoResponse.getClaim("work_phone")); + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/WebKeysTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/WebKeysTest.java new file mode 100644 index 00000000..d1f19be9 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/WebKeysTest.java @@ -0,0 +1,81 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs; + +import static org.testng.Assert.assertEquals; + +import java.math.BigInteger; +import java.security.PublicKey; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPublicKey; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.model.util.Base64Util; +import org.testng.ITestContext; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import sun.security.x509.X509CertImpl; + +/** + * @author Javier Rojas Blum + * @version February 25, 2017 + */ +public class WebKeysTest extends BaseTest { + + @Test(dataProvider = "webKeysDataProvider") + public void webKeyTest(final String n, final String e, final String x5c) throws CertificateException { + showTitle("webKeyTest"); + + byte[] nBytes = Base64Util.base64urldecode(n); + BigInteger modulus = new BigInteger(1, nBytes); + + byte[] eBytes = Base64Util.base64urldecode(e); + BigInteger exponent = new BigInteger(1, eBytes); + + System.out.println("n: " + n); + System.out.println("n: " + modulus); + + System.out.println("e: " + e); + System.out.println("e: " + exponent); + + byte[] certBytes = Base64Util.base64urldecode(x5c); + X509Certificate cert = new X509CertImpl(certBytes); + + PublicKey publicKey = cert.getPublicKey(); + RSAPublicKey rsaPublicKey = (RSAPublicKey) publicKey; + assertEquals(rsaPublicKey.getModulus(), modulus); + assertEquals(rsaPublicKey.getPublicExponent(), exponent); + } + + @DataProvider(name = "webKeysDataProvider") + public Object[][] dataProvider(ITestContext context) { + return new Object[][]{ + { + "5awKF1MZSGSAAlujSf-dRzvrK9D_vV85BMn7fZ-x5E-So580TrTxT9-vgfmTWzhDr0f240DqR6ojF_NGXh8V3QhFRM9i2p7dg7M3LO-mfYlrJ_x2Rlw-EdvMmYargk5gaM7sRQKwWnU6ajRZIDw3XbrLDvGeLWZhH1-RzV3NjlJ_0c85bXhyLg_MT9NpnGTP4CePLF0dLuQwo4ktQkkW_BwPaSUhHgPYA-M6IA9S31_vQLB4ZyN00EpdO57fEbhutkzrpb9iiXJh82DD0D5Z2eYyQdMX_7pN9frLKVhoCUelzZ887it0oIlLfpe8WUzuiDHWYThzQiepQfMBRQMJhQ", + "AQAB", + "MIIDAzCCAeugAwIBAgIgLuZ/WGm/NwCIWOVcrvj2QuLV6yxyWQD7GEsmM1SWgP8wDQYJKoZIhvcNAQELBQAwITEfMB0GA1UEAwwWb3hBdXRoIENBIENlcnRpZmljYXRlczAeFw0xNzAyMDgxMzM1NTJaFw0xODAyMDgxMzM2MDBaMCExHzAdBgNVBAMMFm94QXV0aCBDQSBDZXJ0aWZpY2F0ZXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDlrAoXUxlIZIACW6NJ/51HO+sr0P+9XzkEyft9n7HkT5KjnzROtPFP36+B+ZNbOEOvR/bjQOpHqiMX80ZeHxXdCEVEz2Lant2Dszcs76Z9iWsn/HZGXD4R28yZhquCTmBozuxFArBadTpqNFkgPDddussO8Z4tZmEfX5HNXc2OUn/RzzlteHIuD8xP02mcZM/gJ48sXR0u5DCjiS1CSRb8HA9pJSEeA9gD4zogD1LfX+9AsHhnI3TQSl07nt8RuG62TOulv2KJcmHzYMPQPlnZ5jJB0xf/uk31+sspWGgJR6XNnzzuK3SgiUt+l7xZTO6IMdZhOHNCJ6lB8wFFAwmFAgMBAAGjJzAlMCMGA1UdJQQcMBoGCCsGAQUFBwMBBggrBgEFBQcDAgYEVR0lADANBgkqhkiG9w0BAQsFAAOCAQEAmuyIS597+LbwvKZgeshm6b8YspHYIFMRp9Pr06jp+P94oK7zgOe4x0U13+ReoTiMke0Zbq4aE93BxykyTJg+eL3qi9Nr6o6EPXC6NrSOwi7+OgkOxvy3ffOM0k9uH8kQgrSqyr4ra6GPyhAlEZShJZHtwEWSipohldi4uH1nKBR0QbFYlDrUxs1pErZT5hsDO3yaZ+XCJmsvwNqvcYTWsElbJrhMsiR3ymmjxDkQghT6TYc3LkerlFEjPE5YPT+57LTRr0Clj/NCHtYVJM32vEqZK+trQ44wpW9UfUgivsswgaH7qpUoUd3toAzNyjYq4aRT2f+ClKkJqr30nrt7iQ==" + }, + { + "vZXUxthj0LTSeeR_XMHaakLCIXd5Ua_4hNra-8UOP7ayhiY3c2KzpnmeWJ1SjPpbzS3O7Wc8AJSY_nrLzg9XjH5723cx_9TKbTVvE8_HQu5ZsUH7LgoT4yxMhvOFL6ir3RKEyOiOVFBBb4fWVGxwDchVkR26nBKK8RdAqCxnIEw1vkG6zHEPl1WBiK2IQ6JxhrrOLoTquHGBPc2qT_des8a6Xe6GlbUq1h-3bKAUXjjwSmJ36aau5aUNuvUlnPdEGcI25sTwRp7jCzuM1VN6a7y1nPkIYltYndOdP8EDGsrkS9pfQx1Z8HDdbw-lFGuOK5QFS53TfOfRtt0RQpy-Tw", + "AQAB", + "MIIDAzCCAeugAwIBAgIgT1z8Ptulz9vG6rXRfn8gTEAk7apWNSQTD8cUWLn5DTQwDQYJKoZIhvcNAQELBQAwITEfMB0GA1UEAwwWb3hBdXRoIENBIENlcnRpZmljYXRlczAeFw0xNzAyMjUwMzQyNTVaFw0xODAyMjUwMzQzMDRaMCExHzAdBgNVBAMMFm94QXV0aCBDQSBDZXJ0aWZpY2F0ZXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9ldTG2GPQtNJ55H9cwdpqQsIhd3lRr/iE2tr7xQ4/trKGJjdzYrOmeZ5YnVKM+lvNLc7tZzwAlJj+esvOD1eMfnvbdzH/1MptNW8Tz8dC7lmxQfsuChPjLEyG84UvqKvdEoTI6I5UUEFvh9ZUbHANyFWRHbqcEorxF0CoLGcgTDW+QbrMcQ+XVYGIrYhDonGGus4uhOq4cYE9zapP916zxrpd7oaVtSrWH7dsoBReOPBKYnfppq7lpQ269SWc90QZwjbmxPBGnuMLO4zVU3prvLWc+QhiW1id050/wQMayuRL2l9DHVnwcN1vD6UUa44rlAVLndN859G23RFCnL5PAgMBAAGjJzAlMCMGA1UdJQQcMBoGCCsGAQUFBwMBBggrBgEFBQcDAgYEVR0lADANBgkqhkiG9w0BAQsFAAOCAQEAhyDfbnYMvn2JkfD+RuTnSmvNSiwv0/80yOH4G5Mq8f8DSnCLmdWFtUMuXIUqrsj233etfPmkeZA9hE3+XAokCoHduXzn5evRS46fuhrUuBTjVjH3w+T8iMOquain9dnWCFfbiekcvT962wm2Pf5LcFb1h+M2LNTDun/2uv92BUvrKK/k2+n5I/j8TTG21s8/jFA7SUL6mxKN5w4d2uwMFlLKnkrNKnAKSszn4ptWtAeKWEZFEozog9hSeyg0rv8B7a9mki7pdbBLRMqEo5Fh2uUhkIUgDp8FoW4wnPs4zQC9+y2R9TUwLQS0l8W1X73rwLOI3vD7vkVO6mqvzb5sMA==" + }, + { + "1EV17EwdTr-qEuJpJisBbzxLA9dGQsxFEkXM-JM5XBV54S6Zeon-RymNlt5GiBpT-0fDXK3PNxt5R__cSbKOs6F_pRbGFSWxRxJgKHYp37eiW3PUee3rf6USIl6naj0HcHnycEiZxo2wwB1J12Iw3czqRbGhgcCusPO60EFdaTE3qH5owPLH_3FbdVcGd3BJFrKwT7CaWGfmtUsZtMDBii4tVUn8onaILYV4I0ZCwwvySB1jJbwd5gvMILz-GNMcvZ6-5k_ojQlaZrvmkzLjzi1y61PHGo_vLvpIlJ8EJpCtlZ8MQwadIT94bGXCHUta7B4XhxR5sLFmlShAzWdUcw", + "AQAB", + "MIIDBDCCAeygAwIBAgIhAO9HVmwNIiJv+DBg8oZ4EbnE+irSQ/NuKPf8pBCCew4iMA0GCSqGSIb3DQEBDAUAMCExHzAdBgNVBAMMFm94QXV0aCBDQSBDZXJ0aWZpY2F0ZXMwHhcNMTcwMjI1MDM0MjU1WhcNMTgwMjI1MDM0MzA0WjAhMR8wHQYDVQQDDBZveEF1dGggQ0EgQ2VydGlmaWNhdGVzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1EV17EwdTr+qEuJpJisBbzxLA9dGQsxFEkXM+JM5XBV54S6Zeon+RymNlt5GiBpT+0fDXK3PNxt5R//cSbKOs6F/pRbGFSWxRxJgKHYp37eiW3PUee3rf6USIl6naj0HcHnycEiZxo2wwB1J12Iw3czqRbGhgcCusPO60EFdaTE3qH5owPLH/3FbdVcGd3BJFrKwT7CaWGfmtUsZtMDBii4tVUn8onaILYV4I0ZCwwvySB1jJbwd5gvMILz+GNMcvZ6+5k/ojQlaZrvmkzLjzi1y61PHGo/vLvpIlJ8EJpCtlZ8MQwadIT94bGXCHUta7B4XhxR5sLFmlShAzWdUcwIDAQABoycwJTAjBgNVHSUEHDAaBggrBgEFBQcDAQYIKwYBBQUHAwIGBFUdJQAwDQYJKoZIhvcNAQEMBQADggEBAK4BgzFnyInDQPJQPK0Z27fbDbxXmcNYr9hvM7+nH4TWcW/2NmiIVPIlwUJEVQ0u63fhhDzGGGKuLIyc/K0XU+QJ4+OtS6mxjlZFohPpVyXrEUYAYCSEukDSj/WBr90WrFTYIly+7h2qexm3GCPxqKrMiLUjTBNL/igjQPdu6A1NbBlTKGlYlcV5J8MZzh6+5pkpLgLyiNUUiq1qvcHgTxhxTtIjQnr2Og4a0ZVorq20mbv3tuVyNPJEZfNOQOMvgAa+s/gSlt03vi0i598T4L/MOhMUCWgNVuRf2Y6sSz1W7vErfRMocoFuXjexCneV2aWkURpiUXQM6o9dEgP03S0=" + }, + { + "ucSlx1c0N7sGdpY1ly9tNH9OLzudC30HJiLMQPoxL9b_gXWx-MaErv7C7LxHuBKqT7Cq0SdvAued-bKxhlaNApfVQrbNbrF_tLo9oHgNqH4Jjil422EIBzjjSlaxkWBBhF3oIvmo1e3MAewWbq3sVTKUZKVKdAPVRe_8Ddt352FIBFTgU16lhXwnw3TmHcwxdwGJ719D9GOmtQyRZf_trSWEIORJJyHT8vueCWIgrCi2xEi_7XVWR7VLKl-gl1oxI2VYUrIRYDzBuT9vb0QAljN84Mbr-EDJs63BnPOJkK5pC_jTqKIViazo5CnITnOYAGXi7_JasllCfTSZUsSs2w", + "AQAB", + "MIIDAzCCAeugAwIBAgIgPhwrD54M4f56OVV6s/5k276jK4Qawc07aAn3vT1THJAwDQYJKoZIhvcNAQENBQAwITEfMB0GA1UEAwwWb3hBdXRoIENBIENlcnRpZmljYXRlczAeFw0xNzAyMjUwMzQyNTVaFw0xODAyMjUwMzQzMDRaMCExHzAdBgNVBAMMFm94QXV0aCBDQSBDZXJ0aWZpY2F0ZXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC5xKXHVzQ3uwZ2ljWXL200f04vO50LfQcmIsxA+jEv1v+BdbH4xoSu/sLsvEe4EqpPsKrRJ28C5535srGGVo0Cl9VCts1usX+0uj2geA2ofgmOKXjbYQgHOONKVrGRYEGEXegi+ajV7cwB7BZurexVMpRkpUp0A9VF7/wN23fnYUgEVOBTXqWFfCfDdOYdzDF3AYnvX0P0Y6a1DJFl/+2tJYQg5EknIdPy+54JYiCsKLbESL/tdVZHtUsqX6CXWjEjZVhSshFgPMG5P29vRACWM3zgxuv4QMmzrcGc84mQrmkL+NOoohWJrOjkKchOc5gAZeLv8lqyWUJ9NJlSxKzbAgMBAAGjJzAlMCMGA1UdJQQcMBoGCCsGAQUFBwMBBggrBgEFBQcDAgYEVR0lADANBgkqhkiG9w0BAQ0FAAOCAQEAKbHMN54sX8E9p9J86wfZpkcsdyaNgk8j1cje+nq/7PgLKbxHVhVVdJWdFQC3glMPnmMMNByudcZ+cCT05Gpw016Ts9TVy5jgglbyV5FNJuaYZnLe5mVb6KuUxJZLZmJEYXlTgMW9lYwTgbKOo4lP0GNykTnYaGk6+R7pHcB+2To2uNSsNz3cXX9Pb6lDk78VJKD1DN3/LGHD/Tpavi15j8C8vSEGsZkeeSl5tEEFTXHqZQqu+pLQdH8eEvmTtt1yXk2LdlEvoQmubkELZKW7guYIU+lAZrjdDr5oPaAkefYsH50fl6KaYCtsV9mGNUvoW+LTtCgcUlvXGdGhFJmV3Q==" + } + }; + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/deviceauthz/DeviceAuthzFlowHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/deviceauthz/DeviceAuthzFlowHttpTest.java new file mode 100644 index 00000000..801196c6 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/deviceauthz/DeviceAuthzFlowHttpTest.java @@ -0,0 +1,609 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs.deviceauthz; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.time.Duration; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.DeviceAuthzClient; +import org.gluu.oxauth.client.DeviceAuthzRequest; +import org.gluu.oxauth.client.DeviceAuthzResponse; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.client.UserInfoClient; +import org.gluu.oxauth.client.UserInfoResponse; +import org.gluu.oxauth.model.authorize.AuthorizeErrorResponseType; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.exception.InvalidJwtException; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.token.TokenErrorResponseType; +import org.gluu.oxauth.model.util.StringUtils; +import org.gluu.oxauth.page.DeviceAuthzPage; +import org.gluu.oxauth.page.LoginPage; +import org.gluu.oxauth.page.PageConfig; +import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.FluentWait; +import org.openqa.selenium.support.ui.Wait; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * Test cases for device authorization page. + */ +public class DeviceAuthzFlowHttpTest extends BaseTest { + + /** + * Device authorization complete flow. + */ + @Parameters({"userId", "userSecret"}) + @Test + public void deviceAuthzFlow(final String userId, final String userSecret) throws Exception { + showTitle("deviceAuthzFlow"); + + // 1. Init device authz request from WS + RegisterResponse registerResponse = DeviceAuthzRequestRegistrationTest.registerClientForDeviceAuthz( + AuthenticationMethod.CLIENT_SECRET_BASIC, Collections.singletonList(GrantType.DEVICE_CODE), + null, null, registrationEndpoint); + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Device request registration + final List scopes = Arrays.asList("openid", "profile", "address", "email", "phone", "user_name"); + DeviceAuthzRequest deviceAuthzRequest = new DeviceAuthzRequest(clientId, scopes); + deviceAuthzRequest.setAuthUsername(clientId); + deviceAuthzRequest.setAuthPassword(clientSecret); + + DeviceAuthzClient deviceAuthzClient = new DeviceAuthzClient(deviceAuthzEndpoint); + deviceAuthzClient.setRequest(deviceAuthzRequest); + + DeviceAuthzResponse response = deviceAuthzClient.exec(); + + showClient(deviceAuthzClient); + DeviceAuthzRequestRegistrationTest.validateSuccessfulResponse(response); + + // 3. Load device authz page, process user_code and authorization + WebDriver currentDriver = initWebDriver(false, true); + final PageConfig pageConfig = newPageConfig(currentDriver); + processDeviceAuthzPutUserCodeAndPressContinue(response.getUserCode(), currentDriver, false, pageConfig); + AuthorizationResponse authorizationResponse = processAuthorization(userId, userSecret, currentDriver); + + stopWebDriver(false, currentDriver); + assertSuccessAuthzResponse(authorizationResponse); + + // 4. Token request + TokenResponse tokenResponse1 = processTokens(clientId, clientSecret, response.getDeviceCode()); + validateTokenSuccessfulResponse(tokenResponse1); + + String refreshToken = tokenResponse1.getRefreshToken(); + String idToken = tokenResponse1.getIdToken(); + + // 5. Validate id_token + verifyIdToken(idToken); + + // 6. Request new access token using the refresh token. + TokenResponse tokenResponse2 = processNewTokenWithRefreshToken(StringUtils.implode(scopes, " "), + refreshToken, clientId, clientSecret); + validateTokenSuccessfulResponse(tokenResponse2); + + String accessToken = tokenResponse2.getAccessToken(); + + // 7. Request user info + processUserInfo(accessToken); + } + + /** + * Device authorization with access denied. + */ + @Parameters({"userId", "userSecret"}) + @Test + public void deviceAuthzFlowAccessDenied(final String userId, final String userSecret) throws Exception { + showTitle("deviceAuthzFlowAccessDenied"); + + // 1. Init device authz request from WS + RegisterResponse registerResponse = DeviceAuthzRequestRegistrationTest.registerClientForDeviceAuthz( + AuthenticationMethod.CLIENT_SECRET_BASIC, Collections.singletonList(GrantType.DEVICE_CODE), + null, null, registrationEndpoint); + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Device request registration + final List scopes = Arrays.asList("openid", "profile", "address", "email", "phone", "user_name"); + DeviceAuthzRequest deviceAuthzRequest = new DeviceAuthzRequest(clientId, scopes); + deviceAuthzRequest.setAuthUsername(clientId); + deviceAuthzRequest.setAuthPassword(clientSecret); + + DeviceAuthzClient deviceAuthzClient = new DeviceAuthzClient(deviceAuthzEndpoint); + deviceAuthzClient.setRequest(deviceAuthzRequest); + + DeviceAuthzResponse response = deviceAuthzClient.exec(); + + showClient(deviceAuthzClient); + DeviceAuthzRequestRegistrationTest.validateSuccessfulResponse(response); + + // 3. Load device authz page, process user_code and authorization + WebDriver currentDriver = initWebDriver(false, true); + final PageConfig pageConfig = newPageConfig(currentDriver); + AuthorizationResponse authorizationResponse = processDeviceAuthzDenyAccess(userId, userSecret, + response.getUserCode(), currentDriver, false, pageConfig); + + validateErrorResponse(authorizationResponse, AuthorizeErrorResponseType.ACCESS_DENIED); + + // 4. Token request + TokenResponse tokenResponse = processTokens(clientId, clientSecret, response.getDeviceCode()); + assertNotNull(tokenResponse.getErrorType(), "Error expected, however no error was found"); + assertNotNull(tokenResponse.getErrorDescription(), "Error description expected, however no error was found"); + assertEquals(tokenResponse.getErrorType(), TokenErrorResponseType.ACCESS_DENIED, "Unexpected error"); + } + + /** + * Validate server denies brute forcing + */ + @Test + public void preventBruteForcing() throws Exception { + showTitle("deviceAuthzFlow"); + + WebDriver currentDriver = initWebDriver(false, true); + final PageConfig pageConfig = newPageConfig(currentDriver); + List list = currentDriver.findElements(By.xpath("//*[contains(text(),'Too many failed attemps')]")); + byte limit = 10; + while (list.size() == 0 && limit > 0) { + processDeviceAuthzPutUserCodeAndPressContinue("ABCD-ABCD", currentDriver, false, pageConfig); + Thread.sleep(500); + list = currentDriver.findElements(By.xpath("//*[contains(text(),'Too many failed attemps')]")); + limit--; + } + stopWebDriver(false, currentDriver); + assertTrue(list.size() > 0 && limit > 0, "Brute forcing prevention not working correctly."); + } + + /** + * Verifies that token endpoint should return slow down or authorization pending states when token is in process. + */ + @Test + public void checkSlowDownOrPendingState() throws Exception { + showTitle("checkSlowDownOrPendingState"); + + // 1. Init device authz request from WS + RegisterResponse registerResponse = DeviceAuthzRequestRegistrationTest.registerClientForDeviceAuthz( + AuthenticationMethod.CLIENT_SECRET_BASIC, Collections.singletonList(GrantType.DEVICE_CODE), + null, null, registrationEndpoint); + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Device request registration + final List scopes = Arrays.asList("openid", "profile", "address", "email", "phone", "user_name"); + DeviceAuthzRequest deviceAuthzRequest = new DeviceAuthzRequest(clientId, scopes); + deviceAuthzRequest.setAuthUsername(clientId); + deviceAuthzRequest.setAuthPassword(clientSecret); + + DeviceAuthzClient deviceAuthzClient = new DeviceAuthzClient(deviceAuthzEndpoint); + deviceAuthzClient.setRequest(deviceAuthzRequest); + + DeviceAuthzResponse response = deviceAuthzClient.exec(); + + showClient(deviceAuthzClient); + DeviceAuthzRequestRegistrationTest.validateSuccessfulResponse(response); + + byte count = 3; + while (count > 0) { + TokenResponse tokenResponse = processTokens(clientId, clientSecret, response.getDeviceCode()); + assertNotNull(tokenResponse.getErrorType(), "Error expected, however no error was found"); + assertNotNull(tokenResponse.getErrorDescription(), "Error description expected, however no error was found"); + assertTrue(tokenResponse.getErrorType() == TokenErrorResponseType.AUTHORIZATION_PENDING + || tokenResponse.getErrorType() == TokenErrorResponseType.SLOW_DOWN, "Unexpected error"); + Thread.sleep(200); + count--; + } + } + + /** + * Attempts to get token with a wrong device_code, after that it attempts to get token twice, + * second one should be rejected. + */ + @Parameters({"userId", "userSecret"}) + @Test + public void attemptDifferentFailedValuesToTokenEndpoint(final String userId, final String userSecret) throws Exception { + showTitle("deviceAuthzFlow"); + + // 1. Init device authz request from WS + RegisterResponse registerResponse = DeviceAuthzRequestRegistrationTest.registerClientForDeviceAuthz( + AuthenticationMethod.CLIENT_SECRET_BASIC, Collections.singletonList(GrantType.DEVICE_CODE), + null, null, registrationEndpoint); + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Device request registration + final List scopes = Arrays.asList("openid", "profile", "address", "email", "phone", "user_name"); + DeviceAuthzRequest deviceAuthzRequest = new DeviceAuthzRequest(clientId, scopes); + deviceAuthzRequest.setAuthUsername(clientId); + deviceAuthzRequest.setAuthPassword(clientSecret); + + DeviceAuthzClient deviceAuthzClient = new DeviceAuthzClient(deviceAuthzEndpoint); + deviceAuthzClient.setRequest(deviceAuthzRequest); + + DeviceAuthzResponse response = deviceAuthzClient.exec(); + + showClient(deviceAuthzClient); + DeviceAuthzRequestRegistrationTest.validateSuccessfulResponse(response); + + // 3. Load device authz page, process user_code and authorization + WebDriver currentDriver = initWebDriver(false, true); + final PageConfig pageConfig = newPageConfig(currentDriver); + processDeviceAuthzPutUserCodeAndPressContinue(response.getUserCode(), currentDriver, false, pageConfig); + AuthorizationResponse authorizationResponse = processAuthorization(userId, userSecret, currentDriver); + + stopWebDriver(false, currentDriver); + assertSuccessAuthzResponse(authorizationResponse); + + // 4. Token request with a wrong device code + String wrongDeviceCode = "WRONG" + response.getDeviceCode(); + TokenResponse tokenResponse1 = processTokens(clientId, clientSecret, wrongDeviceCode); + assertNotNull(tokenResponse1.getErrorType(), "Error expected, however no error was found"); + assertNotNull(tokenResponse1.getErrorDescription(), "Error description expected, however no error was found"); + assertEquals(tokenResponse1.getErrorType(), TokenErrorResponseType.EXPIRED_TOKEN, "Unexpected error"); + + // 5. Token request with a right device code value + tokenResponse1 = processTokens(clientId, clientSecret, response.getDeviceCode()); + validateTokenSuccessfulResponse(tokenResponse1); + + // 6. Try to get token again, however this should be rejected by the server + tokenResponse1 = processTokens(clientId, clientSecret, response.getDeviceCode()); + assertNotNull(tokenResponse1.getErrorType(), "Error expected, however no error was found"); + assertNotNull(tokenResponse1.getErrorDescription(), "Error description expected, however no error was found"); + assertEquals(tokenResponse1.getErrorType(), TokenErrorResponseType.EXPIRED_TOKEN, "Unexpected error"); + } + + /** + * Process a complete device authorization flow using verification_uri_complete + */ + @Parameters({"userId", "userSecret"}) + @Test + public void deviceAuthzFlowWithCompleteVerificationUri(final String userId, final String userSecret) throws Exception { + showTitle("deviceAuthzFlow"); + + // 1. Init device authz request from WS + RegisterResponse registerResponse = DeviceAuthzRequestRegistrationTest.registerClientForDeviceAuthz( + AuthenticationMethod.CLIENT_SECRET_BASIC, Collections.singletonList(GrantType.DEVICE_CODE), + null, null, registrationEndpoint); + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Device request registration + final List scopes = Arrays.asList("openid", "profile", "address", "email", "phone", "user_name"); + DeviceAuthzRequest deviceAuthzRequest = new DeviceAuthzRequest(clientId, scopes); + deviceAuthzRequest.setAuthUsername(clientId); + deviceAuthzRequest.setAuthPassword(clientSecret); + + DeviceAuthzClient deviceAuthzClient = new DeviceAuthzClient(deviceAuthzEndpoint); + deviceAuthzClient.setRequest(deviceAuthzRequest); + + DeviceAuthzResponse response = deviceAuthzClient.exec(); + + showClient(deviceAuthzClient); + DeviceAuthzRequestRegistrationTest.validateSuccessfulResponse(response); + + // 3. Load device authz page, process user_code and authorization + WebDriver currentDriver = initWebDriver(false, true); + final PageConfig pageConfig = newPageConfig(currentDriver); + processDeviceAuthzPutUserCodeAndPressContinue(response.getUserCode(), currentDriver, true, pageConfig); + AuthorizationResponse authorizationResponse = processAuthorization(userId, userSecret, currentDriver); + + stopWebDriver(false, currentDriver); + assertSuccessAuthzResponse(authorizationResponse); + + // 4. Token request + TokenResponse tokenResponse1 = processTokens(clientId, clientSecret, response.getDeviceCode()); + validateTokenSuccessfulResponse(tokenResponse1); + + String refreshToken = tokenResponse1.getRefreshToken(); + String idToken = tokenResponse1.getIdToken(); + + // 5. Validate id_token + verifyIdToken(idToken); + + // 6. Request new access token using the refresh token. + TokenResponse tokenResponse2 = processNewTokenWithRefreshToken(StringUtils.implode(scopes, " "), + refreshToken, clientId, clientSecret); + validateTokenSuccessfulResponse(tokenResponse2); + + String accessToken = tokenResponse2.getAccessToken(); + + // 7. Request user info + processUserInfo(accessToken); + } + + /** + * Device authorization with access denied and using complete verification uri. + */ + @Parameters({"userId", "userSecret"}) + @Test + public void deviceAuthzFlowAccessDeniedWithCompleteVerificationUri(final String userId, final String userSecret) throws Exception { + showTitle("deviceAuthzFlowAccessDenied"); + + // 1. Init device authz request from WS + RegisterResponse registerResponse = DeviceAuthzRequestRegistrationTest.registerClientForDeviceAuthz( + AuthenticationMethod.CLIENT_SECRET_BASIC, Collections.singletonList(GrantType.DEVICE_CODE), + null, null, registrationEndpoint); + String clientId = registerResponse.getClientId(); + String clientSecret = registerResponse.getClientSecret(); + + // 2. Device request registration + final List scopes = Arrays.asList("openid", "profile", "address", "email", "phone", "user_name"); + DeviceAuthzRequest deviceAuthzRequest = new DeviceAuthzRequest(clientId, scopes); + deviceAuthzRequest.setAuthUsername(clientId); + deviceAuthzRequest.setAuthPassword(clientSecret); + + DeviceAuthzClient deviceAuthzClient = new DeviceAuthzClient(deviceAuthzEndpoint); + deviceAuthzClient.setRequest(deviceAuthzRequest); + + DeviceAuthzResponse response = deviceAuthzClient.exec(); + + showClient(deviceAuthzClient); + DeviceAuthzRequestRegistrationTest.validateSuccessfulResponse(response); + + // 3. Load device authz page, process user_code and authorization + WebDriver currentDriver = initWebDriver(false, true); + final PageConfig pageConfig = newPageConfig(currentDriver); + AuthorizationResponse authorizationResponse = processDeviceAuthzDenyAccess(userId, userSecret, + response.getUserCode(), currentDriver, true, pageConfig); + + validateErrorResponse(authorizationResponse, AuthorizeErrorResponseType.ACCESS_DENIED); + + // 4. Token request + TokenResponse tokenResponse = processTokens(clientId, clientSecret, response.getDeviceCode()); + assertNotNull(tokenResponse.getErrorType(), "Error expected, however no error was found"); + assertNotNull(tokenResponse.getErrorDescription(), "Error description expected, however no error was found"); + assertEquals(tokenResponse.getErrorType(), TokenErrorResponseType.ACCESS_DENIED, "Unexpected error"); + } + + private void processUserInfo(String accessToken) throws UnrecoverableKeyException, NoSuchAlgorithmException, + KeyStoreException, KeyManagementException { + UserInfoClient userInfoClient = new UserInfoClient(userInfoEndpoint); + userInfoClient.setExecutor(clientEngine(true)); + UserInfoResponse userInfoResponse = userInfoClient.execUserInfo(accessToken); + + showClient(userInfoClient); + assertEquals(userInfoResponse.getStatus(), 200, "Unexpected response code: " + userInfoResponse.getStatus()); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.BIRTHDATE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.FAMILY_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GENDER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.GIVEN_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.MIDDLE_NAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.NICKNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PICTURE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PREFERRED_USERNAME)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PROFILE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.WEBSITE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.EMAIL_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PHONE_NUMBER)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.PHONE_NUMBER_VERIFIED)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ADDRESS)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.LOCALE)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.ZONEINFO)); + assertNotNull(userInfoResponse.getClaim(JwtClaimName.USER_NAME)); + assertNull(userInfoResponse.getClaim("org_name")); + assertNull(userInfoResponse.getClaim("work_phone")); + } + + private TokenResponse processNewTokenWithRefreshToken(String scopes, String refreshToken, String clientId, + String clientSecret) throws UnrecoverableKeyException, + NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + TokenClient tokenClient2 = new TokenClient(tokenEndpoint); + tokenClient2.setExecutor(clientEngine(true)); + TokenResponse tokenResponse2 = tokenClient2.execRefreshToken(scopes, refreshToken, clientId, clientSecret); + + showClient(tokenClient2); + assertEquals(tokenResponse2.getStatus(), 200, "Unexpected response code: " + tokenResponse2.getStatus()); + assertNotNull(tokenResponse2.getEntity(), "The entity is null"); + assertNotNull(tokenResponse2.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse2.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse2.getRefreshToken(), "The refresh token is null"); + assertNotNull(tokenResponse2.getScope(), "The scope is null"); + + return tokenResponse2; + } + + private void verifyIdToken(String idToken) throws InvalidJwtException, UnrecoverableKeyException, + NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.OX_OPENID_CONNECT_VERSION)); + + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID), clientEngine(true)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + + assertTrue(rsaSigner.validate(jwt)); + } + + private TokenResponse processTokens(String clientId, String clientSecret, String deviceCode) { + TokenRequest tokenRequest = new TokenRequest(GrantType.DEVICE_CODE); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret);; + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + tokenRequest.setDeviceCode(deviceCode); + + TokenClient tokenClient1 = newTokenClient(tokenRequest); + TokenResponse tokenResponse1 = tokenClient1.exec(); + + showClient(tokenClient1); + + return tokenResponse1; + } + + private void validateTokenSuccessfulResponse(TokenResponse tokenResponse) { + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + } + + private void assertSuccessAuthzResponse(final AuthorizationResponse authorizationResponse) { + assertNotNull(authorizationResponse.getCode()); + assertNotNull(authorizationResponse.getState()); + assertNull(authorizationResponse.getErrorType()); + } + + private void processDeviceAuthzPutUserCodeAndPressContinue(String userCode, WebDriver currentDriver, + boolean complete, final PageConfig pageConfig) { + String deviceAuthzPageUrl = deviceAuthzEndpoint.replace("/restv1/device_authorization", "/device_authorization.htm") + + (complete ? "?user_code=" + userCode : ""); + output("Device authz flow: page to navigate to put user_code:" + deviceAuthzPageUrl); + + navigateToAuhorizationUrl(currentDriver, deviceAuthzPageUrl); + + DeviceAuthzPage deviceAuthzPage = new DeviceAuthzPage(pageConfig); + if (!complete) { + deviceAuthzPage.fillUserCode(userCode); + } + + deviceAuthzPage.clickContinueButton(); + } + + private AuthorizationResponse processAuthorization(String userId, String userSecret, WebDriver currentDriver) { + Wait wait = new FluentWait(driver) + .withTimeout(Duration.ofSeconds(PageConfig.WAIT_OPERATION_TIMEOUT)) + .pollingEvery(Duration.ofMillis(500)) + .ignoring(NoSuchElementException.class); + + if (userSecret != null) { + final String previousUrl = currentDriver.getCurrentUrl(); + WebElement loginButton = wait.until(d -> currentDriver.findElement(By.id(loginFormLoginButton))); + + if (userId != null) { + WebElement usernameElement = currentDriver.findElement(By.id(loginFormUsername)); + usernameElement.sendKeys(userId); + } + + WebElement passwordElement = currentDriver.findElement(By.id(loginFormPassword)); + passwordElement.sendKeys(userSecret); + + loginButton.click(); + + if (ENABLE_REDIRECT_TO_LOGIN_PAGE) { + waitForPageSwitch(currentDriver, previousUrl); + } + } + + acceptAuthorization(currentDriver, null); + + String deviceAuthzResponseStr = currentDriver.getCurrentUrl(); + + output("Device authz redirection response url: " + deviceAuthzResponseStr); + return new AuthorizationResponse(deviceAuthzResponseStr); + } + + private AuthorizationResponse processDeviceAuthzDenyAccess(String userId, String userSecret, String userCode, + WebDriver currentDriver, boolean complete, + final PageConfig pageConfig) { + String deviceAuthzPageUrl = deviceAuthzEndpoint.replace("/restv1/device_authorization", "/device_authorization.htm") + + (complete ? "?user_code=" + userCode : ""); + output("Device authz flow: page to navigate to put user_code:" + deviceAuthzPageUrl); + + navigateToAuhorizationUrl(currentDriver, deviceAuthzPageUrl); + + DeviceAuthzPage deviceAuthzPage = new DeviceAuthzPage(pageConfig); + + if (!complete) { + deviceAuthzPage.fillUserCode(userCode); + } + deviceAuthzPage.clickContinueButton(); + + Wait wait = new FluentWait(driver) + .withTimeout(Duration.ofSeconds(PageConfig.WAIT_OPERATION_TIMEOUT)) + .pollingEvery(Duration.ofMillis(500)) + .ignoring(NoSuchElementException.class); + + if (userSecret != null) { + final String previousUrl = currentDriver.getCurrentUrl(); + + WebElement loginButton = wait.until(d -> currentDriver.findElement(By.id(loginFormLoginButton))); + + LoginPage loginPage = new LoginPage(pageConfig); + loginPage.enterUsername(userId); + loginPage.enterPassword(userSecret); + + loginButton.click(); + + if (ENABLE_REDIRECT_TO_LOGIN_PAGE) { + waitForPageSwitch(currentDriver, previousUrl); + } + } + + denyAuthorization(currentDriver); + + String deviceAuthzResponseStr = currentDriver.getCurrentUrl(); + stopWebDriver(false, currentDriver); + + output("Device authz redirection response url: " + deviceAuthzResponseStr); + return new AuthorizationResponse(deviceAuthzResponseStr); + } + + protected void denyAuthorization(WebDriver currentDriver) { + String authorizationResponseStr = currentDriver.getCurrentUrl(); + + // Check for authorization form if client has no persistent authorization + if (!authorizationResponseStr.contains("#")) { + Wait wait = new FluentWait(driver) + .withTimeout(Duration.ofSeconds(PageConfig.WAIT_OPERATION_TIMEOUT)) + .pollingEvery(Duration.ofMillis(500)) + .ignoring(NoSuchElementException.class); + + WebElement doNotAllowButton = wait.until(d -> currentDriver.findElement(By.id(authorizeFormDoNotAllowButton))); + final String previousUrl2 = driver.getCurrentUrl(); + doNotAllowButton.click(); + waitForPageSwitch(driver, previousUrl2); + } else { + fail("The authorization form was expected to be shown."); + } + } + + protected void validateErrorResponse(AuthorizationResponse response, AuthorizeErrorResponseType errorType) { + assertNotNull(response.getErrorType(), "Error expected, however no error was found"); + assertNotNull(response.getErrorDescription(), "Error description expected, however no error was found"); + assertEquals(response.getErrorType(), errorType, "Unexpected error"); + } + +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/deviceauthz/DeviceAuthzRequestRegistrationTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/deviceauthz/DeviceAuthzRequestRegistrationTest.java new file mode 100644 index 00000000..03eb386a --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/deviceauthz/DeviceAuthzRequestRegistrationTest.java @@ -0,0 +1,273 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs.deviceauthz; + +import static org.gluu.oxauth.model.util.StringUtils.EASY_TO_READ_CHARACTERS; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.DeviceAuthzClient; +import org.gluu.oxauth.client.DeviceAuthzRequest; +import org.gluu.oxauth.client.DeviceAuthzResponse; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.model.authorize.DeviceAuthzErrorResponseType; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.util.StringUtils; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * Tests for WS used to register device authz requests. + */ +public class DeviceAuthzRequestRegistrationTest extends BaseTest { + + /** + * Verifies normal flow with different scopes, AS should generate user_code, device_code and other data. + * It uses normal client_secret_basic authentication method. + */ + @Test + public void deviceAuthzHappyFlow() { + showTitle("deviceAuthzHappyFlow"); + + // Register client + RegisterResponse registerResponse = registerClientForDeviceAuthz(AuthenticationMethod.CLIENT_SECRET_BASIC, + Collections.singletonList(GrantType.DEVICE_CODE), null, null, registrationEndpoint); + String clientId = registerResponse.getClientId(); + + // 1. OpenId, profile, address and email scopes + List scopes = Arrays.asList("openid", "profile", "address", "email"); + DeviceAuthzRequest authorizationRequest = new DeviceAuthzRequest(clientId, scopes); + authorizationRequest.setAuthUsername(clientId); + authorizationRequest.setAuthPassword(registerResponse.getClientSecret()); + + DeviceAuthzClient deviceAuthzClient = new DeviceAuthzClient(deviceAuthzEndpoint); + deviceAuthzClient.setRequest(authorizationRequest); + + DeviceAuthzResponse response = deviceAuthzClient.exec(); + + showClient(deviceAuthzClient); + validateSuccessfulResponse(response); + + // 2. Only openid scope + scopes = Collections.singletonList("openid"); + authorizationRequest = new DeviceAuthzRequest(clientId, scopes); + authorizationRequest.setAuthUsername(clientId); + authorizationRequest.setAuthPassword(registerResponse.getClientSecret()); + + deviceAuthzClient = new DeviceAuthzClient(deviceAuthzEndpoint); + deviceAuthzClient.setRequest(authorizationRequest); + + response = deviceAuthzClient.exec(); + + showClient(deviceAuthzClient); + validateSuccessfulResponse(response); + } + + /** + * Verifies normal flow with different scopes, AS should generate user_code, device_code and other data. + * It uses normal none authentication method, therefore no client authentication is required. + */ + @Test + public void deviceAuthzHappyFlowPublicClient() { + showTitle("deviceAuthzHappyFlowPublicClient"); + + // Register client + RegisterResponse registerResponse = registerClientForDeviceAuthz(AuthenticationMethod.NONE, + Collections.singletonList(GrantType.DEVICE_CODE), null, null, registrationEndpoint); + String clientId = registerResponse.getClientId(); + + // 1. OpenId, profile, address and email scopes + List scopes = Arrays.asList("openid", "profile", "address", "email"); + DeviceAuthzRequest authorizationRequest = new DeviceAuthzRequest(clientId, scopes); + authorizationRequest.setAuthenticationMethod(AuthenticationMethod.NONE); + + DeviceAuthzClient deviceAuthzClient = new DeviceAuthzClient(deviceAuthzEndpoint); + deviceAuthzClient.setRequest(authorizationRequest); + + DeviceAuthzResponse response = deviceAuthzClient.exec(); + + showClient(deviceAuthzClient); + validateSuccessfulResponse(response); + + // 2. Only openid scope + scopes = Collections.singletonList("openid"); + authorizationRequest = new DeviceAuthzRequest(clientId, scopes); + authorizationRequest.setAuthUsername(clientId); + authorizationRequest.setAuthPassword(registerResponse.getClientSecret()); + + deviceAuthzClient = new DeviceAuthzClient(deviceAuthzEndpoint); + deviceAuthzClient.setRequest(authorizationRequest); + + response = deviceAuthzClient.exec(); + + showClient(deviceAuthzClient); + validateSuccessfulResponse(response); + } + + /** + * Tests that the device authz request is rejected, since client doesnt support that grant type. + */ + @Parameters({"redirectUris", "sectorIdentifierUri"}) + @Test + public void deviceAuthzGrantTypeDoesntSupported(final String redirectUris, final String sectorIdentifierUri) { + showTitle("deviceAuthzGrantTypeDoesntSupported"); + + // Register client + RegisterResponse registerResponse = registerClientForDeviceAuthz(AuthenticationMethod.CLIENT_SECRET_BASIC, + Collections.singletonList(GrantType.AUTHORIZATION_CODE), redirectUris, sectorIdentifierUri, registrationEndpoint); + String clientId = registerResponse.getClientId(); + + // Device authz request registration + List scopes = Arrays.asList("openid", "profile", "address", "email"); + DeviceAuthzRequest authorizationRequest = new DeviceAuthzRequest(clientId, scopes); + authorizationRequest.setAuthUsername(clientId); + authorizationRequest.setAuthPassword(registerResponse.getClientSecret()); + + DeviceAuthzClient deviceAuthzClient = new DeviceAuthzClient(deviceAuthzEndpoint); + deviceAuthzClient.setRequest(authorizationRequest); + + DeviceAuthzResponse response = deviceAuthzClient.exec(); + + showClient(deviceAuthzClient); + validateErrorResponse(response, 400, DeviceAuthzErrorResponseType.INVALID_GRANT); + } + + /** + * AS should authenticate client requests, however these tests are trying to pass device authz requests with + * wrong client authn data. + */ + @Test + public void deviceAuthzNoPublicClientHoweverIncorrectAuthSent() { + showTitle("deviceAuthzNoPublicClientHoweverIncorrectAuthSent"); + + // Register client + RegisterResponse registerResponse = registerClientForDeviceAuthz(AuthenticationMethod.CLIENT_SECRET_BASIC, + Collections.singletonList(GrantType.DEVICE_CODE), null, null, registrationEndpoint); + String clientId = registerResponse.getClientId(); + + // 1. No authentication data sent + List scopes = Arrays.asList("openid", "profile", "address", "email"); + DeviceAuthzRequest authorizationRequest = new DeviceAuthzRequest(clientId, scopes); + authorizationRequest.setAuthenticationMethod(AuthenticationMethod.NONE); + + DeviceAuthzClient deviceAuthzClient = new DeviceAuthzClient(deviceAuthzEndpoint); + deviceAuthzClient.setRequest(authorizationRequest); + + DeviceAuthzResponse response = deviceAuthzClient.exec(); + + showClient(deviceAuthzClient); + validateErrorResponse(response, 401, DeviceAuthzErrorResponseType.INVALID_CLIENT); + + // 2. Invalid authentication + scopes = Arrays.asList("openid", "profile", "address", "email"); + authorizationRequest = new DeviceAuthzRequest(clientId, scopes); + authorizationRequest.setAuthUsername(clientId); + authorizationRequest.setAuthPassword("invalid-client-id-" + System.currentTimeMillis()); + + deviceAuthzClient = new DeviceAuthzClient(deviceAuthzEndpoint); + deviceAuthzClient.setRequest(authorizationRequest); + + response = deviceAuthzClient.exec(); + + showClient(deviceAuthzClient); + validateErrorResponse(response, 401, DeviceAuthzErrorResponseType.INVALID_CLIENT); + } + + /** + * Client that doesnt require authn accept device authz requests even client sends authn data. + */ + @Test + public void deviceAuthzPublicClientAndAuthSent() { + showTitle("deviceAuthzPublicClientAndAuthSent"); + + // Register client + RegisterResponse registerResponse = registerClientForDeviceAuthz(AuthenticationMethod.NONE, + Collections.singletonList(GrantType.DEVICE_CODE), null, null, registrationEndpoint); + String clientId = registerResponse.getClientId(); + + // Device authz request + List scopes = Arrays.asList("openid", "profile", "address", "email"); + DeviceAuthzRequest authorizationRequest = new DeviceAuthzRequest(clientId, scopes); + authorizationRequest.setAuthUsername(clientId); + authorizationRequest.setAuthPassword(registerResponse.getClientSecret()); + + DeviceAuthzClient deviceAuthzClient = new DeviceAuthzClient(deviceAuthzEndpoint); + deviceAuthzClient.setRequest(authorizationRequest); + + DeviceAuthzResponse response = deviceAuthzClient.exec(); + + showClient(deviceAuthzClient); + validateSuccessfulResponse(response); + } + + protected static RegisterResponse registerClientForDeviceAuthz(AuthenticationMethod authenticationMethod, + List grantTypes, String redirectUris, + String sectorIdentifierUri, String registrationEndpoint) { + List responseTypes = Collections.singletonList(ResponseType.CODE); + List scopes = Arrays.asList("openid", "profile", "address", "email", "phone", "user_name"); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", + StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setGrantTypes(grantTypes); + registerRequest.setTokenEndpointAuthMethod(authenticationMethod); + registerRequest.setSectorIdentifierUri(sectorIdentifierUri); + registerRequest.setScope(scopes); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(registerResponse.getStatus(), 200, "Unexpected response code: " + registerResponse.getEntity()); + assertNotNull(registerResponse.getClientId()); + assertNotNull(registerResponse.getClientSecret()); + assertNotNull(registerResponse.getRegistrationAccessToken()); + assertNotNull(registerResponse.getClientIdIssuedAt()); + assertNotNull(registerResponse.getClientSecretExpiresAt()); + + return registerResponse; + } + + protected static void validateSuccessfulResponse(DeviceAuthzResponse response) { + final String regex = "[" + EASY_TO_READ_CHARACTERS + "]{4}-[" + EASY_TO_READ_CHARACTERS + "]{4}"; + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getUserCode(), "User code is null"); + assertNotNull(response.getDeviceCode(), "Device code is null"); + assertNotNull(response.getInterval(), "Interval is null"); + assertTrue(response.getInterval() > 0, "Interval is null"); + assertNotNull(response.getVerificationUri(), "Verification Uri is null"); + assertNotNull(response.getVerificationUriComplete(), "Verification Uri complete is null"); + assertTrue(response.getVerificationUri().length() > 10, "Invalid verification_uri"); + assertTrue(response.getVerificationUriComplete().length() > 10, "Invalid verification_uri_complete"); + assertNotNull(response.getExpiresIn(), "expires_in is null"); + assertTrue(response.getExpiresIn() > 0, "expires_in contains an invalid value"); + assertTrue(response.getUserCode().matches(regex)); + } + + protected static void validateErrorResponse(DeviceAuthzResponse response, int status, DeviceAuthzErrorResponseType errorType) { + assertEquals(response.getStatus(), status, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getErrorType(), "Error expected, however no error was found"); + assertNotNull(response.getErrorDescription(), "Error description expected, however no error was found"); + assertEquals(response.getErrorType(), errorType, "Unexpected error"); + assertNull(response.getUserCode(), "User code must not be null"); + assertNull(response.getDeviceCode(), "Device code must not be null"); + } + +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/internal/StatWSTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/internal/StatWSTest.java new file mode 100644 index 00000000..5a48f95e --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/internal/StatWSTest.java @@ -0,0 +1,38 @@ +package org.gluu.oxauth.ws.rs.internal; + +import static org.testng.Assert.assertTrue; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.service.ClientFactory; +import org.gluu.oxauth.client.service.StatService; +import org.gluu.oxauth.client.uma.wrapper.UmaClient; +import org.gluu.oxauth.model.uma.wrapper.Token; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * @author Yuriy Zabrovarnyy + */ +public class StatWSTest extends BaseTest { + + @Test(enabled = false) + @Parameters({"umaPatClientId", "umaPatClientSecret"}) + public void stat(final String umaPatClientId, final String umaPatClientSecret) throws Exception { + final Token authorization = UmaClient.requestPat(tokenEndpoint, umaPatClientId, umaPatClientSecret); + + final StatService service = ClientFactory.instance().createStatService(issuer + "/oxauth/restv1/internal/stat"); + final JsonNode node = service.stat("Bearer " + authorization.getAccessToken(), "202101", null); + assertTrue(node != null && node.hasNonNull("response")); + } + + @Test(enabled = false) + @Parameters({"umaPatClientId", "umaPatClientSecret"}) + public void statPost(final String umaPatClientId, final String umaPatClientSecret) throws Exception { + final Token authorization = UmaClient.requestPat(tokenEndpoint, umaPatClientId, umaPatClientSecret); + final StatService service = ClientFactory.instance().createStatService(issuer + "/oxauth/restv1/internal/stat"); + final JsonNode node = service.statPost(authorization.getAccessToken(), "202101", null); + assertTrue(node != null && node.hasNonNull("response")); + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/AccessProtectedResourceFlowHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/AccessProtectedResourceFlowHttpTest.java new file mode 100644 index 00000000..12b0452e --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/AccessProtectedResourceFlowHttpTest.java @@ -0,0 +1,207 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs.uma; + +import static org.gluu.oxauth.model.uma.UmaTestUtil.assert_; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +import java.io.UnsupportedEncodingException; + +import javax.ws.rs.ClientErrorException; +import javax.ws.rs.core.Response; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.uma.UmaClientFactory; +import org.gluu.oxauth.client.uma.UmaRptIntrospectionService; +import org.gluu.oxauth.client.uma.UmaTokenService; +import org.gluu.oxauth.client.uma.wrapper.UmaClient; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.uma.UmaMetadata; +import org.gluu.oxauth.model.uma.UmaNeedInfoResponse; +import org.gluu.oxauth.model.uma.UmaTokenResponse; +import org.gluu.oxauth.model.uma.wrapper.Token; +import org.gluu.oxauth.model.util.Util; +import org.openqa.selenium.By; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * Test flow for the accessing protected resource (HTTP) + * + * @author Yuriy Zabrovarnyy + * @author Yuriy Movchan + */ +public class AccessProtectedResourceFlowHttpTest extends BaseTest { + + protected UmaMetadata metadata; + + protected RegisterResourceFlowHttpTest registerResourceTest; + protected UmaRegisterPermissionFlowHttpTest permissionFlowTest; + + protected UmaRptIntrospectionService rptStatusService; + protected UmaTokenService tokenService; + + protected Token pat; + protected String rpt; + protected UmaNeedInfoResponse needInfo; + protected String claimsGatheringTicket; + + @BeforeClass + @Parameters({"umaMetaDataUrl", "umaPatClientId", "umaPatClientSecret"}) + public void init(final String umaMetaDataUrl, final String umaPatClientId, final String umaPatClientSecret) throws Exception { + this.metadata = UmaClientFactory.instance().createMetadataService(umaMetaDataUrl, clientEngine(true)).getMetadata(); + assert_(this.metadata); + + pat = UmaClient.requestPat(tokenEndpoint, umaPatClientId, umaPatClientSecret, clientEngine(true)); + assert_(pat); + + this.registerResourceTest = new RegisterResourceFlowHttpTest(this.metadata); + this.registerResourceTest.pat = this.pat; + + this.permissionFlowTest = new UmaRegisterPermissionFlowHttpTest(this.metadata); + this.permissionFlowTest.registerResourceTest = this.registerResourceTest; + + this.rptStatusService = UmaClientFactory.instance().createRptStatusService(metadata, clientEngine(true)); + this.tokenService = UmaClientFactory.instance().createTokenService(metadata, clientEngine(true)); + } + + /** + * Register resource + */ + @Test + public void registerResource() throws Exception { + showTitle("registerResource"); + this.registerResourceTest.addResource(); + } + + /** + * RS registers permissions for specific resource. + */ + @Test(dependsOnMethods = {"registerResource"}) + public void rsRegisterPermissions() throws Exception { + showTitle("rsRegisterPermissions"); + permissionFlowTest.testRegisterPermission(); + } + + /** + * RP requests RPT with ticket and gets needs_info error (not all claims are provided, so redirect to claims-gathering endpoint) + */ + @Test(dependsOnMethods = {"rsRegisterPermissions"}) + @Parameters({"umaPatClientId", "umaPatClientSecret"}) + public void requestRptAndGetNeedsInfo(String umaPatClientId, String umaPatClientSecret) throws Exception { + showTitle("requestRptAndGetNeedsInfo"); + + try { + tokenService.requestRpt( + "Basic " + encodeCredentials(umaPatClientId, umaPatClientSecret), + GrantType.OXAUTH_UMA_TICKET.getValue(), + permissionFlowTest.ticket, + null, null, null, null, null); + } catch (ClientErrorException ex) { + // expected need_info error : + // sample: {"error":"need_info","ticket":"c024311b-f451-41db-95aa-cd405f16eed4","required_claims":[{"issuer":["https://localhost:8443"],"name":"country","claim_token_format":["http://openid.net/specs/openid-connect-core-1_0.html#IDToken"],"claim_type":"string","friendly_name":"country"},{"issuer":["https://localhost:8443"],"name":"city","claim_token_format":["http://openid.net/specs/openid-connect-core-1_0.html#IDToken"],"claim_type":"string","friendly_name":"city"}],"redirect_user":"https://localhost:8443/restv1/uma/gather_claimsgathering_id=sampleClaimsGathering&&?gathering_id=sampleClaimsGathering&&"} + String entity = (String) ex.getResponse().readEntity(String.class); + System.out.println(entity); + + assertEquals(ex.getResponse().getStatus(), Response.Status.FORBIDDEN.getStatusCode(), "Unexpected response status"); + + needInfo = Util.createJsonMapper().readValue(entity, UmaNeedInfoResponse.class); + assert_(needInfo); + return; + } + + throw new AssertionError("need_info error was not returned"); + } + + + @Test(dependsOnMethods = {"requestRptAndGetNeedsInfo"}) + @Parameters({"umaPatClientId"}) + public void claimsGathering(String umaPatClientId) throws Exception { + String gatheringUrl = needInfo.buildClaimsGatheringUrl(umaPatClientId, this.metadata.getClaimsInteractionEndpoint()); + + System.out.println(gatheringUrl); + System.out.println(); + try { + startSelenium(); + navigateToAuhorizationUrl(driver, gatheringUrl); + System.out.println(driver.getCurrentUrl()); + + driver.findElement(By.id("loginForm:country")).sendKeys("US"); + driver.findElement(By.id("loginForm:gather")).click(); + + Thread.sleep(1000); + System.out.println(driver.getCurrentUrl()); + + driver.findElement(By.id("loginForm:city")).sendKeys("NY"); + driver.findElement(By.id("loginForm:gather")).click(); + Thread.sleep(1200); + // Finally after claims-redirect flow user gets redirect with new ticket + // Sample: https://client.example.com/cb?ticket=e8e7bc0b-75de-4939-a9b1-2425dab3d5ec + System.out.println(driver.getCurrentUrl()); + claimsGatheringTicket = StringUtils.substringAfter(driver.getCurrentUrl(), "ticket="); + } finally { + stopSelenium(); + } + } + + /** + * Request RPT with all claims provided + */ + @Test(dependsOnMethods = {"claimsGathering"}) + @Parameters({"umaPatClientId", "umaPatClientSecret"}) + public void successfulRptRequest(String umaPatClientId, String umaPatClientSecret) throws Exception { + showTitle("successfulRptRequest"); + + UmaTokenResponse response = tokenService.requestRpt( + "Basic " + encodeCredentials(umaPatClientId, umaPatClientSecret), + GrantType.OXAUTH_UMA_TICKET.getValue(), + claimsGatheringTicket, + null, null, null, null, null); + assert_(response); + + this.rpt = response.getAccessToken(); + } + + @Test(dependsOnMethods = {"successfulRptRequest"}) + @Parameters({"umaPatClientId", "umaPatClientSecret"}) + public void repeatRptRequest(String umaPatClientId, String umaPatClientSecret) throws Exception { + showTitle("repeatRptRequest"); + rsRegisterPermissions(); + requestRptAndGetNeedsInfo(umaPatClientId, umaPatClientSecret); + claimsGathering(umaPatClientId); + + showTitle("Request RPT with existing RPT (upgrade case) ... "); + + UmaTokenResponse response = tokenService.requestRpt( + "Basic " + encodeCredentials(umaPatClientId, umaPatClientSecret), + GrantType.OXAUTH_UMA_TICKET.getValue(), + claimsGatheringTicket, + null, null, null, this.rpt, "oxd"); + assert_(response); + assertTrue(response.getUpgraded()); + + this.rpt = response.getAccessToken(); + } + + /** + * RPT status request + */ + @Test(dependsOnMethods = {"repeatRptRequest"}) + @Parameters() + public void rptStatus() throws Exception { + showTitle("rptStatus"); + assert_(this.rptStatusService.requestRptStatus("Bearer " + pat.getAccessToken(), rpt, "")); + } + + public static String encodeCredentials(String username, String password) throws UnsupportedEncodingException { + return Base64.encodeBase64String(Util.getBytes(username + ":" + password)); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/ClientAuthenticationByAccessTokenHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/ClientAuthenticationByAccessTokenHttpTest.java new file mode 100644 index 00000000..758db113 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/ClientAuthenticationByAccessTokenHttpTest.java @@ -0,0 +1,238 @@ +package org.gluu.oxauth.ws.rs.uma; + +import static org.gluu.oxauth.model.uma.UmaTestUtil.assert_; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import javax.ws.rs.ClientErrorException; +import javax.ws.rs.core.Response; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.AuthorizationRequest; +import org.gluu.oxauth.client.AuthorizationResponse; +import org.gluu.oxauth.client.AuthorizeClient; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenRequest; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.client.uma.UmaClientFactory; +import org.gluu.oxauth.client.uma.UmaRptIntrospectionService; +import org.gluu.oxauth.client.uma.UmaTokenService; +import org.gluu.oxauth.client.uma.wrapper.UmaClient; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.Prompt; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.uma.UmaMetadata; +import org.gluu.oxauth.model.uma.UmaNeedInfoResponse; +import org.gluu.oxauth.model.uma.wrapper.Token; +import org.gluu.oxauth.model.util.Util; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author yuriyz + */ +public class ClientAuthenticationByAccessTokenHttpTest extends BaseTest { + + public static final String REDIRECT_URI = "https://client.example.com/cb3"; + protected UmaMetadata metadata; + + protected RegisterResourceFlowHttpTest registerResourceTest; + protected UmaRegisterPermissionFlowHttpTest permissionFlowTest; + + protected UmaRptIntrospectionService rptStatusService; + protected UmaTokenService tokenService; + + protected Token pat; + protected UmaNeedInfoResponse needInfo; + + protected String clientId; + protected String clientSecret; + protected String userAccessToken; + + @BeforeClass + @Parameters({"umaMetaDataUrl", "umaPatClientId", "umaPatClientSecret"}) + public void init(final String umaMetaDataUrl, final String umaPatClientId, final String umaPatClientSecret) throws Exception { + this.metadata = UmaClientFactory.instance().createMetadataService(umaMetaDataUrl, clientEngine(true)).getMetadata(); + assert_(this.metadata); + + pat = UmaClient.requestPat(tokenEndpoint, umaPatClientId, umaPatClientSecret, clientEngine(true)); + assert_(pat); + + this.registerResourceTest = new RegisterResourceFlowHttpTest(this.metadata); + this.registerResourceTest.pat = this.pat; + + this.permissionFlowTest = new UmaRegisterPermissionFlowHttpTest(this.metadata); + this.permissionFlowTest.registerResourceTest = this.registerResourceTest; + + this.rptStatusService = UmaClientFactory.instance().createRptStatusService(metadata, clientEngine(true)); + this.tokenService = UmaClientFactory.instance().createTokenService(metadata, clientEngine(true)); + } + + @Test + public void requestClientRegistrationWithCustomAttributes() throws Exception { + showTitle("requestClientRegistrationWithCustomAttributes"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + List grantTypes = Arrays.asList( + GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS + ); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "oxAuth test app", Collections.singletonList(REDIRECT_URI)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setGrantTypes(grantTypes); + registerRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + registerRequest.addCustomAttribute("oxAuthTrustedClient", "true"); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setExecutor(clientEngine(true)); + registerClient.setRequest(registerRequest); + RegisterResponse response = registerClient.exec(); + + showClient(registerClient); + assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getEntity()); + assertNotNull(response.getClientId()); + assertNotNull(response.getClientSecret()); + assertNotNull(response.getRegistrationAccessToken()); + assertNotNull(response.getClientSecretExpiresAt()); + + clientId = response.getClientId(); + clientSecret = response.getClientSecret(); + } + + @Parameters({"userId", "userSecret"}) + @Test(dependsOnMethods = "requestClientRegistrationWithCustomAttributes") + public void requestAccessTokenCustomClientAuth1(final String userId, final String userSecret) throws Exception { + showTitle("requestAccessTokenCustomClientAuth1"); + + // 1. Request authorization and receive the authorization code. + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + List scopes = Arrays.asList("openid", "profile", "address", "email"); + + String state = UUID.randomUUID().toString(); + String nonce = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, REDIRECT_URI, nonce); + authorizationRequest.setState(state); + authorizationRequest.setAuthUsername(userId); + authorizationRequest.setAuthPassword(userSecret); + authorizationRequest.getPrompts().add(Prompt.NONE); + + AuthorizeClient authorizeClient = new AuthorizeClient(authorizationEndpoint); + authorizeClient.setExecutor(clientEngine(true)); + authorizeClient.setRequest(authorizationRequest); + AuthorizationResponse authorizationResponse = authorizeClient.exec(); + + showClient(authorizeClient); + assertEquals(authorizationResponse.getStatus(), 302, "Unexpected response code: " + authorizationResponse.getStatus()); + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getCode(), "The code is null"); + assertNotNull(authorizationResponse.getIdToken(), "The idToken is null"); + assertNotNull(authorizationResponse.getState(), "The state is null"); + + String authorizationCode = authorizationResponse.getCode(); + String idToken = authorizationResponse.getIdToken(); + + // 2. Validate code and id_token + Jwt jwt = Jwt.parse(idToken); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + assertNotNull(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUDIENCE)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.EXPIRATION_TIME)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.ISSUED_AT)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.CODE_HASH)); + assertNotNull(jwt.getClaims().getClaimAsString(JwtClaimName.AUTHENTICATION_TIME)); + + // 3. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(REDIRECT_URI); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + + TokenClient tokenClient = new TokenClient(tokenEndpoint); + tokenClient.setExecutor(clientEngine(true)); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + assertEquals(tokenResponse.getStatus(), 200, "Unexpected response code: " + tokenResponse.getStatus()); + assertNotNull(tokenResponse.getEntity(), "The entity is null"); + assertNotNull(tokenResponse.getAccessToken(), "The access token is null"); + assertNotNull(tokenResponse.getExpiresIn(), "The expires in value is null"); + assertNotNull(tokenResponse.getTokenType(), "The token type is null"); + assertNotNull(tokenResponse.getRefreshToken(), "The refresh token is null"); + + userAccessToken = tokenResponse.getAccessToken(); + } + + /** + * Register resource + */ + @Test(dependsOnMethods = "requestAccessTokenCustomClientAuth1") + public void registerResource() throws Exception { + showTitle("registerResource"); + this.registerResourceTest.addResource(); + } + + /** + * RS registers permissions for specific resource. + */ + @Test(dependsOnMethods = {"registerResource"}) + public void rsRegisterPermissions() throws Exception { + showTitle("rsRegisterPermissions"); + permissionFlowTest.testRegisterPermission(); + } + + /** + * RP requests RPT with ticket and gets needs_info error (not all claims are provided, so redirect to claims-gathering endpoint) + */ + @Test(dependsOnMethods = {"rsRegisterPermissions"}) + public void requestRptAndGetNeedsInfo() throws Exception { + showTitle("requestRptAndGetNeedsInfo"); + + try { + tokenService.requestRpt( + "AccessToken " + userAccessToken, + GrantType.OXAUTH_UMA_TICKET.getValue(), + permissionFlowTest.ticket, + null, null, null, null, null); + } catch (ClientErrorException ex) { + // expected need_info error : + // sample: {"error":"need_info","ticket":"c024311b-f451-41db-95aa-cd405f16eed4","required_claims":[{"issuer":["https://localhost:8443"],"name":"country","claim_token_format":["http://openid.net/specs/openid-connect-core-1_0.html#IDToken"],"claim_type":"string","friendly_name":"country"},{"issuer":["https://localhost:8443"],"name":"city","claim_token_format":["http://openid.net/specs/openid-connect-core-1_0.html#IDToken"],"claim_type":"string","friendly_name":"city"}],"redirect_user":"https://localhost:8443/restv1/uma/gather_claimsgathering_id=sampleClaimsGathering&&?gathering_id=sampleClaimsGathering&&"} + String entity = (String) ex.getResponse().readEntity(String.class); + System.out.println(entity); + + assertEquals(ex.getResponse().getStatus(), Response.Status.FORBIDDEN.getStatusCode(), "Unexpected response status"); + + needInfo = Util.createJsonMapper().readValue(entity, UmaNeedInfoResponse.class); + assert_(needInfo); + return; + } + + // Expected result is to get need_info error. It means that client was authenticated successfully. + // If client fails to authenticate then we will get `401` invalid client error. + throw new AssertionError("need_info error was not returned"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/MetaDataFlowHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/MetaDataFlowHttpTest.java new file mode 100644 index 00000000..7b0029a1 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/MetaDataFlowHttpTest.java @@ -0,0 +1,48 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs.uma; + +import javax.ws.rs.ClientErrorException; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.uma.UmaClientFactory; +import org.gluu.oxauth.client.uma.UmaMetadataService; +import org.gluu.oxauth.model.uma.UmaMetadata; +import org.gluu.oxauth.model.uma.UmaTestUtil; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * Test cases for getting meta data configuration flow (HTTP) + * + * @author Yuriy Movchan Date: 11/05/2012 + */ +public class MetaDataFlowHttpTest extends BaseTest { + + /** + * Test for getting meta data configuration + */ + @Test + @Parameters({"umaMetaDataUrl"}) + public void testGetUmaMetaDataConfiguration(final String umaMetaDataUrl) throws Exception { + showTitle("testGetUmaMetaDataConfiguration"); + + UmaMetadataService metaDataConfigurationService = UmaClientFactory.instance().createMetadataService(umaMetaDataUrl, clientEngine(true)); + + // Get meta data + UmaMetadata c = null; + try { + c = metaDataConfigurationService.getMetadata(); + } catch (ClientErrorException ex) { + System.err.println(ex.getResponse().readEntity(String.class)); + throw ex; + } + + UmaTestUtil.assert_(c); + } + +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/ObtainPatTokenFlowHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/ObtainPatTokenFlowHttpTest.java new file mode 100644 index 00000000..1c951d2c --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/ObtainPatTokenFlowHttpTest.java @@ -0,0 +1,62 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs.uma; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.TokenClient; +import org.gluu.oxauth.client.TokenResponse; +import org.gluu.oxauth.client.uma.wrapper.UmaClient; +import org.gluu.oxauth.model.uma.UmaTestUtil; +import org.gluu.oxauth.model.uma.wrapper.Token; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * Test cases for the obtaining UMA PAT token flow (HTTP) + * + * @author Yuriy Movchan Date: 10/03/2012 + */ +public class ObtainPatTokenFlowHttpTest extends BaseTest { + + protected Token m_pat; + + /** + * Test for the obtaining UMA PAT token + */ + @Test + @Parameters({"umaPatClientId", "umaPatClientSecret"}) + public void testObtainPatTokenFlow(final String umaPatClientId, final String umaPatClientSecret) throws Exception { + showTitle("testObtainPatTokenFlow"); + + m_pat = UmaClient.requestPat(tokenEndpoint, umaPatClientId, umaPatClientSecret); + UmaTestUtil.assert_(m_pat); + } + + /** + * Test for the obtaining UMA PAT token using refresh token + */ + //@Test(dependsOnMethods = {"testObtainPatTokenFlow"}) + @Parameters({"umaPatClientId", "umaPatClientSecret"}) + public void testObtainPatTokenUsingRefreshTokenFlow(final String umaPatClientId, final String umaPatClientSecret) throws Exception { + showTitle("testObtainPatTokenUsingRefreshTokenFlow"); + + // Request new access token using the refresh token. + TokenClient tokenClient1 = new TokenClient(tokenEndpoint); + TokenResponse response1 = tokenClient1.execRefreshToken(m_pat.getScope(), m_pat.getRefreshToken(), umaPatClientId, umaPatClientSecret); + + showClient(tokenClient1); + assertEquals(response1.getStatus(), 200, "Unexpected response code: " + response1.getStatus()); + assertNotNull(response1.getEntity(), "The entity is null"); + assertNotNull(response1.getAccessToken(), "The access token is null"); + assertNotNull(response1.getTokenType(), "The token type is null"); + assertNotNull(response1.getRefreshToken(), "The refresh token is null"); + assertNotNull(response1.getScope(), "The scope is null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/RegisterResourceFlowHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/RegisterResourceFlowHttpTest.java new file mode 100644 index 00000000..37c0b5ee --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/RegisterResourceFlowHttpTest.java @@ -0,0 +1,263 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs.uma; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; + +import javax.ws.rs.ClientErrorException; +import javax.ws.rs.core.Response; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.uma.UmaClientFactory; +import org.gluu.oxauth.client.uma.UmaResourceService; +import org.gluu.oxauth.client.uma.wrapper.UmaClient; +import org.gluu.oxauth.model.uma.UmaMetadata; +import org.gluu.oxauth.model.uma.UmaResource; +import org.gluu.oxauth.model.uma.UmaResourceResponse; +import org.gluu.oxauth.model.uma.UmaResourceWithId; +import org.gluu.oxauth.model.uma.UmaTestUtil; +import org.gluu.oxauth.model.uma.wrapper.Token; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * Test cases for the registering UMA resources + * + * @author Yuriy Zabrovarnyy + * @author Yuriy Movchan + */ +public class RegisterResourceFlowHttpTest extends BaseTest { + + private static final String START_SCOPE_EXPRESSION = "{\"rule\": {\"and\": [{\"or\": [{\"var\": 0},{\"var\": 1}]},{\"var\": 2}]}," + + " \"data\": [\"http://photoz.example.com/dev/actions/all\",\"http://photoz.example.com/dev/actions/add\",\"http://photoz.example.com/dev/actions/internalClient\"]}"; + private static final String MODIFY_SCOPE_EXPRESSION = "{\"rule\": {\"or\": [{\"or\": [{\"var\": 0},{\"var\": 1}]},{\"var\": 2}]}," + + " \"data\": [\"http://photoz.example.com/dev/actions/all\",\"http://photoz.example.com/dev/actions/add\",\"http://photoz.example.com/dev/actions/internalClient\"]}"; + + protected UmaMetadata metadata; + protected Token pat; + + protected String resourceId; + protected String resourceIdWithScopeExpression; + protected UmaResourceService resourceService; + + public RegisterResourceFlowHttpTest() { + } + + public RegisterResourceFlowHttpTest(UmaMetadata metadataConfiguration) { + this.metadata = metadataConfiguration; + } + + @BeforeClass + @Parameters({"umaMetaDataUrl", "umaPatClientId", "umaPatClientSecret"}) + public void init(final String umaMetaDataUrl, final String umaPatClientId, final String umaPatClientSecret) throws Exception { + if (this.metadata == null) { + this.metadata = UmaClientFactory.instance().createMetadataService(umaMetaDataUrl, clientEngine(true)).getMetadata(); + UmaTestUtil.assert_(this.metadata); + } + + pat = UmaClient.requestPat(tokenEndpoint, umaPatClientId, umaPatClientSecret, clientEngine(true)); + UmaTestUtil.assert_(pat); + } + + public UmaResourceService getResourceService() throws Exception { + if (resourceService == null) { + resourceService = UmaClientFactory.instance().createResourceService(this.metadata, clientEngine(true)); + } + return resourceService; + } + + /** + * Add resource + */ + @Test + public void addResource() throws Exception { + showTitle("addResource"); + registerResource(Arrays.asList("http://photoz.example.com/dev/scopes/view", "http://photoz.example.com/dev/scopes/all")); + registerResourceWithScopeExpression(START_SCOPE_EXPRESSION); + } + + public String registerResource(List scopes) throws Exception { + try { + UmaResource resource = new UmaResource(); + resource.setName("Photo Album"); + resource.setIconUri("http://www.example.com/icons/flower.png"); + resource.setScopes(scopes); + resource.setType("myType"); + + UmaResourceResponse resourceStatus = getResourceService().addResource("Bearer " + pat.getAccessToken(), resource); + UmaTestUtil.assert_(resourceStatus); + + this.resourceId = resourceStatus.getId(); + return this.resourceId; + } catch (ClientErrorException ex) { + System.err.println(ex.getResponse().readEntity(String.class)); + throw ex; + } + } + + public String registerResourceWithScopeExpression(String scopeExpression) throws Exception { + try { + UmaResource resource = new UmaResource(); + resource.setName("Photo Album"); + resource.setIconUri("http://www.example.com/icons/flower.png"); + resource.setScopeExpression(scopeExpression); + resource.setType("myType"); + + UmaResourceResponse resourceStatus = getResourceService().addResource("Bearer " + pat.getAccessToken(), resource); + UmaTestUtil.assert_(resourceStatus); + + this.resourceIdWithScopeExpression = resourceStatus.getId(); + return this.resourceIdWithScopeExpression; + } catch (ClientErrorException ex) { + System.err.println(ex.getResponse().readEntity(String.class)); + throw ex; + } + } + + /** + * Resource modification + */ + @Test(dependsOnMethods = {"addResource"}) + public void modifyResource() throws Exception { + showTitle("modifyResource"); + + // Modify resource description + UmaResourceResponse resourceStatus = null; + try { + UmaResource resource = new UmaResource(); + resource.setName("Photo Album 2"); + resource.setIconUri("http://www.example.com/icons/flower.png"); + resource.setScopes(Arrays.asList("http://photoz.example.com/dev/scopes/view", "http://photoz.example.com/dev/scopes/all")); + resource.setType("myType"); + + resourceStatus = getResourceService().updateResource("Bearer " + pat.getAccessToken(), this.resourceId, resource); + } catch (ClientErrorException ex) { + System.err.println(ex.getResponse().readEntity(String.class)); + throw ex; + } + + try { + UmaResource resource = new UmaResource(); + resource.setName("Photo Album 2"); + resource.setIconUri("http://www.example.com/icons/flower.png"); + resource.setScopeExpression(MODIFY_SCOPE_EXPRESSION); + resource.setType("myType"); + + resourceStatus = getResourceService().updateResource("Bearer " + pat.getAccessToken(), this.resourceIdWithScopeExpression, resource); + } catch (ClientErrorException ex) { + System.err.println(ex.getResponse().readEntity(String.class)); + throw ex; + } + + assertNotNull(resourceStatus, "Resource status is null"); + assertNotNull(this.resourceId, "Resource description id is null"); + } + + /** + * Test non existing UMA resource description modification + */ + @Test(dependsOnMethods = {"modifyResource"}) + public void modifyNotExistingResource() throws Exception { + showTitle("modifyNotExistingResource"); + + try { + UmaResource resource = new UmaResource(); + resource.setName("Photo Album 3"); + resource.setIconUri("http://www.example.com/icons/flower.png"); + resource.setScopes(Arrays.asList("http://photoz.example.com/dev/scopes/view", "http://photoz.example.com/dev/scopes/all")); + + getResourceService().updateResource("Bearer " + pat.getAccessToken(), "fake_resource_id", resource); + } catch (ClientErrorException ex) { + System.err.println(ex.getResponse().readEntity(String.class)); + int status = ex.getResponse().getStatus(); + assertTrue(status != Response.Status.OK.getStatusCode(), "Unexpected response status"); + } + } + + /** + * Test UMA resource description modification with invalid PAT + */ + @Test(dependsOnMethods = {"modifyResource"}) + public void testModifyResourceWithInvalidPat() throws Exception { + showTitle("testModifyResourceWithInvalidPat"); + + UmaResourceResponse resourceStatus = null; + try { + UmaResource resource = new UmaResource(); + resource.setName("Photo Album 4"); + resource.setIconUri("http://www.example.com/icons/flower.png"); + resource.setScopes(Arrays.asList("http://photoz.example.com/dev/scopes/view", "http://photoz.example.com/dev/scopes/all")); + + resourceStatus = getResourceService().updateResource("Bearer " + pat.getAccessToken() + "_invalid", this.resourceId + "_invalid", resource); + } catch (ClientErrorException ex) { + System.err.println(ex.getResponse().readEntity(String.class)); + assertEquals(ex.getResponse().getStatus(), Response.Status.UNAUTHORIZED.getStatusCode(), "Unexpected response status"); + } + + assertNull(resourceStatus, "Resource status is not null"); + } + + /** + * Get resource + */ + @Test(dependsOnMethods = {"modifyResource"}) + public void getOneResource() throws Exception { + showTitle("getOneResource"); + + try { + UmaResourceWithId resource = getResourceService().getResource("Bearer " + pat.getAccessToken(), this.resourceId); + assertEquals(resource.getType(), "myType"); + + UmaResourceWithId resourceWithExpression = getResourceService().getResource("Bearer " + pat.getAccessToken(), this.resourceIdWithScopeExpression); + assertEquals(resourceWithExpression.getScopeExpression(), MODIFY_SCOPE_EXPRESSION); + } catch (ClientErrorException ex) { + System.err.println(ex.getResponse().readEntity(String.class)); + throw ex; + } + } + + /** + * Get resources + */ + @Test(dependsOnMethods = {"getOneResource"}) + public void getResources() throws Exception { + showTitle("getResources"); + + List resources = null; + try { + resources = getResourceService().getResourceList("Bearer " + pat.getAccessToken(), ""); + } catch (ClientErrorException ex) { + System.err.println(ex.getResponse().readEntity(String.class)); + throw ex; + } + + assertNotNull(resources, "Resources is null"); + assertTrue(resources.contains(this.resourceId), "Resource list doesn't contain added resource"); + } + + /** + * Delete resource + */ + @Test(dependsOnMethods = {"getResources"}) + public void deleteResource() throws Exception { + showTitle("testDeleteResource"); + + try { + getResourceService().deleteResource("Bearer " + pat.getAccessToken(), this.resourceId); + } catch (ClientErrorException ex) { + System.err.println(ex.getResponse().readEntity(String.class)); + throw ex; + } + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/ScopeHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/ScopeHttpTest.java new file mode 100644 index 00000000..4a6535a0 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/ScopeHttpTest.java @@ -0,0 +1,32 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs.uma; + +import org.gluu.oxauth.client.uma.UmaClientFactory; +import org.gluu.oxauth.client.uma.UmaScopeService; +import org.gluu.oxauth.model.uma.UmaMetadata; +import org.gluu.oxauth.model.uma.UmaScopeDescription; +import org.gluu.oxauth.model.uma.UmaTestUtil; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 22/04/2013 + */ + +public class ScopeHttpTest { + + @Test + @Parameters({"umaMetaDataUrl"}) + public void scopePresence(final String umaMetaDataUrl) { + final UmaMetadata metadata = UmaClientFactory.instance().createMetadataService(umaMetaDataUrl).getMetadata(); + final UmaScopeService scopeService = UmaClientFactory.instance().createScopeService(metadata.getScopeEndpoint()); + final UmaScopeDescription modifyScope = scopeService.getScope("modify"); + UmaTestUtil.assert_(modifyScope); + } +} diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/UmaRegisterPermissionFlowHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/UmaRegisterPermissionFlowHttpTest.java new file mode 100644 index 00000000..ffa771bb --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/UmaRegisterPermissionFlowHttpTest.java @@ -0,0 +1,130 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ws.rs.uma; + +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; + +import javax.ws.rs.ClientErrorException; +import javax.ws.rs.core.Response; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.uma.UmaClientFactory; +import org.gluu.oxauth.client.uma.UmaPermissionService; +import org.gluu.oxauth.model.uma.PermissionTicket; +import org.gluu.oxauth.model.uma.UmaMetadata; +import org.gluu.oxauth.model.uma.UmaPermission; +import org.gluu.oxauth.model.uma.UmaPermissionList; +import org.gluu.oxauth.model.uma.UmaTestUtil; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +/** + * Test cases for the registering UMA permissions flow (HTTP) + * + * @author Yuriy Zabrovarnyy + * @author Yuriy Movchan + */ +public class UmaRegisterPermissionFlowHttpTest extends BaseTest { + + protected UmaMetadata metadata; + + protected RegisterResourceFlowHttpTest registerResourceTest; + protected String ticket; + protected UmaPermissionService permissionService; + + public UmaRegisterPermissionFlowHttpTest() { + } + + public UmaRegisterPermissionFlowHttpTest(UmaMetadata metadataConfiguration) { + this.metadata = metadataConfiguration; + } + + @BeforeClass + @Parameters({"umaMetaDataUrl", "umaPatClientId", "umaPatClientSecret"}) + public void init(final String umaMetaDataUrl, final String umaPatClientId, final String umaPatClientSecret) throws Exception { + if (this.metadata == null) { + this.metadata = UmaClientFactory.instance().createMetadataService(umaMetaDataUrl, clientEngine(true)).getMetadata(); + UmaTestUtil.assert_(this.metadata); + } + + this.registerResourceTest = new RegisterResourceFlowHttpTest(this.metadata); + this.registerResourceTest.setAuthorizationEndpoint(authorizationEndpoint); + this.registerResourceTest.setTokenEndpoint(tokenEndpoint); + this.registerResourceTest.init(umaMetaDataUrl, umaPatClientId, umaPatClientSecret); + + this.registerResourceTest.addResource(); + } + + @AfterClass + public void clean() throws Exception { + this.registerResourceTest.deleteResource(); + } + + public UmaPermissionService getPermissionService() throws Exception { + if (permissionService == null) { + permissionService = UmaClientFactory.instance().createPermissionService(this.metadata, clientEngine(true)); + } + return permissionService; + } + + /** + * Test for registering permissions for resource + */ + @Test + public void testRegisterPermission() throws Exception { + showTitle("testRegisterPermission"); + registerResourcePermission(this.registerResourceTest.resourceId, Arrays.asList("http://photoz.example.com/dev/scopes/view")); + } + + public String registerResourcePermission(List scopes) throws Exception { + return registerResourcePermission(this.registerResourceTest.resourceId, scopes); + } + + public String registerResourcePermission(String resourceId, List scopes) throws Exception { + + UmaPermission permission = new UmaPermission(); + permission.setResourceId(resourceId); + permission.setScopes(scopes); + + PermissionTicket ticket = getPermissionService().registerPermission( + "Bearer " + this.registerResourceTest.pat.getAccessToken(), UmaPermissionList.instance(permission)); + UmaTestUtil.assert_(ticket); + this.ticket = ticket.getTicket(); + return ticket.getTicket(); + } + + /** + * Test for registering permissions for resource + */ + @Test + public void testRegisterPermissionForInvalidResource() throws Exception { + showTitle("testRegisterPermissionForInvalidResource"); + + UmaPermission permission = new UmaPermission(); + permission.setResourceId(this.registerResourceTest.resourceId + "1"); + permission.setScopes(Arrays.asList("http://photoz.example.com/dev/scopes/view", "http://photoz.example.com/dev/scopes/all")); + + PermissionTicket ticket = null; + try { + ticket = getPermissionService().registerPermission( + "Bearer " + this.registerResourceTest.pat.getAccessToken(), UmaPermissionList.instance(permission)); + } catch (ClientErrorException ex) { + System.err.println(ex.getResponse().readEntity(String.class)); + assertTrue(ex.getResponse().getStatus() != Response.Status.CREATED.getStatusCode() && + ex.getResponse().getStatus() != Response.Status.OK.getStatusCode() + , "Unexpected response status"); + } + + assertNull(ticket, "Resource permission is not null"); + } +} \ No newline at end of file diff --git a/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/UmaSpontaneousScopeHttpTest.java b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/UmaSpontaneousScopeHttpTest.java new file mode 100644 index 00000000..b21b1935 --- /dev/null +++ b/oxAuth/Client/src/test/java/org/gluu/oxauth/ws/rs/uma/UmaSpontaneousScopeHttpTest.java @@ -0,0 +1,140 @@ +package org.gluu.oxauth.ws.rs.uma; + +import static org.gluu.oxauth.model.uma.UmaTestUtil.assert_; +import static org.gluu.oxauth.ws.rs.uma.AccessProtectedResourceFlowHttpTest.encodeCredentials; +import static org.junit.Assert.assertTrue; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.util.Arrays; +import java.util.List; + +import org.gluu.oxauth.BaseTest; +import org.gluu.oxauth.client.RegisterClient; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.client.RegisterResponse; +import org.gluu.oxauth.client.uma.UmaClientFactory; +import org.gluu.oxauth.client.uma.UmaRptIntrospectionService; +import org.gluu.oxauth.client.uma.UmaTokenService; +import org.gluu.oxauth.client.uma.wrapper.UmaClient; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.uma.RptIntrospectionResponse; +import org.gluu.oxauth.model.uma.UmaMetadata; +import org.gluu.oxauth.model.uma.UmaTokenResponse; +import org.gluu.oxauth.model.uma.wrapper.Token; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +import com.google.common.collect.Lists; + +/** + * @author Yuriy Zabrovarnyy + */ +public class UmaSpontaneousScopeHttpTest extends BaseTest { + + private static final String REDIRECT_URI = "https://cb.example.com"; + public static final String USER_2_SCOPE = "/user/2"; + + private UmaMetadata metadata; + + private RegisterResourceFlowHttpTest registerResourceTest; + private UmaRegisterPermissionFlowHttpTest permissionFlowTest; + + private UmaRptIntrospectionService rptStatusService; + private UmaTokenService tokenService; + + private Token pat; + private String rpt; + private RegisterResponse clientResponse; + + private void registerClient() throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + List scopes = Lists.newArrayList( + "openid", "uma_protection", "profile", "address", "email", "phone", "user_name" + ); + + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "UMA Spontaneous scope test", Lists.newArrayList(REDIRECT_URI)); + registerRequest.setResponseTypes(Arrays.asList(ResponseType.values())); + registerRequest.setGrantTypes(Arrays.asList(GrantType.values())); + registerRequest.setScope(scopes); + registerRequest.setAllowSpontaneousScopes(true); // allow spontaneous scopes (which are off by default) + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setExecutor(clientEngine(true)); + registerClient.setRequest(registerRequest); + clientResponse = registerClient.exec(); + + showClient(registerClient); + assertEquals(clientResponse.getStatus(), 200, "Unexpected response code: " + clientResponse.getEntity()); + assertNotNull(clientResponse.getClientId()); + assertNotNull(clientResponse.getClientSecret()); + assertNotNull(clientResponse.getRegistrationAccessToken()); + assertNotNull(clientResponse.getClientIdIssuedAt()); + assertNotNull(clientResponse.getClientSecretExpiresAt()); + } + + @BeforeClass + @Parameters({"umaMetaDataUrl"}) + public void init(final String umaMetaDataUrl) throws Exception { + this.metadata = UmaClientFactory.instance().createMetadataService(umaMetaDataUrl, clientEngine(true)).getMetadata(); + assert_(this.metadata); + + registerClient(); + + pat = UmaClient.requestPat(tokenEndpoint, clientResponse.getClientId(), clientResponse.getClientSecret(), clientEngine(true)); + assert_(pat); + + this.registerResourceTest = new RegisterResourceFlowHttpTest(this.metadata); + this.registerResourceTest.pat = this.pat; + + this.permissionFlowTest = new UmaRegisterPermissionFlowHttpTest(this.metadata); + this.permissionFlowTest.registerResourceTest = this.registerResourceTest; + + this.rptStatusService = UmaClientFactory.instance().createRptStatusService(metadata, clientEngine(true)); + this.tokenService = UmaClientFactory.instance().createTokenService(metadata, clientEngine(true)); + } + + @Test + public void registerResource() throws Exception { + showTitle("registerResource"); + this.registerResourceTest.registerResource(Lists.newArrayList("^/user/.+$")); + } + + + @Test(dependsOnMethods = {"registerResource"}) + public void registerPermissions() throws Exception { + showTitle("registerPermissions"); + permissionFlowTest.registerResourcePermission(Lists.newArrayList(USER_2_SCOPE)); + } + + @Test(dependsOnMethods = {"registerPermissions"}) + public void successfulRptRequest() throws Exception { + showTitle("successfulRptRequest"); + + UmaTokenResponse response = tokenService.requestRpt( + "Basic " + encodeCredentials(clientResponse.getClientId(), clientResponse.getClientSecret()), + GrantType.OXAUTH_UMA_TICKET.getValue(), + permissionFlowTest.ticket, + null, null, null, null, null); + assert_(response); + + this.rpt = response.getAccessToken(); + } + + @Test(dependsOnMethods = {"successfulRptRequest"}) + @Parameters() + public void rptStatus() { + showTitle("rptStatus"); + final RptIntrospectionResponse status = this.rptStatusService.requestRptStatus("Bearer " + pat.getAccessToken(), rpt, ""); + assert_(status); + + // at the end scope registered by permission must be present in RPT permission with scope allowed by spontaneous scope check + assertTrue(status.getPermissions().get(0).getScopes().contains(USER_2_SCOPE)); + } +} diff --git a/oxAuth/Client/src/test/resources/clientkeystore.jks b/oxAuth/Client/src/test/resources/clientkeystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..8e247a12ddb95b694426ea071e5ff27e366c0890 GIT binary patch literal 19673 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 literal 0 HcmV?d00001 diff --git a/oxAuth/Client/src/test/resources/interop_Gluu_OX.properties b/oxAuth/Client/src/test/resources/interop_Gluu_OX.properties new file mode 100644 index 00000000..854dbd45 --- /dev/null +++ b/oxAuth/Client/src/test/resources/interop_Gluu_OX.properties @@ -0,0 +1,44 @@ +swdResource=https://${test.server.name} +#swdResource=https://localhost:8443 +#authorizationEndpoint = https://localhost:8443/restv1/authorize +#tokenEndpoint = https://localhost:8443/restv1/token +#userInfoEndpoint = https://localhost:8443/restv1/userinfo +#checkSessionIFrame = https://localhost:8443/restv1/check_session +#endSessionEndpoint = https://localhost:8443/restv1/end_session +#jwksUri = https://localhost:8443/restv1/jwks +#registrationEndpoint = https://localhost:8443/restv1/register + +userId = ${auth.user.uid} +userSecret = ${auth.user.password} +redirectUri = https://${test.server.name}/oxauth-rp/home.htm +redirectUris = https://${test.server.name}/oxauth-rp/home.htm https://client.example.com/cb https://client.example.com/cb1 https://client.example.com/cb2 +sectorIdentifierUri = https://${test.server.name}/oxauth-client/test/resources/sector_identifier.js + +## Client Resources +requestFileBasePath=/var/www/html/oxAuth +requestFileBaseUrl=http://localhost/oxAuth +clientJwksUri=https://${test.server.name}/oxauth-client/test/resources/jwks.json +## RS256 +RS256_modulus=AJpGcIVu7fmQJLHXeAClhXaJD7SvuABjYiPcT9IbKFWGWj51GgD-CxtyrQGXT0ctGEEsXOzMZM40q-V7GR-5qkJ_OalVTTc_EeKAHao45bZPsPHLxvusNfrfpyhc6JjF2TQhoOqxbgMgQ9L6W9q9fSjgzx-tPlD0d3X0GZOEQ_NYGstZWRRBwHgsxA2IRYtwSH-v76yPpxF9poLIWdnBKtKfSr6UY7p1BrLmMm0DdMhjQLn6j4S_eB-p2WyBwObvsLqO6FdClpZFtGr82Km2uinpHvZ6KJ_MUEW1sijPPI3rIGbaUbLtQJwX5GVynAP5qU2qRVkcsrKt-GeNoz6QNLM +RS256_privateExponent=RkIKAFpyehMRAwTTm8fFriPhSTI1I8ge66HroA3KIpjbBFKkEwue11M0QuM7sXhx8UxYzWaQCfCm0A1tdatCRKJYCivUzHImnPYnjFv5ETvdo2BgMEFPG_86ywD01I5Vyo3-EKPZLAdHnA90QXvGQhWPfieRl6CdvtP5ydqUb39aPazZKzPx4v6hj-7wrrwrQmYq-7li_urR2zhz5HvE_eE66i1xhGTI8VdV9VE2y06Zbzn54qL7kf0nsjvg4X9ERdXv4kkhRwSSc3CExJ-iNbo0n3nQ3KovhpA1FEzd9cwI_2EfLRIvVjJRMTnPHCLmz_8-Htn2Kpi3vV9MWHab8Q +## RS384 +RS384_modulus=AJ125IzZ0TRSSoVas3jwMWuckyMujoGUUeDd8rLjTSCLlgUb3RiT9MbKfWdeCByme5MZ21lvMu6OmMFn8iDb5erLSBJ8bZFq6ruGIVzU8NI833IahlIO9m6JIR4L_go8Szu-1MYPGUjOKDsxc-Fp3fR-Kb0HFAEEs44t9vL9yMKjNeQeAp7Fo2AukDNEZqvEObP7XWLdJFA-TuAXE1f7o49lMr0y4Tqy2XeDKwfklO0bAnbSryZubRg2E7gjiwaiSYVIFphotLlpCd3N4MU46JjHA2dv1GtIe8749HinwhK1stes3PbZb9Gwm2LyK89iRJ35bCmDLnkwP0rTwTZ2Ul8 +RS384_privateExponent=NYqTtADsTaodhLKOi_TAGSMoNLJD6nOQU7GkMIdxVjugSyRqTU0h0eZQNbGXeIZzRlVobESPQOZjsn-xqNKcnvV4EDEW4HdGUXUOKw6MxC_GmnnCamyEBpnCFQFm4_wUaMA-gQnpQwQ2UcpC6Maindu4PXoGp0H9-75NVdpNRUDHO5xY2Ybp2kVv__sUWCKRLK7JaKRA3iGlXFpLPSy9DvmBjy27z1t_z58_vrPrVset7muMwMTwgfDv-EP3NBH4eTg7_Cy952MuhxaUUsHW85LZuv5t_rcoUF8kfDWcclP3a954lAzCbVjdngpvaShNbiTKAf9-bMQKG66v63W54Q +## RS512 +RS512_modulus=AKuc75KyKNwteumhyN5Kxa4ipQZrE_ouULtMZmCYI3Y32oCv3wWkgmrprBo-yCK292wfn77dNdZ9h5OoY-6sDVG-OKi9uwXpFcopyqIdsYOrw-4FKHxpr_7b--cH6HRmGlSFKVJpwfvIjD9Mu8S9bhNgnXfbKoYLcANU7Vjtacr3MvX-U406eRXLI9lZNr6ViQxSJw3A7yYMo2XYMYhO-FHGOYeV815q7fJFUMoCUMNSWlCx-pUCVGg0PuCKlOhUGIoLqvuFqUnBNd0hoAJCtmqya4_e3DLNzOgr2HOEbX7kQEjpi0XdyQ0fbFTAYO9TpXT2gldnmOElZ4UE2lX8J6M +RS512_privateExponent=doYyFGAFxmOG43tAbv63XthAn5kut_hq-6D9iDMrMsfKmlxdLNl81XhDy_CWaxtw8PU6cCj5uQUDsSB4vGuJ224EVc6ML73WtcR9VdAqPOVRsb9QQfUAf4XRibO1gUbPYpaBfpDaUBonesR1XqDyOGHe_9uXl_KoTzTFpEh8a5eCk9mwz85bb08PxQUut5DFdzPPyTi8_k3m7hry97I0TMbHUTiTqjgFpq2ZqSn4KQz77uft1oMwJLvlNHP6Fs25aVYrgAWw1DfTcCDwPAKxXlCD4ZPfGN2LmxZWCYxj1HLVKmQkrjX-FSgvpHs7YUqzJ19whmrTEtODnhWvZhTucQ +## ES256 +ES256_d=AIiNVUvr6-ChpOv2F7HNXyS2pYuoLF3ZqF2kTP0XquzB +## ES384 +ES384_d=S5iDyZaSar7cqcCKYFC1VGVKAXmwdOSHRMrwbrEd_WvmIYi3u8PwHFYAmA0PEwLF +## ES512 +ES512_d=AbedxoxLdftbJpXMYWlcuJkEF6iRotCxYYbH18NyEuOka_vS5dLV6m6Bhx_y_y9NgTQzP5SGzfpkSpgF6JVG7eFL + +# Form Interaction +loginFormUsername = loginForm:username +loginFormPassword = loginForm:password +loginFormLoginButton = loginForm:loginButton +authorizeFormAllowButton = authorizeForm:allowButton +authorizeFormDoNotAllowButton = authorizeForm:doNotAllowButton + +hostnameVerifier=allow_all \ No newline at end of file diff --git a/oxAuth/Client/src/test/resources/interop_NRI_PHP.properties b/oxAuth/Client/src/test/resources/interop_NRI_PHP.properties new file mode 100644 index 00000000..b69d1a40 --- /dev/null +++ b/oxAuth/Client/src/test/resources/interop_NRI_PHP.properties @@ -0,0 +1,39 @@ +swdResource=https://connect.openid4.us +authorizationEndpoint = +tokenEndpoint = +userInfoEndpoint = +checkSessionIFrame = +endSessionEndpoint = +jwksUri = +registrationEndpoint = + +userId = bob +userSecret = underland +redirectUri = https://${test.server.name}/oxauth-rp/home.htm?foo=bar +redirectUris = https://${test.server.name}/oxauth-rp/home.htm https://client.example.com/cb https://client.example.com/cb1 https://client.example.com/cb2 +sectorIdentifierUri = https://${test.server.name}/oxauth-client/test/resources/sector_identifier.js + +## Client Resources +clientJwksUri=https://${test.server.name}/oxauth-client/test/resources/jwks.json +## RS256 +RS256_modulus=AJpGcIVu7fmQJLHXeAClhXaJD7SvuABjYiPcT9IbKFWGWj51GgD-CxtyrQGXT0ctGEEsXOzMZM40q-V7GR-5qkJ_OalVTTc_EeKAHao45bZPsPHLxvusNfrfpyhc6JjF2TQhoOqxbgMgQ9L6W9q9fSjgzx-tPlD0d3X0GZOEQ_NYGstZWRRBwHgsxA2IRYtwSH-v76yPpxF9poLIWdnBKtKfSr6UY7p1BrLmMm0DdMhjQLn6j4S_eB-p2WyBwObvsLqO6FdClpZFtGr82Km2uinpHvZ6KJ_MUEW1sijPPI3rIGbaUbLtQJwX5GVynAP5qU2qRVkcsrKt-GeNoz6QNLM +RS256_privateExponent=RkIKAFpyehMRAwTTm8fFriPhSTI1I8ge66HroA3KIpjbBFKkEwue11M0QuM7sXhx8UxYzWaQCfCm0A1tdatCRKJYCivUzHImnPYnjFv5ETvdo2BgMEFPG_86ywD01I5Vyo3-EKPZLAdHnA90QXvGQhWPfieRl6CdvtP5ydqUb39aPazZKzPx4v6hj-7wrrwrQmYq-7li_urR2zhz5HvE_eE66i1xhGTI8VdV9VE2y06Zbzn54qL7kf0nsjvg4X9ERdXv4kkhRwSSc3CExJ-iNbo0n3nQ3KovhpA1FEzd9cwI_2EfLRIvVjJRMTnPHCLmz_8-Htn2Kpi3vV9MWHab8Q +## RS384 +RS384_modulus=AJ125IzZ0TRSSoVas3jwMWuckyMujoGUUeDd8rLjTSCLlgUb3RiT9MbKfWdeCByme5MZ21lvMu6OmMFn8iDb5erLSBJ8bZFq6ruGIVzU8NI833IahlIO9m6JIR4L_go8Szu-1MYPGUjOKDsxc-Fp3fR-Kb0HFAEEs44t9vL9yMKjNeQeAp7Fo2AukDNEZqvEObP7XWLdJFA-TuAXE1f7o49lMr0y4Tqy2XeDKwfklO0bAnbSryZubRg2E7gjiwaiSYVIFphotLlpCd3N4MU46JjHA2dv1GtIe8749HinwhK1stes3PbZb9Gwm2LyK89iRJ35bCmDLnkwP0rTwTZ2Ul8 +RS384_privateExponent=NYqTtADsTaodhLKOi_TAGSMoNLJD6nOQU7GkMIdxVjugSyRqTU0h0eZQNbGXeIZzRlVobESPQOZjsn-xqNKcnvV4EDEW4HdGUXUOKw6MxC_GmnnCamyEBpnCFQFm4_wUaMA-gQnpQwQ2UcpC6Maindu4PXoGp0H9-75NVdpNRUDHO5xY2Ybp2kVv__sUWCKRLK7JaKRA3iGlXFpLPSy9DvmBjy27z1t_z58_vrPrVset7muMwMTwgfDv-EP3NBH4eTg7_Cy952MuhxaUUsHW85LZuv5t_rcoUF8kfDWcclP3a954lAzCbVjdngpvaShNbiTKAf9-bMQKG66v63W54Q +## RS512 +RS512_modulus=AKuc75KyKNwteumhyN5Kxa4ipQZrE_ouULtMZmCYI3Y32oCv3wWkgmrprBo-yCK292wfn77dNdZ9h5OoY-6sDVG-OKi9uwXpFcopyqIdsYOrw-4FKHxpr_7b--cH6HRmGlSFKVJpwfvIjD9Mu8S9bhNgnXfbKoYLcANU7Vjtacr3MvX-U406eRXLI9lZNr6ViQxSJw3A7yYMo2XYMYhO-FHGOYeV815q7fJFUMoCUMNSWlCx-pUCVGg0PuCKlOhUGIoLqvuFqUnBNd0hoAJCtmqya4_e3DLNzOgr2HOEbX7kQEjpi0XdyQ0fbFTAYO9TpXT2gldnmOElZ4UE2lX8J6M +RS512_privateExponent=doYyFGAFxmOG43tAbv63XthAn5kut_hq-6D9iDMrMsfKmlxdLNl81XhDy_CWaxtw8PU6cCj5uQUDsSB4vGuJ224EVc6ML73WtcR9VdAqPOVRsb9QQfUAf4XRibO1gUbPYpaBfpDaUBonesR1XqDyOGHe_9uXl_KoTzTFpEh8a5eCk9mwz85bb08PxQUut5DFdzPPyTi8_k3m7hry97I0TMbHUTiTqjgFpq2ZqSn4KQz77uft1oMwJLvlNHP6Fs25aVYrgAWw1DfTcCDwPAKxXlCD4ZPfGN2LmxZWCYxj1HLVKmQkrjX-FSgvpHs7YUqzJ19whmrTEtODnhWvZhTucQ +## ES256 +ES256_d=AIiNVUvr6-ChpOv2F7HNXyS2pYuoLF3ZqF2kTP0XquzB +## ES384 +ES384_d=S5iDyZaSar7cqcCKYFC1VGVKAXmwdOSHRMrwbrEd_WvmIYi3u8PwHFYAmA0PEwLF +## ES512 +ES512_d=AbedxoxLdftbJpXMYWlcuJkEF6iRotCxYYbH18NyEuOka_vS5dLV6m6Bhx_y_y9NgTQzP5SGzfpkSpgF6JVG7eFL + +# Form Interaction +loginFormUsername = username +loginFormPassword = password +loginFormLoginButton = +authorizeFormAllowButton = confirmed +authorizeFormDoNotAllowButton = cancel \ No newline at end of file diff --git a/oxAuth/Client/src/test/resources/interop_Nov_Matake_Test.properties b/oxAuth/Client/src/test/resources/interop_Nov_Matake_Test.properties new file mode 100644 index 00000000..a4fda3cf --- /dev/null +++ b/oxAuth/Client/src/test/resources/interop_Nov_Matake_Test.properties @@ -0,0 +1,39 @@ +swdResource=https://connect-op.heroku.com +authorizationEndpoint = +tokenEndpoint = +userInfoEndpoint = +checkSessionIFrame = +endSessionEndpoint = +jwksUri = +registrationEndpoint = + +userId = +userSecret = +redirectUri = https://${test.server.name}/oxauth-rp/home.htm?foo=bar +redirectUris = https://${test.server.name}/oxauth-rp/home.htm https://client.example.com/cb https://client.example.com/cb1 https://client.example.com/cb2 +sectorIdentifierUri = https://${test.server.name}/oxauth-client/test/resources/sector_identifier.js + +## Client Resources +clientJwksUri=https://${test.server.name}/oxauth-client/test/resources/jwks.json +## RS256 +RS256_modulus=AJpGcIVu7fmQJLHXeAClhXaJD7SvuABjYiPcT9IbKFWGWj51GgD-CxtyrQGXT0ctGEEsXOzMZM40q-V7GR-5qkJ_OalVTTc_EeKAHao45bZPsPHLxvusNfrfpyhc6JjF2TQhoOqxbgMgQ9L6W9q9fSjgzx-tPlD0d3X0GZOEQ_NYGstZWRRBwHgsxA2IRYtwSH-v76yPpxF9poLIWdnBKtKfSr6UY7p1BrLmMm0DdMhjQLn6j4S_eB-p2WyBwObvsLqO6FdClpZFtGr82Km2uinpHvZ6KJ_MUEW1sijPPI3rIGbaUbLtQJwX5GVynAP5qU2qRVkcsrKt-GeNoz6QNLM +RS256_privateExponent=RkIKAFpyehMRAwTTm8fFriPhSTI1I8ge66HroA3KIpjbBFKkEwue11M0QuM7sXhx8UxYzWaQCfCm0A1tdatCRKJYCivUzHImnPYnjFv5ETvdo2BgMEFPG_86ywD01I5Vyo3-EKPZLAdHnA90QXvGQhWPfieRl6CdvtP5ydqUb39aPazZKzPx4v6hj-7wrrwrQmYq-7li_urR2zhz5HvE_eE66i1xhGTI8VdV9VE2y06Zbzn54qL7kf0nsjvg4X9ERdXv4kkhRwSSc3CExJ-iNbo0n3nQ3KovhpA1FEzd9cwI_2EfLRIvVjJRMTnPHCLmz_8-Htn2Kpi3vV9MWHab8Q +## RS384 +RS384_modulus=AJ125IzZ0TRSSoVas3jwMWuckyMujoGUUeDd8rLjTSCLlgUb3RiT9MbKfWdeCByme5MZ21lvMu6OmMFn8iDb5erLSBJ8bZFq6ruGIVzU8NI833IahlIO9m6JIR4L_go8Szu-1MYPGUjOKDsxc-Fp3fR-Kb0HFAEEs44t9vL9yMKjNeQeAp7Fo2AukDNEZqvEObP7XWLdJFA-TuAXE1f7o49lMr0y4Tqy2XeDKwfklO0bAnbSryZubRg2E7gjiwaiSYVIFphotLlpCd3N4MU46JjHA2dv1GtIe8749HinwhK1stes3PbZb9Gwm2LyK89iRJ35bCmDLnkwP0rTwTZ2Ul8 +RS384_privateExponent=NYqTtADsTaodhLKOi_TAGSMoNLJD6nOQU7GkMIdxVjugSyRqTU0h0eZQNbGXeIZzRlVobESPQOZjsn-xqNKcnvV4EDEW4HdGUXUOKw6MxC_GmnnCamyEBpnCFQFm4_wUaMA-gQnpQwQ2UcpC6Maindu4PXoGp0H9-75NVdpNRUDHO5xY2Ybp2kVv__sUWCKRLK7JaKRA3iGlXFpLPSy9DvmBjy27z1t_z58_vrPrVset7muMwMTwgfDv-EP3NBH4eTg7_Cy952MuhxaUUsHW85LZuv5t_rcoUF8kfDWcclP3a954lAzCbVjdngpvaShNbiTKAf9-bMQKG66v63W54Q +## RS512 +RS512_modulus=AKuc75KyKNwteumhyN5Kxa4ipQZrE_ouULtMZmCYI3Y32oCv3wWkgmrprBo-yCK292wfn77dNdZ9h5OoY-6sDVG-OKi9uwXpFcopyqIdsYOrw-4FKHxpr_7b--cH6HRmGlSFKVJpwfvIjD9Mu8S9bhNgnXfbKoYLcANU7Vjtacr3MvX-U406eRXLI9lZNr6ViQxSJw3A7yYMo2XYMYhO-FHGOYeV815q7fJFUMoCUMNSWlCx-pUCVGg0PuCKlOhUGIoLqvuFqUnBNd0hoAJCtmqya4_e3DLNzOgr2HOEbX7kQEjpi0XdyQ0fbFTAYO9TpXT2gldnmOElZ4UE2lX8J6M +RS512_privateExponent=doYyFGAFxmOG43tAbv63XthAn5kut_hq-6D9iDMrMsfKmlxdLNl81XhDy_CWaxtw8PU6cCj5uQUDsSB4vGuJ224EVc6ML73WtcR9VdAqPOVRsb9QQfUAf4XRibO1gUbPYpaBfpDaUBonesR1XqDyOGHe_9uXl_KoTzTFpEh8a5eCk9mwz85bb08PxQUut5DFdzPPyTi8_k3m7hry97I0TMbHUTiTqjgFpq2ZqSn4KQz77uft1oMwJLvlNHP6Fs25aVYrgAWw1DfTcCDwPAKxXlCD4ZPfGN2LmxZWCYxj1HLVKmQkrjX-FSgvpHs7YUqzJ19whmrTEtODnhWvZhTucQ +## ES256 +ES256_d=AIiNVUvr6-ChpOv2F7HNXyS2pYuoLF3ZqF2kTP0XquzB +## ES384 +ES384_d=S5iDyZaSar7cqcCKYFC1VGVKAXmwdOSHRMrwbrEd_WvmIYi3u8PwHFYAmA0PEwLF +## ES512 +ES512_d=AbedxoxLdftbJpXMYWlcuJkEF6iRotCxYYbH18NyEuOka_vS5dLV6m6Bhx_y_y9NgTQzP5SGzfpkSpgF6JVG7eFL + +# Form Interaction +loginFormUsername = +loginFormPassword = +loginFormLoginButton = +authorizeFormAllowButton = +authorizeFormDoNotAllowButton = \ No newline at end of file diff --git a/oxAuth/Client/src/test/resources/interop_Oreo.properties b/oxAuth/Client/src/test/resources/interop_Oreo.properties new file mode 100644 index 00000000..e69de29b diff --git a/oxAuth/Client/src/test/resources/interop_Roland_Hedberg_Test.properties b/oxAuth/Client/src/test/resources/interop_Roland_Hedberg_Test.properties new file mode 100644 index 00000000..dfb1e1f8 --- /dev/null +++ b/oxAuth/Client/src/test/resources/interop_Roland_Hedberg_Test.properties @@ -0,0 +1,39 @@ +#swdResource=https://xenosmilus2.umdc.umu.se:8091 +authorizationEndpoint = https://xenosmilus2.umdc.umu.se:8091/authorization +tokenEndpoint = https://xenosmilus2.umdc.umu.se:8091/token +userInfoEndpoint = https://xenosmilus2.umdc.umu.se:8091/userinfo +checkSessionIFrame = +endSessionEndpoint = +jwksUri = https://xenosmilus2.umdc.umu.se:8091/static/jwks.json +registrationEndpoint = https://xenosmilus2.umdc.umu.se:8091/registration + +userId = diana +userSecret = krall +redirectUri = https://${test.server.name}/oxauth-rp/home.htm?foo=bar +redirectUris = https://${test.server.name}/oxauth-rp/home.htm https://client.example.com/cb https://client.example.com/cb1 https://client.example.com/cb2 +sectorIdentifierUri = https://${test.server.name}/oxauth-client/test/resources/sector_identifier.js + +## Client Resources +clientJwksUri=https://${test.server.name}/oxauth-client/test/resources/jwks.json +## RS256 +RS256_modulus=AJpGcIVu7fmQJLHXeAClhXaJD7SvuABjYiPcT9IbKFWGWj51GgD-CxtyrQGXT0ctGEEsXOzMZM40q-V7GR-5qkJ_OalVTTc_EeKAHao45bZPsPHLxvusNfrfpyhc6JjF2TQhoOqxbgMgQ9L6W9q9fSjgzx-tPlD0d3X0GZOEQ_NYGstZWRRBwHgsxA2IRYtwSH-v76yPpxF9poLIWdnBKtKfSr6UY7p1BrLmMm0DdMhjQLn6j4S_eB-p2WyBwObvsLqO6FdClpZFtGr82Km2uinpHvZ6KJ_MUEW1sijPPI3rIGbaUbLtQJwX5GVynAP5qU2qRVkcsrKt-GeNoz6QNLM +RS256_privateExponent=RkIKAFpyehMRAwTTm8fFriPhSTI1I8ge66HroA3KIpjbBFKkEwue11M0QuM7sXhx8UxYzWaQCfCm0A1tdatCRKJYCivUzHImnPYnjFv5ETvdo2BgMEFPG_86ywD01I5Vyo3-EKPZLAdHnA90QXvGQhWPfieRl6CdvtP5ydqUb39aPazZKzPx4v6hj-7wrrwrQmYq-7li_urR2zhz5HvE_eE66i1xhGTI8VdV9VE2y06Zbzn54qL7kf0nsjvg4X9ERdXv4kkhRwSSc3CExJ-iNbo0n3nQ3KovhpA1FEzd9cwI_2EfLRIvVjJRMTnPHCLmz_8-Htn2Kpi3vV9MWHab8Q +## RS384 +RS384_modulus=AJ125IzZ0TRSSoVas3jwMWuckyMujoGUUeDd8rLjTSCLlgUb3RiT9MbKfWdeCByme5MZ21lvMu6OmMFn8iDb5erLSBJ8bZFq6ruGIVzU8NI833IahlIO9m6JIR4L_go8Szu-1MYPGUjOKDsxc-Fp3fR-Kb0HFAEEs44t9vL9yMKjNeQeAp7Fo2AukDNEZqvEObP7XWLdJFA-TuAXE1f7o49lMr0y4Tqy2XeDKwfklO0bAnbSryZubRg2E7gjiwaiSYVIFphotLlpCd3N4MU46JjHA2dv1GtIe8749HinwhK1stes3PbZb9Gwm2LyK89iRJ35bCmDLnkwP0rTwTZ2Ul8 +RS384_privateExponent=NYqTtADsTaodhLKOi_TAGSMoNLJD6nOQU7GkMIdxVjugSyRqTU0h0eZQNbGXeIZzRlVobESPQOZjsn-xqNKcnvV4EDEW4HdGUXUOKw6MxC_GmnnCamyEBpnCFQFm4_wUaMA-gQnpQwQ2UcpC6Maindu4PXoGp0H9-75NVdpNRUDHO5xY2Ybp2kVv__sUWCKRLK7JaKRA3iGlXFpLPSy9DvmBjy27z1t_z58_vrPrVset7muMwMTwgfDv-EP3NBH4eTg7_Cy952MuhxaUUsHW85LZuv5t_rcoUF8kfDWcclP3a954lAzCbVjdngpvaShNbiTKAf9-bMQKG66v63W54Q +## RS512 +RS512_modulus=AKuc75KyKNwteumhyN5Kxa4ipQZrE_ouULtMZmCYI3Y32oCv3wWkgmrprBo-yCK292wfn77dNdZ9h5OoY-6sDVG-OKi9uwXpFcopyqIdsYOrw-4FKHxpr_7b--cH6HRmGlSFKVJpwfvIjD9Mu8S9bhNgnXfbKoYLcANU7Vjtacr3MvX-U406eRXLI9lZNr6ViQxSJw3A7yYMo2XYMYhO-FHGOYeV815q7fJFUMoCUMNSWlCx-pUCVGg0PuCKlOhUGIoLqvuFqUnBNd0hoAJCtmqya4_e3DLNzOgr2HOEbX7kQEjpi0XdyQ0fbFTAYO9TpXT2gldnmOElZ4UE2lX8J6M +RS512_privateExponent=doYyFGAFxmOG43tAbv63XthAn5kut_hq-6D9iDMrMsfKmlxdLNl81XhDy_CWaxtw8PU6cCj5uQUDsSB4vGuJ224EVc6ML73WtcR9VdAqPOVRsb9QQfUAf4XRibO1gUbPYpaBfpDaUBonesR1XqDyOGHe_9uXl_KoTzTFpEh8a5eCk9mwz85bb08PxQUut5DFdzPPyTi8_k3m7hry97I0TMbHUTiTqjgFpq2ZqSn4KQz77uft1oMwJLvlNHP6Fs25aVYrgAWw1DfTcCDwPAKxXlCD4ZPfGN2LmxZWCYxj1HLVKmQkrjX-FSgvpHs7YUqzJ19whmrTEtODnhWvZhTucQ +## ES256 +ES256_d=AIiNVUvr6-ChpOv2F7HNXyS2pYuoLF3ZqF2kTP0XquzB +## ES384 +ES384_d=S5iDyZaSar7cqcCKYFC1VGVKAXmwdOSHRMrwbrEd_WvmIYi3u8PwHFYAmA0PEwLF +## ES512 +ES512_d=AbedxoxLdftbJpXMYWlcuJkEF6iRotCxYYbH18NyEuOka_vS5dLV6m6Bhx_y_y9NgTQzP5SGzfpkSpgF6JVG7eFL + +# Form Interaction +loginFormUsername = login +loginFormPassword = password +loginFormLoginButton = form.commit +authorizeFormAllowButton = +authorizeFormDoNotAllowButton = \ No newline at end of file diff --git a/oxAuth/Client/src/test/resources/interop_Ryo_Ito_Test.properties b/oxAuth/Client/src/test/resources/interop_Ryo_Ito_Test.properties new file mode 100644 index 00000000..e69de29b diff --git a/oxAuth/Client/src/test/resources/interop_Wenou.properties b/oxAuth/Client/src/test/resources/interop_Wenou.properties new file mode 100644 index 00000000..e69de29b diff --git a/oxAuth/Client/src/test/resources/interop_rohe_config.py b/oxAuth/Client/src/test/resources/interop_rohe_config.py new file mode 100644 index 00000000..99f4a1f4 --- /dev/null +++ b/oxAuth/Client/src/test/resources/interop_rohe_config.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python + +import json + +info = { + "interaction": [{ + "matches": { + "url": "https://${test.server.name}/oxauth/login.seam" + }, + "page-type": "login", + "control": { + "type": "form", + "set": { + "loginForm:username": "${auth.user.uid}", + "loginForm:password": "${auth.user.password}" + } + } + }, { + "matches": { + "url": "https://${test.server.name}/oxauth/authorize.seam" + }, + "page-type": "user-consent", + "control": { + "type": "form", + "click": "authorizeForm:allowButton" + } + }], + "provider": { + "version": { + "oauth": "2.0", + "openid": "3.0" + }, + "dynamic": "https://${test.server.name}" + }, + "features": { + "registration": True, + "discovery": True, + "session_management": False, + "key_export": True + }, + "client": { + "redirect_uris": ["https://${test.server.name}/oxauth-rp/home.htm?foo=bar"], + "contact": ["yuriy@gluu.com"], + "application_type": "web", + "application_name": "OIC test tool", + "key_export_url": "https://${test.server.name}/oxauth-client/test/resources/jwks.json", + "keys": { + "RSA": { + "key": "keys/pyoidc", + "use": ["enc", "sig"] + } + }, + "preferences": { + "subject_type": ["pairwise", "public"], + "request_object_signing_alg": ["RS256", "RS384", "RS512", + "HS512", "HS384", "HS256"], + "token_endpoint_auth_methods_supported": ["client_secret_basic", + "client_secret_post", + "client_secret_jwt", + "private_key_jwt"], + "id_token_signed_response_alg": ["RS256", "RS384", "RS512", + "HS512", "HS384", "HS256"], + "default_max_age": 3600, + "require_auth_time": True, + "default_acr": ["2", "1"] + } + } +} + +print json.dumps(info) diff --git a/oxAuth/Client/src/test/resources/interop_rohe_python_config.json b/oxAuth/Client/src/test/resources/interop_rohe_python_config.json new file mode 100644 index 00000000..f5cfa559 --- /dev/null +++ b/oxAuth/Client/src/test/resources/interop_rohe_python_config.json @@ -0,0 +1,35 @@ +#!/usr/bin/env python + +import json + +from default import DEFAULT + +info = DEFAULT.copy() + +info["provider"] = {"dynamic": "https://${test.server.name}"} + +info["interaction"] = [ + { + "matches": { + "url": "https://${test.server.name}/oxauth/login.seam" + }, + "page-type": "login", + "control": { + "type": "form", + "set": {"loginForm:username": "${auth.user.uid}", + "loginForm:password": "${auth.user.password}"} + } + }, + { + "matches": { + "url": "https://${test.server.name}/oxauth/authorize.seam" + }, + "page-type": "user-consent", + "control": { + "type": "form", + "click": "authorizeForm:allowButton" + } + } +] + +print json.dumps(info) diff --git a/oxAuth/Client/src/test/resources/oxauth_test_client_keys.zip b/oxAuth/Client/src/test/resources/oxauth_test_client_keys.zip new file mode 100644 index 0000000000000000000000000000000000000000..980322509687311c461ccda5bf34d0049430ebde GIT binary patch literal 9015 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

}Hzi-R=09{j#MF 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 + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/oxAuth/Client/src/test/resources/testng-multi-authz.xml b/oxAuth/Client/src/test/resources/testng-multi-authz.xml new file mode 100644 index 00000000..383e8fad --- /dev/null +++ b/oxAuth/Client/src/test/resources/testng-multi-authz.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/oxAuth/Client/src/test/resources/testng.properties b/oxAuth/Client/src/test/resources/testng.properties new file mode 100644 index 00000000..a9181f5a --- /dev/null +++ b/oxAuth/Client/src/test/resources/testng.properties @@ -0,0 +1,61 @@ +## Developer test ng properties (convenient way to run separate test without suite) ## +swdResource=${swd.resource} +userId=${auth.user.uid} +userSecret=${auth.user.password} +userId2=${auth.user2.uid} +userSecret2=${auth.user2.password} +userInum=${auth.user.inum} +userEmail=${auth.user.email} +clientId=${auth.client.id} +clientSecret=${auth.client.secret} +redirectUri=https://${test.server.name}/oxauth-rp/home.htm +redirectUris=https://${test.server.name}/oxauth-rp/home.htm https://client.example.com/cb https://client.example.com/cb1 https://client.example.com/cb2 +#redirectUris=https://${test.server.name}/oxauth-rp/home.htm https://client.example.com/cb https://client.example.com/cb1 https://client.example.com/cb2 https://openid.implicit.client.test/login-callback.html +logoutUri=https://${test.server.name}/oxauth-rp/home.htm +postLogoutRedirectUri=https://client.example.com/pl +initiateLoginUri=https://client.example.com/start-3rd-party-initiated-sso +hostnameVerifier=default +## By enabling this block, you require a place to publish files via HTTP or HTTPS +#requestFileBasePath=/var/www/html/oxAuth +requestFileBaseUrl=http://localhost/oxAuth +sectorIdentifierUri=https://${test.server.name}/oxauth/sectoridentifier/${sector.identifier.id} +#sectorIdentifierUri=https://${test.server.name}/sectoridentifier/${sector.identifier.id} + +umaMetaDataUrl=https://${test.server.name}/oxauth/restv1/uma2-configuration +umaUserId=${uma.user.uid} +umaUserSecret=${uma.user.password} +umaPatClientId=${uma.pat.client.id} +umaPatClientSecret=${uma.pat.client.secret} +umaRedirectUri=https://client.example.com/cb?foo=bar +umaClaimsRedirectUri=https://client.example.com/cb?foo=bar + +## Client Resources +dnName=CN=oxAuth CA Certificates +#keyStoreFile=/Users/JAVIER/tmp/mytestkeystore +keyStoreFile=${clientKeyStoreFile} +keyStoreSecret=${clientKeyStoreSecret} +clientJwksUri=https://${test.server.name}/oxauth-client/test/resources/jwks.json +#clientJwksUri=http://localhost/oxauth-client/test/resources/jwks.json +#clientJwksUri=https://ce.gluu.test/resources/jwks.json +RS256_keyId=6fb1859a-54d9-47c6-a293-92ce2cee63e0 +RS384_keyId=a68c61dd-f8f6-4faf-855b-fbbb8bee028a +RS512_keyId=79d12e66-0baa-4b59-8a8b-bd3164260bf5 +ES256_keyId=a8b62c9d-65ea-4384-a491-e52924c4a0e3 +ES384_keyId=0b1a019f-fcfb-4d3d-981b-16b45355dfdf +ES512_keyId=07c917ef-943f-4a9a-961c-d3cba28c81d5 +PS256_keyId=29cef404-59db-4ab9-8f5c-6da8d578d107 +PS384_keyId=6bd7cc0c-e176-4da9-b646-fe7782393dc0 +PS512_keyId=a614d6ae-e80f-469a-a304-51b9bbefc95f +RSA_OAEP_keyId=d91db51d-0e7f-4225-99e5-164444c12d1a +RSA1_5_keyId=a442f0ec-7237-40b3-b7f3-a6039f70d9bd + +# Form Interaction +loginFormUsername = username +loginFormPassword = password +loginFormLoginButton = loginButton +authorizeFormAllowButton = authorizeForm:allowButton +authorizeFormDoNotAllowButton = authorizeForm:doNotAllowButton + +# CIBA +backchannelClientNotificationEndpoint = https://${test.server.name}/oxauth-rp/home.htm +backchannelUserCode = 59b335fb-a2df-4275-be43-1b8d3cc9a5c5 \ No newline at end of file diff --git a/oxAuth/Client/src/test/resources/testng.xml b/oxAuth/Client/src/test/resources/testng.xml new file mode 100644 index 00000000..d4102ae3 --- /dev/null +++ b/oxAuth/Client/src/test/resources/testng.xml @@ -0,0 +1,854 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/oxAuth/LICENSE b/oxAuth/LICENSE new file mode 100644 index 00000000..43f890fd --- /dev/null +++ b/oxAuth/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2020 Gluu, Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/oxAuth/Model/.gitignore b/oxAuth/Model/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/oxAuth/Model/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/oxAuth/Model/pom.xml b/oxAuth/Model/pom.xml new file mode 100644 index 00000000..e5b2bc10 --- /dev/null +++ b/oxAuth/Model/pom.xml @@ -0,0 +1,170 @@ + + + 4.0.0 + oxauth-model + oxAuth Model + jar + + + org.gluu + oxauth + 4.5.6-SNAPSHOT + + + + ${maven.min-version} + + + + oxauth-model + + + + src/main/resources + + **/*.xml + **/*.properties + **/*.js + + + + + + + src/test/resources + + **/*.json + **/*.xml + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + false + + + + + + + + com.google.guava + guava + + + org.gluu + oxeleven-client + + + org.gluu + oxeleven-model + + + org.jboss.resteasy + resteasy-jaxb-provider + + + org.gluu + gluu-orm-annotation + + + org.gluu + oxcore-util + + + org.gluu + oxcore-model + + + org.jboss.resteasy + resteasy-client + + + org.json + json + + + com.fasterxml.jackson.datatype + jackson-datatype-json-org + + + com.fasterxml.jackson.module + jackson-module-jaxb-annotations + + + commons-codec + commons-codec + + + commons-lang + commons-lang + + + commons-io + commons-io + + + + + org.apache.logging.log4j + log4j-api + + + org.apache.logging.log4j + log4j-1.2-api + + + + org.testng + testng + + + + + org.bouncycastle + bcprov-jdk18on + provided + + + org.bouncycastle + bcpkix-jdk18on + provided + + + + com.nimbusds + nimbus-jose-jwt + + + org.bitbucket.b_c + jose4j + + + + javax.validation + validation-api + + + org.jetbrains + annotations + + + com.sun.mail + jakarta.mail + + + + + com.wordnik + swagger-annotations + + + + + \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/authorize/AuthorizeErrorResponseType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/authorize/AuthorizeErrorResponseType.java new file mode 100644 index 00000000..ee8e3621 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/authorize/AuthorizeErrorResponseType.java @@ -0,0 +1,191 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.authorize; + +import org.gluu.oxauth.model.error.IErrorType; + +/** + * Error codes for authorization error responses. + * + * @author Javier Rojas Blum Date: 09.22.2011 + */ +public enum AuthorizeErrorResponseType implements IErrorType { + + /** + * The request is missing a required parameter, includes an + * invalid parameter value, includes a parameter more than + * once, or is otherwise malformed. + */ + INVALID_REQUEST("invalid_request"), + + /** + * The client is not authorized to request an authorization + * code / access token using this method. + */ + UNAUTHORIZED_CLIENT("unauthorized_client"), + + /** + * The client is disabled and can't request an access token using this method. + */ + DISABLED_CLIENT("disabled_client"), + + /** + * The resource owner or authorization server denied the request. + */ + ACCESS_DENIED("access_denied"), + + /** + * AS requires RP to re-send authorization request. + */ + RETRY("retry"), + + /** + * The authorization server does not support obtaining an access token using + * this method. + */ + UNSUPPORTED_RESPONSE_TYPE("unsupported_response_type"), + + /** + * The requested scope is invalid, unknown, or malformed. + */ + INVALID_SCOPE("invalid_scope"), + + /** + * The authorization server encountered an unexpected condition which + * prevented it from fulfilling the request. + */ + SERVER_ERROR("server_error"), + + /** + * The authorization server is currently unable to handle the request due to + * a temporary overloading or maintenance of the server. + */ + TEMPORARILY_UNAVAILABLE("temporarily_unavailable"), + + /** + * The redirect_uri in the Authorization Request does not match any of the + * Client's pre-registered redirect_uris. + */ + INVALID_REQUEST_REDIRECT_URI("invalid_request_redirect_uri"), + + /** + * The Authorization Server requires End-User authentication. This error MAY + * be returned when the prompt parameter in the Authorization Request is set + * to none to request that the Authorization Server should not display any + * user interfaces to the End-User, but the Authorization Request cannot be + * completed without displaying a user interface for user authentication. + */ + LOGIN_REQUIRED("login_required"), + + /** + * The End-User is required to select a session at the Authorization Server. + * The End-User MAY be authenticated at the Authorization Server with + * different associated accounts, but the End-User did not select a session. + * This error MAY be returned when the prompt parameter in the Authorization + * Request is set to none to request that the Authorization Server should + * not display any user interfaces to the End-User, but the Authorization + * Request cannot be completed without displaying a user interface to + * prompt for a session to use. + */ + SESSION_SELECTION_REQUIRED("session_selection_required"), + + /** + * The Authorization Server requires End-User consent. This error MAY be + * returned when the prompt parameter in the Authorization Request is set to + * none to request that the Authorization Server should not display any user + * interfaces to the End-User, but the Authorization Request cannot be + * completed without displaying a user interface for End-User consent. + */ + CONSENT_REQUIRED("consent_required"), + + /** + * The current logged in End-User at the Authorization Server does not match + * the requested user. This error MAY be returned when the prompt parameter + * in the Authorization Request is set to none to request that the Authorization + * Server should not display any user interfaces to the End-User, but the + * Authorization Request cannot be completed without displaying a user interface + * to prompt for the correct End-User authentication. + */ + USER_MISMATCHED("user_mismatched"), + + /** + * "request" parameter is supported by AS. But if it's switched off in configuration by setting + * requestParameterSupported=false then this error is returned from authorization endpoint. + */ + REQUEST_NOT_SUPPORTED("request_not_supported"), + + /** + * "request_uri" parameter is supported by AS. But if it's switched off in configuration by setting + * requestUriParameterSupported=false then this error is returned from authorization endpoint. + */ + REQUEST_URI_NOT_SUPPORTED("request_uri_not_supported"), + + /** + * The request_uri in the Authorization Request returns an error or invalid data. + */ + INVALID_REQUEST_URI("invalid_request_uri"), + + /** + * The request parameter contains an invalid OpenID Request Object. + */ + INVALID_REQUEST_OBJECT("invalid_request_object"), + + /** + * The authorization server can't handle user authentication due to session expiration + */ + AUTHENTICATION_SESSION_INVALID("authentication_session_invalid"), + + /** + * The authorization server can't handle user authentication due to error caused by ACR + */ + INVALID_AUTHENTICATION_METHOD("invalid_authentication_method"); + + private final String paramName; + + private AuthorizeErrorResponseType(String paramName) { + this.paramName = paramName; + } + + /** + * Return the corresponding enumeration from a string parameter. + * + * @param param The parameter to be match. + * @return The enumeration if found, otherwise + * null. + */ + public static AuthorizeErrorResponseType fromString(String param) { + if (param != null) { + for (AuthorizeErrorResponseType err : AuthorizeErrorResponseType + .values()) { + if (param.equals(err.paramName)) { + return err; + } + } + } + return null; + } + + + /** + * Returns a string representation of the object. In this case, the lower + * case code of the error. + */ + @Override + public String toString() { + return paramName; + } + + /** + * Gets error parameter. + * + * @return error parameter + */ + @Override + public String getParameter() { + return paramName; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/authorize/AuthorizeRequestParam.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/authorize/AuthorizeRequestParam.java new file mode 100644 index 00000000..6b1f9ac0 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/authorize/AuthorizeRequestParam.java @@ -0,0 +1,49 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.authorize; + +/** + * @author Javier Rojas Blum + * @version October 7, 2019 + */ +public interface AuthorizeRequestParam { + + String ACCESS_TOKEN = "access_token"; + String RESPONSE_TYPE = "response_type"; + String CLIENT_ID = "client_id"; + String SCOPE = "scope"; + String REDIRECT_URI = "redirect_uri"; + String STATE = "state"; + String RESPONSE_MODE = "response_mode"; + String NONCE = "nonce"; + String DISPLAY = "display"; + String PROMPT = "prompt"; + String MAX_AGE = "max_age"; + String UI_LOCALES = "ui_locales"; + String CLAIMS_LOCALES = "claims_locales"; + String ID_TOKEN_HINT = "id_token_hint"; + String LOGIN_HINT = "login_hint"; + String ACR_VALUES = "acr_values"; + String AMR_VALUES = "amr_values"; + String CLAIMS = "claims"; + String REGISTRATION = "registration"; + String REQUEST = "request"; + String REQUEST_URI = "request_uri"; + String ORIGIN_HEADERS = "origin_headers"; + String CODE_CHALLENGE = "code_challenge"; + String CODE_CHALLENGE_METHOD = "code_challenge_method"; + String CUSTOM_RESPONSE_HEADERS = "custom_response_headers"; + String AUTH_REQ_ID = "auth_req_id"; + String SID = "sid"; + + /** + * String that represents the End-User's login state at the OP. + */ + String SESSION_ID = "session_id"; + + String REQUEST_SESSION_ID = "request_session_id"; +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/authorize/AuthorizeResponseParam.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/authorize/AuthorizeResponseParam.java new file mode 100644 index 00000000..49a4096a --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/authorize/AuthorizeResponseParam.java @@ -0,0 +1,31 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.authorize; + +/** + * @author Javier Rojas Blum + * @version August 9, 2017 + */ +public interface AuthorizeResponseParam { + + String CODE = "code"; + String ACCESS_TOKEN = "access_token"; + String TOKEN_TYPE = "token_type"; + String EXPIRES_IN = "expires_in"; + String SCOPE = "scope"; + String ID_TOKEN = "id_token"; + String STATE = "state"; + String SESSION_STATE = "session_state"; + + /** + * String that represents the End-User's login state at the OP. + */ + String SESSION_ID = "session_id"; + String SID = "sid"; + + String ACR_VALUES = "acr_values"; +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/authorize/CodeVerifier.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/authorize/CodeVerifier.java new file mode 100644 index 00000000..e9eb3692 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/authorize/CodeVerifier.java @@ -0,0 +1,141 @@ +package org.gluu.oxauth.model.authorize; + +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.binary.BaseNCodec; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.lang.RandomStringUtils; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 21/03/2016 + */ + +public class CodeVerifier { + + private static final int MAX_CODE_VERIFIER_LENGTH = 128; + private static final int MIN_CODE_VERIFIER_LENGTH = 43; + private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + + public enum CodeChallengeMethod { + PLAIN("plain", ""), + S256("s256", "SHA-256"); + + private String pkceString; + private String messageDigestString; + + private CodeChallengeMethod(String pkceString, String messageDigestString) { + this.pkceString = pkceString; + this.messageDigestString = messageDigestString; + } + + public String getMessageDigestString() { + return messageDigestString; + } + + public String getPkceString() { + return pkceString; + } + + public static CodeChallengeMethod fromString(String value) { + for (CodeChallengeMethod type : values()) { + if (type.getPkceString().equalsIgnoreCase(value)) { + return type; + } + } + return PLAIN; + } + } + + private String codeVerifier; + private String codeChallenge; + private CodeChallengeMethod transformationType; + + public CodeVerifier() { + this(CodeChallengeMethod.S256); + } + + public CodeVerifier(CodeChallengeMethod transformationType) { + this.codeVerifier = generateCodeVerifier(); + this.transformationType = transformationType; + this.codeChallenge = generateCodeChallenge(transformationType, codeVerifier); + } + + public static String generateCodeChallenge(CodeChallengeMethod codeChallengeMethod, String codeVerifier) { + Preconditions.checkNotNull(codeChallengeMethod); + Preconditions.checkNotNull(codeVerifier); + + switch (codeChallengeMethod) { + case PLAIN: + return codeVerifier; + case S256: + return s256(codeVerifier); + } + throw new RuntimeException("Unsupported code challenge method: " + codeChallengeMethod); + } + + public static boolean matched(String codeChallenge, String codeChallengeMethod, String codeVerifier) { + return matched(codeChallenge, CodeChallengeMethod.fromString(codeChallengeMethod), codeVerifier); + } + + public static boolean matched(String codeChallenge, CodeChallengeMethod codeChallengeMethod, String codeVerifier) { + if (Strings.isNullOrEmpty(codeChallenge) || codeChallengeMethod == null || Strings.isNullOrEmpty(codeVerifier)) { + return false; + } + return generateCodeChallenge(codeChallengeMethod, codeVerifier).equals(codeChallenge); + } + + public static String s256(String codeVerifier) { + byte[] sha256 = DigestUtils.sha256(codeVerifier); + return base64UrlEncode(sha256); + } + + public static String base64UrlEncode(byte[] input) { + Base64 base64 = new Base64(BaseNCodec.MIME_CHUNK_SIZE, EMPTY_BYTE_ARRAY, true); + return base64.encodeAsString(input); + } + + public static String generateCodeVerifier() { + String alphabetic = "abcdefghijklmnopqrstuvwxyz"; + String chars = alphabetic + alphabetic.toUpperCase() + + "1234567890" + "-._~"; + String code = RandomStringUtils.random(MAX_CODE_VERIFIER_LENGTH, chars); + Preconditions.checkState(isCodeVerifierValid(code)); + return code; + } + + public static boolean isCodeVerifierValid(String codeVerifier) { + if (codeVerifier == null) { + return false; + } + int length = codeVerifier.length(); + if (length > MAX_CODE_VERIFIER_LENGTH || length < MIN_CODE_VERIFIER_LENGTH) { + return false; + } + return true; + } + + public String getCodeChallenge() { + return codeChallenge; + } + + public String getCodeVerifier() { + return codeVerifier; + } + + public CodeChallengeMethod getTransformationType() { + return transformationType; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("CodeVerifier"); + sb.append("{codeVerifier='").append(codeVerifier).append('\''); + sb.append(", codeChallenge='").append(codeChallenge).append('\''); + sb.append(", transformationType=").append(transformationType); + sb.append('}'); + return sb.toString(); + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/authorize/DeviceAuthorizationRequestParam.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/authorize/DeviceAuthorizationRequestParam.java new file mode 100644 index 00000000..28c13942 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/authorize/DeviceAuthorizationRequestParam.java @@ -0,0 +1,24 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.authorize; + +/** + * RFC8628 section 3.1 + */ +public interface DeviceAuthorizationRequestParam { + + /** + * The client identifier as described in Section 2.2 of [RFC6749]. + */ + String CLIENT_ID = "client_id"; + + /** + * The scope of the access request as defined by Section 3.3 of [RFC6749]. + */ + String SCOPE = "scope"; + +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/authorize/DeviceAuthorizationResponseParam.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/authorize/DeviceAuthorizationResponseParam.java new file mode 100644 index 00000000..7bb301c5 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/authorize/DeviceAuthorizationResponseParam.java @@ -0,0 +1,50 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.authorize; + +/** + * RFC8628 section 3.2 + */ +public interface DeviceAuthorizationResponseParam { + + /** + * REQUIRED. The device verification code. + */ + String DEVICE_CODE = "device_code"; + + /** + * REQUIRED. The end-user verification code. + */ + String USER_CODE = "user_code"; + + /** + * REQUIRED. The end-user verification URI on the authorization + * server. The URI should be short and easy to remember as end users + * will be asked to manually type it into their user agent. + */ + String VERIFICATION_URI = "verification_uri"; + + /** + * OPTIONAL. A verification URI that includes the "user_code" (or + * other information with the same function as the "user_code"), + * which is designed for non-textual transmission. + */ + String VERIFICATION_URI_COMPLETE = "verification_uri_complete"; + + /** + * REQUIRED. The lifetime in seconds of the "device_code" and "user_code". + */ + String EXPIRES_IN = "expires_in"; + + /** + * OPTIONAL. The minimum amount of time in seconds that the client + * SHOULD wait between polling requests to the token endpoint. If no + * value is provided, clients MUST use 5 as the default. + */ + String INTERVAL = "interval"; + +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/authorize/DeviceAuthzErrorResponseType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/authorize/DeviceAuthzErrorResponseType.java new file mode 100644 index 00000000..1bfb5d85 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/authorize/DeviceAuthzErrorResponseType.java @@ -0,0 +1,63 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.authorize; + +import org.gluu.oxauth.model.error.IErrorType; + +/** + * Error codes for device authz error responses. + */ +public enum DeviceAuthzErrorResponseType implements IErrorType { + + INVALID_CLIENT("invalid_client"), + + INVALID_GRANT("invalid_grant"), + ; + + private final String paramName; + + private DeviceAuthzErrorResponseType(String paramName) { + this.paramName = paramName; + } + + /** + * Return the corresponding enumeration from a string parameter. + * + * @param param The parameter to be match. + * @return The enumeration if found, otherwise + * null. + */ + public static DeviceAuthzErrorResponseType fromString(String param) { + if (param != null) { + for (DeviceAuthzErrorResponseType err : DeviceAuthzErrorResponseType + .values()) { + if (param.equals(err.paramName)) { + return err; + } + } + } + return null; + } + + /** + * Returns a string representation of the object. In this case, the lower case code of the error. + */ + @Override + public String toString() { + return paramName; + } + + /** + * Gets error parameter. + * + * @return error parameter + */ + @Override + public String getParameter() { + return paramName; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/BackchannelAuthenticationErrorResponseType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/BackchannelAuthenticationErrorResponseType.java new file mode 100644 index 00000000..2738b98e --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/BackchannelAuthenticationErrorResponseType.java @@ -0,0 +1,128 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.ciba; + +import org.gluu.oxauth.model.error.IErrorType; + +/** + * @author Javier Rojas Blum + * @version August 20, 2019 + */ +public enum BackchannelAuthenticationErrorResponseType implements IErrorType { + + // HTTP 400 Bad Request + + /** + * The request is missing a required parameter, includes an invalid parameter + * value, includes a parameter more than once, contains more than one of the + * hints, or is otherwise malformed. + */ + INVALID_REQUEST("invalid_request"), + + /** + * The requested scope is invalid, unknown, or malformed. + */ + INVALID_SCOPE("invalid_scope"), + + /** + * The login_hint_token provided in the authentication request is not valid because + * it has expired. + */ + EXPIRED_LOGIN_HINT_TOKEN("expired_login_hint_token"), + + /** + * The OpenID Provider is not able to identify which end-user the Client wishes to + * be authenticated by means of the hint provided in the request (login_hint_token, + * id_token_hint or login_hint). + */ + UNKNOWN_USER_ID("unknown_user_id"), + + /** + * The Client is not authorized to use this authentication flow. + */ + UNAUTHORIZED_CLIENT("unauthorized_client"), + + /** + * User code is required but was missing from the request. + */ + MISSING_USER_CODE("missing_user_code"), + + /** + * User code was invalid. + */ + INVALID_USER_CODE("invalid_user_code"), + + /** + * The binding message is invalid or unacceptable for use in the context of the + * given request. + */ + INVALID_BINDING_MESSAGE("invalid_binding_message"), + + // HTTP 401 Unauthorized + + /** + * Client authentication failed (e.g., invalid client credentials, unknown client, no + * client authentication included, or unsupported authentication method). + */ + INVALID_CLIENT("invalid_client"), + + /** + * The end-user has not registered a device to receive push notifications. + */ + UNAUTHORIZED_END_USER_DEVICE("unauthorized_end_user_device"), + + // HTTP 403 Forbidden + + /** + * The resource owner or OpenID Provider denied the CIBA (Client Initiated + * Backchannel Authentication) request. + */ + ACCESS_DENIED("access_denied"); + + private final String paramName; + + BackchannelAuthenticationErrorResponseType(String paramName) { + this.paramName = paramName; + } + + /** + * Returns the corresponding {@link BackchannelAuthenticationErrorResponseType} from a given string. + * + * @param param The string value to convert. + * @return The corresponding {@link BackchannelAuthenticationErrorResponseType}, otherwise null. + */ + public static BackchannelAuthenticationErrorResponseType fromString(String param) { + if (param != null) { + for (BackchannelAuthenticationErrorResponseType err : BackchannelAuthenticationErrorResponseType.values()) { + if (param.equals(err.paramName)) { + return err; + } + } + } + return null; + } + + /** + * Returns a string representation of the object. In this case the parameter name. + * + * @return The string representation of the object. + */ + @Override + public String toString() { + return paramName; + } + + /** + * Gets error parameter. + * + * @return error parameter + */ + @Override + public String getParameter() { + return paramName; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/BackchannelAuthenticationRequestParam.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/BackchannelAuthenticationRequestParam.java new file mode 100644 index 00000000..b166fc05 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/BackchannelAuthenticationRequestParam.java @@ -0,0 +1,80 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.ciba; + +/** + * @author Javier Rojas Blum + * @version May 28, 2020 + */ +public interface BackchannelAuthenticationRequestParam { + + /** + * The scope of the access request. + * CIBA authentication requests must contain the openid scope value. + * Other scope values may be present. + */ + String SCOPE = "scope"; + + /** + * A bearer token provided by the Client that will be used by the OpenID Provider to authenticate the callback + * request to the Client. Required if the Client is registered to use Ping or Push modes. + */ + String CLIENT_NOTIFICATION_TOKEN = "client_notification_token"; + + /** + * Requested Authentication Context Class Reference values. + */ + String ACR_VALUES = "acr_values"; + + /** + * A token containing information identifying the end-user for whom authentication is being requested. + */ + String LOGIN_HINT_TOKEN = "login_hint_token"; + + /** + * An ID Token previously issued to the Client by the OpenID Provider being passed back as a hint to identify + * the end-user for whom authentication is being requested. + */ + String ID_TOKEN_HINT = "id_token_hint"; + + /** + * A hint to the OpenID Provider regarding the end-user for whom authentication is being requested. + */ + String LOGIN_HINT = "login_hint"; + + /** + * A human readable identifier or message intended to be displayed on both the consumption device and the + * authentication device to interlock them together for the transaction by way of a visual cue for the end-user. + */ + String BINDING_MESSAGE = "binding_message"; + + /** + * A secret code, such as password or pin, known only to the user but verifiable by the OP. + * The code is used to authorize sending an authentication request to user's authentication device. + */ + String USER_CODE = "user_code"; + + /** + * A positive integer allowing the client to request the expires_in value for the auth_req_id the server will return. + */ + String REQUESTED_EXPIRY = "requested_expiry"; + + /** + * An string containing all data about the request as a single JWT + */ + String REQUEST = "request"; + + /** + * Url where OP could get the request object related to the authorization. + */ + String REQUEST_URI = "request_uri"; + + String CLIENT_ID = "client_id"; + String CLIENT_SECRET = "client_secret"; + String CLIENT_ASSERTION_TYPE = "client_assertion_type"; + String CLIENT_ASSERTION = "client_assertion"; +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/BackchannelAuthenticationResponseParam.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/BackchannelAuthenticationResponseParam.java new file mode 100644 index 00000000..fb13b6ff --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/BackchannelAuthenticationResponseParam.java @@ -0,0 +1,30 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.ciba; + +/** + * @author Javier Rojas Blum + * @version August 20, 2019 + */ +public interface BackchannelAuthenticationResponseParam { + + /** + * A unique identifier to identify the authentication request made by the Client. + */ + String AUTH_REQ_ID = "auth_req_id"; + + /** + * The expiration time of the "auth_req_id" in seconds since the authentication request was received. + */ + String EXPIRES_IN = "expires_in"; + + /** + * The minimum amount of time in seconds that the Client must wait between polling requests to the token endpoint. + * This parameter will only be present if the Client is registered to use the Poll or Ping modes. + */ + String INTERVAL = "interval"; +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/BackchannelDeviceRegistrationErrorResponseType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/BackchannelDeviceRegistrationErrorResponseType.java new file mode 100644 index 00000000..235450a0 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/BackchannelDeviceRegistrationErrorResponseType.java @@ -0,0 +1,84 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.ciba; + +import org.gluu.oxauth.model.error.IErrorType; + +/** + * @author Javier Rojas Blum + * @version August 20, 2019 + */ +public enum BackchannelDeviceRegistrationErrorResponseType implements IErrorType { + + // HTTP 400 Bad Request + + /** + * The request is missing a required parameter, includes an invalid parameter value, + * includes a parameter more than once, or is otherwise malformed. + */ + INVALID_REQUEST("invalid_request"), + + /** + * The OpenID Provider is not able to identify the end-user. + */ + UNKNOWN_USER_ID("unknown_user_id"), + + /** + * The Client is not authorized to use this authentication flow. + */ + UNAUTHORIZED_CLIENT("unauthorized_client"), + + // HTTP 403 Forbidden + + /** + * The resource owner or OpenID Provider denied the request. + */ + ACCESS_DENIED("access_denied"); + + private final String paramName; + + BackchannelDeviceRegistrationErrorResponseType(String paramName) { + this.paramName = paramName; + } + + /** + * Returns the corresponding {@link BackchannelDeviceRegistrationErrorResponseType} from a given string. + * + * @param param The string value to convert. + * @return The corresponding {@link BackchannelDeviceRegistrationErrorResponseType}, otherwise null. + */ + public static BackchannelDeviceRegistrationErrorResponseType fromString(String param) { + if (param != null) { + for (BackchannelDeviceRegistrationErrorResponseType err : BackchannelDeviceRegistrationErrorResponseType.values()) { + if (param.equals(err.paramName)) { + return err; + } + } + } + return null; + } + + /** + * Returns a string representation of the object. In this case the parameter name. + * + * @return The string representation of the object. + */ + @Override + public String toString() { + return paramName; + } + + /** + * Gets error parameter. + * + * @return error parameter + */ + @Override + public String getParameter() { + return paramName; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/FirebaseCloudMessagingRequestParam.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/FirebaseCloudMessagingRequestParam.java new file mode 100644 index 00000000..d75da05e --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/FirebaseCloudMessagingRequestParam.java @@ -0,0 +1,41 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.ciba; + +/** + * @author Javier Rojas Blum + * @version August 20, 2019 + */ +public interface FirebaseCloudMessagingRequestParam { + + /** + * This parameter specifies the recipient of a message. + * The value can be a device's registration token, a device group's notification key, or a single topic. + */ + String TO = "to"; + + /** + * This parameter specifies the predefined, user-visible key-value pairs of the notification payload. + */ + String NOTIFICATION = "notification"; + + /** + * The notification's title. + * This field is not visible on iOS phones and tablets. + */ + String TITLE = "title"; + + /** + * The notification's body text. + */ + String BODY = "body"; + + /** + * The action associated with a user click on the notification. + */ + String CLICK_ACTION = "click_action"; +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/FirebaseCloudMessagingResponseParam.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/FirebaseCloudMessagingResponseParam.java new file mode 100644 index 00000000..f8333cbb --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/FirebaseCloudMessagingResponseParam.java @@ -0,0 +1,41 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.ciba; + +/** + * @author Javier Rojas Blum + * @version August 20, 2019 + */ +public interface FirebaseCloudMessagingResponseParam { + + /** + * Unique ID (number) identifying the multicast message. + */ + String MULTICAST_ID = "multicast_id"; + + /** + * Number of messages that were processed without an error. + */ + String SUCCESS = "success"; + + /** + * Number of messages that could not be processed. + */ + String FAILURE = "failure"; + + /** + * Array of objects representing the status of the messages processed. + * The objects are listed in the same order as the request (i.e., for each registration ID in the request, + * its result is listed in the same index in the response). + */ + String RESULTS = "results"; + + /** + * String specifying a unique ID for each successfully processed message. + */ + String MESSAGE_ID = "message_id"; +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/PushErrorRequestParam.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/PushErrorRequestParam.java new file mode 100644 index 00000000..ae5fe991 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/PushErrorRequestParam.java @@ -0,0 +1,19 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.ciba; + +/** + * @author Javier Rojas Blum + * @version May 9, 2019 + */ +public interface PushErrorRequestParam { + + String AUTHORIZATION_REQUEST_ID = "auth_req_id"; + String ERROR = "error"; + String ERROR_DESCRIPTION = "error_description"; + String ERROR_URI = "error_uri"; +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/PushErrorResponseType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/PushErrorResponseType.java new file mode 100644 index 00000000..4455b297 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/PushErrorResponseType.java @@ -0,0 +1,76 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.ciba; + +import org.gluu.oxauth.model.error.IErrorType; + +/** + * @author Javier Rojas Blum + * @version May 9, 2020 + */ +public enum PushErrorResponseType implements IErrorType { + + /** + * The end-user denied the authorization request. + */ + ACCESS_DENIED("access_denied"), + + /** + * The auth_req_id has expired. The Client will need to make a new Authentication Request. + */ + EXPIRED_TOKEN("expired_token"), + + /** + * The OpenID Provider encountered an unexpected condition that prevented it from successfully completing the + * transaction. This general case error code can be used to inform the Client that the CIBA transaction was + * unsuccessful for reasons other than those explicitly defined by access_denied and expired_token. + */ + TRANSACTION_FAILED("transaction_failed"); + + private final String paramName; + + PushErrorResponseType(String paramName) { + this.paramName = paramName; + } + + /** + * Returns the corresponding {@link PushErrorResponseType} from a given string. + * + * @param param The string value to convert. + * @return The corresponding {@link PushErrorResponseType}, otherwise null. + */ + public static PushErrorResponseType fromString(String param) { + if (param != null) { + for (PushErrorResponseType err : PushErrorResponseType.values()) { + if (param.equals(err.paramName)) { + return err; + } + } + } + return null; + } + + /** + * Returns a string representation of the object. In this case the parameter name. + * + * @return The string representation of the object. + */ + @Override + public String toString() { + return paramName; + } + + /** + * Gets error parameter. + * + * @return error parameter + */ + @Override + public String getParameter() { + return paramName; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/PushTokenDeliveryRequestParam.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/PushTokenDeliveryRequestParam.java new file mode 100644 index 00000000..0c76f700 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/ciba/PushTokenDeliveryRequestParam.java @@ -0,0 +1,21 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.ciba; + +/** + * @author Javier Rojas Blum + * @version August 20, 2019 + */ +public interface PushTokenDeliveryRequestParam { + + String AUTHORIZATION_REQUEST_ID = "auth_req_id"; + String ACCESS_TOKEN = "access_token"; + String TOKEN_TYPE = "token_type"; + String REFRESH_TOKEN = "refresh_token"; + String EXPIRES_IN = "expires_in"; + String ID_TOKEN = "id_token"; +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/AuthenticationMethod.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/AuthenticationMethod.java new file mode 100644 index 00000000..6523dca3 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/AuthenticationMethod.java @@ -0,0 +1,102 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * @author Javier Rojas Blum Date: 03.23.2012 + */ +public enum AuthenticationMethod { + + /** + * Clients in possession of a client password authenticate with the Authorization Server + * using HTTP Basic authentication scheme. Default one if not client authentication is specified. + */ + CLIENT_SECRET_BASIC("client_secret_basic"), + + /** + * Clients in possession of a client password authenticate with the Authorization Server + * by including the client credentials in the request body. + */ + CLIENT_SECRET_POST("client_secret_post"), + + /** + * Clients in possession of a client password create a JWT using the HMAC-SHA algorithm. + * The HMAC (Hash-based Message Authentication Code) is calculated using the client_secret + * as the shared key. + */ + CLIENT_SECRET_JWT("client_secret_jwt"), + + /** + * Clients that have registered a public key sign a JWT using the RSA algorithm if a RSA + * key was registered or the ECDSA algorithm if an Elliptic Curve key was registered. + */ + PRIVATE_KEY_JWT("private_key_jwt"), + + /** + * Authenticates client by access token. + */ + ACCESS_TOKEN("access_token"), + + /** + * Indicates that client authentication to the authorization server + * will occur with mutual TLS utilizing the PKI method of associating + * a certificate to a client. + */ + TLS_CLIENT_AUTH("tls_client_auth"), + + /** + * Indicates that client authentication to the authorization server + * will occur using mutual TLS with the client utilizing a self- + * signed certificate. + */ + SELF_SIGNED_TLS_CLIENT_AUTH("self_signed_tls_client_auth"), + + /** + * The Client does not authenticate itself at the Token Endpoint, either because it uses only the Implicit Flow + * (and so does not use the Token Endpoint) or because it is a Public Client with no Client Secret or other + * authentication mechanism. + */ + NONE("none"); + + private final String paramName; + + private AuthenticationMethod(String paramName) { + this.paramName = paramName; + } + + /** + * Returns the corresponding {@link AuthenticationMethod} for an authentication method parameter. + * + * @param param The parameter. + * @return The corresponding authentication method if found, otherwise + * null. + */ + @JsonCreator + public static AuthenticationMethod fromString(String param) { + if (param != null) { + for (AuthenticationMethod rt : AuthenticationMethod.values()) { + if (param.equals(rt.paramName)) { + return rt; + } + } + } + return null; + } + + /** + * Returns a string representation of the object. In this case the parameter + * name for the authentication method parameter. + */ + @Override + @JsonValue + public String toString() { + return paramName; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/AuthorizationMethod.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/AuthorizationMethod.java new file mode 100644 index 00000000..45521035 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/AuthorizationMethod.java @@ -0,0 +1,68 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +/** + * @author Javier Rojas Blum Date: 03.30.2012 + */ +public enum AuthorizationMethod { + + /** + * When sending the access token in the "Authorization" request header + * field defined by HTTP/1.1, Part 7 [I-D.ietf-httpbis-p7-auth], the + * client uses the "Bearer" authentication scheme to transmit the access + * token. + */ + AUTHORIZATION_REQUEST_HEADER_FIELD, + /** + * When sending the access token in the HTTP request entity-body, the + * client adds the access token to the request body using the + * "access_token" parameter. The client MUST NOT use this method unless + * all of the following conditions are met: + *

+ * - The HTTP request entity-header includes the "Content-Type" header + * field set to "application/x-www-form-urlencoded". + *

+ * - The entity-body follows the encoding requirements of the + * "application/x-www-form-urlencoded" content-type as defined by + * HTML 4.01 [W3C.REC-html401-19991224]. + *

+ * - The HTTP request entity-body is single-part. + *

+ * - The content to be encoded in the entity-body MUST consist entirely + * of ASCII [USASCII] characters. + *

+ * - The HTTP request method is one for which the request body has + * defined semantics. In particular, this means that the "GET" + * method MUST NOT be used. + *

+ * The entity-body MAY include other request-specific parameters, in + * which case, the "access_token" parameter MUST be properly separated + * from the request-specific parameters using "&" character(s) (ASCII + * code 38). + */ + FORM_ENCODED_BODY_PARAMETER, + /** + * When sending the access token in the HTTP request URI, the client + * adds the access token to the request URI query component as defined + * by Uniform Resource Identifier (URI) [RFC3986] using the + * "access_token" parameter. + *

+ * The HTTP request URI query can include other request-specific + * parameters, in which case, the "access_token" parameter MUST be + * properly separated from the request-specific parameters using "&" + * character(s) (ASCII code 38). + *

+ * Because of the security weaknesses associated with the URI method + * (see Section 5), including the high likelihood that the URL + * containing the access token will be logged, it SHOULD NOT be used + * unless it is impossible to transport the access token in the + * "Authorization" request header field or the HTTP request entity-body. + * Resource servers MAY support this method. + */ + URL_QUERY_PARAMETER; +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/BackchannelTokenDeliveryMode.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/BackchannelTokenDeliveryMode.java new file mode 100644 index 00000000..76eeb96d --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/BackchannelTokenDeliveryMode.java @@ -0,0 +1,94 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import org.gluu.persist.annotation.AttributeEnum; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Javier Rojas Blum + * @version August 20, 2019 + */ +public enum BackchannelTokenDeliveryMode implements HasParamName, AttributeEnum { + + POLL("poll"), + PING("ping"), + PUSH("push"); + + private final String value; + + private static Map mapByValues = new HashMap<>(); + + static { + for (BackchannelTokenDeliveryMode enumType : values()) { + mapByValues.put(enumType.getValue(), enumType); + } + } + + BackchannelTokenDeliveryMode (String value) { + this.value = value; + } + + /** + * Gets param name. + * + * @return param name + */ + public String getParamName() { + return value; + } + + @Override + public String getValue() { + return value; + } + + /** + * Returns the corresponding {@link BackchannelTokenDeliveryMode} for a parameter backchannel_token_delivery_mode of + * the access token requests. + * + * @param param The backchannel_token_delivery_mode parameter. + * @return The corresponding Backchannel Token Delivery Mode if found, otherwise + * null. + */ + @JsonCreator + public static BackchannelTokenDeliveryMode fromString(String param) { + if (param != null) { + for (BackchannelTokenDeliveryMode deliveryMode : BackchannelTokenDeliveryMode.values()) { + if (param.equals(deliveryMode.value)) { + return deliveryMode; + } + } + } + + return null; + } + + public static BackchannelTokenDeliveryMode getByValue(String value) { + return mapByValues.get(value); + } + + public Enum resolveByValue(String value) { + return getByValue(value); + } + + /** + * Returns a string representation of the object. In this case the parameter + * name for the backchannel_token_delivery_mode parameter. + * + * @return The string representation of the object. + */ + @Override + @JsonValue + public String toString() { + return value; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/CallerType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/CallerType.java new file mode 100644 index 00000000..db9a766c --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/CallerType.java @@ -0,0 +1,11 @@ +package org.gluu.oxauth.model.common; + +/** + * @author Yuriy Z + */ +public enum CallerType { + COMMON, + AUTHORIZE, + USERINFO, + ID_TOKEN +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/Display.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/Display.java new file mode 100644 index 00000000..38ab1ade --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/Display.java @@ -0,0 +1,87 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * An ASCII string value that specifies how the Authorization Server displays + * the authentication and consent user interface pages to the End-User. + * + * @author Javier Rojas Blum Date: 02.10.2012 + */ +public enum Display implements HasParamName { + + /** + * The Authorization Server SHOULD display authentication and consent UI + * consistent with a full user-agent page view. If the display parameter + * is not specified this is the default display mode. + */ + PAGE("page"), + /** + * The Authorization Server SHOULD display authentication and consent UI + * consistent with a popup user-agent window. The popup user-agent window + * SHOULD be 450 pixels wide and 500 pixels tall. + */ + POPUP("popup"), + /** + * The Authorization Server SHOULD display authentication and consent UI + * consistent with a device that leverages a touch interface. + * The Authorization Server MAY attempt to detect the touch device and + * further customize the interface. + */ + TOUCH("touch"), + /** + * The Authorization Server SHOULD display authentication and consent UI + * consistent with a "feature phone" type display. + */ + WAP("wap"), + /** + * The Authorization Server SHOULD display authentication and consent UI + * consistent with the limitations of an embedded user-agent. + */ + EMBEDDED("embedded"); + + private final String paramName; + + private Display(String paramName) { + this.paramName = paramName; + } + + public String getParamName() { + return paramName; + } + + /** + * Returns the corresponding {@link Display} for a parameter + * display of the authorization endpoint. + * + * @param param The parameter. + * @return The corresponding response type if found, otherwise null. + */ + @JsonCreator + public static Display fromString(String param) { + if (param != null) { + for (Display rt : Display.values()) { + if (param.equals(rt.paramName)) { + return rt; + } + } + } + return null; + } + + /** + * Returns a string representation of the object. In this case the parameter name. + */ + @Override + @JsonValue + public String toString() { + return paramName; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/GrantType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/GrantType.java new file mode 100644 index 00000000..f757249e --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/GrantType.java @@ -0,0 +1,178 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import org.gluu.persist.annotation.AttributeEnum; + +import java.util.HashMap; +import java.util.Map; + +/** + * This class allows to enumerate and identify the possible values of the + * parameter grant_type for access token requests. + * + * @author Javier Rojas Blum + * @version February 25, 2020 + */ +public enum GrantType implements HasParamName, AttributeEnum { + + NONE("none"), + + /** + * The authorization code is obtained by using an authorization server as an + * intermediary between the client and resource owner. Instead of requesting + * authorization directly from the resource owner, the client directs the + * resource owner to an authorization server (via its user- agent as defined + * in [RFC2616]), which in turn directs the resource owner back to the + * client with the authorization code. + */ + AUTHORIZATION_CODE("authorization_code"), + + /** + * The implicit grant type is used to obtain access tokens (it does not + * support the issuance of refresh tokens) and is optimized for public + * clients known to operate a particular redirection URI. These clients + * are typically implemented in a browser using a scripting language + * such as JavaScript. + */ + IMPLICIT("implicit"), + + /** + * The resource owner password credentials (i.e. username and password) can + * be used directly as an authorization grant to obtain an access token. The + * credentials should only be used when there is a high degree of trust + * between the resource owner and the client (e.g. its device operating + * system or a highly privileged application), and when other authorization + * grant types are not available (such as an authorization code). + */ + RESOURCE_OWNER_PASSWORD_CREDENTIALS("password"), + + /** + * The client credentials (or other forms of client authentication) can be + * used as an authorization grant when the authorization scope is limited to + * the protected resources under the control of the client, or to protected + * resources previously arranged with the authorization server. Client + * credentials are used as an authorization grant typically when the client + * is acting on its own behalf (the client is also the resource owner), or + * is requesting access to protected resources based on an authorization + * previously arranged with the authorization server. + */ + CLIENT_CREDENTIALS("client_credentials"), + + /** + * If the authorization server issued a refresh token to the client, the + * client makes a refresh request to the token endpoint. + */ + REFRESH_TOKEN("refresh_token"), + + /** + * Representing a requesting party, to use a permission ticket to request + * an OAuth 2.0 access token to gain access to a protected resource + * asynchronously from the time a resource owner grants access. + */ + OXAUTH_UMA_TICKET("urn:ietf:params:oauth:grant-type:uma-ticket"), + + /** + * CIBA (Client Initiated Backchannel Authentication) Grant Type. + */ + CIBA("urn:openid:params:grant-type:ciba"), + + /** + * Device Authorization Grant Type for OAuth 2.0 + */ + DEVICE_CODE("urn:ietf:params:oauth:grant-type:device_code"), + ; + + private final String value; + + private static Map mapByValues = new HashMap(); + + static { + for (GrantType enumType : values()) { + mapByValues.put(enumType.getValue(), enumType); + } + } + + private GrantType() { + this.value = null; + } + + private GrantType(String value) { + this.value = value; + } + + /** + * Gets param name. + * + * @return param name + */ + public String getParamName() { + return value; + } + + @Override + public String getValue() { + return value; + } + + /** + * Returns the corresponding {@link GrantType} for a parameter grant_type of + * the access token requests. For the extension grant type, the parameter + * should be a valid URI. + * + * @param param The grant_type parameter. + * @return The corresponding grant type if found, otherwise + * null. + */ + @JsonCreator + public static GrantType fromString(String param) { + if (param != null) { + for (GrantType gt : GrantType.values()) { + if (param.equals(gt.value)) { + return gt; + } + } + } + + return null; + } + + public static String[] toStringArray(GrantType[] grantTypes) { + if (grantTypes == null) { + return null; + } + + String[] resultGrantTypes = new String[grantTypes.length]; + for (int i = 0; i < grantTypes.length; i++) { + resultGrantTypes[i] = grantTypes[i].getValue(); + } + + return resultGrantTypes; + } + + public static GrantType getByValue(String value) { + return mapByValues.get(value); + } + + public Enum resolveByValue(String value) { + return getByValue(value); + } + + /** + * Returns a string representation of the object. In this case the parameter + * name for the grant_type parameter. + * + * @return The string representation of the object. + */ + @Override + @JsonValue + public String toString() { + return value; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/HasParamName.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/HasParamName.java new file mode 100644 index 00000000..393d0a6e --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/HasParamName.java @@ -0,0 +1,16 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 20/12/2012 + */ + +public interface HasParamName { + String getParamName(); +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/Holder.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/Holder.java new file mode 100644 index 00000000..cfc96442 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/Holder.java @@ -0,0 +1,32 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 15/03/2013 + */ + +public class Holder { + + private T m_t; + + public Holder() { + } + + public Holder(T p_t) { + m_t = p_t; + } + + public T getT() { + return m_t; + } + + public void setT(T p_t) { + m_t = p_t; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/Id.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/Id.java new file mode 100644 index 00000000..9e776ad7 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/Id.java @@ -0,0 +1,52 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import java.io.Serializable; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.jboss.resteasy.annotations.providers.jaxb.IgnoreMediaTypes; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 26/06/2013 + */ +@IgnoreMediaTypes("application/*+json") +@XmlRootElement +public class Id implements Serializable { + + private String id; + + public Id() { + } + + public Id(String p_id) { + id = p_id; + } + + @JsonProperty(value = "id") + @XmlElement(name = "id") + public String getId() { + return id; + } + + public void setId(String p_id) { + id = p_id; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("Id"); + sb.append("{id='").append(id).append('\''); + sb.append('}'); + return sb.toString(); + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/IdType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/IdType.java new file mode 100644 index 00000000..392754c2 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/IdType.java @@ -0,0 +1,66 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import org.apache.commons.lang.StringUtils; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 26/06/2013 + */ + +public enum IdType { + + PEOPLE("people", "people", "0000", "New Unique People Inum Generator"), + ORGANIZATION("organization", "0001", "organization","New Unique Organization Inum Generator"), + CONFIGURATION("configuration", "configuration", "0002", "New Unique configuration Inum Generator"), + GROUP("group", "group", "0003", "New Unique Group Inum Generator"), + SERVER("server", "server", "0004", "New Unique Server Inum Generator"), + ATTRIBUTE("attribute", "attribute", "0005", "New Unique Attribute Inum Generator"), + TRUST_RELATIONSHIP("trelationship", "0006", "trustRelationship", "New Unique Trust Relationship Inum Generator"), + LINK_CONTRACTS("lcontracts", "linkContracts", "0007", "New Unique Link Contracts Inum Generator"), + CLIENTS("oclient", "openidConnectClient", "0008", "New Unique Openid Connect Client Inum Generator"); + + private final String m_type; + private final String m_value; + private final String m_inum; + private final String m_htmlText; + + private IdType(String p_type, String p_value, String p_inum, String p_htmlText) { + m_type = p_type; + m_value = p_value; + m_inum = p_inum; + m_htmlText = p_htmlText; + } + + public String getInum() { + return m_inum; + } + + public String getHtmlText() { + return m_htmlText; + } + + public String getType() { + return m_type; + } + + public String getValue() { + return m_value; + } + + public static IdType fromString(String p_string) { + if (StringUtils.isNotBlank(p_string)) { + for (IdType t : values()) { + if (t.getType().equalsIgnoreCase(p_string) || t.getValue().equalsIgnoreCase(p_string)) { + return t; + } + } + } + return null; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/IntrospectionResponse.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/IntrospectionResponse.java new file mode 100644 index 00000000..50cebfaf --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/IntrospectionResponse.java @@ -0,0 +1,177 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +import org.gluu.oxauth.model.common.converter.ListConverter; +import org.jboss.resteasy.annotations.providers.jaxb.IgnoreMediaTypes; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 17/09/2013 + */ +@JsonPropertyOrder({"active", "scope", "client_id", "username", "token_type", "exp", "iat", "sub", "aud", "iss", "jti", "acr_values"}) +// ignore jettison as it's recommended here: http://docs.jboss.org/resteasy/docs/2.3.4.Final/userguide/html/json.html +@IgnoreMediaTypes("application/*+json") +@JsonIgnoreProperties(ignoreUnknown = true) +public class IntrospectionResponse { + + @JsonProperty(value = "active") + private boolean active; // according spec, must be "active" http://tools.ietf.org/html/draft-richer-oauth-introspection-03#section-2.2 + @JsonProperty(value = "scope") + @JsonDeserialize(converter = ListConverter.class) // Force use of List even when value in actual json content is String + private List scope; + @JsonProperty(value = "client_id") + private String clientId; + @JsonProperty(value = "username") + private String username; + @JsonProperty(value = "token_type") + private String tokenType; + @JsonProperty(value = "exp") + private Integer expiresAt; + @JsonProperty(value = "iat") + private Integer issuedAt; + @JsonProperty(value = "sub") + private String subject; + @JsonProperty(value = "aud") + private String audience; + @JsonProperty(value = "iss") + private String issuer; + @JsonProperty(value = "jti") + private String jti; + @JsonProperty(value = "acr_values") + private String acrValues; + + public IntrospectionResponse() { + } + + public IntrospectionResponse(boolean p_active) { + active = p_active; + } + + public String getAcrValues() { + return acrValues; + } + + public void setAcrValues(String p_authMode) { + acrValues = p_authMode; + } + + public boolean isActive() { + return active; + } + + public void setActive(boolean p_active) { + active = p_active; + } + + public List getScope() { + return scope; + } + + public void setScope(Collection scope) { + this.scope = scope != null ? new ArrayList(scope) : new ArrayList();; + } + + public Integer getExpiresAt() { + return expiresAt; + } + + public void setExpiresAt(Integer expiresAt) { + this.expiresAt = expiresAt; + } + + public Integer getIssuedAt() { + return issuedAt; + } + + public void setIssuedAt(Integer issuedAt) { + this.issuedAt = issuedAt; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getTokenType() { + return tokenType; + } + + public void setTokenType(String tokenType) { + this.tokenType = tokenType; + } + + public String getSubject() { + return subject; + } + + public void setSub(String subject) { + this.subject = subject; + } + + public String getAudience() { + return audience; + } + + public void setAudience(String audience) { + this.audience = audience; + } + + public String getIssuer() { + return issuer; + } + + public void setIssuer(String issuer) { + this.issuer = issuer; + } + + public String getJti() { + return jti; + } + + public void setJti(String jti) { + this.jti = jti; + } + + @Override + public String toString() { + return "IntrospectionResponse{" + + "active=" + active + + ", scope=" + scope + + ", clientId='" + clientId + '\'' + + ", username='" + username + '\'' + + ", tokenType='" + tokenType + '\'' + + ", expiresAt=" + expiresAt + + ", issuedAt=" + issuedAt + + ", subject='" + subject + '\'' + + ", audience='" + audience + '\'' + + ", issuer='" + issuer + '\'' + + ", jti='" + jti + '\'' + + ", acrValues='" + acrValues + '\'' + + '}'; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/JSONable.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/JSONable.java new file mode 100644 index 00000000..42ea85a2 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/JSONable.java @@ -0,0 +1,18 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * @author Javier Rojas Blum Date: 13.01.2013 + */ +public interface JSONable { + + JSONObject toJSONObject() throws JSONException; +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/PairwiseIdType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/PairwiseIdType.java new file mode 100644 index 00000000..3319a063 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/PairwiseIdType.java @@ -0,0 +1,41 @@ +package org.gluu.oxauth.model.common; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * @author Javier Rojas Blum + * @version February 15, 2015 + */ +public enum PairwiseIdType { + + ALGORITHMIC("algorithmic"), + PERSISTENT("persistent"); + + private final String m_value; + + private PairwiseIdType(String p_value) { + m_value = p_value; + } + + public String getValue() { + return m_value; + } + + @JsonCreator + public static PairwiseIdType fromString(String p_string) { + for (PairwiseIdType v : values()) { + if (v.getValue().equalsIgnoreCase(p_string)) { + return v; + } + } + return null; + } + + @Override + @JsonValue + public String toString() { + return m_value; + } + +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/ProgrammingLanguage.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/ProgrammingLanguage.java new file mode 100644 index 00000000..b5c899b8 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/ProgrammingLanguage.java @@ -0,0 +1,47 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 22/02/2013 + */ + +public enum ProgrammingLanguage { + PYTHON("Python"), + JAVA_SCRIPT("JavaScript"); + + private final String m_value; + + private ProgrammingLanguage(String p_value) { + m_value = p_value; + } + + public String getValue() { + return m_value; + } + + @JsonCreator + public static ProgrammingLanguage fromString(String p_string) { + for (ProgrammingLanguage v : values()) { + if (v.getValue().equalsIgnoreCase(p_string)) { + return v; + } + } + return null; + } + + @Override + @JsonValue + public String toString() { + return m_value; + } + +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/Prompt.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/Prompt.java new file mode 100644 index 00000000..bbab0d01 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/Prompt.java @@ -0,0 +1,116 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * An ASCII string values that specifies whether the Authorization Server + * prompts the End-User for re-authentication and consent. + * + * @author Javier Rojas Blum Date: 02.10.2012 + */ +public enum Prompt implements HasParamName { + + /** + * The Authorization Server MUST NOT display any authentication or + * consent user interface pages. An error is returned if the End-User + * is not already authenticated or the Client does not have pre-configured + * consent for the requested scopes. This can be used as a method to + * check for existing authentication and/or consent. + */ + NONE("none"), + /** + * The Authorization Server MUST prompt the End-User for re-authentication + */ + LOGIN("login"), + /** + * The Authorization Server MUST prompt the End-User for consent before + * returning information to the Client. + */ + CONSENT("consent"), + /** + * The Authorization Server MUST prompt the End-User to select a user account. + * This allows a user who has multiple accounts at the Authorization Server to + * select amongst the multiple accounts that they may have current sessions for. + */ + SELECT_ACCOUNT("select_account"); + + private final String paramName; + + private Prompt(String paramName) { + this.paramName = paramName; + } + + /** + * Returns the corresponding {@link Prompt} for a parameter + * prompt of the authorization endpoint. + * + * @param param The parameter. + * @return The corresponding response type if found, otherwise null. + */ + public static Prompt fromString(String param) { + if (param != null) { + for (Prompt rt : Prompt.values()) { + if (param.equals(rt.paramName)) { + return rt; + } + } + } + return null; + } + + /** + * Gets param name. + * + * @return param name + */ + public String getParamName() { + return paramName; + } + + /** + * Returns a list of the corresponding {@link Prompt} from a space-separated + * list of prompt parameters. + * + * @param paramList A space-separated list of prompt parameters. + * @param separator The separator of the string list. + * @return A list of the recognized response types. + */ + @JsonCreator + public static List fromString(String paramList, String separator) { + List prompts = new ArrayList(); + + if (paramList != null && !paramList.isEmpty()) { + String[] params = paramList.split(separator); + for (String param : params) { + for (Prompt p : Prompt.values()) { + if (param.equals(p.paramName)) { + if (!prompts.contains(p)) { + prompts.add(p); + } + } + } + } + } + + return prompts; + } + + /** + * Returns a string representation of the object. In this case the parameter name. + */ + @Override + @JsonValue + public String toString() { + return paramName; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/ResponseMode.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/ResponseMode.java new file mode 100644 index 00000000..9758e784 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/ResponseMode.java @@ -0,0 +1,90 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import org.gluu.persist.annotation.AttributeEnum; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Javier Rojas Blum + * @version July 10, 2019 + */ +public enum ResponseMode implements HasParamName, AttributeEnum { + + /** + * In this mode, Authorization Response parameters are encoded in the query string added to the redirect_uri when + * redirecting back to the Client. + */ + QUERY("query"), + /** + * In this mode, Authorization Response parameters are encoded in the fragment added to the redirect_uri when + * redirecting back to the Client. + */ + FRAGMENT("fragment"), + /** + * In this mode, Authorization Response parameters are encoded as HTML form values that are auto-submitted in the + * User Agent, and thus are transmitted via the HTTP POST method to the Client, with the result parameters being + * encoded in the body using the application/x-www-form-urlencoded format. + */ + FORM_POST("form_post"); + + private final String value; + + private static Map mapByValues = new HashMap(); + + static { + for (ResponseMode enumType : values()) { + mapByValues.put(enumType.getParamName(), enumType); + } + } + + ResponseMode(String value) { + this.value = value; + } + + public static ResponseMode getByValue(String value) { + return mapByValues.get(value); + } + + @JsonCreator + public static ResponseMode fromString(String param) { + if (param != null) { + for (ResponseMode rm : ResponseMode.values()) { + if (param.equals(rm.value)) { + return rm; + } + } + } + + return null; + } + + @Override + public String getParamName() { + return value; + } + + @Override + @JsonValue + public String toString() { + return value; + } + + @Override + public String getValue() { + return value; + } + + @Override + public Enum resolveByValue(String value) { + return getByValue(value); + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/ResponseType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/ResponseType.java new file mode 100644 index 00000000..e5f0b8ca --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/ResponseType.java @@ -0,0 +1,182 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonValue; +import com.google.common.collect.Lists; +import org.gluu.persist.annotation.AttributeEnum; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + *

+ * This class allows to enumerate and identify the possible values of the + * parameter response_type for the authorization endpoint. + *

+ *

+ * The client informs the authorization server of the desired grant type. + *

+ *

+ * The authorization endpoint is used by the authorization code grant type and + * implicit grant type flows. + *

+ * + * @author Javier Rojas Blum + * @version July 18, 2017 + */ +public enum ResponseType implements HasParamName, AttributeEnum { + + /** + * Used for the authorization code grant type. + */ + @JsonProperty("code") + CODE("code", "Authorization Code Grant Type"), + /** + * Used for the implicit grant type. + */ + @JsonProperty("token") + TOKEN("token", "Implicit Grant Type"), + /** + * Include an ID Token in the authorization response. + */ + @JsonProperty("id_token") + ID_TOKEN("id_token", "ID Token"); + + private final String value; + private final String displayName; + + private static Map mapByValues = new HashMap(); + + static { + for (ResponseType enumType : values()) { + mapByValues.put(enumType.getValue(), enumType); + } + } + + private ResponseType(String value, String displayName) { + this.value = value; + this.displayName = displayName; + } + + /** + * Returns the corresponding {@link ResponseType} for a single parameter response_type. + * + * @param param The response_type parameter. + * @return The corresponding response type if found, otherwise null. + */ + @JsonCreator + public static ResponseType fromString(String param) { + if (param != null) { + for (ResponseType rt : ResponseType.values()) { + if (param.equals(rt.value)) { + return rt; + } + } + } + + return null; + } + + /** + * Gets param name. + * + * @return param name + */ + public String getParamName() { + return value; + } + + /** + * Gets display name + * + * @return display name name + */ + public String getDisplayName() { + return displayName; + } + + @Override + public String getValue() { + return value; + } + + /** + * Returns a list of the corresponding {@link ResponseType} from a space-separated + * list of response_type parameters. + * + * @param paramList A space-separated list of response_type parameters. + * @param separator The separator of the string list. + * @return A list of the recognized response types. + */ + public static List fromString(String paramList, String separator) { + List responseTypes = new ArrayList(); + + if (paramList != null && !paramList.isEmpty()) { + String[] params = paramList.split(separator); + for (String param : params) { + for (ResponseType rt : ResponseType.values()) { + if (param.equals(rt.value)) { + if (!responseTypes.contains(rt)) { + responseTypes.add(rt); + } + } + } + } + } + + return responseTypes; + } + + public static boolean isImplicitFlow(String responseTypes) { + return !responseTypes.contains("code") && (responseTypes.contains("id_token") || responseTypes.contains("token")); + } + + public static List toStringList(List responseTypes) { + if (responseTypes == null) { + return Lists.newArrayList(); + } + return responseTypes.stream().map(ResponseType::getValue).collect(Collectors.toList()); + } + + public static String[] toStringArray(ResponseType[] responseTypes) { + if (responseTypes == null) { + return null; + } + + String[] resultResponseTypes = new String[responseTypes.length]; + for (int i = 0; i < responseTypes.length; i++) { + resultResponseTypes[i] = responseTypes[i].getValue(); + } + + return resultResponseTypes; + } + + public static ResponseType getByValue(String value) { + return mapByValues.get(value); + } + + public Enum resolveByValue(String value) { + return getByValue(value); + } + + /** + * Returns a string representation of the object. In this case the parameter + * name for the response_type parameter. + */ + @Override + @JsonValue + public String toString() { + return value; + } + +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/ScopeConstants.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/ScopeConstants.java new file mode 100644 index 00000000..41481238 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/ScopeConstants.java @@ -0,0 +1,13 @@ +package org.gluu.oxauth.model.common; + +/** + * @author Yuriy Zabrovarnyy + */ +public class ScopeConstants { + + public static final String OPENID = "openid"; + public static final String OFFLINE_ACCESS = "offline_access"; + + private ScopeConstants() { + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/ScopeType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/ScopeType.java new file mode 100644 index 00000000..1630a4eb --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/ScopeType.java @@ -0,0 +1,128 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import java.util.HashMap; +import java.util.Map; + +import org.gluu.persist.annotation.AttributeEnum; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Scope types + * + * @author Yuriy Movchan + * @author Javier Rojas Blum + * @version January 19, 2017 + */ +public enum ScopeType implements AttributeEnum { + + /** + * Specify what access privileges are being requested for Access Tokens. + * The scopes associated with Access Tokens determine what resources will + * be available when they are used to access OAuth 2.0 protected endpoints. + * For OpenID Connect, scopes can be used to request that specific sets of + * information be made available as Claim Values. + * OpenID Connect defines the following scope values that are used to request Claims: + *

+ *

    + *
  • + * profile. This scope value requests access to the End-User's default profile Claims, + * which are: name, family_name, given_name, middle_name, nickname, preferred_username, profile, + * picture, website, gender, birthdate, zoneinfo, locale, and updated_at. + *
  • + *
  • + * email. This scope value requests access to the email and email_verified Claims. + *
  • + *
  • + * address. This scope value requests access to the address Claim. + *
  • + *
  • + * phone. This scope value requests access to the phone_number and phone_number_verified Claims. + *
  • + *
+ *

+ * The Claims requested by the profile, email, address, and phone scope values are returned from the + * UserInfo Endpoint. + */ + OPENID("openid", "OpenID"), + + /** + * Dynamic scope calls scripts which add claims dynamically. + */ + DYNAMIC("dynamic", "Dynamic"), + + UMA("uma", "UMA"), + + SPONTANEOUS("spontaneous", "Spontaneous"), + + /** + * OAuth 2.0 Scopes for any of their API's. + * This scope type would only have a description, but no claims. + * Once a client obtains this token, it may be passed to the backend API (let's say the calendar API). + */ + OAUTH("oauth", "OAuth"); + + private final String value; + private final String displayName; + + private static Map mapByValues = new HashMap(); + + static { + for (ScopeType enumType : values()) { + mapByValues.put(enumType.getValue(), enumType); + } + } + + private ScopeType(String value, String displayName) { + this.value = value; + this.displayName = displayName; + } + + @JsonCreator + public static ScopeType fromString(String param) { + if (param != null) { + for (ScopeType st : ScopeType.values()) { + if (param.equals(st.value)) { + return st; + } + } + } + return null; + } + + @Override + public String getValue() { + return value; + } + + /** + * Gets display name + * + * @return display name name + */ + public String getDisplayName() { + return displayName; + } + + public static ScopeType getByValue(String value) { + return mapByValues.get(value); + } + + public Enum resolveByValue(String value) { + return getByValue(value); + } + + @Override + @JsonValue + public String toString() { + return value; + } + +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/SoftwareStatementValidationType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/SoftwareStatementValidationType.java new file mode 100644 index 00000000..f68ecc92 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/SoftwareStatementValidationType.java @@ -0,0 +1,40 @@ +package org.gluu.oxauth.model.common; + +import com.fasterxml.jackson.annotation.JsonCreator; +import org.apache.commons.lang.StringUtils; + +/** + * @author Yuriy Zabrovarnyy + */ +public enum SoftwareStatementValidationType { + NONE("none"), + JWKS("jwks"), + JWKS_URI("jwks_uri"), + SCRIPT("script"); + + public static final SoftwareStatementValidationType DEFAULT = SCRIPT; + + private final String value; + + SoftwareStatementValidationType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @JsonCreator + public static SoftwareStatementValidationType fromString(String param) { + if (StringUtils.isBlank(param)) { + return null; + } + + for (SoftwareStatementValidationType v : SoftwareStatementValidationType.values()) { + if (param.equals(v.value)) { + return v; + } + } + return null; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/SubjectType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/SubjectType.java new file mode 100644 index 00000000..0f0a147a --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/SubjectType.java @@ -0,0 +1,54 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * @author Javier Rojas Blum Date: 05.11.2012 + */ +public enum SubjectType { + + PAIRWISE("pairwise"), + PUBLIC("public"); + + private final String paramName; + + private SubjectType(String paramName) { + this.paramName = paramName; + } + + /** + * Returns the corresponding {@link SubjectType} for an user id type parameter. + * + * @param param The parameter. + * @return The corresponding user id type if found, otherwise + * null. + */ + @JsonCreator + public static SubjectType fromString(String param) { + if (param != null) { + for (SubjectType uit : SubjectType.values()) { + if (param.equals(uit.paramName)) { + return uit; + } + } + } + return null; + } + + /** + * Returns a string representation of the object. In this case the parameter + * name for the user id type parameter. + */ + @Override + @JsonValue + public String toString() { + return paramName; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/TokenType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/TokenType.java new file mode 100644 index 00000000..4b5e3c7d --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/TokenType.java @@ -0,0 +1,64 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * The access token type provides the client with the information required to + * successfully utilize the access token to make a protected resource request + * (along with type-specific attributes). The client MUST NOT use an access + * token if it does not understand or does not trust the token type. + * + * @author Javier Rojas Blum Date: 09.20.2011 + */ +public enum TokenType { + /** + * The bearer token type is defined in [ietf-oauth-v2-bearer] + */ + BEARER("bearer"); + + private final String name; + + private TokenType(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + /** + * Returns the corresponding {@link TokenType} for a parameter token_type. + * + * @param param The token_type parameter. + * @return The corresponding token type if found, otherwise + * null. + */ + @JsonCreator + public static TokenType fromString(String param) { + if (param != null) { + for (TokenType rt : TokenType.values()) { + if (param.equals(rt.name)) { + return rt; + } + } + } + return null; + } + + /** + * Returns a string representation of the object. In this case the parameter + * name for the token_type parameter. + */ + @Override + @JsonValue + public String toString() { + return name; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/TokenTypeHint.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/TokenTypeHint.java new file mode 100644 index 00000000..902514d5 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/TokenTypeHint.java @@ -0,0 +1,94 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import java.util.HashMap; +import java.util.Map; + +import org.gluu.persist.annotation.AttributeEnum; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * @author Javier Rojas Blum + * @version January 16, 2019 + */ +public enum TokenTypeHint implements HasParamName, AttributeEnum { + + /** + * An access token as defined in RFC6749, Section 1.4 + */ + ACCESS_TOKEN("access_token"), + + /** + * A refresh token as defined in RFC6749, Section 1.5 + */ + REFRESH_TOKEN("refresh_token"); + + private final String value; + + private static Map mapByValues = new HashMap(); + + static { + for (TokenTypeHint enumType : values()) { + mapByValues.put(enumType.getValue(), enumType); + } + } + + TokenTypeHint(String value) { + this.value = value; + } + + /** + * Gets param name. + * + * @return param name + */ + @Override + public String getParamName() { + return value; + } + + @Override + public String getValue() { + return value; + } + + @JsonCreator + public static TokenTypeHint fromString(String param) { + if (param != null) { + for (TokenTypeHint tth : TokenTypeHint.values()) { + if (param.equals(tth.value)) { + return tth; + } + } + } + return null; + } + + public static TokenTypeHint getByValue(String value) { + return mapByValues.get(value); + } + + @Override + public Enum resolveByValue(String s) { + return getByValue(value); + } + + /** + * Returns a string representation of the object. In this case the parameter + * name for the grant_type parameter. + * + * @return The string representation of the object. + */ + @Override + @JsonValue + public String toString() { + return value; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/WebKeyStorage.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/WebKeyStorage.java new file mode 100644 index 00000000..f0d778fc --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/WebKeyStorage.java @@ -0,0 +1,50 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * @author Javier Rojas Blum + * @version June 15, 2016 + */ +public enum WebKeyStorage { + KEYSTORE("keystore"), + PKCS11("pkcs11"); + + private final String value; + + private WebKeyStorage(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @JsonCreator + public static WebKeyStorage fromString(String string) { + for (WebKeyStorage v : values()) { + if (v.getValue().equalsIgnoreCase(string)) { + return v; + } + } + return KEYSTORE; + } + + /** + * Returns a string representation of the object. In this case the parameter name. + * + * @return The string representation of the object. + */ + @Override + @JsonValue + public String toString() { + return value; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/converter/ListConverter.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/converter/ListConverter.java new file mode 100644 index 00000000..61d569d1 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/common/converter/ListConverter.java @@ -0,0 +1,41 @@ +package org.gluu.oxauth.model.common.converter; + +import com.fasterxml.jackson.databind.util.StdConverter; + +import org.gluu.oxauth.model.util.StringUtils; + +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.List; +import java.util.Objects; + +/** + * A class to facilitate two-step deserialization. + */ +public class ListConverter extends StdConverter> { + + /** + * Converts a value to a List of Strings. Conversion is attempted only + * if parameter obj is already a String or a List. In case of String, a + * whitespace is assumed as elements separator + * @param obj Input object + * @return A list of strings, null if obj is null or does not have the expected type + */ + public List convert(Object obj) { + + if (obj != null) { + if (List.class.isAssignableFrom(obj.getClass())) { + // json data already looks like a list... + Stream stream = List.class.cast(obj).stream() + .filter(Objects::nonNull).map(Object::toString); + return stream.collect(Collectors.toList()); + + } else if (String.class.equals(obj.getClass())) { + return StringUtils.spaceSeparatedToList(obj.toString()); + } + } + return null; + + } + +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/AppConfiguration.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/AppConfiguration.java new file mode 100644 index 00000000..43a1f018 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/AppConfiguration.java @@ -0,0 +1,2237 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.configuration; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.google.common.collect.Lists; + +import org.gluu.oxauth.model.common.*; +import org.gluu.oxauth.model.error.ErrorHandlingMethod; +import org.gluu.oxauth.model.jwk.KeySelectionStrategy; + +import java.util.*; + +/** + * Represents the configuration JSON file. + * + * @author Javier Rojas Blum + * @author Yuriy Zabrovarnyy + * @author Yuriy Movchan + * @version November 20, 2019 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class AppConfiguration implements Configuration { + + public static final int DEFAULT_SESSION_ID_LIFETIME = 86400; + public static final KeySelectionStrategy DEFAULT_KEY_SELECTION_STRATEGY = KeySelectionStrategy.OLDER; + public static final String DEFAULT_STAT_SCOPE = "jans_stat"; + + private String issuer; + private String baseEndpoint; + private String authorizationEndpoint; + private String tokenEndpoint; + private String tokenRevocationEndpoint; + private String userInfoEndpoint; + private String clientInfoEndpoint; + private String checkSessionIFrame; + private String endSessionEndpoint; + private String jwksUri; + private String registrationEndpoint; + private String openIdDiscoveryEndpoint; + private String openIdConfigurationEndpoint; + private String idGenerationEndpoint; + private String introspectionEndpoint; + private String deviceAuthzEndpoint; + + private int discoveryCacheLifetimeInMinutes = 60; + private int sectorIdentifierCacheLifetimeInMinutes = 1440; + + private Boolean sessionAsJwt = false; + private Boolean forceRopcInAuthorizationEndpoint = false; + + private String umaConfigurationEndpoint; + private Boolean umaRptAsJwt = false; + private int umaRptLifetime; + private int umaTicketLifetime; + private int umaPctLifetime; + private int umaResourceLifetime; + private Boolean umaAddScopesAutomatically; + private Boolean umaValidateClaimToken = false; + private Boolean umaGrantAccessIfNoPolicies = false; + private Boolean umaRestrictResourceToAssociatedClient = false; + + private Boolean statEnabled = true; + private String statAuthorizationScope; + private int statTimerIntervalInSeconds; + private int statWebServiceIntervalLimitInSeconds; + + private Boolean allowSpontaneousScopes = true; + private int spontaneousScopeLifetime; + private String openidSubAttribute; + private Set> responseTypesSupported; + private Set responseModesSupported; + private Set grantTypesSupported; + private List subjectTypesSupported; + private String defaultSubjectType; + private List userInfoSigningAlgValuesSupported; + private List userInfoEncryptionAlgValuesSupported; + private List userInfoEncryptionEncValuesSupported; + private List idTokenSigningAlgValuesSupported; + private List idTokenEncryptionAlgValuesSupported; + private List idTokenEncryptionEncValuesSupported; + private List requestObjectSigningAlgValuesSupported; + private List requestObjectEncryptionAlgValuesSupported; + private List requestObjectEncryptionEncValuesSupported; + private List tokenEndpointAuthMethodsSupported; + private List tokenEndpointAuthSigningAlgValuesSupported; + private List dynamicRegistrationCustomAttributes; + private List displayValuesSupported; + private List claimTypesSupported; + private List jwksAlgorithmsSupported; + private String serviceDocumentation; + private List claimsLocalesSupported; + private List idTokenTokenBindingCnfValuesSupported; + private List uiLocalesSupported; + private Boolean claimsParameterSupported; + private Boolean requestParameterSupported; + private Boolean requestUriParameterSupported; + private Boolean requestUriHashVerificationEnabled; + private Boolean requireRequestUriRegistration; + private List requestUriBlockList; + private String opPolicyUri; + private String opTosUri; + private int authorizationCodeLifetime; + private int refreshTokenLifetime; + private int idTokenLifetime; + private int accessTokenLifetime; + + private int cleanServiceInterval; + private int cleanServiceBatchChunkSize = 100; + + private Boolean keyRegenerationEnabled; + private int keyRegenerationInterval; + private String defaultSignatureAlgorithm; + private String oxOpenIdConnectVersion; + private String oxId; + private Boolean dynamicRegistrationEnabled; + private int dynamicRegistrationExpirationTime = -1; + private Boolean dynamicRegistrationPersistClientAuthorizations; + private Boolean trustedClientEnabled; + private Boolean skipAuthorizationForOpenIdScopeAndPairwiseId = false; + private Boolean dynamicRegistrationScopesParamEnabled; + private Boolean dynamicRegistrationDisableFallbackScopesAssigning; + private Boolean dynamicRegistrationPasswordGrantTypeEnabled = false; + private List dynamicRegistrationAllowedPasswordGrantScopes; + private String dynamicRegistrationCustomObjectClass; + private List personCustomObjectClassList; + + private Boolean persistIdTokenInLdap = false; + private Boolean persistRefreshTokenInLdap = true; + private Boolean allowPostLogoutRedirectWithoutValidation = false; + private Boolean invalidateSessionCookiesAfterAuthorizationFlow = false; + private Boolean returnClientSecretOnRead = false; + private Boolean rejectJwtWithNoneAlg = true; + private Boolean expirationNotificatorEnabled = false; + private Boolean useNestedJwtDuringEncryption = true; + private int expirationNotificatorMapSizeLimit = 100000; + private int expirationNotificatorIntervalInSeconds = 600; + + private Boolean useCacheForAllImplicitFlowObjects = false; + + private Boolean authenticationFiltersEnabled; + private Boolean clientAuthenticationFiltersEnabled; + private Boolean clientRegDefaultToCodeFlowWithRefresh; + private Boolean grantTypesAndResponseTypesAutofixEnabled; + private List authenticationFilters; + private List clientAuthenticationFilters; + private List corsConfigurationFilters; + + private int sessionIdUnusedLifetime; + private int sessionIdUnauthenticatedUnusedLifetime = 120; // 120 seconds + private Boolean sessionIdPersistOnPromptNone; + private Boolean sessionIdRequestParameterEnabled = false; // #1195 + private Boolean changeSessionIdOnAuthentication = true; + private Boolean sessionIdPersistInCache = false; + /** + * SessionId will be expired after sessionIdLifetime seconds + */ + private Integer sessionIdLifetime = DEFAULT_SESSION_ID_LIFETIME; + private Integer serverSessionIdLifetime = sessionIdLifetime; // by default same as sessionIdLifetime + private int configurationUpdateInterval; + + private Boolean logNotFoundEntityAsError; + private Boolean enableClientGrantTypeUpdate; + private Set dynamicGrantTypeDefault; + + private String cssLocation; + private String jsLocation; + private String imgLocation; + private int metricReporterInterval; + private int metricReporterKeepDataDays; + private Boolean metricReporterEnabled = true; + private String pairwiseIdType; // persistent, algorithmic + private String pairwiseCalculationKey; + private String pairwiseCalculationSalt; + private Boolean shareSubjectIdBetweenClientsWithSameSectorId = false; + private Boolean subjectIdentifierBasedOnWholeUriBackwardCompatibility = false; // todo remove in 5.0 + + private WebKeyStorage webKeysStorage; + private String dnName; + // oxAuth KeyStore + private String keyStoreFile; + private String keyStoreSecret; + private KeySelectionStrategy keySelectionStrategy = DEFAULT_KEY_SELECTION_STRATEGY; + private List keyAlgsAllowedForGeneration = new ArrayList<>(); + + //oxEleven + private String oxElevenTestModeToken; + private String oxElevenGenerateKeyEndpoint; + private String oxElevenSignEndpoint; + private String oxElevenVerifySignatureEndpoint; + private String oxElevenDeleteKeyEndpoint; + + private Boolean introspectionAccessTokenMustHaveUmaProtectionScope = false; + private Boolean introspectionSkipAuthorization; + private Boolean introspectionRestrictBasicAuthnToOwnTokens = false; + + private Boolean endSessionWithAccessToken; + private String cookieDomain; + private Boolean enabledOAuthAuditLogging; + private Set jmsBrokerURISet; + private String jmsUserName; + private String jmsPassword; + private Boolean allowWildcardRedirectUri; + private List clientWhiteList; + private List clientBlackList; + private Boolean legacyIdTokenClaims; + private Boolean customHeadersWithAuthorizationResponse; + private Boolean frontChannelLogoutSessionSupported; + private String loggingLevel; + private String loggingLayout; + private Boolean updateUserLastLogonTime; + private Boolean updateClientAccessTime; + private Boolean logClientIdOnClientAuthentication; + private Boolean logClientNameOnClientAuthentication; + private Boolean disableJdkLogger = true; + private Set authorizationRequestCustomAllowedParameters; + private Boolean legacyDynamicRegistrationScopeParam; + private Boolean openidScopeBackwardCompatibility = false; + private Boolean disableU2fEndpoint = false; + + private Boolean useLocalCache = false; + private Boolean fapiCompatibility = false; + private Boolean forceIdTokenHintPrecense = false; + private Boolean rejectEndSessionIfIdTokenExpired = false; + private Boolean allowEndSessionWithUnmatchedSid = false; + private Boolean forceOfflineAccessScopeToEnableRefreshToken = true; + private Boolean errorReasonEnabled = false; + private Boolean removeRefreshTokensForClientOnLogout = true; + private Boolean skipRefreshTokenDuringRefreshing = false; + private Boolean refreshTokenExtendLifetimeOnRotation = false; + private Boolean checkUserPresenceOnRefreshToken = false; + private Boolean consentGatheringScriptBackwardCompatibility = false; // means ignore client configuration (as defined in 4.2) and determine it globally (as in 4.1 and earlier) + private Boolean introspectionScriptBackwardCompatibility = false; // means ignore client configuration (as defined in 4.2) and determine it globally (as in 4.1 and earlier) + private Boolean introspectionResponseScopesBackwardCompatibility = false; // See #1499 + private Boolean clientAuthorizationBackwardCompatibility = false; // search client authorization by filter (instead of key) + + private String softwareStatementValidationType = SoftwareStatementValidationType.DEFAULT.getValue(); + private String softwareStatementValidationClaimName; + + private AuthenticationProtectionConfiguration authenticationProtectionConfiguration; + + private ErrorHandlingMethod errorHandlingMethod = ErrorHandlingMethod.REMOTE; + + private Boolean keepAuthenticatorAttributesOnAcrChange = false; + private Boolean disableAuthnForMaxAgeZero = false; + private int deviceAuthzRequestExpiresIn; + private int deviceAuthzTokenPollInterval; + private String deviceAuthzResponseTypeToProcessAuthz; + + // CIBA + private String backchannelClientId; + private String backchannelRedirectUri; + private String backchannelAuthenticationEndpoint; + private String backchannelDeviceRegistrationEndpoint; + private List backchannelTokenDeliveryModesSupported; + private List backchannelAuthenticationRequestSigningAlgValuesSupported; + private Boolean backchannelUserCodeParameterSupported; + private String backchannelBindingMessagePattern; + private int backchannelAuthenticationResponseExpiresIn; + private int backchannelAuthenticationResponseInterval; + private List backchannelLoginHintClaims; + private CIBAEndUserNotificationConfig cibaEndUserNotificationConfig; + private int backchannelRequestsProcessorJobIntervalSec; + private int backchannelRequestsProcessorJobChunkSize; + private int cibaGrantLifeExtraTimeSec; + private int cibaMaxExpirationTimeAllowedSec; + private Boolean cibaEnabled; + + private Boolean return200OnClientRegistration = true; + private Map dateFormatterPatterns = new HashMap<>(); + + private Boolean allowBlankValuesInDiscoveryResponse; + + private Boolean skipAuthenticationFilterOptionsMethod = false; + + public Boolean getSubjectIdentifierBasedOnWholeUriBackwardCompatibility() { + return subjectIdentifierBasedOnWholeUriBackwardCompatibility; + } + + public void setSubjectIdentifierBasedOnWholeUriBackwardCompatibility(Boolean subjectIdentifierBasedOnWholeUriBackwardCompatibility) { + this.subjectIdentifierBasedOnWholeUriBackwardCompatibility = subjectIdentifierBasedOnWholeUriBackwardCompatibility; + } + + public Boolean getLogNotFoundEntityAsError() { + if (logNotFoundEntityAsError == null) logNotFoundEntityAsError = false; + return logNotFoundEntityAsError; + } + + public void setLogNotFoundEntityAsError(Boolean logNotFoundEntityAsError) { + this.logNotFoundEntityAsError = logNotFoundEntityAsError; + } + + public Boolean getUseNestedJwtDuringEncryption() { + if (useNestedJwtDuringEncryption == null) useNestedJwtDuringEncryption = true; + return useNestedJwtDuringEncryption; + } + + public void setUseNestedJwtDuringEncryption(Boolean useNestedJwtDuringEncryption) { + this.useNestedJwtDuringEncryption = useNestedJwtDuringEncryption; + } + + public KeySelectionStrategy getKeySelectionStrategy() { + if (keySelectionStrategy == null) keySelectionStrategy = DEFAULT_KEY_SELECTION_STRATEGY; + return keySelectionStrategy; + } + + public void setKeySelectionStrategy(KeySelectionStrategy keySelectionStrategy) { + this.keySelectionStrategy = keySelectionStrategy; + } + + public List getKeyAlgsAllowedForGeneration() { + if (keyAlgsAllowedForGeneration == null) keyAlgsAllowedForGeneration = new ArrayList<>(); + return keyAlgsAllowedForGeneration; + } + + public void setKeyAlgsAllowedForGeneration(List keyAlgsAllowedForGeneration) { + this.keyAlgsAllowedForGeneration = keyAlgsAllowedForGeneration; + } + + public int getDiscoveryCacheLifetimeInMinutes() { + return discoveryCacheLifetimeInMinutes; + } + + public void setDiscoveryCacheLifetimeInMinutes(int discoveryCacheLifetimeInMinutes) { + this.discoveryCacheLifetimeInMinutes = discoveryCacheLifetimeInMinutes; + } + + public int getSectorIdentifierCacheLifetimeInMinutes() { + return sectorIdentifierCacheLifetimeInMinutes; + } + + public void setSectorIdentifierCacheLifetimeInMinutes(int sectorIdentifierCacheLifetimeInMinutes) { + this.sectorIdentifierCacheLifetimeInMinutes = sectorIdentifierCacheLifetimeInMinutes; + } + + public String getSoftwareStatementValidationType() { + if (softwareStatementValidationType == null) return softwareStatementValidationType = SoftwareStatementValidationType.DEFAULT.getValue(); + return softwareStatementValidationType; + } + + public String getSoftwareStatementValidationClaimName() { + return softwareStatementValidationClaimName; + } + + public void setSoftwareStatementValidationType(String softwareStatementValidationType) { + this.softwareStatementValidationType = softwareStatementValidationType; + } + + public void setSoftwareStatementValidationClaimName(String softwareStatementValidationClaimName) { + this.softwareStatementValidationClaimName = softwareStatementValidationClaimName; + } + + public Boolean getSkipRefreshTokenDuringRefreshing() { + if (skipRefreshTokenDuringRefreshing == null) skipRefreshTokenDuringRefreshing = false; + return skipRefreshTokenDuringRefreshing; + } + + public void setSkipRefreshTokenDuringRefreshing(Boolean skipRefreshTokenDuringRefreshing) { + this.skipRefreshTokenDuringRefreshing = skipRefreshTokenDuringRefreshing; + } + + public Boolean getClientAuthorizationBackwardCompatibility() { + if (clientAuthorizationBackwardCompatibility == null) clientAuthorizationBackwardCompatibility = false; + return clientAuthorizationBackwardCompatibility; + } + + public void setClientAuthorizationBackwardCompatibility(Boolean clientAuthorizationBackwardCompatibility) { + this.clientAuthorizationBackwardCompatibility = clientAuthorizationBackwardCompatibility; + } + public Boolean getRefreshTokenExtendLifetimeOnRotation() { + if (refreshTokenExtendLifetimeOnRotation == null) refreshTokenExtendLifetimeOnRotation = false; + return refreshTokenExtendLifetimeOnRotation; + } + + public void setRefreshTokenExtendLifetimeOnRotation(Boolean refreshTokenExtendLifetimeOnRotation) { + this.refreshTokenExtendLifetimeOnRotation = refreshTokenExtendLifetimeOnRotation; + } + + public Boolean getCheckUserPresenceOnRefreshToken() { + if (checkUserPresenceOnRefreshToken == null) checkUserPresenceOnRefreshToken = false; + return checkUserPresenceOnRefreshToken; + } + + public void setCheckUserPresenceOnRefreshToken(Boolean checkUserPresenceOnRefreshToken) { + this.checkUserPresenceOnRefreshToken = checkUserPresenceOnRefreshToken; + } + + public Boolean getExpirationNotificatorEnabled() { + if (expirationNotificatorEnabled == null) expirationNotificatorEnabled = false; + return expirationNotificatorEnabled; + } + + public void setExpirationNotificatorEnabled(Boolean expirationNotificatorEnabled) { + this.expirationNotificatorEnabled = expirationNotificatorEnabled; + } + + public int getExpirationNotificatorMapSizeLimit() { + if (expirationNotificatorMapSizeLimit == 0) expirationNotificatorMapSizeLimit = 100000; + return expirationNotificatorMapSizeLimit; + } + + public void setExpirationNotificatorMapSizeLimit(int expirationNotificatorMapSizeLimit) { + this.expirationNotificatorMapSizeLimit = expirationNotificatorMapSizeLimit; + } + + public int getExpirationNotificatorIntervalInSeconds() { + return expirationNotificatorIntervalInSeconds; + } + + public void setExpirationNotificatorIntervalInSeconds(int expirationNotificatorIntervalInSeconds) { + this.expirationNotificatorIntervalInSeconds = expirationNotificatorIntervalInSeconds; + } + + public Boolean getRejectJwtWithNoneAlg() { + if (rejectJwtWithNoneAlg == null) rejectJwtWithNoneAlg = true; + return rejectJwtWithNoneAlg; + } + + public void setRejectJwtWithNoneAlg(Boolean rejectJwtWithNoneAlg) { + this.rejectJwtWithNoneAlg = rejectJwtWithNoneAlg; + } + + public Boolean getIntrospectionScriptBackwardCompatibility() { + if (introspectionScriptBackwardCompatibility == null) introspectionScriptBackwardCompatibility = false; + return introspectionScriptBackwardCompatibility; + } + + public void setIntrospectionScriptBackwardCompatibility(Boolean introspectionScriptBackwardCompatibility) { + this.introspectionScriptBackwardCompatibility = introspectionScriptBackwardCompatibility; + } + + public Boolean getIntrospectionResponseScopesBackwardCompatibility() { + if (introspectionResponseScopesBackwardCompatibility == null) introspectionResponseScopesBackwardCompatibility = false; + return introspectionScriptBackwardCompatibility; + } + + public void setIntrospectionResponseScopesBackwardCompatibility(Boolean introspectionResponseScopesBackwardCompatibility) { + this.introspectionResponseScopesBackwardCompatibility = introspectionResponseScopesBackwardCompatibility; + } + + public Boolean getConsentGatheringScriptBackwardCompatibility() { + if (consentGatheringScriptBackwardCompatibility == null) consentGatheringScriptBackwardCompatibility = false; + return consentGatheringScriptBackwardCompatibility; + } + + public void setConsentGatheringScriptBackwardCompatibility(Boolean consentGatheringScriptBackwardCompatibility) { + this.consentGatheringScriptBackwardCompatibility = consentGatheringScriptBackwardCompatibility; + } + + public Boolean getErrorReasonEnabled() { + if (errorReasonEnabled == null) errorReasonEnabled = false; + return errorReasonEnabled; + } + + public void setErrorReasonEnabled(Boolean errorReasonEnabled) { + this.errorReasonEnabled = errorReasonEnabled; + } + + public Boolean getForceOfflineAccessScopeToEnableRefreshToken() { + if (forceOfflineAccessScopeToEnableRefreshToken == null) forceOfflineAccessScopeToEnableRefreshToken = true; + return forceOfflineAccessScopeToEnableRefreshToken; + } + + public void setForceOfflineAccessScopeToEnableRefreshToken(Boolean forceOfflineAccessScopeToEnableRefreshToken) { + this.forceOfflineAccessScopeToEnableRefreshToken = forceOfflineAccessScopeToEnableRefreshToken; + } + + public Boolean getSessionIdPersistInCache() { + if (sessionIdPersistInCache == null) sessionIdPersistInCache = false; + return sessionIdPersistInCache; + } + + public void setSessionIdPersistInCache(Boolean sessionIdPersistInCache) { + this.sessionIdPersistInCache = sessionIdPersistInCache; + } + + public Boolean getChangeSessionIdOnAuthentication() { + if (changeSessionIdOnAuthentication == null) changeSessionIdOnAuthentication = true; + return changeSessionIdOnAuthentication; + } + + public void setChangeSessionIdOnAuthentication(Boolean changeSessionIdOnAuthentication) { + this.changeSessionIdOnAuthentication = changeSessionIdOnAuthentication; + } + + public Boolean getReturnClientSecretOnRead() { + if (returnClientSecretOnRead == null) returnClientSecretOnRead = false; + return returnClientSecretOnRead; + } + + public void setReturnClientSecretOnRead(Boolean returnClientSecretOnRead) { + this.returnClientSecretOnRead = returnClientSecretOnRead; + } + + public Boolean getFapiCompatibility() { + if (fapiCompatibility == null) fapiCompatibility = false; + return fapiCompatibility; + } + + public void setFapiCompatibility(Boolean fapiCompatibility) { + this.fapiCompatibility = fapiCompatibility; + } + + public Boolean getForceIdTokenHintPrecense() { + if (forceIdTokenHintPrecense == null) forceIdTokenHintPrecense = false; + return forceIdTokenHintPrecense; + } + + public void setForceIdTokenHintPrecense(Boolean forceIdTokenHintPrecense) { + this.forceIdTokenHintPrecense = forceIdTokenHintPrecense; + } + + public Boolean getRejectEndSessionIfIdTokenExpired() { + if (rejectEndSessionIfIdTokenExpired == null) rejectEndSessionIfIdTokenExpired = false; + return rejectEndSessionIfIdTokenExpired; + } + + public void setRejectEndSessionIfIdTokenExpired(Boolean rejectEndSessionIfIdTokenExpired) { + this.rejectEndSessionIfIdTokenExpired = rejectEndSessionIfIdTokenExpired; + } + + public Boolean getAllowEndSessionWithUnmatchedSid() { + if (allowEndSessionWithUnmatchedSid == null) allowEndSessionWithUnmatchedSid = false; + return allowEndSessionWithUnmatchedSid; + } + + public void setAllowEndSessionWithUnmatchedSid(Boolean allowEndSessionWithUnmatchedSid) { + this.allowEndSessionWithUnmatchedSid = allowEndSessionWithUnmatchedSid; + } + + public Boolean getRemoveRefreshTokensForClientOnLogout() { + if (removeRefreshTokensForClientOnLogout == null) removeRefreshTokensForClientOnLogout = true; + return removeRefreshTokensForClientOnLogout; + } + + public void setRemoveRefreshTokensForClientOnLogout(Boolean removeRefreshTokensForClientOnLogout) { + this.removeRefreshTokensForClientOnLogout = removeRefreshTokensForClientOnLogout; + } + + public Boolean getDisableJdkLogger() { + return disableJdkLogger; + } + + public void setDisableJdkLogger(Boolean disableJdkLogger) { + this.disableJdkLogger = disableJdkLogger; + } + + /** + * Used in ServletLoggingFilter to enable http request/response logging. + */ + private Boolean httpLoggingEnabled; + + /** + * Used in ServletLoggingFilter to exclude some paths from logger. Paths example: ["/oxauth/img", "/oxauth/stylesheet"] + */ + private Set httpLoggingExludePaths; + + /** + * Path to external log4j2 configuration file. This property might be configured from oxTrust: /identity/logviewer/configure + */ + private String externalLoggerConfiguration; + + public Boolean getFrontChannelLogoutSessionSupported() { + return frontChannelLogoutSessionSupported; + } + + public void setFrontChannelLogoutSessionSupported( + Boolean frontChannelLogoutSessionSupported) { + this.frontChannelLogoutSessionSupported = frontChannelLogoutSessionSupported; + } + + public Boolean getIntrospectionAccessTokenMustHaveUmaProtectionScope() { + return introspectionAccessTokenMustHaveUmaProtectionScope; + } + + public void setIntrospectionAccessTokenMustHaveUmaProtectionScope(Boolean introspectionAccessTokenMustHaveUmaProtectionScope) { + this.introspectionAccessTokenMustHaveUmaProtectionScope = introspectionAccessTokenMustHaveUmaProtectionScope; + } + + public Boolean getIntrospectionSkipAuthorization() { + if (introspectionSkipAuthorization == null) introspectionSkipAuthorization = false; + return introspectionSkipAuthorization; + } + + public void setIntrospectionSkipAuthorization(Boolean introspectionSkipAuthorization) { + this.introspectionSkipAuthorization = introspectionSkipAuthorization; + } + + public Boolean getIntrospectionRestrictBasicAuthnToOwnTokens() { + if (introspectionRestrictBasicAuthnToOwnTokens == null) introspectionRestrictBasicAuthnToOwnTokens = false; + return introspectionRestrictBasicAuthnToOwnTokens; + } + + public void setIntrospectionRestrictBasicAuthnToOwnTokens(Boolean introspectionRestrictBasicAuthnToOwnTokens) { + this.introspectionRestrictBasicAuthnToOwnTokens = introspectionRestrictBasicAuthnToOwnTokens; + } + + public Boolean getUmaRptAsJwt() { + return umaRptAsJwt; + } + + public void setUmaRptAsJwt(Boolean umaRptAsJwt) { + this.umaRptAsJwt = umaRptAsJwt; + } + + public Boolean getForceRopcInAuthorizationEndpoint() { + if (forceRopcInAuthorizationEndpoint == null) forceRopcInAuthorizationEndpoint = false; + return forceRopcInAuthorizationEndpoint; + } + + public void setForceRopcInAuthorizationEndpoint(Boolean forceRopcInAuthorizationEndpoint) { + this.forceRopcInAuthorizationEndpoint = forceRopcInAuthorizationEndpoint; + } + + public Boolean getSessionAsJwt() { + return sessionAsJwt; + } + + public void setSessionAsJwt(Boolean sessionAsJwt) { + this.sessionAsJwt = sessionAsJwt; + } + + public Boolean getUmaAddScopesAutomatically() { + return umaAddScopesAutomatically; + } + + public void setUmaAddScopesAutomatically(Boolean p_umaAddScopesAutomatically) { + umaAddScopesAutomatically = p_umaAddScopesAutomatically; + } + + public Boolean getUmaValidateClaimToken() { + return umaValidateClaimToken; + } + + public void setUmaValidateClaimToken(Boolean umaValidateClaimToken) { + this.umaValidateClaimToken = umaValidateClaimToken; + } + + public Boolean getUmaGrantAccessIfNoPolicies() { + return umaGrantAccessIfNoPolicies; + } + + public void setUmaGrantAccessIfNoPolicies(Boolean umaGrantAccessIfNoPolicies) { + this.umaGrantAccessIfNoPolicies = umaGrantAccessIfNoPolicies; + } + + public Boolean getUmaRestrictResourceToAssociatedClient() { + return umaRestrictResourceToAssociatedClient; + } + + public void setUmaRestrictResourceToAssociatedClient(Boolean umaRestrictResourceToAssociatedClient) { + this.umaRestrictResourceToAssociatedClient = umaRestrictResourceToAssociatedClient; + } + + /** + * Returns the issuer identifier. + * + * @return The issuer identifier. + */ + public String getIssuer() { + return issuer; + } + + /** + * Sets the issuer identifier. + * + * @param issuer The issuer identifier. + */ + public void setIssuer(String issuer) { + this.issuer = issuer; + } + + /** + * Returns the base URI of the endpoints. + * + * @return The base URI of endpoints. + */ + public String getBaseEndpoint() { + return baseEndpoint; + } + + /** + * Sets the base URI of the endpoints. + * + * @param baseEndpoint The base URI of the endpoints. + */ + public void setBaseEndpoint(String baseEndpoint) { + this.baseEndpoint = baseEndpoint; + } + + /** + * Returns the URL of the Authentication and Authorization endpoint. + * + * @return The URL of the Authentication and Authorization endpoint. + */ + public String getAuthorizationEndpoint() { + return authorizationEndpoint; + } + + /** + * Sets the URL of the Authentication and Authorization endpoint. + * + * @param authorizationEndpoint The URL of the Authentication and Authorization endpoint. + */ + public void setAuthorizationEndpoint(String authorizationEndpoint) { + this.authorizationEndpoint = authorizationEndpoint; + } + + /** + * Returns the URL of the Token endpoint. + * + * @return The URL of the Token endpoint. + */ + public String getTokenEndpoint() { + return tokenEndpoint; + } + + /** + * Sets the URL of the Token endpoint. + * + * @param tokenEndpoint The URL of the Token endpoint. + */ + public void setTokenEndpoint(String tokenEndpoint) { + this.tokenEndpoint = tokenEndpoint; + } + + /** + * Returns the URL of the Token Revocation endpoint. + * + * @return The URL of the Token Revocation endpoint. + */ + public String getTokenRevocationEndpoint() { + return tokenRevocationEndpoint; + } + + /** + * Sets the URL of the Token Revocation endpoint. + * + * @param tokenRevocationEndpoint The URL of the Token Revocation endpoint. + */ + public void setTokenRevocationEndpoint(String tokenRevocationEndpoint) { + this.tokenRevocationEndpoint = tokenRevocationEndpoint; + } + + /** + * Returns the URL of the User Info endpoint. + * + * @return The URL of the User Info endpoint. + */ + public String getUserInfoEndpoint() { + return userInfoEndpoint; + } + + /** + * Sets the URL for the User Info endpoint. + * + * @param userInfoEndpoint The URL for the User Info endpoint. + */ + public void setUserInfoEndpoint(String userInfoEndpoint) { + this.userInfoEndpoint = userInfoEndpoint; + } + + /** + * Returns the URL od the Client Info endpoint. + * + * @return The URL of the Client Info endpoint. + */ + public String getClientInfoEndpoint() { + return clientInfoEndpoint; + } + + /** + * Sets the URL for the Client Info endpoint. + * + * @param clientInfoEndpoint The URL for the Client Info endpoint. + */ + public void setClientInfoEndpoint(String clientInfoEndpoint) { + this.clientInfoEndpoint = clientInfoEndpoint; + } + + /** + * Returns the URL of an OP endpoint that provides a page to support cross-origin + * communications for session state information with the RP client. + * + * @return The Check Session iFrame URL. + */ + public String getCheckSessionIFrame() { + return checkSessionIFrame; + } + + /** + * Sets the URL of an OP endpoint that provides a page to support cross-origin + * communications for session state information with the RP client. + * + * @param checkSessionIFrame The Check Session iFrame URL. + */ + public void setCheckSessionIFrame(String checkSessionIFrame) { + this.checkSessionIFrame = checkSessionIFrame; + } + + /** + * Returns the URL of the End Session endpoint. + * + * @return The URL of the End Session endpoint. + */ + public String getEndSessionEndpoint() { + return endSessionEndpoint; + } + + /** + * Sets the URL of the End Session endpoint. + * + * @param endSessionEndpoint The URL of the End Session endpoint. + */ + public void setEndSessionEndpoint(String endSessionEndpoint) { + this.endSessionEndpoint = endSessionEndpoint; + } + + /** + * Returns the URL of the OP's JSON Web Key Set (JWK) document that contains the Server's signing key(s) + * that are used for signing responses to the Client. + * The JWK Set may also contain the Server's encryption key(s) that are used by the Client to encrypt + * requests to the Server. + * + * @return The URL of the OP's JSON Web Key Set (JWK) document. + */ + public String getJwksUri() { + return jwksUri; + } + + /** + * Sets the URL of the OP's JSON Web Key Set (JWK) document that contains the Server's signing key(s) + * that are used for signing responses to the Client. + * The JWK Set may also contain the Server's encryption key(s) that are used by the Client to encrypt + * requests to the Server. + * + * @param jwksUri The URL of the OP's JSON Web Key Set (JWK) document. + */ + public void setJwksUri(String jwksUri) { + this.jwksUri = jwksUri; + } + + /** + * Returns the URL of the Dynamic Client Registration endpoint. + * + * @return The URL of the Dynamic Client Registration endpoint. + */ + public String getRegistrationEndpoint() { + return registrationEndpoint; + } + + /** + * Sets the URL of the Dynamic Client Registration endpoint. + * + * @param registrationEndpoint The URL of the Dynamic Client Registration endpoint. + */ + public void setRegistrationEndpoint(String registrationEndpoint) { + this.registrationEndpoint = registrationEndpoint; + } + + public String getOpenIdDiscoveryEndpoint() { + return openIdDiscoveryEndpoint; + } + + public void setOpenIdDiscoveryEndpoint(String openIdDiscoveryEndpoint) { + this.openIdDiscoveryEndpoint = openIdDiscoveryEndpoint; + } + + public String getUmaConfigurationEndpoint() { + return umaConfigurationEndpoint; + } + + public void setUmaConfigurationEndpoint(String p_umaConfigurationEndpoint) { + umaConfigurationEndpoint = p_umaConfigurationEndpoint; + } + + public String getOpenidSubAttribute() { + return openidSubAttribute; + } + + public void setOpenidSubAttribute(String openidSubAttribute) { + this.openidSubAttribute = openidSubAttribute; + } + + public String getIdGenerationEndpoint() { + return idGenerationEndpoint; + } + + public void setIdGenerationEndpoint(String p_idGenerationEndpoint) { + idGenerationEndpoint = p_idGenerationEndpoint; + } + + public String getIntrospectionEndpoint() { + return introspectionEndpoint; + } + + public void setIntrospectionEndpoint(String p_introspectionEndpoint) { + introspectionEndpoint = p_introspectionEndpoint; + } + + public String getOpenIdConfigurationEndpoint() { + return openIdConfigurationEndpoint; + } + + public void setOpenIdConfigurationEndpoint(String openIdConfigurationEndpoint) { + this.openIdConfigurationEndpoint = openIdConfigurationEndpoint; + } + + public Set> getResponseTypesSupported() { + return responseTypesSupported; + } + + public void setResponseTypesSupported(Set> responseTypesSupported) { + this.responseTypesSupported = responseTypesSupported; + } + + public Set getResponseModesSupported() { + return responseModesSupported; + } + + public void setResponseModesSupported(Set responseModesSupported) { + this.responseModesSupported = responseModesSupported; + } + + public Set getGrantTypesSupported() { + return grantTypesSupported; + } + + public void setGrantTypesSupported(Set grantTypesSupported) { + this.grantTypesSupported = grantTypesSupported; + } + + public List getSubjectTypesSupported() { + return subjectTypesSupported; + } + + public void setSubjectTypesSupported(List subjectTypesSupported) { + this.subjectTypesSupported = subjectTypesSupported; + } + + public String getDefaultSubjectType() { + return defaultSubjectType; + } + + public void setDefaultSubjectType(String defaultSubjectType) { + this.defaultSubjectType = defaultSubjectType; + } + + public Boolean getStatEnabled() { + if (statEnabled == null) statEnabled = true; + return statEnabled; + } + + public void setStatEnabled(Boolean statEnabled) { + this.statEnabled = statEnabled; + } + + public String getStatAuthorizationScope() { + if (statAuthorizationScope == null) statAuthorizationScope = DEFAULT_STAT_SCOPE; + return statAuthorizationScope; + } + + public void setStatAuthorizationScope(String statAuthorizationScope) { + this.statAuthorizationScope = statAuthorizationScope; + } + + public int getStatWebServiceIntervalLimitInSeconds() { + return statWebServiceIntervalLimitInSeconds; + } + + public void setStatWebServiceIntervalLimitInSeconds(int statWebServiceIntervalLimitInSeconds) { + this.statWebServiceIntervalLimitInSeconds = statWebServiceIntervalLimitInSeconds; + } + + public int getStatTimerIntervalInSeconds() { + return statTimerIntervalInSeconds; + } + + public void setStatTimerIntervalInSeconds(int statTimerIntervalInSeconds) { + this.statTimerIntervalInSeconds = statTimerIntervalInSeconds; + } + + public List getUserInfoSigningAlgValuesSupported() { + return userInfoSigningAlgValuesSupported; + } + + public void setUserInfoSigningAlgValuesSupported(List userInfoSigningAlgValuesSupported) { + this.userInfoSigningAlgValuesSupported = userInfoSigningAlgValuesSupported; + } + + public List getUserInfoEncryptionAlgValuesSupported() { + return userInfoEncryptionAlgValuesSupported; + } + + public void setUserInfoEncryptionAlgValuesSupported(List userInfoEncryptionAlgValuesSupported) { + this.userInfoEncryptionAlgValuesSupported = userInfoEncryptionAlgValuesSupported; + } + + public List getUserInfoEncryptionEncValuesSupported() { + return userInfoEncryptionEncValuesSupported; + } + + public void setUserInfoEncryptionEncValuesSupported(List userInfoEncryptionEncValuesSupported) { + this.userInfoEncryptionEncValuesSupported = userInfoEncryptionEncValuesSupported; + } + + public List getIdTokenSigningAlgValuesSupported() { + return idTokenSigningAlgValuesSupported; + } + + public void setIdTokenSigningAlgValuesSupported(List idTokenSigningAlgValuesSupported) { + this.idTokenSigningAlgValuesSupported = idTokenSigningAlgValuesSupported; + } + + public List getIdTokenEncryptionAlgValuesSupported() { + return idTokenEncryptionAlgValuesSupported; + } + + public void setIdTokenEncryptionAlgValuesSupported(List idTokenEncryptionAlgValuesSupported) { + this.idTokenEncryptionAlgValuesSupported = idTokenEncryptionAlgValuesSupported; + } + + public List getIdTokenEncryptionEncValuesSupported() { + return idTokenEncryptionEncValuesSupported; + } + + public void setIdTokenEncryptionEncValuesSupported(List idTokenEncryptionEncValuesSupported) { + this.idTokenEncryptionEncValuesSupported = idTokenEncryptionEncValuesSupported; + } + + public List getRequestObjectSigningAlgValuesSupported() { + return requestObjectSigningAlgValuesSupported; + } + + public void setRequestObjectSigningAlgValuesSupported(List requestObjectSigningAlgValuesSupported) { + this.requestObjectSigningAlgValuesSupported = requestObjectSigningAlgValuesSupported; + } + + public List getRequestObjectEncryptionAlgValuesSupported() { + return requestObjectEncryptionAlgValuesSupported; + } + + public void setRequestObjectEncryptionAlgValuesSupported(List requestObjectEncryptionAlgValuesSupported) { + this.requestObjectEncryptionAlgValuesSupported = requestObjectEncryptionAlgValuesSupported; + } + + public List getRequestObjectEncryptionEncValuesSupported() { + return requestObjectEncryptionEncValuesSupported; + } + + public void setRequestObjectEncryptionEncValuesSupported(List requestObjectEncryptionEncValuesSupported) { + this.requestObjectEncryptionEncValuesSupported = requestObjectEncryptionEncValuesSupported; + } + + public List getTokenEndpointAuthMethodsSupported() { + return tokenEndpointAuthMethodsSupported; + } + + public void setTokenEndpointAuthMethodsSupported(List tokenEndpointAuthMethodsSupported) { + this.tokenEndpointAuthMethodsSupported = tokenEndpointAuthMethodsSupported; + } + + public List getTokenEndpointAuthSigningAlgValuesSupported() { + return tokenEndpointAuthSigningAlgValuesSupported; + } + + public void setTokenEndpointAuthSigningAlgValuesSupported(List tokenEndpointAuthSigningAlgValuesSupported) { + this.tokenEndpointAuthSigningAlgValuesSupported = tokenEndpointAuthSigningAlgValuesSupported; + } + + public List getDynamicRegistrationCustomAttributes() { + return dynamicRegistrationCustomAttributes; + } + + public void setDynamicRegistrationCustomAttributes(List p_dynamicRegistrationCustomAttributes) { + dynamicRegistrationCustomAttributes = p_dynamicRegistrationCustomAttributes; + } + + public List getDisplayValuesSupported() { + return displayValuesSupported; + } + + public void setDisplayValuesSupported(List displayValuesSupported) { + this.displayValuesSupported = displayValuesSupported; + } + + public List getClaimTypesSupported() { + return claimTypesSupported; + } + + public void setClaimTypesSupported(List claimTypesSupported) { + this.claimTypesSupported = claimTypesSupported; + } + + public List getJwksAlgorithmsSupported() { + return jwksAlgorithmsSupported; + } + + public void setJwksAlgorithmsSupported(List jwksAlgorithmsSupported) { + this.jwksAlgorithmsSupported = jwksAlgorithmsSupported; + } + + public String getServiceDocumentation() { + return serviceDocumentation; + } + + public void setServiceDocumentation(String serviceDocumentation) { + this.serviceDocumentation = serviceDocumentation; + } + + public List getClaimsLocalesSupported() { + return claimsLocalesSupported; + } + + public void setClaimsLocalesSupported(List claimsLocalesSupported) { + this.claimsLocalesSupported = claimsLocalesSupported; + } + + public List getIdTokenTokenBindingCnfValuesSupported() { + if (idTokenTokenBindingCnfValuesSupported == null) { + idTokenTokenBindingCnfValuesSupported = new ArrayList(); + } + return idTokenTokenBindingCnfValuesSupported; + } + + public void setIdTokenTokenBindingCnfValuesSupported(List idTokenTokenBindingCnfValuesSupported) { + this.idTokenTokenBindingCnfValuesSupported = idTokenTokenBindingCnfValuesSupported; + } + + public List getUiLocalesSupported() { + return uiLocalesSupported; + } + + public void setUiLocalesSupported(List uiLocalesSupported) { + this.uiLocalesSupported = uiLocalesSupported; + } + + public Boolean getClaimsParameterSupported() { + return claimsParameterSupported; + } + + public void setClaimsParameterSupported(Boolean claimsParameterSupported) { + this.claimsParameterSupported = claimsParameterSupported; + } + + public Boolean getRequestParameterSupported() { + return requestParameterSupported; + } + + public void setRequestParameterSupported(Boolean requestParameterSupported) { + this.requestParameterSupported = requestParameterSupported; + } + + public Boolean getRequestUriParameterSupported() { + return requestUriParameterSupported; + } + + public void setRequestUriParameterSupported(Boolean requestUriParameterSupported) { + this.requestUriParameterSupported = requestUriParameterSupported; + } + + public Boolean getRequireRequestUriRegistration() { + return requireRequestUriRegistration; + } + + public void setRequireRequestUriRegistration(Boolean requireRequestUriRegistration) { + this.requireRequestUriRegistration = requireRequestUriRegistration; + } + + public String getOpPolicyUri() { + return opPolicyUri; + } + + public void setOpPolicyUri(String opPolicyUri) { + this.opPolicyUri = opPolicyUri; + } + + public String getOpTosUri() { + return opTosUri; + } + + public void setOpTosUri(String opTosUri) { + this.opTosUri = opTosUri; + } + + public int getAuthorizationCodeLifetime() { + return authorizationCodeLifetime; + } + + public void setAuthorizationCodeLifetime(int authorizationCodeLifetime) { + this.authorizationCodeLifetime = authorizationCodeLifetime; + } + + public int getRefreshTokenLifetime() { + return refreshTokenLifetime; + } + + public void setRefreshTokenLifetime(int refreshTokenLifetime) { + this.refreshTokenLifetime = refreshTokenLifetime; + } + + public int getIdTokenLifetime() { + return idTokenLifetime; + } + + public void setIdTokenLifetime(int idTokenLifetime) { + this.idTokenLifetime = idTokenLifetime; + } + + public int getAccessTokenLifetime() { + return accessTokenLifetime; + } + + public void setAccessTokenLifetime(int accessTokenLifetime) { + this.accessTokenLifetime = accessTokenLifetime; + } + + public int getUmaRptLifetime() { + return umaRptLifetime; + } + + public void setUmaRptLifetime(int umaRptLifetime) { + this.umaRptLifetime = umaRptLifetime; + } + + public int getUmaTicketLifetime() { + return umaTicketLifetime; + } + + public void setUmaTicketLifetime(int umaTicketLifetime) { + this.umaTicketLifetime = umaTicketLifetime; + } + + public int getUmaResourceLifetime() { + return umaResourceLifetime; + } + + public void setUmaResourceLifetime(int umaResourceLifetime) { + this.umaResourceLifetime = umaResourceLifetime; + } + + public int getUmaPctLifetime() { + return umaPctLifetime; + } + + public void setUmaPctLifetime(int umaPctLifetime) { + this.umaPctLifetime = umaPctLifetime; + } + + public Boolean getAllowSpontaneousScopes() { + if (allowSpontaneousScopes == null) allowSpontaneousScopes = true; + return allowSpontaneousScopes; + } + + public void setAllowSpontaneousScopes(Boolean allowSpontaneousScopes) { + this.allowSpontaneousScopes = allowSpontaneousScopes; + } + + public int getSpontaneousScopeLifetime() { + return spontaneousScopeLifetime; + } + + public void setSpontaneousScopeLifetime(int spontaneousScopeLifetime) { + this.spontaneousScopeLifetime = spontaneousScopeLifetime; + } + + public int getCleanServiceInterval() { + return cleanServiceInterval; + } + + public void setCleanServiceInterval(int p_cleanServiceInterval) { + cleanServiceInterval = p_cleanServiceInterval; + } + + public int getCleanServiceBatchChunkSize() { + return cleanServiceBatchChunkSize; + } + + public void setCleanServiceBatchChunkSize(int cleanServiceBatchChunkSize) { + this.cleanServiceBatchChunkSize = cleanServiceBatchChunkSize; + } + + public Boolean getKeyRegenerationEnabled() { + return keyRegenerationEnabled; + } + + public void setKeyRegenerationEnabled(Boolean keyRegenerationEnabled) { + this.keyRegenerationEnabled = keyRegenerationEnabled; + } + + public int getKeyRegenerationInterval() { + return keyRegenerationInterval; + } + + public void setKeyRegenerationInterval(int keyRegenerationInterval) { + this.keyRegenerationInterval = keyRegenerationInterval; + } + + public String getDefaultSignatureAlgorithm() { + return defaultSignatureAlgorithm; + } + + public void setDefaultSignatureAlgorithm(String defaultSignatureAlgorithm) { + this.defaultSignatureAlgorithm = defaultSignatureAlgorithm; + } + + public String getOxOpenIdConnectVersion() { + return oxOpenIdConnectVersion; + } + + public void setOxOpenIdConnectVersion(String oxOpenIdConnectVersion) { + this.oxOpenIdConnectVersion = oxOpenIdConnectVersion; + } + + public String getOxId() { + return oxId; + } + + public void setOxId(String oxId) { + this.oxId = oxId; + } + + public Boolean getDynamicRegistrationEnabled() { + if (dynamicRegistrationEnabled == null) dynamicRegistrationEnabled = false; + return dynamicRegistrationEnabled; + } + + public void setDynamicRegistrationEnabled(Boolean dynamicRegistrationEnabled) { + this.dynamicRegistrationEnabled = dynamicRegistrationEnabled; + } + + public int getDynamicRegistrationExpirationTime() { + return dynamicRegistrationExpirationTime; + } + + public void setDynamicRegistrationExpirationTime(int dynamicRegistrationExpirationTime) { + this.dynamicRegistrationExpirationTime = dynamicRegistrationExpirationTime; + } + + public Boolean getDynamicRegistrationPersistClientAuthorizations() { + return dynamicRegistrationPersistClientAuthorizations; + } + + public void setDynamicRegistrationPersistClientAuthorizations(Boolean dynamicRegistrationPersistClientAuthorizations) { + this.dynamicRegistrationPersistClientAuthorizations = dynamicRegistrationPersistClientAuthorizations; + } + + public Boolean getTrustedClientEnabled() { + return trustedClientEnabled; + } + + public void setTrustedClientEnabled(Boolean trustedClientEnabled) { + this.trustedClientEnabled = trustedClientEnabled; + } + + public Boolean getSkipAuthorizationForOpenIdScopeAndPairwiseId() { + return skipAuthorizationForOpenIdScopeAndPairwiseId; + } + + public void setSkipAuthorizationForOpenIdScopeAndPairwiseId(Boolean skipAuthorizationForOpenIdScopeAndPairwiseId) { + this.skipAuthorizationForOpenIdScopeAndPairwiseId = skipAuthorizationForOpenIdScopeAndPairwiseId; + } + + public Boolean getDynamicRegistrationScopesParamEnabled() { + return dynamicRegistrationScopesParamEnabled; + } + + public void setDynamicRegistrationScopesParamEnabled(Boolean dynamicRegistrationScopesParamEnabled) { + this.dynamicRegistrationScopesParamEnabled = dynamicRegistrationScopesParamEnabled; + } + + public Boolean getDynamicRegistrationDisableFallbackScopesAssigning() { + if (dynamicRegistrationDisableFallbackScopesAssigning == null) dynamicRegistrationDisableFallbackScopesAssigning = false; + return dynamicRegistrationDisableFallbackScopesAssigning; + } + + public void setDynamicRegistrationDisableFallbackScopesAssigning(Boolean dynamicRegistrationDisableFallbackScopesAssigning) { + this.dynamicRegistrationDisableFallbackScopesAssigning = dynamicRegistrationDisableFallbackScopesAssigning; + } + + public Boolean getPersistIdTokenInLdap() { + return persistIdTokenInLdap; + } + + public void setPersistIdTokenInLdap(Boolean persistIdTokenInLdap) { + this.persistIdTokenInLdap = persistIdTokenInLdap; + } + + public Boolean getPersistRefreshTokenInLdap() { + return persistRefreshTokenInLdap; + } + + public void setPersistRefreshTokenInLdap(Boolean persistRefreshTokenInLdap) { + this.persistRefreshTokenInLdap = persistRefreshTokenInLdap; + } + + public Boolean getAllowPostLogoutRedirectWithoutValidation() { + if (allowPostLogoutRedirectWithoutValidation == null) allowPostLogoutRedirectWithoutValidation = false; + return allowPostLogoutRedirectWithoutValidation; + } + + public void setAllowPostLogoutRedirectWithoutValidation(Boolean allowPostLogoutRedirectWithoutValidation) { + this.allowPostLogoutRedirectWithoutValidation = allowPostLogoutRedirectWithoutValidation; + } + + public Boolean getInvalidateSessionCookiesAfterAuthorizationFlow() { + if (invalidateSessionCookiesAfterAuthorizationFlow == null) { + invalidateSessionCookiesAfterAuthorizationFlow = false; + } + return invalidateSessionCookiesAfterAuthorizationFlow; + } + + public void setInvalidateSessionCookiesAfterAuthorizationFlow(Boolean invalidateSessionCookiesAfterAuthorizationFlow) { + this.invalidateSessionCookiesAfterAuthorizationFlow = invalidateSessionCookiesAfterAuthorizationFlow; + } + + public Boolean getUseCacheForAllImplicitFlowObjects() { + return useCacheForAllImplicitFlowObjects; + } + + public void setUseCacheForAllImplicitFlowObjects(Boolean useCacheForAllImplicitFlowObjects) { + this.useCacheForAllImplicitFlowObjects = useCacheForAllImplicitFlowObjects; + } + + public String getDynamicRegistrationCustomObjectClass() { + return dynamicRegistrationCustomObjectClass; + } + + public void setDynamicRegistrationCustomObjectClass(String p_dynamicRegistrationCustomObjectClass) { + dynamicRegistrationCustomObjectClass = p_dynamicRegistrationCustomObjectClass; + } + + public List getPersonCustomObjectClassList() { + return personCustomObjectClassList; + } + + public void setPersonCustomObjectClassList(List personCustomObjectClassList) { + this.personCustomObjectClassList = personCustomObjectClassList; + } + + public Boolean getAuthenticationFiltersEnabled() { + return authenticationFiltersEnabled; + } + + public void setAuthenticationFiltersEnabled(Boolean authenticationFiltersEnabled) { + this.authenticationFiltersEnabled = authenticationFiltersEnabled; + } + + public Boolean getClientAuthenticationFiltersEnabled() { + return clientAuthenticationFiltersEnabled; + } + + public void setClientAuthenticationFiltersEnabled(Boolean p_clientAuthenticationFiltersEnabled) { + clientAuthenticationFiltersEnabled = p_clientAuthenticationFiltersEnabled; + } + + public List getAuthenticationFilters() { + if (authenticationFilters == null) { + authenticationFilters = new ArrayList(); + } + + return authenticationFilters; + } + + public List getClientAuthenticationFilters() { + if (clientAuthenticationFilters == null) { + clientAuthenticationFilters = new ArrayList(); + } + + return clientAuthenticationFilters; + } + + + public List getCorsConfigurationFilters() { + if (corsConfigurationFilters == null) { + corsConfigurationFilters = new ArrayList(); + } + + return corsConfigurationFilters; + } + + public int getSessionIdUnusedLifetime() { + return sessionIdUnusedLifetime; + } + + public void setSessionIdUnusedLifetime(int p_sessionIdUnusedLifetime) { + sessionIdUnusedLifetime = p_sessionIdUnusedLifetime; + } + + public int getSessionIdUnauthenticatedUnusedLifetime() { + return sessionIdUnauthenticatedUnusedLifetime; + } + + public void setSessionIdUnauthenticatedUnusedLifetime(int sessionIdUnauthenticatedUnusedLifetime) { + this.sessionIdUnauthenticatedUnusedLifetime = sessionIdUnauthenticatedUnusedLifetime; + } + + public Boolean getSessionIdPersistOnPromptNone() { + return sessionIdPersistOnPromptNone; + } + + public void setSessionIdPersistOnPromptNone(Boolean sessionIdPersistOnPromptNone) { + this.sessionIdPersistOnPromptNone = sessionIdPersistOnPromptNone; + } + + public Boolean getSessionIdRequestParameterEnabled() { + if (sessionIdRequestParameterEnabled == null) { + sessionIdRequestParameterEnabled = false; + } + return sessionIdRequestParameterEnabled; + } + + public void setSessionIdRequestParameterEnabled(Boolean sessionIdRequestParameterEnabled) { + this.sessionIdRequestParameterEnabled = sessionIdRequestParameterEnabled; + } + + public int getConfigurationUpdateInterval() { + return configurationUpdateInterval; + } + + public void setConfigurationUpdateInterval(int p_configurationUpdateInterval) { + configurationUpdateInterval = p_configurationUpdateInterval; + } + + public String getJsLocation() { + return jsLocation; + } + + public void setJsLocation(String jsLocation) { + this.jsLocation = jsLocation; + } + + public String getCssLocation() { + return cssLocation; + } + + public void setCssLocation(String cssLocation) { + this.cssLocation = cssLocation; + } + + public String getImgLocation() { + return imgLocation; + } + + public void setImgLocation(String imgLocation) { + this.imgLocation = imgLocation; + } + + public int getMetricReporterInterval() { + return metricReporterInterval; + } + + public void setMetricReporterInterval(int metricReporterInterval) { + this.metricReporterInterval = metricReporterInterval; + } + + public int getMetricReporterKeepDataDays() { + return metricReporterKeepDataDays; + } + + public void setMetricReporterKeepDataDays(int metricReporterKeepDataDays) { + this.metricReporterKeepDataDays = metricReporterKeepDataDays; + } + + public Boolean getMetricReporterEnabled() { + return metricReporterEnabled; + } + + public void setMetricReporterEnabled(Boolean metricReporterEnabled) { + this.metricReporterEnabled = metricReporterEnabled; + } + + public String getPairwiseIdType() { + return pairwiseIdType; + } + + public void setPairwiseIdType(String pairwiseIdType) { + this.pairwiseIdType = pairwiseIdType; + } + + public String getPairwiseCalculationKey() { + return pairwiseCalculationKey; + } + + public void setPairwiseCalculationKey(String pairwiseCalculationKey) { + this.pairwiseCalculationKey = pairwiseCalculationKey; + } + + public String getPairwiseCalculationSalt() { + return pairwiseCalculationSalt; + } + + public void setPairwiseCalculationSalt(String pairwiseCalculationSalt) { + this.pairwiseCalculationSalt = pairwiseCalculationSalt; + } + + public Boolean isShareSubjectIdBetweenClientsWithSameSectorId() { + return shareSubjectIdBetweenClientsWithSameSectorId; + } + + public void setShareSubjectIdBetweenClientsWithSameSectorId(Boolean shareSubjectIdBetweenClientsWithSameSectorId) { + this.shareSubjectIdBetweenClientsWithSameSectorId = shareSubjectIdBetweenClientsWithSameSectorId; + } + + public WebKeyStorage getWebKeysStorage() { + return webKeysStorage; + } + + public void setWebKeysStorage(WebKeyStorage webKeysStorage) { + this.webKeysStorage = webKeysStorage; + } + + public String getDnName() { + return dnName; + } + + public void setDnName(String dnName) { + this.dnName = dnName; + } + + public String getKeyStoreFile() { + return keyStoreFile; + } + + public void setKeyStoreFile(String keyStoreFile) { + this.keyStoreFile = keyStoreFile; + } + + public String getKeyStoreSecret() { + return keyStoreSecret; + } + + public void setKeyStoreSecret(String keyStoreSecret) { + this.keyStoreSecret = keyStoreSecret; + } + + public String getOxElevenTestModeToken() { + return oxElevenTestModeToken; + } + + public void setOxElevenTestModeToken(String oxElevenTestModeToken) { + this.oxElevenTestModeToken = oxElevenTestModeToken; + } + + public String getOxElevenGenerateKeyEndpoint() { + return oxElevenGenerateKeyEndpoint; + } + + public void setOxElevenGenerateKeyEndpoint(String oxElevenGenerateKeyEndpoint) { + this.oxElevenGenerateKeyEndpoint = oxElevenGenerateKeyEndpoint; + } + + public String getOxElevenSignEndpoint() { + return oxElevenSignEndpoint; + } + + public void setOxElevenSignEndpoint(String oxElevenSignEndpoint) { + this.oxElevenSignEndpoint = oxElevenSignEndpoint; + } + + public String getOxElevenVerifySignatureEndpoint() { + return oxElevenVerifySignatureEndpoint; + } + + public void setOxElevenVerifySignatureEndpoint(String oxElevenVerifySignatureEndpoint) { + this.oxElevenVerifySignatureEndpoint = oxElevenVerifySignatureEndpoint; + } + + public String getOxElevenDeleteKeyEndpoint() { + return oxElevenDeleteKeyEndpoint; + } + + public void setOxElevenDeleteKeyEndpoint(String oxElevenDeleteKeyEndpoint) { + this.oxElevenDeleteKeyEndpoint = oxElevenDeleteKeyEndpoint; + } + + public Boolean getEndSessionWithAccessToken() { + return endSessionWithAccessToken; + } + + public void setEndSessionWithAccessToken(Boolean endSessionWithAccessToken) { + this.endSessionWithAccessToken = endSessionWithAccessToken; + } + + public String getCookieDomain() { + return cookieDomain; + } + + public void setCookieDomain(String cookieDomain) { + this.cookieDomain = cookieDomain; + } + + public Boolean getEnabledOAuthAuditLogging() { + return enabledOAuthAuditLogging; + } + + public void setEnabledOAuthAuditLogging(Boolean enabledOAuthAuditLogging) { + this.enabledOAuthAuditLogging = enabledOAuthAuditLogging; + } + + public Set getJmsBrokerURISet() { + return jmsBrokerURISet; + } + + public void setJmsBrokerURISet(Set jmsBrokerURISet) { + this.jmsBrokerURISet = jmsBrokerURISet; + } + + public String getJmsUserName() { + return jmsUserName; + } + + public void setJmsUserName(String jmsUserName) { + this.jmsUserName = jmsUserName; + } + + public String getJmsPassword() { + return jmsPassword; + } + + public void setJmsPassword(String jmsPassword) { + this.jmsPassword = jmsPassword; + } + + public Boolean getAllowWildcardRedirectUri() { + return allowWildcardRedirectUri; + } + + public void setAllowWildcardRedirectUri(Boolean allowWildcardRedirectUri) { + this.allowWildcardRedirectUri = allowWildcardRedirectUri; + } + + public List getClientWhiteList() { + return clientWhiteList; + } + + public void setClientWhiteList(List clientWhiteList) { + this.clientWhiteList = clientWhiteList; + } + + public List getClientBlackList() { + return clientBlackList; + } + + public void setClientBlackList(List clientBlackList) { + this.clientBlackList = clientBlackList; + } + + public Boolean getLegacyIdTokenClaims() { + return legacyIdTokenClaims; + } + + public void setLegacyIdTokenClaims(Boolean legacyIdTokenClaims) { + this.legacyIdTokenClaims = legacyIdTokenClaims; + } + + public Boolean getCustomHeadersWithAuthorizationResponse() { + if (customHeadersWithAuthorizationResponse == null) { + return false; + } + + return customHeadersWithAuthorizationResponse; + } + + public void setCustomHeadersWithAuthorizationResponse(Boolean customHeadersWithAuthorizationResponse) { + this.customHeadersWithAuthorizationResponse = customHeadersWithAuthorizationResponse; + } + + public Boolean getUpdateUserLastLogonTime() { + return updateUserLastLogonTime != null ? updateUserLastLogonTime : false; + } + + public void setUpdateUserLastLogonTime(Boolean updateUserLastLogonTime) { + this.updateUserLastLogonTime = updateUserLastLogonTime; + } + + public Boolean getUpdateClientAccessTime() { + return updateClientAccessTime != null ? updateClientAccessTime : false; + } + + public void setUpdateClientAccessTime(Boolean updateClientAccessTime) { + this.updateClientAccessTime = updateClientAccessTime; + } + + public Boolean getHttpLoggingEnabled() { + return httpLoggingEnabled; + } + + public void setHttpLoggingEnabled(Boolean httpLoggingEnabled) { + this.httpLoggingEnabled = httpLoggingEnabled; + } + + public Set getHttpLoggingExludePaths() { + return httpLoggingExludePaths; + } + + public void setHttpLoggingExludePaths(Set httpLoggingExludePaths) { + this.httpLoggingExludePaths = httpLoggingExludePaths; + } + + public String getLoggingLevel() { + return loggingLevel; + } + + public void setLoggingLevel(String loggingLevel) { + this.loggingLevel = loggingLevel; + } + + public String getLoggingLayout() { + return loggingLayout; + } + + public void setLoggingLayout(String loggingLayout) { + this.loggingLayout = loggingLayout; + } + + public Boolean getEnableClientGrantTypeUpdate() { + return enableClientGrantTypeUpdate; + } + + public void setEnableClientGrantTypeUpdate(Boolean enableClientGrantTypeUpdate) { + this.enableClientGrantTypeUpdate = enableClientGrantTypeUpdate; + } + + public Set getDynamicGrantTypeDefault() { + return dynamicGrantTypeDefault; + } + + public void setDynamicGrantTypeDefault(Set dynamicGrantTypeDefault) { + this.dynamicGrantTypeDefault = dynamicGrantTypeDefault; + } + + /** + * @return session_id lifetime. If null or value is zero or less then session_id lifetime is not set and will expire when browser session ends. + */ + public Integer getSessionIdLifetime() { + return sessionIdLifetime; + } + + public void setSessionIdLifetime(Integer sessionIdLifetime) { + this.sessionIdLifetime = sessionIdLifetime; + } + + public Integer getServerSessionIdLifetime() { + return serverSessionIdLifetime; + } + + public void setServerSessionIdLifetime(Integer serverSessionIdLifetime) { + this.serverSessionIdLifetime = serverSessionIdLifetime; + } + + public Boolean getLogClientIdOnClientAuthentication() { + return logClientIdOnClientAuthentication; + } + + public void setLogClientIdOnClientAuthentication(Boolean logClientIdOnClientAuthentication) { + this.logClientIdOnClientAuthentication = logClientIdOnClientAuthentication; + } + + public Boolean getLogClientNameOnClientAuthentication() { + return logClientNameOnClientAuthentication; + } + + public void setLogClientNameOnClientAuthentication(Boolean logClientNameOnClientAuthentication) { + this.logClientNameOnClientAuthentication = logClientNameOnClientAuthentication; + } + + public String getExternalLoggerConfiguration() { + return externalLoggerConfiguration; + } + + public void setExternalLoggerConfiguration(String externalLoggerConfiguration) { + this.externalLoggerConfiguration = externalLoggerConfiguration; + } + + public Set getAuthorizationRequestCustomAllowedParameters() { + return authorizationRequestCustomAllowedParameters; + } + + public void setAuthorizationRequestCustomAllowedParameters(Set authorizationRequestCustomAllowedParameters) { + this.authorizationRequestCustomAllowedParameters = authorizationRequestCustomAllowedParameters; + } + + public Boolean getLegacyDynamicRegistrationScopeParam() { + return Boolean.TRUE.equals(legacyDynamicRegistrationScopeParam); + } + + public void setLegacyDynamicRegistrationScopeParam(Boolean legacyDynamicRegistrationScopeParam) { + this.legacyDynamicRegistrationScopeParam = legacyDynamicRegistrationScopeParam; + } + + public Boolean getOpenidScopeBackwardCompatibility() { + return openidScopeBackwardCompatibility; + } + + public void setOpenidScopeBackwardCompatibility(Boolean openidScopeBackwardCompatibility) { + this.openidScopeBackwardCompatibility = openidScopeBackwardCompatibility; + } + + public Boolean getDisableU2fEndpoint() { + return disableU2fEndpoint; + } + + public void setDisableU2fEndpoint(Boolean disableU2fEndpoint) { + this.disableU2fEndpoint = disableU2fEndpoint; + } + + public AuthenticationProtectionConfiguration getAuthenticationProtectionConfiguration() { + return authenticationProtectionConfiguration; + } + + public void setAuthenticationProtectionConfiguration(AuthenticationProtectionConfiguration authenticationProtectionConfiguration) { + this.authenticationProtectionConfiguration = authenticationProtectionConfiguration; + } + + public ErrorHandlingMethod getErrorHandlingMethod() { + return errorHandlingMethod; + } + + public void setErrorHandlingMethod(ErrorHandlingMethod errorHandlingMethod) { + this.errorHandlingMethod = errorHandlingMethod; + } + + public Boolean getUseLocalCache() { + return useLocalCache; + } + + public void setUseLocalCache(Boolean useLocalCache) { + this.useLocalCache = useLocalCache; + } + + public Boolean getKeepAuthenticatorAttributesOnAcrChange() { + return keepAuthenticatorAttributesOnAcrChange; + } + + public void setKeepAuthenticatorAttributesOnAcrChange(Boolean keepAuthenticatorAttributesOnAcrChange) { + this.keepAuthenticatorAttributesOnAcrChange = keepAuthenticatorAttributesOnAcrChange; + } + + public Boolean getDisableAuthnForMaxAgeZero() { + return disableAuthnForMaxAgeZero; + } + + public void setDisableAuthnForMaxAgeZero(Boolean disableAuthnForMaxAgeZero) { + this.disableAuthnForMaxAgeZero = disableAuthnForMaxAgeZero; + } + + public String getBackchannelClientId() { + return backchannelClientId; + } + + public void setBackchannelClientId(String backchannelClientId) { + this.backchannelClientId = backchannelClientId; + } + + public String getBackchannelRedirectUri() { + return backchannelRedirectUri; + } + + public void setBackchannelRedirectUri(String backchannelRedirectUri) { + this.backchannelRedirectUri = backchannelRedirectUri; + } + + public String getBackchannelAuthenticationEndpoint() { + return backchannelAuthenticationEndpoint; + } + + public void setBackchannelAuthenticationEndpoint(String backchannelAuthenticationEndpoint) { + this.backchannelAuthenticationEndpoint = backchannelAuthenticationEndpoint; + } + + public String getBackchannelDeviceRegistrationEndpoint() { + return backchannelDeviceRegistrationEndpoint; + } + + public void setBackchannelDeviceRegistrationEndpoint(String backchannelDeviceRegistrationEndpoint) { + this.backchannelDeviceRegistrationEndpoint = backchannelDeviceRegistrationEndpoint; + } + + public List getBackchannelTokenDeliveryModesSupported() { + if (backchannelTokenDeliveryModesSupported == null) backchannelTokenDeliveryModesSupported = Lists.newArrayList(); + return backchannelTokenDeliveryModesSupported; + } + + public void setBackchannelTokenDeliveryModesSupported(List backchannelTokenDeliveryModesSupported) { + this.backchannelTokenDeliveryModesSupported = backchannelTokenDeliveryModesSupported; + } + + public List getBackchannelAuthenticationRequestSigningAlgValuesSupported() { + if (backchannelAuthenticationRequestSigningAlgValuesSupported == null) backchannelAuthenticationRequestSigningAlgValuesSupported = Lists.newArrayList(); + return backchannelAuthenticationRequestSigningAlgValuesSupported; + } + + public void setBackchannelAuthenticationRequestSigningAlgValuesSupported(List backchannelAuthenticationRequestSigningAlgValuesSupported) { + this.backchannelAuthenticationRequestSigningAlgValuesSupported = backchannelAuthenticationRequestSigningAlgValuesSupported; + } + + public Boolean getBackchannelUserCodeParameterSupported() { + return backchannelUserCodeParameterSupported; + } + + public void setBackchannelUserCodeParameterSupported(Boolean backchannelUserCodeParameterSupported) { + this.backchannelUserCodeParameterSupported = backchannelUserCodeParameterSupported; + } + + public String getBackchannelBindingMessagePattern() { + return backchannelBindingMessagePattern; + } + + public void setBackchannelBindingMessagePattern(String backchannelBindingMessagePattern) { + this.backchannelBindingMessagePattern = backchannelBindingMessagePattern; + } + + /** + * Returns a number with a positive integer value indicating the expiration time + * of the "auth_req_id" in seconds since the authentication request was received. + * + * @return Default expires_in value. + */ + public int getBackchannelAuthenticationResponseExpiresIn() { + return backchannelAuthenticationResponseExpiresIn; + } + + public void setBackchannelAuthenticationResponseExpiresIn(int backchannelAuthenticationResponseExpiresIn) { + this.backchannelAuthenticationResponseExpiresIn = backchannelAuthenticationResponseExpiresIn; + } + + /** + * Returns a number with a positive integer value indicating the minimum amount + * of time in seconds that the Client must wait between polling requests to the + * token endpoint. + * This parameter will only be present if the Client is registered to use the + * Poll or Ping modes. + * + * @return Interval value. + */ + public int getBackchannelAuthenticationResponseInterval() { + return backchannelAuthenticationResponseInterval; + } + + public void setBackchannelAuthenticationResponseInterval(int backchannelAuthenticationResponseInterval) { + this.backchannelAuthenticationResponseInterval = backchannelAuthenticationResponseInterval; + } + + public List getBackchannelLoginHintClaims() { + return backchannelLoginHintClaims; + } + + public void setBackchannelLoginHintClaims(List backchannelLoginHintClaims) { + this.backchannelLoginHintClaims = backchannelLoginHintClaims; + } + + public CIBAEndUserNotificationConfig getCibaEndUserNotificationConfig() { + return cibaEndUserNotificationConfig; + } + + public void setCibaEndUserNotificationConfig(CIBAEndUserNotificationConfig cibaEndUserNotificationConfig) { + this.cibaEndUserNotificationConfig = cibaEndUserNotificationConfig; + } + + public List getDynamicRegistrationAllowedPasswordGrantScopes() { + if (dynamicRegistrationAllowedPasswordGrantScopes == null) dynamicRegistrationAllowedPasswordGrantScopes = Lists.newArrayList(); + return dynamicRegistrationAllowedPasswordGrantScopes; + } + + public void setDynamicRegistrationAllowedPasswordGrantScopes(List dynamicRegistrationAllowedPasswordGrantScopes) { + this.dynamicRegistrationAllowedPasswordGrantScopes = dynamicRegistrationAllowedPasswordGrantScopes; + } + + /** + * Returns a flag to determinate if oxAuth supports password grant type for + * dynamic client registration. + * @return Boolean, true if it supports, false if it doesn't support. + */ + public Boolean getDynamicRegistrationPasswordGrantTypeEnabled() { + return dynamicRegistrationPasswordGrantTypeEnabled; + } + + /** + * This method sets the flag that define if oxAuth supports or not password + * grant type for dynamic client registration. + * @param dynamicRegistrationPasswordGrantTypeEnabled Boolean value for + * the flag. + */ + public void setDynamicRegistrationPasswordGrantTypeEnabled(Boolean dynamicRegistrationPasswordGrantTypeEnabled) { + this.dynamicRegistrationPasswordGrantTypeEnabled = dynamicRegistrationPasswordGrantTypeEnabled; + } + + public int getBackchannelRequestsProcessorJobIntervalSec() { + return backchannelRequestsProcessorJobIntervalSec; + } + + public void setBackchannelRequestsProcessorJobIntervalSec(int backchannelRequestsProcessorJobIntervalSec) { + this.backchannelRequestsProcessorJobIntervalSec = backchannelRequestsProcessorJobIntervalSec; + } + + public int getCibaGrantLifeExtraTimeSec() { + return cibaGrantLifeExtraTimeSec; + } + + public void setCibaGrantLifeExtraTimeSec(int cibaGrantLifeExtraTimeSec) { + this.cibaGrantLifeExtraTimeSec = cibaGrantLifeExtraTimeSec; + } + + public int getCibaMaxExpirationTimeAllowedSec() { + return cibaMaxExpirationTimeAllowedSec; + } + + public void setCibaMaxExpirationTimeAllowedSec(int cibaMaxExpirationTimeAllowedSec) { + this.cibaMaxExpirationTimeAllowedSec = cibaMaxExpirationTimeAllowedSec; + } + + public int getBackchannelRequestsProcessorJobChunkSize() { + return backchannelRequestsProcessorJobChunkSize; + } + + public void setBackchannelRequestsProcessorJobChunkSize(int backchannelRequestsProcessorJobChunkSize) { + this.backchannelRequestsProcessorJobChunkSize = backchannelRequestsProcessorJobChunkSize; + } + + public Boolean getClientRegDefaultToCodeFlowWithRefresh() { + if (clientRegDefaultToCodeFlowWithRefresh == null) clientRegDefaultToCodeFlowWithRefresh = false; + return clientRegDefaultToCodeFlowWithRefresh; + } + + public void setClientRegDefaultToCodeFlowWithRefresh(Boolean clientRegDefaultToCodeFlowWithRefresh) { + this.clientRegDefaultToCodeFlowWithRefresh = clientRegDefaultToCodeFlowWithRefresh; + } + + public Boolean getGrantTypesAndResponseTypesAutofixEnabled() { + if (grantTypesAndResponseTypesAutofixEnabled == null) grantTypesAndResponseTypesAutofixEnabled = false; + return grantTypesAndResponseTypesAutofixEnabled; + } + + public void setGrantTypesAndResponseTypesAutofixEnabled(Boolean grantTypesAndResponseTypesAutofixEnabled) { + this.grantTypesAndResponseTypesAutofixEnabled = grantTypesAndResponseTypesAutofixEnabled; + } + + public String getDeviceAuthzEndpoint() { + return deviceAuthzEndpoint; + } + + public void setDeviceAuthzEndpoint(String deviceAuthzEndpoint) { + this.deviceAuthzEndpoint = deviceAuthzEndpoint; + } + + public int getDeviceAuthzRequestExpiresIn() { + return deviceAuthzRequestExpiresIn; + } + + public void setDeviceAuthzRequestExpiresIn(int deviceAuthzRequestExpiresIn) { + this.deviceAuthzRequestExpiresIn = deviceAuthzRequestExpiresIn; + } + + public int getDeviceAuthzTokenPollInterval() { + return deviceAuthzTokenPollInterval; + } + + public void setDeviceAuthzTokenPollInterval(int deviceAuthzTokenPollInterval) { + this.deviceAuthzTokenPollInterval = deviceAuthzTokenPollInterval; + } + + public String getDeviceAuthzResponseTypeToProcessAuthz() { + return deviceAuthzResponseTypeToProcessAuthz; + } + + public void setDeviceAuthzResponseTypeToProcessAuthz(String deviceAuthzResponseTypeToProcessAuthz) { + this.deviceAuthzResponseTypeToProcessAuthz = deviceAuthzResponseTypeToProcessAuthz; + } + + public Boolean getCibaEnabled() { + if (cibaEnabled == null) { + return false; + } + return cibaEnabled; + } + + public void setCibaEnabled(Boolean cibaEnabled) { + this.cibaEnabled = cibaEnabled; + } + + public List getRequestUriBlockList() { + if (requestUriBlockList == null) requestUriBlockList = Lists.newArrayList(); + return requestUriBlockList; + } + + public void setRequestUriBlockList(List requestUriBlockList) { + this.requestUriBlockList = requestUriBlockList; + } + + public Boolean getRequestUriHashVerificationEnabled() { + return requestUriHashVerificationEnabled != null ? requestUriHashVerificationEnabled : false; + } + + public void setRequestUriHashVerificationEnabled(Boolean requestUriHashVerificationEnabled) { + this.requestUriHashVerificationEnabled = requestUriHashVerificationEnabled; + } + + public Boolean getReturn200OnClientRegistration() { + return return200OnClientRegistration; + } + + public void setReturn200OnClientRegistration(Boolean return200OnClientRegistration) { + this.return200OnClientRegistration = return200OnClientRegistration; + } + + public Map getDateFormatterPatterns() { + return dateFormatterPatterns; + } + + public void setDateFormatterPatterns(Map dateFormatterPatterns) { + this.dateFormatterPatterns = dateFormatterPatterns; + } + + public Boolean isAllowBlankValuesInDiscoveryResponse() { + if (allowBlankValuesInDiscoveryResponse == null) allowBlankValuesInDiscoveryResponse = false; + return allowBlankValuesInDiscoveryResponse; + } + + public void setAllowBlankValuesInDiscoveryResponse(Boolean allowBlankValuesInDiscoveryResponse) { + this.allowBlankValuesInDiscoveryResponse = allowBlankValuesInDiscoveryResponse; + } + + public Boolean isSkipAuthenticationFilterOptionsMethod() { + if (skipAuthenticationFilterOptionsMethod == null) skipAuthenticationFilterOptionsMethod = false; + return skipAuthenticationFilterOptionsMethod; + } + + public void setSkipAuthenticationFilterOptionsMethod(Boolean skipAuthenticationFilterOptionsMethod) { + this.skipAuthenticationFilterOptionsMethod = skipAuthenticationFilterOptionsMethod; + } + +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/AuthenticationFilter.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/AuthenticationFilter.java new file mode 100644 index 00000000..deed2e3b --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/AuthenticationFilter.java @@ -0,0 +1,23 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.configuration; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlType; + +/** + * Represents the authentication filter. + * + * @author Yuriy Movchan + * @author Javier Rojas Blum + * @version April 13, 2016 + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "auth-filter") +public class AuthenticationFilter extends BaseFilter { +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/AuthenticationProtectionConfiguration.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/AuthenticationProtectionConfiguration.java new file mode 100644 index 00000000..b24c0176 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/AuthenticationProtectionConfiguration.java @@ -0,0 +1,52 @@ +package org.gluu.oxauth.model.configuration; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Brute Force authentication configuration + * + * @author Yuriy Movchan Date: 08/22/2018 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class AuthenticationProtectionConfiguration { + + private int attemptExpiration; + private int maximumAllowedAttemptsWithoutDelay; + + private int delayTime; + + private Boolean bruteForceProtectionEnabled; + + public final int getAttemptExpiration() { + return attemptExpiration; + } + + public final void setAttemptExpiration(int attemptExpiration) { + this.attemptExpiration = attemptExpiration; + } + + public final int getMaximumAllowedAttemptsWithoutDelay() { + return maximumAllowedAttemptsWithoutDelay; + } + + public final void setMaximumAllowedAttemptsWithoutDelay(int maximumAllowedAttemptsWithoutDelay) { + this.maximumAllowedAttemptsWithoutDelay = maximumAllowedAttemptsWithoutDelay; + } + + public final int getDelayTime() { + return delayTime; + } + + public final void setDelayTime(int delayTime) { + this.delayTime = delayTime; + } + + public final Boolean getBruteForceProtectionEnabled() { + return bruteForceProtectionEnabled; + } + + public final void setBruteForceProtectionEnabled(Boolean bruteForceProtectionEnabled) { + this.bruteForceProtectionEnabled = bruteForceProtectionEnabled; + } + +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/BaseFilter.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/BaseFilter.java new file mode 100644 index 00000000..9b7c104d --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/BaseFilter.java @@ -0,0 +1,71 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.configuration; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; + +/** + * @author Yuriy Movchan + * @author Yuriy Zabrovarnyy + * @author Javier Rojas Blum + * @version April 13, 2016 + */ +@XmlAccessorType(XmlAccessType.FIELD) +public class BaseFilter { + + @XmlElement(name = "filter", required = true) + private String filter; + + @XmlElement(name = "bind", required = false) + private Boolean bind; + + @XmlElement(name = "bind-password-attribute", required = false) + private String bindPasswordAttribute; + + @XmlElement(name = "base-dn", required = true) + private String baseDn; + + public String getFilter() { + return filter; + } + + public void setFilter(String filter) { + this.filter = filter; + } + + public Boolean getBind() { + return bind; + } + + public void setBind(Boolean bind) { + this.bind = bind; + } + + public String getBaseDn() { + return baseDn; + } + + public void setBaseDn(String baseDn) { + this.baseDn = baseDn; + } + + public String getBindPasswordAttribute() { + return bindPasswordAttribute; + } + + public void setBindPasswordAttribute(String bindPasswordAttribute) { + this.bindPasswordAttribute = bindPasswordAttribute; + } + + @Override + public String toString() { + return String.format("BaseFilter [filter=%s, bind=%s, bindPasswordAttribute=%s, baseDn=%s]", filter, bind, (bindPasswordAttribute == null ? null : "not_null"), baseDn); + } + +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/CIBAEndUserNotificationConfig.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/CIBAEndUserNotificationConfig.java new file mode 100644 index 00000000..c23b3377 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/CIBAEndUserNotificationConfig.java @@ -0,0 +1,105 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.configuration; + +/** + * @author Javier Rojas Blum + * @version August 20, 2019 + */ +public class CIBAEndUserNotificationConfig { + + private String apiKey; + private String authDomain; + private String databaseURL; + private String projectId; + private String storageBucket; + private String messagingSenderId; + private String appId; + private String notificationUrl; + private String notificationKey; + private String publicVapidKey; + + public String getApiKey() { + return apiKey; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + public String getAuthDomain() { + return authDomain; + } + + public void setAuthDomain(String authDomain) { + this.authDomain = authDomain; + } + + public String getDatabaseURL() { + return databaseURL; + } + + public void setDatabaseURL(String databaseURL) { + this.databaseURL = databaseURL; + } + + public String getProjectId() { + return projectId; + } + + public void setProjectId(String projectId) { + this.projectId = projectId; + } + + public String getStorageBucket() { + return storageBucket; + } + + public void setStorageBucket(String storageBucket) { + this.storageBucket = storageBucket; + } + + public String getMessagingSenderId() { + return messagingSenderId; + } + + public void setMessagingSenderId(String messagingSenderId) { + this.messagingSenderId = messagingSenderId; + } + + public String getAppId() { + return appId; + } + + public void setAppId(String appId) { + this.appId = appId; + } + + public String getNotificationUrl() { + return notificationUrl; + } + + public void setNotificationUrl(String notificationUrl) { + this.notificationUrl = notificationUrl; + } + + public String getNotificationKey() { + return notificationKey; + } + + public void setNotificationKey(String notificationKey) { + this.notificationKey = notificationKey; + } + + public String getPublicVapidKey() { + return publicVapidKey; + } + + public void setPublicVapidKey(String publicVapidKey) { + this.publicVapidKey = publicVapidKey; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/ClientAuthenticationFilter.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/ClientAuthenticationFilter.java new file mode 100644 index 00000000..ba1e113e --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/ClientAuthenticationFilter.java @@ -0,0 +1,21 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.configuration; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlType; + +/** + * @author Yuriy Zabrovarnyy + * @author Javier Rojas Blum + * @version April 13, 2016 + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "client-auth-filter") +public class ClientAuthenticationFilter extends BaseFilter { +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/Configuration.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/Configuration.java new file mode 100644 index 00000000..b43e6347 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/Configuration.java @@ -0,0 +1,10 @@ +package org.gluu.oxauth.model.configuration; + +/** + * base interface for all oxAuth configurations + * + * @author Yuriy Movchan + * @version 04/12/2017 + */ +public interface Configuration { +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/ConfigurationResponseClaim.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/ConfigurationResponseClaim.java new file mode 100644 index 00000000..4c349390 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/ConfigurationResponseClaim.java @@ -0,0 +1,77 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.configuration; + +/** + * @author Javier Rojas Blum + * @version August 20, 2019 + */ +public interface ConfigurationResponseClaim { + + String ISSUER = "issuer"; + String AUTHORIZATION_ENDPOINT = "authorization_endpoint"; + String TOKEN_ENDPOINT = "token_endpoint"; + @Deprecated // remove in 5.x version + String TOKEN_REVOCATION_ENDPOINT = "token_revocation_endpoint"; + String REVOCATION_ENDPOINT = "revocation_endpoint"; + String SESSION_REVOCATION_ENDPOINT = "session_revocation_endpoint"; + String USER_INFO_ENDPOINT = "userinfo_endpoint"; + String CLIENT_INFO_ENDPOINT = "clientinfo_endpoint"; + String CHECK_SESSION_IFRAME = "check_session_iframe"; + String END_SESSION_ENDPOINT = "end_session_endpoint"; + String JWKS_URI = "jwks_uri"; + String REGISTRATION_ENDPOINT = "registration_endpoint"; + String ID_GENERATION_ENDPOINT = "id_generation_endpoint"; + String INTROSPECTION_ENDPOINT = "introspection_endpoint"; + String DEVICE_AUTHZ_ENDPOINT = "device_authorization_endpoint"; + String SCOPES_SUPPORTED = "scopes_supported"; + String SCOPE_TO_CLAIMS_MAPPING = "scope_to_claims_mapping"; + String RESPONSE_TYPES_SUPPORTED = "response_types_supported"; + String RESPONSE_MODES_SUPPORTED = "response_modes_supported"; + String GRANT_TYPES_SUPPORTED = "grant_types_supported"; + String ACR_VALUES_SUPPORTED = "acr_values_supported"; + String SUBJECT_TYPES_SUPPORTED = "subject_types_supported"; + String USER_INFO_SIGNING_ALG_VALUES_SUPPORTED = "userinfo_signing_alg_values_supported"; + String USER_INFO_ENCRYPTION_ALG_VALUES_SUPPORTED = "userinfo_encryption_alg_values_supported"; + String USER_INFO_ENCRYPTION_ENC_VALUES_SUPPORTED = "userinfo_encryption_enc_values_supported"; + String ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED = "id_token_signing_alg_values_supported"; + String ID_TOKEN_ENCRYPTION_ALG_VALUES_SUPPORTED = "id_token_encryption_alg_values_supported"; + String ID_TOKEN_ENCRYPTION_ENC_VALUES_SUPPORTED = "id_token_encryption_enc_values_supported"; + String REQUEST_OBJECT_SIGNING_ALG_VALUES_SUPPORTED = "request_object_signing_alg_values_supported"; + String REQUEST_OBJECT_ENCRYPTION_ALG_VALUES_SUPPORTED = "request_object_encryption_alg_values_supported"; + String REQUEST_OBJECT_ENCRYPTION_ENC_VALUES_SUPPORTED = "request_object_encryption_enc_values_supported"; + String TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED = "token_endpoint_auth_methods_supported"; + String TOKEN_ENDPOINT_AUTH_SIGNING_ALG_VALUES_SUPPORTED = "token_endpoint_auth_signing_alg_values_supported"; + String DISPLAY_VALUES_SUPPORTED = "display_values_supported"; + String CLAIM_TYPES_SUPPORTED = "claim_types_supported"; + String CLAIMS_SUPPORTED = "claims_supported"; + String SERVICE_DOCUMENTATION = "service_documentation"; + String CLAIMS_LOCALES_SUPPORTED = "claims_locales_supported"; + String UI_LOCALES_SUPPORTED = "ui_locales_supported"; + String CLAIMS_PARAMETER_SUPPORTED = "claims_parameter_supported"; + String REQUEST_PARAMETER_SUPPORTED = "request_parameter_supported"; + String REQUEST_URI_PARAMETER_SUPPORTED = "request_uri_parameter_supported"; + String REQUIRE_REQUEST_URI_REGISTRATION = "require_request_uri_registration"; + String OP_POLICY_URI = "op_policy_uri"; + String OP_TOS_URI = "op_tos_uri"; + String SCOPE_KEY = "scope"; + String CLAIMS_KEY = "claims"; + String ID_TOKEN_TOKEN_BINDING_CNF_VALUES_SUPPORTED = "id_token_token_binding_cnf_values_supported"; + String TLS_CLIENT_CERTIFICATE_BOUND_ACCESS_TOKENS = "tls_client_certificate_bound_access_tokens"; + String FRONTCHANNEL_LOGOUT_SUPPORTED = "frontchannel_logout_supported"; + String FRONTCHANNEL_LOGOUT_SESSION_SUPPORTED = "frontchannel_logout_session_supported"; + String AUTH_LEVEL_MAPPING = "auth_level_mapping"; + String FRONT_CHANNEL_LOGOUT_SESSION_SUPPORTED = "frontchannel_logout_session_supported"; + String BACKCHANNEL_LOGOUT_SUPPORTED = "backchannel_logout_supported"; + String BACKCHANNEL_LOGOUT_SESSION_SUPPORTED = "backchannel_logout_session_supported"; + + // CIBA + String BACKCHANNEL_AUTHENTICATION_ENDPOINT = "backchannel_authentication_endpoint"; + String BACKCHANNEL_TOKEN_DELIVERY_MODES_SUPPORTED = "backchannel_token_delivery_modes_supported"; + String BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG_VALUES_SUPPORTED = "backchannel_authentication_request_signing_alg_values_supported"; + String BACKCHANNEL_USER_CODE_PAREMETER_SUPPORTED = "backchannel_user_code_parameter_supported"; +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/CorsConfigurationFilter.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/CorsConfigurationFilter.java new file mode 100644 index 00000000..e74ef988 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/configuration/CorsConfigurationFilter.java @@ -0,0 +1,145 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.configuration; + +import org.apache.commons.lang.StringUtils; + +/** + * @author Javier Rojas Blum + * @version February 15, 2017 + */ +public class CorsConfigurationFilter { + + private String filterName; + private Boolean corsEnabled; + private String corsAllowedOrigins; + private String corsAllowedMethods; + private String corsAllowedHeaders; + private String corsExposedHeaders; + private Boolean corsSupportCredentials; + private Boolean corsLoggingEnabled; + private Integer corsPreflightMaxAge; + private Boolean corsRequestDecorate; + + public static final Boolean DEFAULT_CORS_ENABLED = true; + public static final String DEFAULT_CORS_ALLOWED_ORIGINS = "*"; + public static final String DEFAULT_CORS_ALLOWED_METHODS = "GET,POST,HEAD,OPTIONS"; + public static final String DEFAULT_CORS_ALLOWED_HEADERS = "Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers"; + public static final String DEFAULT_CORS_EXPOSED_HEADERS = ""; + public static final Boolean DEFAULT_CORS_SUPPORT_CREDENTIALS = true; + public static final Boolean DEFAULT_CORS_LOGGING_ENABLED = false; + public static final Integer DEFAULT_CORS_PREFLIGHT_MAX_AGE = 1800; + public static final Boolean DEFAULT_CORS_REQUEST_DECORATE = true; + + public String getFilterName() { + return filterName; + } + + public void setFilterName(String filterName) { + this.filterName = filterName; + } + + public Boolean getCorsEnabled() { + if (corsEnabled == null) { + corsEnabled = DEFAULT_CORS_ENABLED; + } + return corsEnabled; + } + + public void setCorsEnabled(Boolean corsEnabled) { + this.corsEnabled = corsEnabled; + } + + public String getCorsAllowedOrigins() { + if (StringUtils.isEmpty(corsAllowedOrigins)) { + corsAllowedOrigins = DEFAULT_CORS_ALLOWED_ORIGINS; + } + return corsAllowedOrigins; + } + + public void setCorsAllowedOrigins(String corsAllowedOrigins) { + this.corsAllowedOrigins = corsAllowedOrigins; + } + + public String getCorsAllowedMethods() { + if (StringUtils.isEmpty(corsAllowedMethods)) { + corsAllowedMethods = DEFAULT_CORS_ALLOWED_METHODS; + } + return corsAllowedMethods; + } + + public void setCorsAllowedMethods(String corsAllowedMethods) { + this.corsAllowedMethods = corsAllowedMethods; + } + + public String getCorsAllowedHeaders() { + if (StringUtils.isEmpty(corsAllowedHeaders)) { + corsAllowedHeaders = DEFAULT_CORS_ALLOWED_HEADERS; + } + return corsAllowedHeaders; + } + + public void setCorsAllowedHeaders(String corsAllowedHeaders) { + this.corsAllowedHeaders = corsAllowedHeaders; + } + + public String getCorsExposedHeaders() { + if (StringUtils.isEmpty(corsExposedHeaders)) { + corsExposedHeaders = DEFAULT_CORS_EXPOSED_HEADERS; + } + return corsExposedHeaders; + } + + public void setCorsExposedHeaders(String corsExposedHeaders) { + this.corsExposedHeaders = corsExposedHeaders; + } + + public Boolean getCorsSupportCredentials() { + if (corsSupportCredentials == null) { + corsSupportCredentials = DEFAULT_CORS_SUPPORT_CREDENTIALS; + } + + return corsSupportCredentials; + } + + public void setCorsSupportCredentials(Boolean corsSupportCredentials) { + this.corsSupportCredentials = corsSupportCredentials; + } + + public Boolean getCorsLoggingEnabled() { + if (corsLoggingEnabled == null) { + corsLoggingEnabled = DEFAULT_CORS_LOGGING_ENABLED; + } + return corsLoggingEnabled; + } + + public void setCorsLoggingEnabled(Boolean corsLoggingEnabled) { + this.corsLoggingEnabled = corsLoggingEnabled; + } + + public Integer getCorsPreflightMaxAge() { + if (corsPreflightMaxAge == null) { + corsPreflightMaxAge = DEFAULT_CORS_PREFLIGHT_MAX_AGE; + } + return corsPreflightMaxAge; + } + + public void setCorsPreflightMaxAge(Integer corsPreflightMaxAge) { + this.corsPreflightMaxAge = corsPreflightMaxAge; + } + + public Boolean getCorsRequestDecorate() { + if (corsRequestDecorate == null) { + corsRequestDecorate = DEFAULT_CORS_REQUEST_DECORATE; + } + return corsRequestDecorate; + } + + public void setCorsRequestDecorate(Boolean corsRequestDecorate) { + this.corsRequestDecorate = corsRequestDecorate; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/AbstractCryptoProvider.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/AbstractCryptoProvider.java new file mode 100644 index 00000000..63eee238 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/AbstractCryptoProvider.java @@ -0,0 +1,235 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ +package org.gluu.oxauth.model.crypto; + +import com.google.common.collect.Lists; +import org.apache.log4j.Logger; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.crypto.signature.AlgorithmFamily; +import org.gluu.oxauth.model.crypto.signature.ECEllipticCurve; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jwk.Algorithm; +import org.gluu.oxauth.model.jwk.JSONWebKey; +import org.gluu.oxauth.model.jwk.JSONWebKeySet; +import org.gluu.oxauth.model.jwk.Use; +import org.gluu.oxauth.model.util.Base64Util; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.math.BigInteger; +import java.security.*; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.*; +import java.text.SimpleDateFormat; +import java.util.*; + +import static org.gluu.oxauth.model.jwk.JWKParameter.*; + +/** + * @author Javier Rojas Blum + * @version February 12, 2019 + */ +public abstract class AbstractCryptoProvider { + + protected static final Logger LOG = Logger.getLogger(AbstractCryptoProvider.class); + + private int keyRegenerationIntervalInDays = -1; + + public JSONObject generateKey(Algorithm algorithm, Long expirationTime) throws Exception { + return generateKey(algorithm, expirationTime, Use.SIGNATURE); + } + + public abstract JSONObject generateKey(Algorithm algorithm, Long expirationTime, Use use) throws Exception; + + public abstract JSONObject generateKey(Algorithm algorithm, Long expirationTime, Use use, int keyLength) throws Exception; + + public abstract String sign(String signingInput, String keyId, String sharedSecret, SignatureAlgorithm signatureAlgorithm) throws Exception; + + public abstract boolean verifySignature(String signingInput, String encodedSignature, String keyId, JSONObject jwks, String sharedSecret, SignatureAlgorithm signatureAlgorithm) throws Exception; + + public abstract boolean deleteKey(String keyId) throws Exception; + + public abstract boolean containsKey(String keyId); + + public List getKeys() { + return Lists.newArrayList(); + } + + public abstract PrivateKey getPrivateKey(String keyId) throws Exception; + + public String getKeyId(JSONWebKeySet jsonWebKeySet, Algorithm algorithm, Use use) throws Exception { + if (algorithm == null || AlgorithmFamily.HMAC.equals(algorithm.getFamily())) { + return null; + } + for (JSONWebKey key : jsonWebKeySet.getKeys()) { + if (algorithm == key.getAlg() && (use == null || use == key.getUse())) { + return key.getKid(); + } + } + + return null; + } + + public static JSONObject generateJwks(AbstractCryptoProvider cryptoProvider, AppConfiguration configuration) { + GregorianCalendar expirationTime = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + expirationTime.add(GregorianCalendar.HOUR, configuration.getKeyRegenerationInterval()); + expirationTime.add(GregorianCalendar.SECOND, configuration.getIdTokenLifetime()); + + long expiration = expirationTime.getTimeInMillis(); + + final List allowedAlgs = configuration.getKeyAlgsAllowedForGeneration(); + JSONArray keys = new JSONArray(); + + for (Algorithm alg : Algorithm.values()) { + try { + if (!allowedAlgs.isEmpty() && !allowedAlgs.contains(alg.getParamName())) { + LOG.debug("Key generation for " + alg + " is skipped because it's not allowed by keyAlgsAllowedForGeneration configuration property."); + continue; + } + keys.put(cryptoProvider.generateKey(alg, expiration, alg.getUse())); + } catch (Exception ex) { + LOG.error("Algorithm: " + alg + ex.getMessage(), ex); + } + } + + JSONObject jsonObject = new JSONObject(); + jsonObject.put(JSON_WEB_KEY_SET, keys); + + return jsonObject; + } + + public PublicKey getPublicKey(String alias, JSONObject jwks, Algorithm requestedAlgorithm) throws Exception { + JSONArray webKeys = jwks.getJSONArray(JSON_WEB_KEY_SET); + + try { + if (alias == null) { + if (webKeys.length() == 1) { + JSONObject key = webKeys.getJSONObject(0); + return processKey(requestedAlgorithm, alias, key); + } else { + return null; + } + } + for (int i = 0; i < webKeys.length(); i++) { + JSONObject key = webKeys.getJSONObject(i); + if (alias.equals(key.getString(KEY_ID))) { + PublicKey publicKey = processKey(requestedAlgorithm, alias, key); + if (publicKey != null) { + return publicKey; + } + } + } + } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidParameterSpecException | + InvalidParameterException e) { + throw new Exception(e); + } + + return null; + } + + private PublicKey processKey(Algorithm requestedAlgorithm, String alias, JSONObject key) throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidParameterSpecException, InvalidParameterException { + PublicKey publicKey = null; + AlgorithmFamily algorithmFamily = null; + + if (key.has(ALGORITHM)) { + Algorithm algorithm = Algorithm.fromString(key.optString(ALGORITHM)); + + if (requestedAlgorithm != null && !requestedAlgorithm.equals(algorithm)) { + LOG.trace("kid matched but algorithm does not match. kid algorithm:" + algorithm + + ", requestedAlgorithm:" + requestedAlgorithm + ", kid:" + alias); + return null; + } + algorithmFamily = algorithm.getFamily(); + } else if (key.has(KEY_TYPE)) { + algorithmFamily = AlgorithmFamily.fromString(key.getString(KEY_TYPE)); + } else { + throw new InvalidParameterException("Wrong key (JSONObject): doesn't contain 'alg' and 'kty' properties"); + } + + switch (algorithmFamily) { + case RSA: { + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + RSAPublicKeySpec pubKeySpec = new RSAPublicKeySpec( + new BigInteger(1, Base64Util.base64urldecode(key.getString(MODULUS))), + new BigInteger(1, Base64Util.base64urldecode(key.getString(EXPONENT)))); + publicKey = keyFactory.generatePublic(pubKeySpec); + break; + } + case EC: { + ECEllipticCurve curve = ECEllipticCurve.fromString(key.optString(CURVE)); + AlgorithmParameters parameters = AlgorithmParameters.getInstance(AlgorithmFamily.EC.toString()); + parameters.init(new ECGenParameterSpec(curve.getAlias())); + ECParameterSpec ecParameters = parameters.getParameterSpec(ECParameterSpec.class); + publicKey = KeyFactory.getInstance(AlgorithmFamily.EC.toString()) + .generatePublic(new ECPublicKeySpec( + new ECPoint( + new BigInteger(1, Base64Util.base64urldecode(key.getString(X))), + new BigInteger(1, Base64Util.base64urldecode(key.getString(Y)))), + ecParameters)); + break; + } + default: { + throw new InvalidParameterException(String.format("Wrong AlgorithmFamily value: %s", algorithmFamily)); + } + } + + if (key.has(EXPIRATION_TIME)) { + checkKeyExpiration(alias, key.getLong(EXPIRATION_TIME)); + } + + return publicKey; + } + + protected void checkKeyExpiration(String alias, Long expirationTime) { + try { + Date expirationDate = new Date(expirationTime); + SimpleDateFormat ft = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date today = new Date(); + long expiresInDays = (expirationTime - today.getTime()) / (24 * 60 * 60 * 1000); + if (expiresInDays == 0) { + LOG.warn("\nWARNING! Key will expire soon, alias: " + alias + + "\n\tExpires On: " + ft.format(expirationDate) + + "\n\tToday's Date: " + ft.format(today)); + return; + } + if (expiresInDays < 0) { + LOG.warn("\nWARNING! Expired Key is used, alias: " + alias + + "\n\tExpires On: " + ft.format(expirationDate) + + "\n\tToday's Date: " + ft.format(today)); + return; + } + + // re-generation interval is unknown, therefore we default to 30 days period warning + if (keyRegenerationIntervalInDays <= 0 && expiresInDays < 30) { + LOG.warn("\nWARNING! Key with alias: " + alias + + "\n\tExpires In: " + expiresInDays + " days" + + "\n\tExpires On: " + ft.format(expirationDate) + + "\n\tToday's Date: " + ft.format(today)); + return; + } + + if (expiresInDays < keyRegenerationIntervalInDays) { + LOG.warn("\nWARNING! Key with alias: " + alias + + "\n\tExpires In: " + expiresInDays + " days" + + "\n\tExpires On: " + ft.format(expirationDate) + + "\n\tKey Regeneration In: " + keyRegenerationIntervalInDays + " days" + + "\n\tToday's Date: " + ft.format(today)); + } + } catch (Exception e) { + LOG.error("Failed to check key expiration.", e); + } + } + + public int getKeyRegenerationIntervalInDays() { + return keyRegenerationIntervalInDays; + } + + public void setKeyRegenerationIntervalInDays(int keyRegenerationIntervalInDays) { + this.keyRegenerationIntervalInDays = keyRegenerationIntervalInDays; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/Certificate.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/Certificate.java new file mode 100644 index 00000000..0c3ee695 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/Certificate.java @@ -0,0 +1,105 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.crypto; + +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; +import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPublicKey; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.json.JSONArray; +import org.json.JSONException; +import org.gluu.oxauth.model.crypto.signature.ECDSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.util.StringUtils; + +import java.io.IOException; +import java.io.StringWriter; +import java.security.cert.X509Certificate; +import java.security.interfaces.ECPublicKey; +import java.util.Arrays; + +/** + * @author Javier Rojas Blum + * @version June 29, 2016 + */ +public class Certificate { + + private SignatureAlgorithm signatureAlgorithm; + private X509Certificate x509Certificate; + + public Certificate(SignatureAlgorithm signatureAlgorithm, X509Certificate x509Certificate) { + this.signatureAlgorithm = signatureAlgorithm; + this.x509Certificate = x509Certificate; + } + + public PublicKey getPublicKey() { + PublicKey publicKey = null; + + if (x509Certificate != null && x509Certificate.getPublicKey() instanceof java.security.interfaces.RSAPublicKey) { + java.security.interfaces.RSAPublicKey jcersaPublicKey = (java.security.interfaces.RSAPublicKey) x509Certificate.getPublicKey(); + + publicKey = new RSAPublicKey(jcersaPublicKey.getModulus(), jcersaPublicKey.getPublicExponent()); + } else if (x509Certificate != null && x509Certificate.getPublicKey() instanceof ECPublicKey) { + ECPublicKey jceecPublicKey = (ECPublicKey) x509Certificate.getPublicKey(); + + publicKey = new ECDSAPublicKey(signatureAlgorithm, jceecPublicKey.getW().getAffineX(), jceecPublicKey.getW().getAffineY()); + } + + return publicKey; + } + + public RSAPublicKey getRsaPublicKey() { + RSAPublicKey rsaPublicKey = null; + + if (x509Certificate != null && x509Certificate.getPublicKey() instanceof java.security.interfaces.RSAPublicKey) { + java.security.interfaces.RSAPublicKey publicKey = (java.security.interfaces.RSAPublicKey) x509Certificate.getPublicKey(); + rsaPublicKey = new RSAPublicKey(publicKey.getModulus(), publicKey.getPublicExponent()); + } + + return rsaPublicKey; + } + + public ECDSAPublicKey getEcdsaPublicKey() { + ECDSAPublicKey ecdsaPublicKey = null; + + if (x509Certificate != null && x509Certificate.getPublicKey() instanceof ECPublicKey) { + ECPublicKey publicKey = (ECPublicKey) x509Certificate.getPublicKey(); + ecdsaPublicKey = new ECDSAPublicKey(signatureAlgorithm, publicKey.getW().getAffineX(), publicKey.getW().getAffineY()); + } + + return ecdsaPublicKey; + } + + public JSONArray toJSONArray() throws JSONException { + String cert = toString(); + + cert = cert.replace("\n", ""); + cert = cert.replace("-----BEGIN CERTIFICATE-----", ""); + cert = cert.replace("-----END CERTIFICATE-----", ""); + + return new JSONArray(Arrays.asList(cert)); + } + + @Override + public String toString() { + try { + StringWriter stringWriter = new StringWriter(); + JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter); + try { + pemWriter.writeObject(x509Certificate); + pemWriter.flush(); + return stringWriter.toString(); + } finally { + pemWriter.close(); + } + } catch (IOException e) { + return StringUtils.EMPTY_STRING; + } catch (Exception e) { + return StringUtils.EMPTY_STRING; + } + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/CryptoProviderFactory.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/CryptoProviderFactory.java new file mode 100644 index 00000000..ac966833 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/CryptoProviderFactory.java @@ -0,0 +1,66 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.crypto; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.common.WebKeyStorage; +import org.gluu.oxauth.model.configuration.AppConfiguration; + +/** + * @author Javier Rojas Blum + * @version April 25, 2017 + */ +public class CryptoProviderFactory { + + private static OxAuthCryptoProvider keyStoreProvider = null; + + public static AbstractCryptoProvider getCryptoProvider(AppConfiguration configuration) throws Exception { + AbstractCryptoProvider cryptoProvider = null; + WebKeyStorage webKeyStorage = configuration.getWebKeysStorage(); + if (webKeyStorage == null) { + return null; + } + + switch (webKeyStorage) { + case KEYSTORE: + cryptoProvider = getKeyStoreProvider(configuration); + + break; + case PKCS11: + cryptoProvider = new OxElevenCryptoProvider( + configuration.getOxElevenGenerateKeyEndpoint(), + configuration.getOxElevenSignEndpoint(), + configuration.getOxElevenVerifySignatureEndpoint(), + configuration.getOxElevenDeleteKeyEndpoint(), + configuration.getOxElevenTestModeToken()); + break; + } + + if (configuration.getKeyRegenerationEnabled()) { // set interval only if re-generation is enabled + cryptoProvider.setKeyRegenerationIntervalInDays(configuration.getKeyRegenerationInterval() / 24); + } + return cryptoProvider; + } + + private static AbstractCryptoProvider getKeyStoreProvider(AppConfiguration configuration) throws Exception { + if (keyStoreProvider != null && + StringUtils.isNotBlank(keyStoreProvider.getKeyStoreFile()) && + StringUtils.isNotBlank(keyStoreProvider.getKeyStoreSecret()) && + StringUtils.isNotBlank(keyStoreProvider.getDnName()) && + keyStoreProvider.getKeyStoreFile().equals(configuration.getKeyStoreFile()) && + keyStoreProvider.getKeyStoreSecret().equals(configuration.getKeyStoreSecret()) && + keyStoreProvider.getDnName().equals(configuration.getDnName())) { + return keyStoreProvider; + } + + return keyStoreProvider = new OxAuthCryptoProvider(configuration.getKeyStoreFile(), configuration.getKeyStoreSecret(), configuration.getDnName(), configuration.getRejectJwtWithNoneAlg(), configuration.getKeySelectionStrategy()); + } + + public static void reset() { + keyStoreProvider = null; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/Key.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/Key.java new file mode 100644 index 00000000..ff14c8eb --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/Key.java @@ -0,0 +1,131 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.crypto; + +import static org.gluu.oxauth.model.jwk.JWKParameter.*; + +import org.json.JSONException; +import org.json.JSONObject; +import org.gluu.oxauth.model.common.JSONable; +import org.gluu.oxauth.model.util.StringUtils; + +/** + * @author Javier Rojas Blum + * @version February 17, 2016 + */ +public class Key implements JSONable { + + private String keyType; + private String use; + private String algorithm; + private String keyId; + private Long expirationTime; + private Object curve; + private E privateKey; + private F publicKey; + private Certificate certificate; + + public String getKeyType() { + return keyType; + } + + public void setKeyType(String keyType) { + this.keyType = keyType; + } + + public String getUse() { + return use; + } + + public void setUse(String use) { + this.use = use; + } + + public String getAlgorithm() { + return algorithm; + } + + public void setAlgorithm(String algorithm) { + this.algorithm = algorithm; + } + + public String getKeyId() { + return keyId; + } + + public void setKeyId(String keyId) { + this.keyId = keyId; + } + + public Long getExpirationTime() { + return expirationTime; + } + + public void setExpirationTime(Long expirationTime) { + this.expirationTime = expirationTime; + } + + public Object getCurve() { + return curve; + } + + public void setCurve(Object curve) { + this.curve = curve; + } + + public E getPrivateKey() { + return privateKey; + } + + public void setPrivateKey(E privateKey) { + this.privateKey = privateKey; + } + + public F getPublicKey() { + return publicKey; + } + + public void setPublicKey(F publicKey) { + this.publicKey = publicKey; + } + + public Certificate getCertificate() { + return certificate; + } + + public void setCertificate(Certificate certificate) { + this.certificate = certificate; + } + + @Override + public JSONObject toJSONObject() throws JSONException { + JSONObject jsonObject = new JSONObject(); + + jsonObject.put(KEY_TYPE, getKeyType()); + jsonObject.put(KEY_USE, getUse()); + jsonObject.put(ALGORITHM, getAlgorithm()); + jsonObject.put(KEY_ID, getKeyId()); + jsonObject.put(EXPIRATION_TIME, getExpirationTime() == null ? JSONObject.NULL : getExpirationTime()); + jsonObject.put(CURVE, getCurve()); + jsonObject.put(PRIVATE_KEY, getPrivateKey().toJSONObject()); + jsonObject.put(PUBLIC_KEY, getPublicKey().toJSONObject()); + jsonObject.put(CERTIFICATE_CHAIN, getCertificate().toJSONArray()); + + return jsonObject; + } + + @Override + public String toString() { + try { + return toJSONObject().toString(4).replace("\\/", "/"); + } catch (JSONException e) { + return StringUtils.EMPTY_STRING; + } catch (Exception e) { + return StringUtils.EMPTY_STRING; + } + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/KeyFactory.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/KeyFactory.java new file mode 100644 index 00000000..0eabc766 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/KeyFactory.java @@ -0,0 +1,31 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.crypto; + +/** + * Factory to create asymmetric Public and Private Keys + * + * @author Javier Rojas Blum Date: 10.22.2012 + */ +public abstract class KeyFactory { + + public abstract E getPrivateKey(); + + public abstract F getPublicKey(); + + public abstract Certificate getCertificate(); + + public Key getKey() { + Key key = new Key(); + + key.setPrivateKey(getPrivateKey()); + key.setPublicKey(getPublicKey()); + key.setCertificate(getCertificate()); + + return key; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/OxAuthCryptoProvider.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/OxAuthCryptoProvider.java new file mode 100644 index 00000000..363ff419 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/OxAuthCryptoProvider.java @@ -0,0 +1,585 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.crypto; + +import com.google.common.collect.Lists; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.crypto.impl.ECDSA; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.cert.CertIOException; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.crypto.signature.AlgorithmFamily; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jwk.Algorithm; +import org.gluu.oxauth.model.jwk.JSONWebKey; +import org.gluu.oxauth.model.jwk.JSONWebKeySet; +import org.gluu.oxauth.model.jwk.KeySelectionStrategy; +import org.gluu.oxauth.model.jwk.Use; +import org.gluu.oxauth.model.util.Base64Util; +import org.gluu.oxauth.model.util.CertUtils; +import org.gluu.oxauth.model.util.Util; +import org.gluu.util.security.SecurityProviderUtility; +import org.gluu.util.security.SecurityProviderUtility.SecurityModeType; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.math.BigInteger; +import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.*; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.ECGenParameterSpec; +import java.util.*; +import java.util.stream.Collectors; + +import static org.gluu.oxauth.model.jwk.JWKParameter.*; + +/** + * @author Javier Rojas Blum + * @author Yuriy Movchan + * @author Sergey Manoylo + * @version April 25, 2022 + */ +public class OxAuthCryptoProvider extends AbstractCryptoProvider { + + protected static final Logger LOG = Logger.getLogger(OxAuthCryptoProvider.class); + + private KeyStore keyStore; + private String keyStoreFile; + private String keyStoreSecret; + private String dnName; + private final boolean rejectNoneAlg; + private final KeySelectionStrategy keySelectionStrategy; + + public OxAuthCryptoProvider() throws Exception { + this(null, null, null); + } + + public OxAuthCryptoProvider(String keyStoreFile, String keyStoreSecret, String dnName) throws Exception { + this(keyStoreFile, keyStoreSecret, dnName, false); + } + + public OxAuthCryptoProvider(String keyStoreFile, String keyStoreSecret, String dnName, boolean rejectNoneAlg) throws Exception { + this(keyStoreFile, keyStoreSecret, dnName, rejectNoneAlg, AppConfiguration.DEFAULT_KEY_SELECTION_STRATEGY); + } + + public OxAuthCryptoProvider(String keyStoreFile, String keyStoreSecret, String dnName, boolean rejectNoneAlg, KeySelectionStrategy keySelectionStrategy) throws Exception { + this.rejectNoneAlg = rejectNoneAlg; + this.keySelectionStrategy = keySelectionStrategy != null ? keySelectionStrategy : AppConfiguration.DEFAULT_KEY_SELECTION_STRATEGY; + if (!Util.isNullOrEmpty(keyStoreFile) && !Util.isNullOrEmpty(keyStoreSecret) /* && !Util.isNullOrEmpty(dnName) */) { + this.keyStoreFile = keyStoreFile; + this.keyStoreSecret = keyStoreSecret; + this.dnName = dnName; + SecurityProviderUtility.KeyStorageType keyStorageType = solveKeyStorageType(); + switch (keyStorageType) { + case JKS_KS: { + keyStore = KeyStore.getInstance("JKS"); + break; + } + case PKCS12_KS: { + keyStore = KeyStore.getInstance("PKCS12", SecurityProviderUtility.getBCProvider()); + break; + } + case BCFKS_KS: { + keyStore = KeyStore.getInstance("BCFKS", SecurityProviderUtility.getBCProvider()); + break; + } + } + try { + File f = new File(keyStoreFile); + if (!f.exists()) { + keyStore.load(null, keyStoreSecret.toCharArray()); + FileOutputStream fos = new FileOutputStream(keyStoreFile); + keyStore.store(fos, keyStoreSecret.toCharArray()); + fos.close(); + } + final InputStream is = new FileInputStream(keyStoreFile); + keyStore.load(is, keyStoreSecret.toCharArray()); + LOG.debug("Loaded keys from keystore."); + LOG.debug("Security Mode: " + SecurityProviderUtility.getSecurityMode().toString()); + LOG.debug("Keystore Type: " + keyStorageType.toString()); + LOG.trace("Loaded keys:"+ getKeys()); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + LOG.error("Check type of keystorage. Expected keystorage type: '" + keyStorageType.toString() + "'"); + } + } + } + + public void load(String keyStoreSecret) { + this.keyStoreSecret = keyStoreSecret; + SecurityProviderUtility.KeyStorageType keyStorageType = solveKeyStorageType(); + try(InputStream is = new FileInputStream(keyStoreFile)) { + switch (keyStorageType) { + case JKS_KS: { + keyStore = KeyStore.getInstance("JKS"); + break; + } + case PKCS12_KS: { + keyStore = KeyStore.getInstance("PKCS12", SecurityProviderUtility.getBCProvider()); + break; + } + case BCFKS_KS: { + keyStore = KeyStore.getInstance("BCFKS", SecurityProviderUtility.getBCProvider()); + break; + } + } + keyStore.load(is, keyStoreSecret.toCharArray()); + LOG.debug("Loaded keys from keystore."); + LOG.debug("Security Mode: " + SecurityProviderUtility.getSecurityMode().toString()); + LOG.debug("Keystore Type: " + keyStorageType.toString()); + LOG.trace("Loaded keys:"+ getKeys()); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + LOG.error("Check type of keystorage. Expected keystorage type: '" + keyStorageType.toString() + "'"); + } + } + + public String getKeyStoreFile() { + return keyStoreFile; + } + + public String getKeyStoreSecret() { + return keyStoreSecret; + } + + public String getDnName() { + return dnName; + } + + @Override + public JSONObject generateKey(Algorithm algorithm, Long expirationTime, Use use) throws Exception { + return generateKey(algorithm, expirationTime, use, 2048); + } + + @Override + public JSONObject generateKey(Algorithm algorithm, Long expirationTime, Use use, int keyLength) throws Exception { + + KeyPairGenerator keyGen = null; + + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.fromString(algorithm.getParamName()); + if (signatureAlgorithm == null) { + signatureAlgorithm = SignatureAlgorithm.RS256; + } + + if (algorithm == null) { + throw new RuntimeException("The signature algorithm parameter cannot be null"); + } else if (AlgorithmFamily.RSA.equals(algorithm.getFamily())) { + keyGen = KeyPairGenerator.getInstance(algorithm.getFamily().toString(), SecurityProviderUtility.getBCProvider()); + keyGen.initialize(keyLength, new SecureRandom()); + + } else if (AlgorithmFamily.EC.equals(algorithm.getFamily())) { + ECGenParameterSpec eccgen = new ECGenParameterSpec(signatureAlgorithm.getCurve().getAlias()); + keyGen = KeyPairGenerator.getInstance(algorithm.getFamily().toString(), SecurityProviderUtility.getBCProvider()); + keyGen.initialize(eccgen, new SecureRandom()); + } else { + throw new RuntimeException("The provided signature algorithm parameter is not supported"); + } + + // Generate the key + KeyPair keyPair = keyGen.generateKeyPair(); + java.security.PrivateKey pk = keyPair.getPrivate(); + + // Java API requires a certificate chain + X509Certificate cert = generateV3Certificate(keyPair, dnName, signatureAlgorithm.getAlgorithm(), expirationTime); + X509Certificate[] chain = new X509Certificate[1]; + chain[0] = cert; + + String alias = UUID.randomUUID().toString() + getKidSuffix(use, algorithm); + keyStore.setKeyEntry(alias, pk, keyStoreSecret.toCharArray(), chain); + + final String oldAliasByAlgorithm = getAliasByAlgorithmForDeletion(algorithm, alias, use); + if (StringUtils.isNotBlank(oldAliasByAlgorithm)) { + keyStore.deleteEntry(oldAliasByAlgorithm); + LOG.trace("New key: " + alias + ", deleted key: " + oldAliasByAlgorithm); + } + + FileOutputStream stream = new FileOutputStream(keyStoreFile); + keyStore.store(stream, keyStoreSecret.toCharArray()); + stream.close(); + + PublicKey publicKey = keyPair.getPublic(); + + JSONObject jsonObject = new JSONObject(); + jsonObject.put(KEY_TYPE, algorithm.getFamily()); + jsonObject.put(KEY_ID, alias); + jsonObject.put(KEY_USE, use.getParamName()); + jsonObject.put(ALGORITHM, algorithm.getParamName()); + jsonObject.put(EXPIRATION_TIME, expirationTime); + if (publicKey instanceof RSAPublicKey) { + RSAPublicKey rsaPublicKey = (RSAPublicKey) publicKey; + jsonObject.put(MODULUS, Base64Util.base64urlencodeUnsignedBigInt(rsaPublicKey.getModulus())); + jsonObject.put(EXPONENT, Base64Util.base64urlencodeUnsignedBigInt(rsaPublicKey.getPublicExponent())); + } else if (publicKey instanceof ECPublicKey) { + ECPublicKey ecPublicKey = (ECPublicKey) publicKey; + jsonObject.put(CURVE, signatureAlgorithm.getCurve().getName()); + jsonObject.put(X, Base64Util.base64urlencodeUnsignedBigInt(ecPublicKey.getW().getAffineX())); + jsonObject.put(Y, Base64Util.base64urlencodeUnsignedBigInt(ecPublicKey.getW().getAffineY())); + } + JSONArray x5c = new JSONArray(); + x5c.put(Base64.encodeBase64String(cert.getEncoded())); + jsonObject.put(CERTIFICATE_CHAIN, x5c); + + return jsonObject; + } + + private static String getKidSuffix(Use use, Algorithm algorithm) { + return "_" + use.getParamName().toLowerCase() + "_" + algorithm.getParamName().toLowerCase(); + } + + public String getAliasByAlgorithmForDeletion(Algorithm algorithm, String newAlias, Use use) throws KeyStoreException { + for (String alias : Collections.list(keyStore.aliases())) { + + if (newAlias.equals(alias)) { // skip newly created alias + continue; + } + + if (alias.endsWith(getKidSuffix(use, algorithm))) { + return alias; + } + } + return null; + } + + @Override + public boolean containsKey(String keyId) { + try { + if (StringUtils.isBlank(keyId)){ + return false; + } + return keyStore.getKey(keyId, keyStoreSecret.toCharArray()) != null; + } catch (Exception e) { + LOG.error(e.getMessage(), e); + return false; + } + } + + @Override + public String sign(String signingInput, String alias, String sharedSecret, SignatureAlgorithm signatureAlgorithm) throws Exception { + if (signatureAlgorithm == SignatureAlgorithm.NONE) { + return ""; + } else if (AlgorithmFamily.HMAC.equals(signatureAlgorithm.getFamily())) { + SecretKey secretKey = new SecretKeySpec(sharedSecret.getBytes(Util.UTF8_STRING_ENCODING), signatureAlgorithm.getAlgorithm()); + Mac mac = Mac.getInstance(signatureAlgorithm.getAlgorithm()); + mac.init(secretKey); + byte[] sig = mac.doFinal(signingInput.getBytes()); + return Base64Util.base64urlencode(sig); + } else { // EC or RSA + PrivateKey privateKey = getPrivateKey(alias); + if (privateKey == null) { + final String error = "Failed to find private key by kid: " + alias + + ", signatureAlgorithm: " + signatureAlgorithm + + "(check whether web keys JSON in persistence corresponds to keystore file), keySelectionStrategy: " + keySelectionStrategy; + LOG.error(error); + throw new RuntimeException(error); + } + + Signature signer = Signature.getInstance(signatureAlgorithm.getAlgorithm(), SecurityProviderUtility.getBCProvider()); + signer.initSign(privateKey); + signer.update(signingInput.getBytes()); + + byte[] signature = signer.sign(); + if (AlgorithmFamily.EC.equals(signatureAlgorithm.getFamily())) { + int signatureLenght = ECDSA.getSignatureByteArrayLength(JWSAlgorithm.parse(signatureAlgorithm.getName())); + signature = ECDSA.transcodeSignatureToConcat(signature, signatureLenght); + } + + return Base64Util.base64urlencode(signature); + } + } + + @Override + public boolean verifySignature(String signingInput, String encodedSignature, String alias, JSONObject jwks, String sharedSecret, SignatureAlgorithm signatureAlgorithm) throws Exception { + if (rejectNoneAlg && signatureAlgorithm == SignatureAlgorithm.NONE) { + LOG.trace("None algorithm is forbidden by `rejectJwtWithNoneAlg` property."); + return false; + } + + if (signatureAlgorithm == SignatureAlgorithm.NONE) { + return Util.isNullOrEmpty(encodedSignature); + } else if (AlgorithmFamily.HMAC.equals(signatureAlgorithm.getFamily())) { + String expectedSignature = sign(signingInput, null, sharedSecret, signatureAlgorithm); + return expectedSignature.equals(encodedSignature); + } else { // EC or RSA + PublicKey publicKey = null; + + try { + if (jwks == null) { + publicKey = getPublicKey(alias); + } else { + publicKey = getPublicKey(alias, jwks, signatureAlgorithm.getAlg()); + } + if (publicKey == null) { + return false; + } + + byte[] signature = Base64Util.base64urldecode(encodedSignature); + byte[] signatureDer = signature; + if (AlgorithmFamily.EC.equals(signatureAlgorithm.getFamily())) { + signatureDer = ECDSA.transcodeSignatureToDER(signatureDer); + } + + Signature verifier = Signature.getInstance(signatureAlgorithm.getAlgorithm(), SecurityProviderUtility.getBCProvider()); + verifier.initVerify(publicKey); + verifier.update(signingInput.getBytes()); + try { + return verifier.verify(signatureDer); + } catch (SignatureException e) { + // Fall back to old format + // TODO: remove in Gluu 5.0 + return verifier.verify(signature); + } + } catch (Exception e) { + LOG.error(e.getMessage(), e); + return false; + } + } + } + + private String getJWKSValue(JSONObject jwks, String node) throws JSONException { + try { + return jwks.getString(node); + } catch (Exception ex) { + JSONObject publicKey = jwks.getJSONObject(PUBLIC_KEY); + return publicKey.getString(node); + } + } + + @Override + public boolean deleteKey(String alias) throws Exception { + keyStore.deleteEntry(alias); + FileOutputStream stream = new FileOutputStream(keyStoreFile); + keyStore.store(stream, keyStoreSecret.toCharArray()); + stream.close(); + return true; + } + + public PublicKey getPublicKey(String alias) { + PublicKey publicKey = null; + + try { + if (Util.isNullOrEmpty(alias)) { + return null; + } + + java.security.cert.Certificate certificate = keyStore.getCertificate(alias); + if (certificate == null) { + return null; + } + publicKey = certificate.getPublicKey(); + + checkKeyExpiration(alias); + } catch (KeyStoreException e) { + e.printStackTrace(); + } + + return publicKey; + } + + public String getKeyId(JSONWebKeySet jsonWebKeySet, Algorithm algorithm, Use use) throws Exception { + if (algorithm == null || AlgorithmFamily.HMAC.equals(algorithm.getFamily())) { + return null; + } + + String kid = null; + final List keys = jsonWebKeySet.getKeys(); + LOG.trace("WebKeys:" + keys.stream().map(JSONWebKey::getKid).collect(Collectors.toList())); + LOG.trace("KeyStoreKeys:" + getKeys()); + + List keysByAlgAndUse = new ArrayList<>(); + + for (JSONWebKey key : keys) { + if (algorithm == key.getAlg() && (use == null || use == key.getUse())) { + kid = key.getKid(); + Key keyFromStore = keyStore.getKey(kid, keyStoreSecret.toCharArray()); + if (keyFromStore != null) { + keysByAlgAndUse.add(key); + } + } + } + + if (keysByAlgAndUse.isEmpty()) { + LOG.trace("kid is not in keystore, algorithm: " + algorithm + ", kid: " + kid + ", keyStorePath:" + keyStoreFile); + return kid; + } + + if (LOG.isTraceEnabled()) + LOG.trace("Select among keys (älg: "+ algorithm+", use: " + use + "): " + traceWithExp(keysByAlgAndUse)); + final JSONWebKey selectedKey = keySelectionStrategy.select(keysByAlgAndUse); + final String selectedKid = selectedKey != null ? selectedKey.getKid() : null; + LOG.trace("Selected kid: " + selectedKid + ", keySelectionStrategy: " + keySelectionStrategy); + return selectedKid; + } + + private String traceWithExp(List list) { + StringBuilder sb = new StringBuilder("["); + for (JSONWebKey key : list) + sb.append("{\"kid\":").append(key.getKid()).append(",\"exp\":").append(key.getExp()).append("},"); + sb.append("]"); + return sb.toString(); + } + + public PrivateKey getPrivateKey(String alias) + throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException { + if (Util.isNullOrEmpty(alias)) { + return null; + } + + Key key = keyStore.getKey(alias, keyStoreSecret.toCharArray()); + if (key == null) { + return null; + } + PrivateKey privateKey = (PrivateKey) key; + + checkKeyExpiration(alias); + + return privateKey; + } + + public X509Certificate generateV3Certificate(KeyPair keyPair, String issuer, String signatureAlgorithm, Long expirationTime) throws CertIOException, OperatorCreationException, CertificateException { + PrivateKey privateKey = keyPair.getPrivate(); + PublicKey publicKey = keyPair.getPublic(); + + // Signers name + X500Name issuerName = new X500Name(issuer); + + // Subjects name - the same as we are self signed. + X500Name subjectName = new X500Name(issuer); + + // Serial + BigInteger serial = new BigInteger(256, new SecureRandom()); + + // Not before + Date notBefore = new Date(System.currentTimeMillis() - 10000); + Date notAfter = new Date(expirationTime); + + // Create the certificate - version 3 + JcaX509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(issuerName, serial, notBefore, notAfter, subjectName, publicKey); + + ASN1EncodableVector purposes = new ASN1EncodableVector(); + purposes.add(KeyPurposeId.id_kp_serverAuth); + purposes.add(KeyPurposeId.id_kp_clientAuth); + purposes.add(KeyPurposeId.anyExtendedKeyUsage); + + ASN1ObjectIdentifier extendedKeyUsage = new ASN1ObjectIdentifier("2.5.29.37").intern(); + builder.addExtension(extendedKeyUsage, false, new DERSequence(purposes)); + + ContentSigner signer = new JcaContentSignerBuilder(signatureAlgorithm).setProvider(SecurityProviderUtility.getBCProvider()).build(privateKey); + X509CertificateHolder holder = builder.build(signer); + X509Certificate cert = new JcaX509CertificateConverter().setProvider(SecurityProviderUtility.getBCProvider()).getCertificate(holder); + + return cert; + } + + public List getKeys() { + try { + return Collections.list(this.keyStore.aliases()); + } catch (KeyStoreException e) { + LOG.error(e.getMessage(), e); + return Lists.newArrayList(); + } + } + + public SignatureAlgorithm getSignatureAlgorithm(String alias) throws KeyStoreException { + Certificate[] chain = keyStore.getCertificateChain(alias); + if ((chain == null) || chain.length == 0) { + return null; + } + + X509Certificate cert = (X509Certificate) chain[0]; + return CertUtils.getSignatureAlgorithm(cert); + } + + + private void checkKeyExpiration(String alias) { + try { + Date expirationDate = ((X509Certificate) keyStore.getCertificate(alias)).getNotAfter(); + checkKeyExpiration(alias, expirationDate.getTime()); + } catch (KeyStoreException e) { + e.printStackTrace(); + } + } + + public KeyStore getKeyStore() { + return keyStore; + } + + /** + * Checks, if SecurityModeType value correspondent to the keystorage extension value + * + * @param extension extension value + * @param securityMode SecurityModeType value + * @return boolean result + */ + public static boolean checkExtension(final String extension, final SecurityModeType securityMode) { + boolean res = false; + if (securityMode != null) { + res = securityMode.toString().equals(extension); + } + return res; + } + + /** + * + * @return + */ + private SecurityProviderUtility.KeyStorageType solveKeyStorageType() { + SecurityProviderUtility.SecurityModeType securityMode = SecurityProviderUtility.getSecurityMode(); + if (securityMode == null) { + throw new InvalidParameterException("Security Mode wasn't initialized. Call installBCProvider() before"); + } + String keyStoreExt = FilenameUtils.getExtension(keyStoreFile); + SecurityProviderUtility.KeyStorageType keyStorageType = SecurityProviderUtility.KeyStorageType.fromExtension(keyStoreExt); + boolean ksTypeFound = false; + for (SecurityProviderUtility.KeyStorageType ksType : securityMode.getKeystorageTypes()) { + if (keyStorageType == ksType) { + ksTypeFound = true; + break; + } + } + if (!ksTypeFound) { + switch (securityMode) { + case BCFIPS_SECURITY_MODE: { + keyStorageType = SecurityProviderUtility.KeyStorageType.BCFKS_KS; + break; + } + case BCPROV_SECURITY_MODE: { + keyStorageType = SecurityProviderUtility.KeyStorageType.PKCS12_KS; + break; + } + } + } + return keyStorageType; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/OxElevenCryptoProvider.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/OxElevenCryptoProvider.java new file mode 100644 index 00000000..7299186d --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/OxElevenCryptoProvider.java @@ -0,0 +1,168 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.crypto; + +import org.apache.http.HttpStatus; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jwk.Algorithm; +import org.gluu.oxauth.model.jwk.Use; +import org.gluu.oxeleven.client.*; +import org.gluu.oxeleven.model.JwksRequestParam; +import org.gluu.oxeleven.model.KeyRequestParam; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.security.PrivateKey; +import java.security.SignatureException; +import java.util.ArrayList; + +import static org.gluu.oxauth.model.jwk.JWKParameter.*; + +/** + * @author Javier Rojas Blum + * @version February 12, 2019 + */ +public class OxElevenCryptoProvider extends AbstractCryptoProvider { + + private String generateKeyEndpoint; + private String signEndpoint; + private String verifySignatureEndpoint; + private String deleteKeyEndpoint; + private String accessToken; + + public OxElevenCryptoProvider(String generateKeyEndpoint, String signEndpoint, String verifySignatureEndpoint, + String deleteKeyEndpoint, String accessToken) { + this.generateKeyEndpoint = generateKeyEndpoint; + this.signEndpoint = signEndpoint; + this.verifySignatureEndpoint = verifySignatureEndpoint; + this.deleteKeyEndpoint = deleteKeyEndpoint; + this.accessToken = accessToken; + } + + @Override + public boolean containsKey(String keyId) { + return false; + } + + @Override + public JSONObject generateKey(Algorithm algorithm, Long expirationTime, Use use) throws Exception { + return generateKey(algorithm, expirationTime, use, 2048); + } + + @Override + public JSONObject generateKey(Algorithm algorithm, Long expirationTime, Use use, int keyLength) throws Exception { + GenerateKeyRequest request = new GenerateKeyRequest(); + request.setSignatureAlgorithm(algorithm.toString()); + request.setExpirationTime(expirationTime); + request.setAccessToken(accessToken); + + GenerateKeyClient client = new GenerateKeyClient(generateKeyEndpoint); + client.setRequest(request); + + GenerateKeyResponse response = client.exec(); + if (response.getStatus() == HttpStatus.SC_OK && response.getKeyId() != null) { + return response.getJSONEntity(); + } else { + throw new Exception(response.getEntity()); + } + } + + @Override + public String sign(String signingInput, String keyId, String shardSecret, SignatureAlgorithm signatureAlgorithm) throws Exception { + SignRequest request = new SignRequest(); + request.getSignRequestParam().setSigningInput(signingInput); + request.getSignRequestParam().setAlias(keyId); + request.getSignRequestParam().setSharedSecret(shardSecret); + request.getSignRequestParam().setSignatureAlgorithm(signatureAlgorithm.getName()); + request.setAccessToken(accessToken); + + SignClient client = new SignClient(signEndpoint); + client.setRequest(request); + + SignResponse response = client.exec(); + if (response.getStatus() == HttpStatus.SC_OK && response.getSignature() != null) { + return response.getSignature(); + } else { + throw new Exception(response.getEntity()); + } + } + + @Override + public boolean verifySignature(String signingInput, String encodedSignature, String keyId, JSONObject jwks, String sharedSecret, SignatureAlgorithm signatureAlgorithm) throws Exception { + VerifySignatureRequest request = new VerifySignatureRequest(); + request.getVerifySignatureRequestParam().setSigningInput(signingInput); + request.getVerifySignatureRequestParam().setSignature(encodedSignature); + request.getVerifySignatureRequestParam().setAlias(keyId); + request.getVerifySignatureRequestParam().setSharedSecret(sharedSecret); + request.getVerifySignatureRequestParam().setSignatureAlgorithm(signatureAlgorithm.getName()); + request.setAccessToken(accessToken); + if (jwks != null) { + request.getVerifySignatureRequestParam().setJwksRequestParam(getJwksRequestParam(jwks)); + } + + VerifySignatureClient client = new VerifySignatureClient(verifySignatureEndpoint); + client.setRequest(request); + + VerifySignatureResponse response = client.exec(); + if (response.getStatus() == HttpStatus.SC_OK) { + return response.isVerified(); + } else { + throw new SignatureException(response.getEntity()); + } + } + + private static JwksRequestParam getJwksRequestParam(JSONObject jwksJsonObject) throws JSONException { + JwksRequestParam jwks = new JwksRequestParam(); + jwks.setKeyRequestParams(new ArrayList<>()); + + JSONArray keys = jwksJsonObject.getJSONArray(JSON_WEB_KEY_SET); + for (int i = 0; i < keys.length(); i++) { + jwks.getKeyRequestParams().add(mapToKeyRequestParam(keys.getJSONObject(i))); + } + + return jwks; + } + + private static KeyRequestParam mapToKeyRequestParam(JSONObject jwk) { + KeyRequestParam key = new KeyRequestParam(); + key.setAlg(jwk.getString(ALGORITHM)); + key.setKid(jwk.getString(KEY_ID)); + key.setUse(jwk.getString(KEY_USE)); + key.setKty(jwk.getString(KEY_TYPE)); + + key.setN(jwk.optString(MODULUS)); + key.setE(jwk.optString(EXPONENT)); + + key.setCrv(jwk.optString(CURVE)); + key.setX(jwk.optString(X)); + key.setY(jwk.optString(Y)); + return key; + } + + @Override + public boolean deleteKey(String keyId) throws Exception { + DeleteKeyRequest request = new DeleteKeyRequest(); + request.setAlias(keyId); + request.setAccessToken(accessToken); + + DeleteKeyClient client = new DeleteKeyClient(deleteKeyEndpoint); + client.setRequest(request); + + DeleteKeyResponse response = client.exec(); + if (response.getStatus() == org.apache.http.HttpStatus.SC_OK) { + return response.isDeleted(); + } else { + throw new Exception(response.getEntity()); + } + } + + @Override + public PrivateKey getPrivateKey(String keyId) { + throw new UnsupportedOperationException("Method not implemented."); + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/PrivateKey.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/PrivateKey.java new file mode 100644 index 00000000..92a47c75 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/PrivateKey.java @@ -0,0 +1,40 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.crypto; + +import org.gluu.oxauth.model.common.JSONable; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; + +/** + * The Private Key for Cryptography algorithms + * + * @author Javier Rojas Blum + * @version June 25, 2016 + */ +public abstract class PrivateKey implements JSONable { + + private String keyId; + + private SignatureAlgorithm signatureAlgorithm; + + public String getKeyId() { + return keyId; + } + + public void setKeyId(String keyId) { + this.keyId = keyId; + } + + public SignatureAlgorithm getSignatureAlgorithm() { + return signatureAlgorithm; + } + + public void setSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + } + +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/PublicKey.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/PublicKey.java new file mode 100644 index 00000000..08ce819e --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/PublicKey.java @@ -0,0 +1,49 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.crypto; + +import org.gluu.oxauth.model.common.JSONable; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; + +/** + * The Public Key for Cryptography algorithms + * + * @author Javier Rojas Blum + * @version June 25, 2016 + */ +public abstract class PublicKey implements JSONable { + + private String keyId; + + private SignatureAlgorithm signatureAlgorithm; + + private Certificate certificate; + + public String getKeyId() { + return keyId; + } + + public void setKeyId(String keyId) { + this.keyId = keyId; + } + + public SignatureAlgorithm getSignatureAlgorithm() { + return signatureAlgorithm; + } + + public void setSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + } + + public Certificate getCertificate() { + return certificate; + } + + public void setCertificate(Certificate certificate) { + this.certificate = certificate; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBinding.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBinding.java new file mode 100644 index 00000000..456358bc --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBinding.java @@ -0,0 +1,60 @@ +package org.gluu.oxauth.model.crypto.binding; + +import java.util.Arrays; + +/** + * struct { + * TokenBindingType tokenbinding_type; + * TokenBindingID tokenbindingid; + * opaque signature<64..2^16-1>; Signature over the concatenation + * of tokenbinding_type, + * key_parameters and exported + * keying material (EKM) + * TB_Extension extensions<0..2^16-1>; + * } TokenBinding; + * + * @author Yuriy Zabrovarnyy + */ +public class TokenBinding { + + private TokenBindingType tokenBindingType; + private TokenBindingID tokenBindingID; + private byte[] signature; + private TokenBindingExtension extension; + + public TokenBinding() { + } + + public TokenBinding(TokenBindingType tokenBindingType, TokenBindingID tokenBindingID, byte[] signature, TokenBindingExtension extension) { + this.tokenBindingType = tokenBindingType; + this.tokenBindingID = tokenBindingID; + this.signature = signature; + this.extension = extension; + } + + public TokenBindingType getTokenBindingType() { + return tokenBindingType; + } + + public TokenBindingID getTokenBindingID() { + return tokenBindingID; + } + + public byte[] getSignature() { + return signature; + } + + public TokenBindingExtension getExtension() { + return extension; + } + + @Override + public String toString() { + return "TokenBinding{" + + "tokenBindingType=" + tokenBindingType + + ", tokenBindingID=" + tokenBindingID + + ", signature=" + Arrays.toString(signature) + + ", extension=" + extension + + '}'; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingExtension.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingExtension.java new file mode 100644 index 00000000..828d5ebe --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingExtension.java @@ -0,0 +1,41 @@ +package org.gluu.oxauth.model.crypto.binding; + +import java.util.Arrays; + +/** + * struct { + * TB_ExtensionType extension_type; + * opaque extension_data<0..2^16-1>; + * } TB_Extension; + * + * @author Yuriy Zabrovarnyy + */ +public class TokenBindingExtension { + + private TokenBindingExtensionType extensionType; + private byte[] extensionData; + + public TokenBindingExtension() { + } + + public TokenBindingExtension(TokenBindingExtensionType extensionType, byte[] extensionData) { + this.extensionType = extensionType; + this.extensionData = extensionData; + } + + public TokenBindingExtensionType getExtensionType() { + return extensionType; + } + + public byte[] getExtensionData() { + return extensionData; + } + + @Override + public String toString() { + return "TokenBindingExtension{" + + "extensionType=" + extensionType + + ", extensionData=" + Arrays.toString(extensionData) + + '}'; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingExtensionType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingExtensionType.java new file mode 100644 index 00000000..2436ff07 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingExtensionType.java @@ -0,0 +1,12 @@ +package org.gluu.oxauth.model.crypto.binding; + +/** + * enum { + * (255) No initial TB_ExtensionType registrations + * } TB_ExtensionType; + * + * @author Yuriy Zabrovarnyy + */ +public enum TokenBindingExtensionType { + UNKNOWN +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingID.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingID.java new file mode 100644 index 00000000..82622f92 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingID.java @@ -0,0 +1,54 @@ +package org.gluu.oxauth.model.crypto.binding; + +import org.apache.commons.codec.digest.DigestUtils; +import org.gluu.oxauth.model.util.Base64Util; + +/** + *

+ * struct {
+ *    TokenBindingKeyParameters key_parameters;
+ *    uint16 key_length;       Length (in bytes) of the following TokenBindingID.TokenBindingPublicKey
+ *    select (key_parameters) {
+ *       case rsa2048_pkcs1.5:
+ *       case rsa2048_pss:
+ *          RSAPublicKey rsapubkey;
+ *       case ecdsap256:
+ *          TB_ECPoint point;
+ *    } TokenBindingPublicKey;
+ * } TokenBindingID;
+ * 
+ * + * @author Yuriy Zabrovarnyy + */ +public class TokenBindingID { + + private TokenBindingKeyParameters keyParameters; + private byte[] publicKey; + private byte[] raw; + + public TokenBindingID(TokenBindingKeyParameters keyParameters, byte[] publicKey, byte[] raw) { + this.keyParameters = keyParameters; + this.publicKey = publicKey; + this.raw = raw; + } + + public TokenBindingKeyParameters getKeyParameters() { + return keyParameters; + } + + public byte[] getPublicKey() { + return publicKey; + } + + public byte[] getRaw() { + return raw; + } + + public byte[] sha256() { + return DigestUtils.sha256(raw); + } + + public String sha256base64url() { + return Base64Util.base64urlencode(sha256()); + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingKeyParameters.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingKeyParameters.java new file mode 100644 index 00000000..32407e8a --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingKeyParameters.java @@ -0,0 +1,49 @@ +package org.gluu.oxauth.model.crypto.binding; + +/** + *
+ *  enum {
+ *     rsa2048_pkcs1.5(0), rsa2048_pss(1), ecdsap256(2), (255)
+ *  } TokenBindingKeyParameters;
+ *  
+ * + * @author Yuriy Zabrovarnyy + */ +public enum TokenBindingKeyParameters { + RSA2048_PKCS1_5("rsa2048_pkcs1.5", 0), + RSA2048_PSS("rsa2048_pss", 1), + ECDSAP256("ecdsap256", 2); + + private final String value; + private final int byteValue; + + TokenBindingKeyParameters(String value, int byteValue) { + this.value = value; + this.byteValue = byteValue; + } + + public String getValue() { + return value; + } + + public int getByteValue() { + return byteValue; + } + + public static TokenBindingKeyParameters valueOf(int byteValue) { + for (TokenBindingKeyParameters v : values()) { + if (v.getByteValue() == byteValue) { + return v; + } + } + throw new RuntimeException("Failed to identify TokenBindingKeyParameters by byteValue"); + } + + @Override + public String toString() { + return "TokenBindingKeyParameters{" + + "value='" + value + '\'' + + ", byteValue=" + byteValue + + "} " + super.toString(); + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingMessage.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingMessage.java new file mode 100644 index 00000000..71073107 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingMessage.java @@ -0,0 +1,102 @@ +package org.gluu.oxauth.model.crypto.binding; + +import com.google.common.base.Function; +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.json.JSONException; +import org.json.JSONObject; +import org.gluu.oxauth.model.token.JsonWebResponse; + +import java.util.ArrayList; +import java.util.List; + +/** + *
+ * struct {
+ *     TokenBinding tokenbindings<132..2^16-1>;
+ * } TokenBindingMessage;
+ * 
+ * + * @author Yuriy Zabrovarnyy + */ +public class TokenBindingMessage { + + private static final Logger log = Logger.getLogger(TokenBindingMessage.class); + + private List tokenBindings = new ArrayList(); + + public TokenBindingMessage(String base64urlencoded) throws TokenBindingParseException { + this(TokenBindingMessageParser.parseBase64UrlEncoded(base64urlencoded)); + } + + public TokenBindingMessage(byte[] raw) throws TokenBindingParseException { + this(TokenBindingMessageParser.parseBytes(raw)); + } + + public TokenBindingMessage(List tokenBindings) { + this.tokenBindings = tokenBindings; + } + + public List getTokenBindings() { + return tokenBindings; + } + + public TokenBinding getFirstTokenBindingByType(TokenBindingType type) { + for (TokenBinding binding : tokenBindings) { + if (binding.getTokenBindingType() == type) { + return binding; + } + } + return null; + } + + public static Function createIdTokenTokingBindingPreprocessing(String tokenBindingMessageAsString, final String rpTokenBindingMessageHashClaimKey) throws TokenBindingParseException { + final boolean tokenBindingMessagePresent = StringUtils.isNotBlank(tokenBindingMessageAsString); + final boolean rpKeyPresent = StringUtils.isNotBlank(rpTokenBindingMessageHashClaimKey); + + log.trace("TokenBindingMessage present: " + tokenBindingMessagePresent + ", rpCnfKey: " + rpTokenBindingMessageHashClaimKey); + + if (tokenBindingMessagePresent && rpKeyPresent) { + TokenBindingMessage message = new TokenBindingMessage(tokenBindingMessageAsString); + final TokenBinding referredBinding = message.getFirstTokenBindingByType(TokenBindingType.REFERRED_TOKEN_BINDING); + return new Function() { + @Override + public Void apply(JsonWebResponse jsonWebResponse) { + setCnfClaim(jsonWebResponse, referredBinding.getTokenBindingID().sha256base64url(), rpTokenBindingMessageHashClaimKey); + return null; + } + }; + } + return null; + } + + public static void setCnfClaim(JsonWebResponse jsonWebResponse, String tokenBindingIdHash, String rpTokenBindingMessageHashClaimKey) { + try { + JSONObject value = jsonWebResponse.getClaims().getClaimAsJSON("cnf"); + if (value == null) { + value = new JSONObject(); + } + value.put(rpTokenBindingMessageHashClaimKey, tokenBindingIdHash); + + jsonWebResponse.getClaims().setClaim("cnf", value); + } catch (JSONException e) { + log.error("Failed to create cnf JSON object", e); + } + } + + public static String getTokenBindingIdHashFromTokenBindingMessage(String tokenBindingMessageAsString, final String rpTokenBindingMessageHashClaimKey) throws TokenBindingParseException { + if (StringUtils.isNotBlank(tokenBindingMessageAsString) && StringUtils.isNotBlank(rpTokenBindingMessageHashClaimKey)) { + TokenBindingMessage message = new TokenBindingMessage(tokenBindingMessageAsString); + final TokenBinding referredBinding = message.getFirstTokenBindingByType(TokenBindingType.REFERRED_TOKEN_BINDING); + return referredBinding.getTokenBindingID().sha256base64url(); + } + return null; + } + + @Override + public String toString() { + return "TokenBindingMessage{" + + "tokenBindings=" + tokenBindings + + '}'; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingMessageParser.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingMessageParser.java new file mode 100644 index 00000000..70e057a9 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingMessageParser.java @@ -0,0 +1,80 @@ +package org.gluu.oxauth.model.crypto.binding; + +import com.google.common.base.Preconditions; +import org.apache.log4j.Logger; +import org.gluu.oxauth.model.util.Base64Util; +import org.gluu.oxauth.model.util.ByteUtils; + +import java.io.ByteArrayInputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * @author Yuriy Zabrovarnyy + */ +public class TokenBindingMessageParser { + + private static final Logger log = Logger.getLogger(TokenBindingMessageParser.class); + + private TokenBindingMessageParser() { + } + + public static List parseBase64UrlEncoded(String base64UrlEncodedString) throws TokenBindingParseException { + return parseBytes(Base64Util.base64urldecode(base64UrlEncodedString)); + } + + public static List parseBytes(byte[] raw) throws TokenBindingParseException { + try { + int length = ByteUtils.twoBytesAsInt(raw[0], raw[1]); + + if (length != (raw.length - 2)) { + log.error("Invalid token binding message. First two bytes length value: " + length + "does not match actual bytes length: " + raw.length); + throw new TokenBindingParseException("Invalid token binding message. First two bytes length value does not match actual bytes length."); + } + + List result = new ArrayList(); + + TokenBindingStream stream = new TokenBindingStream(raw, 2, raw.length); + Preconditions.checkState(stream.getPos() == 2); + + while (stream.available() > 0) { + int tokenTypeAsByteValue = stream.read(); + + TokenBindingType tokenBindingType = TokenBindingType.valueOf(tokenTypeAsByteValue); + if (tokenBindingType == null) { + throw new TokenBindingParseException("Failed to identify TokenBindingType, byteValue: " + tokenTypeAsByteValue); + } + + int fromID = stream.getPos(); + int keyParametersAsByteValue = stream.read(); + + TokenBindingKeyParameters tokenBindingKeyParameters = TokenBindingKeyParameters.valueOf(keyParametersAsByteValue); + if (tokenBindingKeyParameters == null) { + throw new TokenBindingParseException("Failed to identify TokenBindingKeyParameters, byteValue: " + keyParametersAsByteValue); + } + + byte[] publicKey = readBytesWithSuffixLength(stream); + byte[] bindingIdRaw = Arrays.copyOfRange(raw, fromID, stream.getPos()); + + byte[] signature = readBytesWithSuffixLength(stream); + byte[] extensions = readBytesWithSuffixLength(stream); + + TokenBindingID id = new TokenBindingID(tokenBindingKeyParameters, publicKey, bindingIdRaw); + + result.add(new TokenBinding(tokenBindingType, id, signature, new TokenBindingExtension(TokenBindingExtensionType.UNKNOWN, extensions))); + } + return result; + } catch (Exception e) { + throw new TokenBindingParseException("Failed to parse TokenBindingMessage, raw: " + Base64Util.base64urlencode(raw), e); + } + } + + private static byte[] readBytesWithSuffixLength(ByteArrayInputStream stream) { + int length = ByteUtils.twoIntsAsInt(stream.read(), stream.read()); + + byte[] data = new byte[length]; + stream.read(data, 0, length); + return data; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingParseException.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingParseException.java new file mode 100644 index 00000000..de095acc --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingParseException.java @@ -0,0 +1,26 @@ +package org.gluu.oxauth.model.crypto.binding; + +/** + * @author Yuriy Zabrovarnyy + */ +public class TokenBindingParseException extends Exception { + + public TokenBindingParseException() { + } + + public TokenBindingParseException(String message) { + super(message); + } + + public TokenBindingParseException(String message, Throwable cause) { + super(message, cause); + } + + public TokenBindingParseException(Throwable cause) { + super(cause); + } + + public TokenBindingParseException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingStream.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingStream.java new file mode 100644 index 00000000..61b07fa4 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingStream.java @@ -0,0 +1,20 @@ +package org.gluu.oxauth.model.crypto.binding; + +import java.io.ByteArrayInputStream; + +/** + * @author Yuriy Zabrovarnyy + */ +public class TokenBindingStream extends ByteArrayInputStream { + public TokenBindingStream(byte[] buf) { + super(buf); + } + + public TokenBindingStream(byte[] buf, int offset, int length) { + super(buf, offset, length); + } + + public int getPos() { + return pos; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingType.java new file mode 100644 index 00000000..70d9ad40 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/binding/TokenBindingType.java @@ -0,0 +1,48 @@ +package org.gluu.oxauth.model.crypto.binding; + +/** + *
+ * enum {
+ *    provided_token_binding(0), referred_token_binding(1), (255)
+ * } TokenBindingType;
+ * 
+ * + * @author Yuriy Zabrovarnyy + */ +public enum TokenBindingType { + PROVIDED_TOKEN_BINDING("provided_token_binding", 0), + REFERRED_TOKEN_BINDING("referred_token_binding", 1); + + private final String value; + private final int byteValue; + + TokenBindingType(String value, int byteValue) { + this.value = value; + this.byteValue = byteValue; + } + + public String getValue() { + return value; + } + + public int getByteValue() { + return byteValue; + } + + public static TokenBindingType valueOf(int byteValue) { + for (TokenBindingType v : values()) { + if (v.getByteValue() == byteValue) { + return v; + } + } + throw new RuntimeException("Failed to identify TokenBindingType by byteValue"); + } + + @Override + public String toString() { + return "TokenBindingType{" + + "value='" + value + '\'' + + ", byteValue=" + byteValue + + "} " + super.toString(); + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/encryption/BlockEncryptionAlgorithm.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/encryption/BlockEncryptionAlgorithm.java new file mode 100644 index 00000000..b22450e8 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/encryption/BlockEncryptionAlgorithm.java @@ -0,0 +1,103 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.crypto.encryption; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * @author Javier Rojas Blum Date: 12.03.2012 + */ +public enum BlockEncryptionAlgorithm { + + A128CBC_PLUS_HS256("A128CBC+HS256", "CBC", "AES/CBC/PKCS5Padding", "SHA-256", "HMACSHA256", 256, 128, 128), + A256CBC_PLUS_HS512("A256CBC+HS512", "CBC", "AES/CBC/PKCS5Padding", "SHA-512", "HMACSHA512", 512, 128, 256), + A128GCM("A128GCM", "GCM", "AES/GCM/NoPadding", 128, 96), + A256GCM("A256GCM", "GCM", "AES/GCM/NoPadding", 256, 96); + + private final String name; + private final String family; + private final String algorithm; + private final String messageDiggestAlgorithm; + private final String integrityValueAlgorithm; + private final int cmkLength; + private final int initVectorLength; + private final Integer cekLength; + + private BlockEncryptionAlgorithm(String name, String family, String algorithm, int cmkLength, int initVectorLength) { + this.name = name; + this.family = family; + this.algorithm = algorithm; + this.messageDiggestAlgorithm = null; + this.integrityValueAlgorithm = null; + this.cmkLength = cmkLength; + this.initVectorLength=initVectorLength; + this.cekLength = null; + } + + private BlockEncryptionAlgorithm(String name, String family, String algorithm, String messageDiggestAlgorithm, + String integrityValueAlgorithm, int cmkLength, int initVectorLength, int cekLength) { + this.name = name; + this.family = family; + this.algorithm = algorithm; + this.messageDiggestAlgorithm = messageDiggestAlgorithm; + this.integrityValueAlgorithm = integrityValueAlgorithm; + this.cmkLength = cmkLength; + this.initVectorLength=initVectorLength; + this.cekLength = cekLength; + } + + public String getName() { + return name; + } + + public String getFamily() { + return family; + } + + public String getMessageDiggestAlgorithm() { + return messageDiggestAlgorithm; + } + + public String getIntegrityValueAlgorithm() { + return integrityValueAlgorithm; + } + + public String getAlgorithm() { + return algorithm; + } + + public int getCmkLength() { + return cmkLength; + } + + public int getInitVectorLength() { + return initVectorLength; + } + + public Integer getCekLength() { + return cekLength; + } + + @JsonCreator + public static BlockEncryptionAlgorithm fromName(String name) { + if (name != null) { + for (BlockEncryptionAlgorithm a : BlockEncryptionAlgorithm.values()) { + if (name.equals(a.name)) { + return a; + } + } + } + return null; + } + + @Override + @JsonValue + public String toString() { + return name; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/encryption/KeyEncryptionAlgorithm.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/encryption/KeyEncryptionAlgorithm.java new file mode 100644 index 00000000..cee83cba --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/encryption/KeyEncryptionAlgorithm.java @@ -0,0 +1,79 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.crypto.encryption; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import org.gluu.oxauth.model.jwk.Algorithm; + +/** + * @author Javier Rojas Blum Date: 12.03.2012 + */ +public enum KeyEncryptionAlgorithm { + + RSA1_5("RSA1_5", "RSA", "RSA/ECB/PKCS1Padding"), + RSA_OAEP("RSA-OAEP", "RSA", "RSA/ECB/OAEPWithSHA1AndMGF1Padding"), + A128KW("A128KW"), + A256KW("A256KW"); + //DIR("dir"), // Not supported + //ECDH_ES("ECDH-ES"), // Not supported + //ECDH_ES_PLUS_A128KW("ECDH-ES+A128KW"), // Not supported + //ECDH_ES_A256KW("ECDH-ES+A256KW"); // Not supported + + private final String name; + private final String family; + private final String algorithm; + private final Algorithm alg; + + private KeyEncryptionAlgorithm(String name) { + this.name = name; + this.family = null; + this.algorithm = null; + this.alg = Algorithm.fromString(name); + } + + private KeyEncryptionAlgorithm(String name, String family, String algorithm) { + this.name = name; + this.family = family; + this.algorithm = algorithm; + this.alg = Algorithm.fromString(name); + } + + public Algorithm getAlg() { + return alg; + } + + public String getName() { + return name; + } + + public String getFamily() { + return family; + } + + public String getAlgorithm() { + return algorithm; + } + + @JsonCreator + public static KeyEncryptionAlgorithm fromName(String name) { + if (name != null) { + for (KeyEncryptionAlgorithm a : KeyEncryptionAlgorithm.values()) { + if (name.equals(a.name)) { + return a; + } + } + } + return null; + } + + @Override + @JsonValue + public String toString() { + return name; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/AbstractSigner.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/AbstractSigner.java new file mode 100644 index 00000000..68596264 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/AbstractSigner.java @@ -0,0 +1,23 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ +package org.gluu.oxauth.model.crypto.signature; + +/** + * @author Javier Rojas Blum + * @version April 22, 2016 + */ +public abstract class AbstractSigner implements Signer { + + private SignatureAlgorithm signatureAlgorithm; + + protected AbstractSigner(SignatureAlgorithm signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + } + + public SignatureAlgorithm getSignatureAlgorithm() { + return signatureAlgorithm; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/AlgorithmFamily.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/AlgorithmFamily.java new file mode 100644 index 00000000..93d12678 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/AlgorithmFamily.java @@ -0,0 +1,50 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.crypto.signature; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * @author Javier Rojas Blum + * @version February 12, 2019 + */ +public enum AlgorithmFamily { + HMAC("HMAC"), + RSA("RSA"), + EC("EC"); + + private final String value; + + AlgorithmFamily(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + @JsonValue + public String toString() { + return value; + } + + @JsonCreator + public static AlgorithmFamily fromString(String param) { + if (param != null) { + for (AlgorithmFamily gt : AlgorithmFamily.values()) { + if (param.equals(gt.value)) { + return gt; + } + } + } + + return null; + } + +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/AsymmetricSignatureAlgorithm.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/AsymmetricSignatureAlgorithm.java new file mode 100644 index 00000000..47fb3ae7 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/AsymmetricSignatureAlgorithm.java @@ -0,0 +1,138 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.crypto.signature; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import org.gluu.oxauth.model.common.HasParamName; +import org.gluu.oxauth.model.jwt.JwtType; +import org.gluu.persist.annotation.AttributeEnum; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Javier Rojas Blum + * @version August 20, 2019 + */ +public enum AsymmetricSignatureAlgorithm implements HasParamName, AttributeEnum { + + RS256("RS256", AlgorithmFamily.RSA, "SHA256WITHRSA"), + RS384("RS384", AlgorithmFamily.RSA, "SHA384WITHRSA"), + RS512("RS512", AlgorithmFamily.RSA, "SHA512WITHRSA"), + ES256("ES256", AlgorithmFamily.EC, "SHA256WITHECDSA", ECEllipticCurve.P_256), + ES384("ES384", AlgorithmFamily.EC, "SHA384WITHECDSA", ECEllipticCurve.P_384), + ES512("ES512", AlgorithmFamily.EC, "SHA512WITHECDSA", ECEllipticCurve.P_521), + PS256("PS256", AlgorithmFamily.RSA, "SHA256withRSAandMGF1"), + PS384("PS384", AlgorithmFamily.RSA, "SHA384withRSAandMGF1"), + PS512("PS512", AlgorithmFamily.RSA, "SHA512withRSAandMGF1"); + + private final String name; + private final AlgorithmFamily family; + private final String algorithm; + private final ECEllipticCurve curve; + private final JwtType jwtType; + + private static Map mapByValues = new HashMap<>(); + + static { + for (AsymmetricSignatureAlgorithm enumType : values()) { + mapByValues.put(enumType.getValue(), enumType); + } + } + + AsymmetricSignatureAlgorithm(String name, AlgorithmFamily family, String algorithm, ECEllipticCurve curve) { + this.name = name; + this.family = family; + this.algorithm = algorithm; + this.curve = curve; + this.jwtType = JwtType.JWT; + } + + AsymmetricSignatureAlgorithm(String name, AlgorithmFamily family, String algorithm) { + this(name, family, algorithm, null); + } + + @Override + public String getParamName() { + return name; + } + + @Override + public String getValue() { + return name; + } + + public AlgorithmFamily getFamily() { + return family; + } + + public String getAlgorithm() { + return algorithm; + } + + public ECEllipticCurve getCurve() { + return curve; + } + + public JwtType getJwtType() { + return jwtType; + } + + public static List fromString(String[] params) { + List asymmetricSignatureAlgorithms = new ArrayList<>(); + + for (String param : params) { + AsymmetricSignatureAlgorithm asymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.fromString(param); + if (asymmetricSignatureAlgorithm != null) { + asymmetricSignatureAlgorithms.add(asymmetricSignatureAlgorithm); + } + } + + return asymmetricSignatureAlgorithms; + } + + /** + * Returns the corresponding {@link AsymmetricSignatureAlgorithm} for a parameter alg of the JWK endpoint. + * + * @param param The alg parameter. + * @return The corresponding alg if found, otherwise null. + */ + @JsonCreator + public static AsymmetricSignatureAlgorithm fromString(String param) { + if (param != null) { + for (AsymmetricSignatureAlgorithm sa : AsymmetricSignatureAlgorithm.values()) { + if (param.equals(sa.name)) { + return sa; + } + } + } + return null; + } + + public static AsymmetricSignatureAlgorithm getByValue(String value) { + return mapByValues.get(value); + } + + @Override + public Enum resolveByValue(String value) { + return getByValue(value); + } + + /** + * Returns a string representation of the object. In this case the parameter name. + * + * @return The string representation of the object. + */ + @Override + @JsonValue + public String toString() { + return name; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/ECDSAKeyFactory.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/ECDSAKeyFactory.java new file mode 100644 index 00000000..c7f635f7 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/ECDSAKeyFactory.java @@ -0,0 +1,131 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.crypto.signature; + +import org.apache.commons.lang.StringUtils; +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.cert.CertIOException; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.gluu.oxauth.model.crypto.Certificate; +import org.gluu.oxauth.model.crypto.KeyFactory; +import org.gluu.util.security.SecurityProviderUtility; + +import java.math.BigInteger; +import java.security.AlgorithmParameters; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidParameterException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.InvalidParameterSpecException; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; + +/** + * Factory to create asymmetric Public and Private Keys for the Elliptic Curve Digital Signature Algorithm (ECDSA) + * + * @author Javier Rojas Blum + * @version June 15, 2016 + */ +public class ECDSAKeyFactory extends KeyFactory { + + private SignatureAlgorithm signatureAlgorithm; + private KeyPair keyPair; + + private ECDSAPrivateKey ecdsaPrivateKey; + private ECDSAPublicKey ecdsaPublicKey; + private Certificate certificate; + + public ECDSAKeyFactory(SignatureAlgorithm signatureAlgorithm, String dnName) throws NoSuchAlgorithmException, InvalidParameterSpecException, + InvalidAlgorithmParameterException, OperatorCreationException, CertificateException, CertIOException { + if (signatureAlgorithm == null) { + throw new InvalidParameterException("The signature algorithm cannot be null"); + } + + this.signatureAlgorithm = signatureAlgorithm; + + AlgorithmParameters parameters = AlgorithmParameters.getInstance("EC", SecurityProviderUtility.getBCProvider()); + + parameters.init(new ECGenParameterSpec(signatureAlgorithm.getCurve().getName())); + ECParameterSpec ecParameters = parameters.getParameterSpec(ECParameterSpec.class); + + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC", SecurityProviderUtility.getBCProvider()); + keyGen.initialize(ecParameters, new SecureRandom()); + + keyPair = keyGen.generateKeyPair(); + + ECPrivateKey privateKeySpec = (ECPrivateKey) keyPair.getPrivate(); + ECPublicKey publicKeySpec = (ECPublicKey) keyPair.getPublic(); + + BigInteger x = publicKeySpec.getW().getAffineX(); + BigInteger y = publicKeySpec.getW().getAffineY(); + BigInteger s = privateKeySpec.getS(); + + this.ecdsaPrivateKey = new ECDSAPrivateKey(s); + this.ecdsaPublicKey = new ECDSAPublicKey(signatureAlgorithm, x, y); + + if (StringUtils.isNotBlank(dnName)) { + // Create certificate + GregorianCalendar startDate = new GregorianCalendar(); // time from which certificate is valid + GregorianCalendar expiryDate = new GregorianCalendar(); // time after which certificate is not valid + expiryDate.add(Calendar.YEAR, 1); + + this.certificate = generateV3Certificate(startDate.getTime(), expiryDate.getTime(), dnName); + } + } + + public Certificate generateV3Certificate(Date startDate, Date expirationDate, String dnName) throws OperatorCreationException, CertificateException, CertIOException { + BigInteger serialNumber = new BigInteger(1024, new SecureRandom()); // serial number for certificate + X500Name name = new X500Name(dnName); + + JcaX509v3CertificateBuilder certGen = new JcaX509v3CertificateBuilder(name, serialNumber, startDate, expirationDate, name, keyPair.getPublic()); + + ASN1EncodableVector purposes = new ASN1EncodableVector(); + purposes.add(KeyPurposeId.id_kp_serverAuth); + purposes.add(KeyPurposeId.id_kp_clientAuth); + purposes.add(KeyPurposeId.anyExtendedKeyUsage); + + ASN1ObjectIdentifier extendedKeyUsage = new ASN1ObjectIdentifier("2.5.29.37").intern(); + certGen.addExtension(extendedKeyUsage, false, new DERSequence(purposes)); + + X509CertificateHolder certHolder = certGen.build(new JcaContentSignerBuilder(signatureAlgorithm.getAlgorithm()).setProvider(SecurityProviderUtility.getBCProviderName()).build(keyPair.getPrivate())); + X509Certificate x509Certificate = new JcaX509CertificateConverter().setProvider(SecurityProviderUtility.getBCProviderName()).getCertificate(certHolder); + + return new Certificate(signatureAlgorithm, x509Certificate); + } + + @Override + public ECDSAPrivateKey getPrivateKey() { + return ecdsaPrivateKey; + } + + @Override + public ECDSAPublicKey getPublicKey() { + return ecdsaPublicKey; + } + + @Override + public Certificate getCertificate() { + return certificate; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/ECDSAPrivateKey.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/ECDSAPrivateKey.java new file mode 100644 index 00000000..66245380 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/ECDSAPrivateKey.java @@ -0,0 +1,65 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.crypto.signature; + +import org.json.JSONException; +import org.json.JSONObject; +import org.gluu.oxauth.model.crypto.PrivateKey; +import org.gluu.oxauth.model.util.Base64Util; +import org.gluu.oxauth.model.util.StringUtils; + +import static org.gluu.oxauth.model.jwk.JWKParameter.*; + +import java.math.BigInteger; + +/** + * The Private Key for the Elliptic Curve Digital Signature Algorithm (ECDSA) + * + * @author Javier Rojas Blum + * @version July 31, 2016 + */ +public class ECDSAPrivateKey extends PrivateKey { + + private BigInteger d; + + public ECDSAPrivateKey(BigInteger d) { + this.d = d; + } + + public ECDSAPrivateKey(String d) { + this.d = new BigInteger(1, Base64Util.base64urldecode(d)); + } + + public BigInteger getD() { + return d; + } + + public void setD(BigInteger d) { + this.d = d; + } + + @Override + public JSONObject toJSONObject() throws JSONException { + JSONObject jsonObject = new JSONObject(); + jsonObject.put(MODULUS, JSONObject.NULL); + jsonObject.put(EXPONENT, JSONObject.NULL); + jsonObject.put(D, Base64Util.base64urlencodeUnsignedBigInt(d)); + + return jsonObject; + } + + @Override + public String toString() { + try { + return toJSONObject().toString(4); + } catch (JSONException e) { + return StringUtils.EMPTY_STRING; + } catch (Exception e) { + return StringUtils.EMPTY_STRING; + } + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/ECDSAPublicKey.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/ECDSAPublicKey.java new file mode 100644 index 00000000..d20ab8f4 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/ECDSAPublicKey.java @@ -0,0 +1,92 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.crypto.signature; + +import org.json.JSONException; +import org.json.JSONObject; +import org.gluu.oxauth.model.crypto.PublicKey; +import org.gluu.oxauth.model.util.Base64Util; +import org.gluu.oxauth.model.util.StringUtils; + +import static org.gluu.oxauth.model.jwk.JWKParameter.*; + +import java.math.BigInteger; + +/** + * The Public Key for the Elliptic Curve Digital Signature Algorithm (ECDSA) + * + * @author Javier Rojas Blum + * @version July 31, 2016 + */ +public class ECDSAPublicKey extends PublicKey { + + private static final String ECDSA_ALGORITHM = "EC"; + private static final String USE = "sig"; + + private SignatureAlgorithm signatureAlgorithm; + private BigInteger x; + private BigInteger y; + + public ECDSAPublicKey(SignatureAlgorithm signatureAlgorithm, BigInteger x, BigInteger y) { + this.signatureAlgorithm = signatureAlgorithm; + this.x = x; + this.y = y; + } + + public ECDSAPublicKey(SignatureAlgorithm signatureAlgorithm, String x, String y) { + this(signatureAlgorithm, + new BigInteger(1, Base64Util.base64urldecode(x)), + new BigInteger(1, Base64Util.base64urldecode(y))); + } + + public SignatureAlgorithm getSignatureAlgorithm() { + return signatureAlgorithm; + } + + public void setSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + } + + public BigInteger getX() { + return x; + } + + public void setX(BigInteger x) { + this.x = x; + } + + public BigInteger getY() { + return y; + } + + public void setY(BigInteger y) { + this.y = y; + } + + @Override + public JSONObject toJSONObject() throws JSONException { + JSONObject jsonObject = new JSONObject(); + + jsonObject.put(MODULUS, JSONObject.NULL); + jsonObject.put(EXPONENT, JSONObject.NULL); + jsonObject.put(X, Base64Util.base64urlencodeUnsignedBigInt(x)); + jsonObject.put(Y, Base64Util.base64urlencodeUnsignedBigInt(y)); + + return jsonObject; + } + + @Override + public String toString() { + try { + return toJSONObject().toString(4); + } catch (JSONException e) { + return StringUtils.EMPTY_STRING; + } catch (Exception e) { + return StringUtils.EMPTY_STRING; + } + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/ECEllipticCurve.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/ECEllipticCurve.java new file mode 100644 index 00000000..042408e2 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/ECEllipticCurve.java @@ -0,0 +1,66 @@ +package org.gluu.oxauth.model.crypto.signature; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * @author Javier Rojas Blum + * @version June 15, 2016 + */ +public enum ECEllipticCurve { + + P_256("P-256", "secp256r1", "1.2.840.10045.3.1.7"), + P_384("P-384", "secp384r1", "1.3.132.0.34"), + P_521("P-521", "secp521r1", "1.3.132.0.35"); + + private final String name; + private final String alias; + private final String oid; + + private ECEllipticCurve(String name, String alias, String oid) { + this.name = name; + this.alias = alias; + this.oid = oid; + } + + public String getName() { + return name; + } + + public String getAlias() { + return alias; + } + + public String getOid() { + return oid; + } + + /** + * Returns the corresponding {@link ECEllipticCurve} for a parameter crv of the JWK endpoint. + * + * @param param The crv parameter. + * @return The corresponding curve if found, otherwise null. + */ + @JsonCreator + public static ECEllipticCurve fromString(String param) { + if (param != null) { + for (ECEllipticCurve ec : ECEllipticCurve.values()) { + if (param.equals(ec.name) || param.equalsIgnoreCase(ec.name())) { + return ec; + } + } + } + return null; + } + + /** + * Returns a string representation of the object. In this case the parameter name. + * + * @return The string representation of the object. + */ + @Override + @JsonValue + public String toString() { + return name; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/RSAKeyFactory.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/RSAKeyFactory.java new file mode 100644 index 00000000..73baa979 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/RSAKeyFactory.java @@ -0,0 +1,141 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.crypto.signature; + +import org.apache.commons.lang.StringUtils; +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.cert.CertIOException; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.gluu.oxauth.model.crypto.Certificate; +import org.gluu.oxauth.model.crypto.KeyFactory; +import org.gluu.oxauth.model.jwk.JSONWebKey; +import org.gluu.util.security.SecurityProviderUtility; + +import java.math.BigInteger; +import java.security.InvalidParameterException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; + +/** + * Factory to create asymmetric Public and Private Keys for the RSA algorithm + * + * @author Javier Rojas Blum + * @version June 15, 2016 + */ +@Deprecated +public class RSAKeyFactory extends KeyFactory { + + public static final int DEF_KEYLENGTH = 2048; + + private SignatureAlgorithm signatureAlgorithm; + private KeyPair keyPair; + + private RSAPrivateKey rsaPrivateKey; + private RSAPublicKey rsaPublicKey; + private Certificate certificate; + + @Deprecated + public RSAKeyFactory(SignatureAlgorithm signatureAlgorithm, String dnName) throws NoSuchAlgorithmException, OperatorCreationException, CertificateException, CertIOException { + if (signatureAlgorithm == null) { + throw new InvalidParameterException("The signature algorithm cannot be null"); + } + + this.signatureAlgorithm = signatureAlgorithm; + + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA", SecurityProviderUtility.getBCProvider()); + keyGen.initialize(2048, new SecureRandom()); + + keyPair = keyGen.generateKeyPair(); + + java.security.interfaces.RSAPrivateKey jcersaPrivateCrtKey = (java.security.interfaces.RSAPrivateKey) keyPair.getPrivate(); + java.security.interfaces.RSAPublicKey jcersaPublicKey = (java.security.interfaces.RSAPublicKey) keyPair.getPublic(); + + rsaPrivateKey = new RSAPrivateKey(jcersaPrivateCrtKey.getModulus(), + jcersaPrivateCrtKey.getPrivateExponent()); + + rsaPublicKey = new RSAPublicKey(jcersaPublicKey.getModulus(), + jcersaPublicKey.getPublicExponent()); + + if (StringUtils.isNotBlank(dnName)) { + // Create certificate + GregorianCalendar startDate = new GregorianCalendar(); // time from which certificate is valid + GregorianCalendar expiryDate = new GregorianCalendar(); // time after which certificate is not valid + expiryDate.add(Calendar.YEAR, 1); + + this.certificate = generateV3Certificate(startDate.getTime(), expiryDate.getTime(), dnName); + } + } + + public Certificate generateV3Certificate(Date startDate, Date expirationDate, String dnName) throws OperatorCreationException, CertificateException, CertIOException { + BigInteger serialNumber = new BigInteger(1024, new SecureRandom()); // serial number for certificate + X500Name name = new X500Name(dnName); + + JcaX509v3CertificateBuilder certGen = new JcaX509v3CertificateBuilder(name, serialNumber, startDate, expirationDate, name, keyPair.getPublic()); + + ASN1EncodableVector purposes = new ASN1EncodableVector(); + purposes.add(KeyPurposeId.id_kp_serverAuth); + purposes.add(KeyPurposeId.id_kp_clientAuth); + purposes.add(KeyPurposeId.anyExtendedKeyUsage); + + ASN1ObjectIdentifier extendedKeyUsage = new ASN1ObjectIdentifier("2.5.29.37").intern(); + certGen.addExtension(extendedKeyUsage, false, new DERSequence(purposes)); + + X509CertificateHolder certHolder = certGen.build(new JcaContentSignerBuilder(signatureAlgorithm.getAlgorithm()).setProvider(SecurityProviderUtility.getBCProviderName()).build(keyPair.getPrivate())); + X509Certificate x509Certificate = new JcaX509CertificateConverter().setProvider(SecurityProviderUtility.getBCProviderName()).getCertificate(certHolder); + + return new Certificate(signatureAlgorithm, x509Certificate); + } + + @Deprecated + public RSAKeyFactory(JSONWebKey p_key) { + if (p_key == null) { + throw new IllegalArgumentException("Key value must not be null."); + } + + rsaPrivateKey = new RSAPrivateKey( + p_key.getN(), + p_key.getE()); + rsaPublicKey = new RSAPublicKey( + p_key.getN(), + p_key.getE()); + certificate = null; + } + + public static RSAKeyFactory valueOf(JSONWebKey p_key) { + return new RSAKeyFactory(p_key); + } + + @Override + public RSAPrivateKey getPrivateKey() { + return rsaPrivateKey; + } + + @Override + public RSAPublicKey getPublicKey() { + return rsaPublicKey; + } + + @Override + public Certificate getCertificate() { + return certificate; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/RSAPrivateKey.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/RSAPrivateKey.java new file mode 100644 index 00000000..48940650 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/RSAPrivateKey.java @@ -0,0 +1,77 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.crypto.signature; + +import org.json.JSONException; +import org.json.JSONObject; +import org.gluu.oxauth.model.crypto.PrivateKey; +import org.gluu.oxauth.model.util.Base64Util; +import org.gluu.oxauth.model.util.StringUtils; + +import static org.gluu.oxauth.model.jwk.JWKParameter.*; + +import java.math.BigInteger; + +/** + * The Private Key for the RSA Algorithm + * + * @author Javier Rojas Blum + * @version July 31, 2016 + */ +public class RSAPrivateKey extends PrivateKey { + + private BigInteger modulus; + private BigInteger privateExponent; + + public RSAPrivateKey(BigInteger modulus, BigInteger privateExponent) { + this.modulus = modulus; + this.privateExponent = privateExponent; + } + + public RSAPrivateKey(String modulus, String privateExponent) { + this.modulus = new BigInteger(1, Base64Util.base64urldecode(modulus)); + this.privateExponent = new BigInteger(1, Base64Util.base64urldecode(privateExponent)); + } + + public BigInteger getModulus() { + return modulus; + } + + public void setModulus(BigInteger modulus) { + this.modulus = modulus; + } + + public BigInteger getPrivateExponent() { + return privateExponent; + } + + public void setPrivateExponent(BigInteger privateExponent) { + this.privateExponent = privateExponent; + } + + @Override + public JSONObject toJSONObject() throws JSONException { + JSONObject jsonObject = new JSONObject(); + + jsonObject.put(MODULUS, Base64Util.base64urlencodeUnsignedBigInt(modulus)); + jsonObject.put(EXPONENT, Base64Util.base64urlencodeUnsignedBigInt(privateExponent)); + jsonObject.put(D, JSONObject.NULL); + + return jsonObject; + } + + @Override + public String toString() { + try { + return toJSONObject().toString(4); + } catch (JSONException e) { + return StringUtils.EMPTY_STRING; + } catch (Exception e) { + return StringUtils.EMPTY_STRING; + } + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/RSAPublicKey.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/RSAPublicKey.java new file mode 100644 index 00000000..41a70742 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/RSAPublicKey.java @@ -0,0 +1,81 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.crypto.signature; + +import org.json.JSONException; +import org.json.JSONObject; +import org.gluu.oxauth.model.crypto.PublicKey; +import org.gluu.oxauth.model.util.Base64Util; +import org.gluu.oxauth.model.util.StringUtils; + +import static org.gluu.oxauth.model.jwk.JWKParameter.*; + +import java.math.BigInteger; + +/** + * The Public Key for the RSA Algorithm + * + * @author Javier Rojas Blum + * @version July 31, 2016 + */ +public class RSAPublicKey extends PublicKey { + + private static final String RSA_ALGORITHM = "RSA"; + private static final String USE = "sig"; + + private BigInteger modulus; + private BigInteger publicExponent; + + public RSAPublicKey(BigInteger modulus, BigInteger publicExponent) { + this.modulus = modulus; + this.publicExponent = publicExponent; + } + + public RSAPublicKey(String modulus, String publicExponent) { + this(new BigInteger(1, Base64Util.base64urldecode(modulus)), + new BigInteger(1, Base64Util.base64urldecode(publicExponent))); + } + + public BigInteger getModulus() { + return modulus; + } + + public void setModulus(BigInteger modulus) { + this.modulus = modulus; + } + + public BigInteger getPublicExponent() { + return publicExponent; + } + + public void setPublicExponent(BigInteger publicExponent) { + this.publicExponent = publicExponent; + } + + @Override + public JSONObject toJSONObject() throws JSONException { + JSONObject jsonObject = new JSONObject(); + + jsonObject.put(MODULUS, Base64Util.base64urlencodeUnsignedBigInt(modulus)); + jsonObject.put(EXPONENT, Base64Util.base64urlencodeUnsignedBigInt(publicExponent)); + jsonObject.put(X, JSONObject.NULL); + jsonObject.put(Y, JSONObject.NULL); + + return jsonObject; + } + + @Override + public String toString() { + try { + return toJSONObject().toString(4); + } catch (JSONException e) { + return StringUtils.EMPTY_STRING; + } catch (Exception e) { + return StringUtils.EMPTY_STRING; + } + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/SignatureAlgorithm.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/SignatureAlgorithm.java new file mode 100644 index 00000000..df5c1e2a --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/SignatureAlgorithm.java @@ -0,0 +1,133 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.crypto.signature; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.nimbusds.jose.JWSAlgorithm; +import org.gluu.oxauth.model.jwk.Algorithm; +import org.gluu.oxauth.model.jwt.JwtType; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Javier Rojas Blum + * @version February 12, 2019 + */ +public enum SignatureAlgorithm { + + NONE("none"), + HS256("HS256", AlgorithmFamily.HMAC, "HMACSHA256", JWSAlgorithm.HS256), + HS384("HS384", AlgorithmFamily.HMAC, "HMACSHA384", JWSAlgorithm.HS384), + HS512("HS512", AlgorithmFamily.HMAC, "HMACSHA512", JWSAlgorithm.HS512), + RS256("RS256", AlgorithmFamily.RSA, "SHA256WITHRSA", JWSAlgorithm.RS256), + RS384("RS384", AlgorithmFamily.RSA, "SHA384WITHRSA", JWSAlgorithm.RS384), + RS512("RS512", AlgorithmFamily.RSA, "SHA512WITHRSA", JWSAlgorithm.RS512), + ES256("ES256", AlgorithmFamily.EC, "SHA256WITHECDSA", ECEllipticCurve.P_256, JWSAlgorithm.ES256), + ES384("ES384", AlgorithmFamily.EC, "SHA384WITHECDSA", ECEllipticCurve.P_384, JWSAlgorithm.ES384), + ES512("ES512", AlgorithmFamily.EC, "SHA512WITHECDSA", ECEllipticCurve.P_521, JWSAlgorithm.ES512), + PS256("PS256", AlgorithmFamily.RSA, "SHA256withRSAandMGF1", JWSAlgorithm.PS256), + PS384("PS384", AlgorithmFamily.RSA, "SHA384withRSAandMGF1", JWSAlgorithm.PS384), + PS512("PS512", AlgorithmFamily.RSA, "SHA512withRSAandMGF1", JWSAlgorithm.PS512); + + private final String name; + private final AlgorithmFamily family; + private final String algorithm; + private final ECEllipticCurve curve; + private final JwtType jwtType; + private final JWSAlgorithm jwsAlgorithm; + private final Algorithm alg; + + SignatureAlgorithm(String name, AlgorithmFamily family, String algorithm, ECEllipticCurve curve, JWSAlgorithm jwsAlgorithm) { + this.name = name; + this.family = family; + this.algorithm = algorithm; + this.curve = curve; + this.jwtType = JwtType.JWT; + this.jwsAlgorithm = jwsAlgorithm; + this.alg = Algorithm.fromString(name); + } + + SignatureAlgorithm(String name, AlgorithmFamily family, String algorithm, JWSAlgorithm jwsAlgorithm) { + this(name, family, algorithm, null, jwsAlgorithm); + } + + SignatureAlgorithm(String name) { + this(name, null, null, null, null); + } + + public Algorithm getAlg() { + return alg; + } + + public String getName() { + return name; + } + + public AlgorithmFamily getFamily() { + return family; + } + + public String getAlgorithm() { + return algorithm; + } + + public ECEllipticCurve getCurve() { + return curve; + } + + public JwtType getJwtType() { + return jwtType; + } + + public static List fromString(String[] params) { + List signatureAlgorithms = new ArrayList(); + + for (String param : params) { + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.fromString(param); + if (signatureAlgorithm != null) { + signatureAlgorithms.add(signatureAlgorithm); + } + } + + return signatureAlgorithms; + } + + /** + * Returns the corresponding {@link SignatureAlgorithm} for a parameter alg of the JWK endpoint. + * + * @param param The alg parameter. + * @return The corresponding alg if found, otherwise null. + */ + @JsonCreator + public static SignatureAlgorithm fromString(String param) { + if (param != null) { + for (SignatureAlgorithm sa : SignatureAlgorithm.values()) { + if (param.equals(sa.name)) { + return sa; + } + } + } + return null; + } + + /** + * Returns a string representation of the object. In this case the parameter name. + * + * @return The string representation of the object. + */ + @Override + @JsonValue + public String toString() { + return name; + } + + public JWSAlgorithm getJwsAlgorithm() { + return jwsAlgorithm; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/Signer.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/Signer.java new file mode 100644 index 00000000..e490b654 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/crypto/signature/Signer.java @@ -0,0 +1,18 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.crypto.signature; + +/** + * @author Javier Rojas Blum + * @version April 22, 2016 + */ +public interface Signer { + + public String sign(String signingInput) throws Exception; + + public boolean verifySignature(String signingInput, String signature) throws Exception; +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/discovery/OAuth2Discovery.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/discovery/OAuth2Discovery.java new file mode 100644 index 00000000..d09f0af6 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/discovery/OAuth2Discovery.java @@ -0,0 +1,324 @@ +package org.gluu.oxauth.model.discovery; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import org.jboss.resteasy.annotations.providers.jaxb.IgnoreMediaTypes; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Arrays; + +/** + * OAuth discovery + * + * @author yuriyz on 05/17/2017. + */ +@IgnoreMediaTypes("application/*+json") +// try to ignore jettison as it's recommended here: http://docs.jboss.org/resteasy/docs/2.3.4.Final/userguide/html/json.html +@JsonPropertyOrder({ + "issuer", + "authorization_endpoint", + "token_endpoint", + "jwks_uri", + "registration_endpoint", + "response_types_supported", + "response_modes_supported", + "grant_types_supported", + "token_endpoint_auth_methods_supported", + "token_endpoint_auth_signing_alg_values_supported", + "service_documentation", + "ui_locales_supported", + "op_policy_uri", + "op_tos_uri", + "revocation_endpoint", + "revocation_endpoint_auth_methods_supported", + "revocation_endpoint_auth_signing_alg_values_supported", + "introspection_endpoint", + "introspection_endpoint_auth_methods_supported", + "introspection_endpoint_auth_signing_alg_values_supported", + "code_challenge_methods_supported" +}) +@XmlRootElement +@JsonIgnoreProperties(ignoreUnknown = true) +public class OAuth2Discovery { + + @JsonProperty(value = "issuer") + @XmlElement(name = "issuer") + private String issuer; + + @JsonProperty(value = "authorization_endpoint") + @XmlElement(name = "authorization_endpoint") + private String authorizationEndpoint; + + @JsonProperty(value = "token_endpoint") + @XmlElement(name = "token_endpoint") + private String tokenEndpoint; + + @JsonProperty(value = "jwks_uri") + @XmlElement(name = "jwks_uri") + private String jwksUri; + + @JsonProperty(value = "registration_endpoint") + @XmlElement(name = "registration_endpoint") + private String registrationEndpoint; + + @JsonProperty(value = "response_types_supported") + @XmlElement(name = "response_types_supported") + private String[] responseTypesSupported; + +// @JsonProperty(value = "response_modes_supported") +// @XmlElement(name = "response_modes_supported") +// private String[] responseModesSupported; + + @JsonProperty(value = "grant_types_supported") + @XmlElement(name = "grant_types_supported") + private String[] grantTypesSupported; + + @JsonProperty(value = "token_endpoint_auth_methods_supported") + @XmlElement(name = "token_endpoint_auth_methods_supported") + private String[] tokenEndpointAuthMethodsSupported; + + @JsonProperty(value = "token_endpoint_auth_signing_alg_values_supported") + @XmlElement(name = "token_endpoint_auth_signing_alg_values_supported") + private String[] tokenEndpointAuthSigningAlgValuesSupported; + + @JsonProperty(value = "service_documentation") + @XmlElement(name = "service_documentation") + private String serviceDocumentation; + + @JsonProperty(value = "ui_locales_supported") + @XmlElement(name = "ui_locales_supported") + private String[] uiLocalesSupported; + + @JsonProperty(value = "op_policy_uri") + @XmlElement(name = "op_policy_uri") + private String opPolicyUri; + + @JsonProperty(value = "op_tos_uri") + @XmlElement(name = "op_tos_uri") + private String opTosUri; + +// @JsonProperty(value = "revocation_endpoint") +// @XmlElement(name = "revocation_endpoint") +// private String revocationEndpoint; +// +// @JsonProperty(value = "revocation_endpoint_auth_methods_supported") +// @XmlElement(name = "revocation_endpoint_auth_methods_supported") +// private String[] revocation_endpoint_auth_methods_supported; +// +// @JsonProperty(value = "revocation_endpoint_auth_signing_alg_values_supported") +// @XmlElement(name = "revocation_endpoint_auth_signing_alg_values_supported") +// private String[] revocationEndpointAuthSigningAlgValuesSupported; + + @JsonProperty(value = "introspection_endpoint") + @XmlElement(name = "introspection_endpoint") + private String introspectionEndpoint; + +// @JsonProperty(value = "introspection_endpoint_auth_methods_supported") +// @XmlElement(name = "introspection_endpoint_auth_methods_supported") +// private String[] introspectionEndpointAuthMethodsSupported; +// +// @JsonProperty(value = "introspection_endpoint_auth_signing_alg_values_supported") +// @XmlElement(name = "introspection_endpoint_auth_signing_alg_values_supported") +// private String[] introspectionEndpointAuthSigningAlgValuesSupported; + + @JsonProperty(value = "code_challenge_methods_supported") + @XmlElement(name = "code_challenge_methods_supported") + private String[] codeChallengeMethodsSupported; + + public String getIssuer() { + return issuer; + } + + public void setIssuer(String issuer) { + this.issuer = issuer; + } + + public String getAuthorizationEndpoint() { + return authorizationEndpoint; + } + + public void setAuthorizationEndpoint(String authorizationEndpoint) { + this.authorizationEndpoint = authorizationEndpoint; + } + + public String getTokenEndpoint() { + return tokenEndpoint; + } + + public void setTokenEndpoint(String tokenEndpoint) { + this.tokenEndpoint = tokenEndpoint; + } + + public String getJwksUri() { + return jwksUri; + } + + public void setJwksUri(String jwksUri) { + this.jwksUri = jwksUri; + } + + public String getRegistrationEndpoint() { + return registrationEndpoint; + } + + public void setRegistrationEndpoint(String registrationEndpoint) { + this.registrationEndpoint = registrationEndpoint; + } + + public String[] getResponseTypesSupported() { + return responseTypesSupported; + } + + public void setResponseTypesSupported(String[] responseTypesSupported) { + this.responseTypesSupported = responseTypesSupported; + } + +// public String[] getResponseModesSupported() { +// return responseModesSupported; +// } +// +// public void setResponseModesSupported(String[] responseModesSupported) { +// this.responseModesSupported = responseModesSupported; +// } + + public String[] getGrantTypesSupported() { + return grantTypesSupported; + } + + public void setGrantTypesSupported(String[] grantTypesSupported) { + this.grantTypesSupported = grantTypesSupported; + } + + public String[] getTokenEndpointAuthMethodsSupported() { + return tokenEndpointAuthMethodsSupported; + } + + public void setTokenEndpointAuthMethodsSupported(String[] tokenEndpointAuthMethodsSupported) { + this.tokenEndpointAuthMethodsSupported = tokenEndpointAuthMethodsSupported; + } + + public String[] getTokenEndpointAuthSigningAlgValuesSupported() { + return tokenEndpointAuthSigningAlgValuesSupported; + } + + public void setTokenEndpointAuthSigningAlgValuesSupported(String[] tokenEndpointAuthSigningAlgValuesSupported) { + this.tokenEndpointAuthSigningAlgValuesSupported = tokenEndpointAuthSigningAlgValuesSupported; + } + + public String getServiceDocumentation() { + return serviceDocumentation; + } + + public void setServiceDocumentation(String serviceDocumentation) { + this.serviceDocumentation = serviceDocumentation; + } + + public String[] getUiLocalesSupported() { + return uiLocalesSupported; + } + + public void setUiLocalesSupported(String[] uiLocalesSupported) { + this.uiLocalesSupported = uiLocalesSupported; + } + + public String getOpPolicyUri() { + return opPolicyUri; + } + + public void setOpPolicyUri(String opPolicyUri) { + this.opPolicyUri = opPolicyUri; + } + + public String getOpTosUri() { + return opTosUri; + } + + public void setOpTosUri(String opTosUri) { + this.opTosUri = opTosUri; + } + +// public String getRevocationEndpoint() { +// return revocationEndpoint; +// } +// +// public void setRevocationEndpoint(String revocationEndpoint) { +// this.revocationEndpoint = revocationEndpoint; +// } +// +// public String[] getRevocation_endpoint_auth_methods_supported() { +// return revocation_endpoint_auth_methods_supported; +// } +// +// public void setRevocation_endpoint_auth_methods_supported(String[] revocation_endpoint_auth_methods_supported) { +// this.revocation_endpoint_auth_methods_supported = revocation_endpoint_auth_methods_supported; +// } +// +// public String[] getRevocationEndpointAuthSigningAlgValuesSupported() { +// return revocationEndpointAuthSigningAlgValuesSupported; +// } +// +// public void setRevocationEndpointAuthSigningAlgValuesSupported(String[] revocationEndpointAuthSigningAlgValuesSupported) { +// this.revocationEndpointAuthSigningAlgValuesSupported = revocationEndpointAuthSigningAlgValuesSupported; +// } + + public String getIntrospectionEndpoint() { + return introspectionEndpoint; + } + + public void setIntrospectionEndpoint(String introspectionEndpoint) { + this.introspectionEndpoint = introspectionEndpoint; + } + +// public String[] getIntrospectionEndpointAuthMethodsSupported() { +// return introspectionEndpointAuthMethodsSupported; +// } +// +// public void setIntrospectionEndpointAuthMethodsSupported(String[] introspectionEndpointAuthMethodsSupported) { +// this.introspectionEndpointAuthMethodsSupported = introspectionEndpointAuthMethodsSupported; +// } +// +// public String[] getIntrospectionEndpointAuthSigningAlgValuesSupported() { +// return introspectionEndpointAuthSigningAlgValuesSupported; +// } +// +// public void setIntrospectionEndpointAuthSigningAlgValuesSupported(String[] introspectionEndpointAuthSigningAlgValuesSupported) { +// this.introspectionEndpointAuthSigningAlgValuesSupported = introspectionEndpointAuthSigningAlgValuesSupported; +// } + + public String[] getCodeChallengeMethodsSupported() { + return codeChallengeMethodsSupported; + } + + public void setCodeChallengeMethodsSupported(String[] codeChallengeMethodsSupported) { + this.codeChallengeMethodsSupported = codeChallengeMethodsSupported; + } + + @Override + public String toString() { + return "OAuth2Discovery{" + + "issuer='" + issuer + '\'' + + ", authorizationEndpoint='" + authorizationEndpoint + '\'' + + ", tokenEndpoint='" + tokenEndpoint + '\'' + + ", jwksUri='" + jwksUri + '\'' + + ", registrationEndpoint='" + registrationEndpoint + '\'' + + ", responseTypesSupported=" + Arrays.toString(responseTypesSupported) + +// ", responseModesSupported=" + Arrays.toString(responseModesSupported) + + ", grantTypesSupported=" + Arrays.toString(grantTypesSupported) + + ", tokenEndpointAuthMethodsSupported=" + Arrays.toString(tokenEndpointAuthMethodsSupported) + + ", tokenEndpointAuthSigningAlgValuesSupported=" + Arrays.toString(tokenEndpointAuthSigningAlgValuesSupported) + + ", serviceDocumentation='" + serviceDocumentation + '\'' + + ", uiLocalesSupported=" + Arrays.toString(uiLocalesSupported) + + ", opPolicyUri='" + opPolicyUri + '\'' + + ", opTosUri='" + opTosUri + '\'' + +// ", revocationEndpoint='" + revocationEndpoint + '\'' + +// ", revocation_endpoint_auth_methods_supported=" + Arrays.toString(revocation_endpoint_auth_methods_supported) + +// ", revocationEndpointAuthSigningAlgValuesSupported=" + Arrays.toString(revocationEndpointAuthSigningAlgValuesSupported) + + ", introspectionEndpoint='" + introspectionEndpoint + '\'' + +// ", introspectionEndpointAuthMethodsSupported=" + Arrays.toString(introspectionEndpointAuthMethodsSupported) + +// ", introspectionEndpointAuthSigningAlgValuesSupported=" + Arrays.toString(introspectionEndpointAuthSigningAlgValuesSupported) + + ", codeChallengeMethodsSupported=" + Arrays.toString(codeChallengeMethodsSupported) + + '}'; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/discovery/WebFingerLink.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/discovery/WebFingerLink.java new file mode 100644 index 00000000..8ba70e98 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/discovery/WebFingerLink.java @@ -0,0 +1,32 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.discovery; + +/** + * @author Javier Rojas Blum Date: 01.28.2013 + */ +public class WebFingerLink { + + private String rel; + private String href; + + public String getRel() { + return rel; + } + + public void setRel(String rel) { + this.rel = rel; + } + + public String getHref() { + return href; + } + + public void setHref(String href) { + this.href = href; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/discovery/WebFingerParam.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/discovery/WebFingerParam.java new file mode 100644 index 00000000..d7f44537 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/discovery/WebFingerParam.java @@ -0,0 +1,34 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.discovery; + +/** + * @author Javier Rojas Blum Date: 09.16.2013 + */ +public interface WebFingerParam { + + /** + * Identifier of the target End-User that is the subject of the discovery request. + */ + public static final String RESOURCE = "resource"; + + /** + * Server where a WebFinger service is hosted. + */ + public static final String HOST = "host"; + + /** + * URI identifying the type of service whose location is requested. + */ + public static final String REL = "rel"; + + public static final String REL_VALUE = "http://openid.net/specs/connect/1.0/issuer"; + + public static final String SUBJECT = "subject"; + public static final String LINKS = "links"; + public static final String HREF = "href"; +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/error/DefaultErrorResponse.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/error/DefaultErrorResponse.java new file mode 100644 index 00000000..900d8d73 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/error/DefaultErrorResponse.java @@ -0,0 +1,51 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.error; + +/** + * @author Yuriy Zabrovarnyy + * @version August 20, 2019 + */ +public class DefaultErrorResponse extends ErrorResponse { + + private IErrorType type; + private String state; + + /** + * Returns the error response type. + * + * @return The error response type. + */ + public IErrorType getType() { + return type; + } + + /** + * Sets the {@link IErrorType} that represents the code of the error that occurred. + * + * @param type The error response type. + */ + public void setType(IErrorType type) { + this.type = type; + } + + @Override + public String getState() { + return state; + } + + public void setState(String p_state) { + state = p_state; + } + + @Override + public String getErrorCode() { + if (type != null) + return type.toString(); + return null; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/error/ErrorHandlingMethod.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/error/ErrorHandlingMethod.java new file mode 100644 index 00000000..bf94cb84 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/error/ErrorHandlingMethod.java @@ -0,0 +1,116 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.error; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.HasParamName; +import org.gluu.persist.annotation.AttributeEnum; + +import java.util.HashMap; +import java.util.Map; + +/** + * This class define error handling methods + * + * @author Javier Rojas Blum + * @author Yuriy Movchan Date: 12/07/2018 + */ +public enum ErrorHandlingMethod implements HasParamName, AttributeEnum { + + INTERNAL("internal"), + + REMOTE("remote"); + + private final String value; + + private static Map mapByValues = new HashMap(); + + static { + for (ErrorHandlingMethod enumType : values()) { + mapByValues.put(enumType.getValue(), enumType); + } + } + + private ErrorHandlingMethod() { + this.value = null; + } + + private ErrorHandlingMethod(String value) { + this.value = value; + } + + /** + * Gets param name. + * + * @return param name + */ + public String getParamName() { + return value; + } + + @Override + public String getValue() { + return value; + } + + /** + * Returns the corresponding {@link GrantType} for a parameter grant_type of + * the access token requests. For the extension grant type, the parameter + * should be a valid URI. + * + * @param param The grant_type parameter. + * @return The corresponding grant type if found, otherwise + * null. + */ + @JsonCreator + public static ErrorHandlingMethod fromString(String param) { + if (param != null) { + for (ErrorHandlingMethod hm : ErrorHandlingMethod.values()) { + if (param.equalsIgnoreCase(hm.value)) { + return hm; + } + } + } + + return null; + } + + public static String[] toStringArray(ErrorHandlingMethod[] grantTypes) { + if (grantTypes == null) { + return null; + } + + String[] resultGrantTypes = new String[grantTypes.length]; + for (int i = 0; i < grantTypes.length; i++) { + resultGrantTypes[i] = grantTypes[i].getValue(); + } + + return resultGrantTypes; + } + + public static ErrorHandlingMethod getByValue(String value) { + return mapByValues.get(value); + } + + public Enum resolveByValue(String value) { + return getByValue(value); + } + + /** + * Returns a string representation of the object. In this case the parameter + * name for the grant_type parameter. + * + * @return The string representation of the object. + */ + @Override + @JsonValue + public String toString() { + return value; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/error/ErrorResponse.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/error/ErrorResponse.java new file mode 100644 index 00000000..34c3835d --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/error/ErrorResponse.java @@ -0,0 +1,189 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.error; + +import org.apache.commons.lang.StringUtils; +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +/** + * Base class for error responses. + * + * @author Javier Rojas Blum + * @version August 20, 2019 + */ +public abstract class ErrorResponse { + + private final static Logger log = LoggerFactory.getLogger(ErrorResponse.class); + + private int status; + private String errorDescription; + private String errorUri; + private String reason; + + /** + * Return the HTTP response status code. + * + * @return The response status. + */ + public int getStatus() { + return status; + } + + /** + * Sets the HTTP response status code. + * + * @param status The response status. + */ + public void setStatus(int status) { + this.status = status; + } + + /** + * Returns the error code of the response. + * + * @return The error code. + */ + public abstract String getErrorCode(); + + /** + * If a valid state parameter was present in the request, it returns the + * exact value received from the client. + * + * @return The state value of the request. + */ + public abstract String getState(); + + /** + * Returns a human-readable UTF-8 encoded text providing additional + * information, used to assist the client developer in understanding the + * error that occurred. + * + * @return Description about the error. + */ + public String getErrorDescription() { + return errorDescription; + } + + /** + * Sets a human-readable UTF-8 encoded text providing additional + * information, used to assist the client developer in understanding the + * error that occurred. + * + * @param errorDescription + * Description about the error. + */ + public void setErrorDescription(String errorDescription) { + this.errorDescription = errorDescription; + } + + /** + * Return an URI identifying a human-readable web page with information + * about the error, used to provide the client developer with additional + * information about the error. + * + * @return URI with more information about the error. + */ + public String getErrorUri() { + return errorUri; + } + + /** + * Sets an URI identifying a human-readable web page with information about + * the error, used to provide the client developer with additional + * information about the error. + * + * @param errorUri + * URI with more information about the error. + */ + public void setErrorUri(String errorUri) { + this.errorUri = errorUri; + } + + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } + + /** + * Returns a query string representation of the object. + * + * @return The object represented in a query string. + */ + public String toQueryString() { + StringBuilder queryStringBuilder = new StringBuilder(); + + try { + queryStringBuilder.append("error=").append(getErrorCode()); + + if (errorDescription != null && !errorDescription.isEmpty()) { + queryStringBuilder.append("&error_description=").append( + URLEncoder.encode(errorDescription, "UTF-8")); + } + + if (errorUri != null && !errorUri.isEmpty()) { + queryStringBuilder.append("&error_uri=").append( + URLEncoder.encode(errorUri, "UTF-8")); + } + + if (StringUtils.isNotBlank(reason)) { + queryStringBuilder.append("&reason=").append(URLEncoder.encode(reason, "UTF-8")); + } + + if (getState() != null && !getState().isEmpty()) { + queryStringBuilder.append("&state=").append(getState()); + } + } catch (UnsupportedEncodingException e) { + log.error(e.getMessage(), e); + return null; + } + + return queryStringBuilder.toString(); + } + + /** + * Return a JSon string representation of the object. + * + * @return The object represented in a JSon string. + */ + public String toJSonString() { + JSONObject jsonObj = new JSONObject(); + + try { + jsonObj.put("error", getErrorCode()); + + if (errorDescription != null && !errorDescription.isEmpty()) { + jsonObj.put("error_description", errorDescription); + } + + if (errorUri != null && !errorUri.isEmpty()) { + jsonObj.put("error_uri", errorUri); + } + + if (StringUtils.isNotBlank(reason)) { + jsonObj.put("reason", reason); + } + + if (getState() != null && !getState().isEmpty()) { + jsonObj.put("state", getState()); + } + + return jsonObj.toString(4).replace("\\/", "/"); + } catch (JSONException e) { + log.error(e.getMessage(), e); + return null; + } + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/error/IErrorType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/error/IErrorType.java new file mode 100644 index 00000000..8f835880 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/error/IErrorType.java @@ -0,0 +1,22 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.error; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 13/09/2012 + */ + +public interface IErrorType { + + /** + * Gets error parameter. + * + * @return error parameter + */ + public String getParameter(); +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/exception/InvalidClaimException.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/exception/InvalidClaimException.java new file mode 100644 index 00000000..f06973f0 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/exception/InvalidClaimException.java @@ -0,0 +1,17 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.exception; + +/** + * @author Javier Rojas Blum Date: 03.21.2012 + */ +public class InvalidClaimException extends Exception { + + public InvalidClaimException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/exception/InvalidJweException.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/exception/InvalidJweException.java new file mode 100644 index 00000000..4e36be68 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/exception/InvalidJweException.java @@ -0,0 +1,25 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.exception; + +/** + * @author Javier Rojas Blum Date: 12.06.2012 + */ +public class InvalidJweException extends Exception { + + public InvalidJweException(String message) { + super(message); + } + + public InvalidJweException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidJweException(Throwable cause) { + super(cause); + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/exception/InvalidJwtException.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/exception/InvalidJwtException.java new file mode 100644 index 00000000..4f848e9e --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/exception/InvalidJwtException.java @@ -0,0 +1,25 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.exception; + +/** + * @author Javier Rojas Blum Date: 03.09.2012 + */ +public class InvalidJwtException extends Exception { + + public InvalidJwtException(String message) { + super(message); + } + + public InvalidJwtException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidJwtException(Throwable cause) { + super(cause); + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/exception/InvalidParameterException.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/exception/InvalidParameterException.java new file mode 100644 index 00000000..e2da8b86 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/exception/InvalidParameterException.java @@ -0,0 +1,25 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.exception; + +/** + * @author Javier Rojas Blum Date: 10.22.2012 + */ +public class InvalidParameterException extends Exception { + + public InvalidParameterException(String message) { + super(message); + } + + public InvalidParameterException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidParameterException(Throwable cause) { + super(cause); + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/exception/SignatureException.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/exception/SignatureException.java new file mode 100644 index 00000000..904b4bc7 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/exception/SignatureException.java @@ -0,0 +1,25 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.exception; + +/** + * @author Javier Rojas Blum Date: 11.12.2012 + */ +public class SignatureException extends Exception { + + public SignatureException(String message) { + super(message); + } + + public SignatureException(String message, Throwable cause) { + super(message, cause); + } + + public SignatureException(Throwable cause) { + super(cause); + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/DeviceRegistrationStatus.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/DeviceRegistrationStatus.java new file mode 100644 index 00000000..8bbf5186 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/DeviceRegistrationStatus.java @@ -0,0 +1,72 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.fido.u2f; + +import java.util.HashMap; +import java.util.Map; + +import org.gluu.persist.annotation.AttributeEnum; + +/** + * Device registration types + * + * @author Yuriy Movchan Date: 06/02/2015 + */ +public enum DeviceRegistrationStatus implements AttributeEnum { + //TODO: remove this class and reuse the one found in fido2-model + ACTIVE("active", "Active device registration"), + COMPROMISED("compromised", "Compromised device registration"), + MIGRATED("migrated", "Migrated to Fido2"); + + private final String value; + private final String displayName; + + private static Map mapByValues = new HashMap(); + + static { + for (DeviceRegistrationStatus enumType : values()) { + mapByValues.put(enumType.getValue(), enumType); + } + } + + private DeviceRegistrationStatus(String value, String displayName) { + this.value = value; + this.displayName = displayName; + } + + public static DeviceRegistrationStatus fromString(String param) { + return getByValue(param); + } + + @Override + public String getValue() { + return value; + } + + /** + * Gets display name + * + * @return display name name + */ + public String getDisplayName() { + return displayName; + } + + public static DeviceRegistrationStatus getByValue(String value) { + return mapByValues.get(value); + } + + public Enum resolveByValue(String value) { + return getByValue(value); + } + + @Override + public String toString() { + return value; + } + +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/U2fConfiguration.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/U2fConfiguration.java new file mode 100644 index 00000000..f665f49b --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/U2fConfiguration.java @@ -0,0 +1,75 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2015, Gluu + */ + +package org.gluu.oxauth.model.fido.u2f; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import org.jboss.resteasy.annotations.providers.jaxb.IgnoreMediaTypes; + +/** + * FIDO U2F metadata configuration + * + * @author Yuriy Movchan Date: 05/13/2015 + */ +@IgnoreMediaTypes("application/*+json") +@JsonPropertyOrder({ "version", "issuer", "registration_endpoint", "authentication_endpoint" }) +public class U2fConfiguration { + + @JsonProperty(value = "version") + private String version; + + @JsonProperty(value = "issuer") + private String issuer; + + @JsonProperty(value = "registration_endpoint") + private String registrationEndpoint; + + @JsonProperty(value = "authentication_endpoint") + private String authenticationEndpoint; + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getIssuer() { + return issuer; + } + + public void setIssuer(String issuer) { + this.issuer = issuer; + } + + public String getRegistrationEndpoint() { + return registrationEndpoint; + } + + public void setRegistrationEndpoint(String registrationEndpoint) { + this.registrationEndpoint = registrationEndpoint; + } + + public String getAuthenticationEndpoint() { + return authenticationEndpoint; + } + + public void setAuthenticationEndpoint(String authenticationEndpoint) { + this.authenticationEndpoint = authenticationEndpoint; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("U2fConfiguration [version=").append(version).append(", issuer=").append(issuer).append(", registrationEndpoint=") + .append(registrationEndpoint).append(", authenticationEndpoint=").append(authenticationEndpoint).append("]"); + return builder.toString(); + } + + +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/U2fConstants.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/U2fConstants.java new file mode 100644 index 00000000..4b23fc2a --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/U2fConstants.java @@ -0,0 +1,20 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2015, Gluu + */ + +package org.gluu.oxauth.model.fido.u2f; + +/** + * Static FIDO U2F server variables + * + * @author Yuriy Movchan Date: 05/14/2015 + */ +public class U2fConstants { + + public static final String U2F_PROTOCOL_VERSION = "U2F_V2"; + + public static final String U2F_ENROLLMENT_CODE_ATTRIBUTE = "oxEnrollmentCode"; + +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/U2fErrorResponseType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/U2fErrorResponseType.java new file mode 100644 index 00000000..a2773a7d --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/U2fErrorResponseType.java @@ -0,0 +1,97 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2015, Gluu + */ + +package org.gluu.oxauth.model.fido.u2f; + +import java.util.HashMap; +import java.util.Map; + +import org.gluu.oxauth.model.error.IErrorType; + +/** + * Error codes for FIDO U2F server + * + * @author Yuriy Movchan Date: 05/13/2015 + */ +public enum U2fErrorResponseType implements IErrorType { + + /** + * The FIDO U2F server encountered an unexpected condition which prevented + * it from fulfilling the request. + */ + SERVER_ERROR("server_error"), + + /** + * The authentication or registration request contains invalid data or signature. + */ + INVALID_REQUEST("invalid_request"), + + /** + * The user has no registered devices needed to build authentication request. + */ + NO_ELIGABLE_DEVICES("no_eligable_devices"), + + /** + * The registered device was compromised. + */ + DEVICE_COMPROMISED("device_compromised"), + + /** + * The authentication or registration session was expired + */ + SESSION_EXPIRED("session_expired"), + + /** + * The user has registered device already. + */ + REGISTRATION_NOT_ALLOWED("registration_not_allowed"); + + + private static Map lookup = new HashMap(); + + static { + for (U2fErrorResponseType enumType : values()) { + lookup.put(enumType.getParameter(), enumType); + } + } + + private final String paramName; + + private U2fErrorResponseType(String paramName) { + this.paramName = paramName; + } + + /** + * Return the corresponding enumeration from a string parameter. + * + * @param param + * The parameter to be match. + * @return The enumeration if found, otherwise + * null. + */ + public static U2fErrorResponseType fromString(String param) { + return lookup.get(param); + } + + /** + * Returns a string representation of the object. In this case, the lower + * case code of the error. + */ + @Override + public String toString() { + return paramName; + } + + /** + * Gets error parameter. + * + * @return error parameter + */ + @Override + public String getParameter() { + return paramName; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/exception/BadInputException.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/exception/BadInputException.java new file mode 100644 index 00000000..efb0b70b --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/exception/BadInputException.java @@ -0,0 +1,23 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2015, Gluu + */ + +package org.gluu.oxauth.model.fido.u2f.exception; + +/** + * @author Yuriy Movchan Date: 05/13/2015 + */ +public class BadInputException extends RuntimeException { + + private static final long serialVersionUID = -2738024707341148557L; + + public BadInputException(String message) { + super(message); + } + + public BadInputException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/exception/RegistrationNotAllowed.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/exception/RegistrationNotAllowed.java new file mode 100644 index 00000000..79f67adb --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/exception/RegistrationNotAllowed.java @@ -0,0 +1,23 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2015, Gluu + */ + +package org.gluu.oxauth.model.fido.u2f.exception; + +/** + * @author Yuriy Movchan Date: 07/13/2016 + */ +public class RegistrationNotAllowed extends RuntimeException { + + private static final long serialVersionUID = -2738024707341148557L; + + public RegistrationNotAllowed(String message) { + super(message); + } + + public RegistrationNotAllowed(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/message/RawAuthenticateResponse.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/message/RawAuthenticateResponse.java new file mode 100644 index 00000000..b0453da0 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/message/RawAuthenticateResponse.java @@ -0,0 +1,61 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.fido.u2f.message; + +import org.gluu.oxauth.model.fido.u2f.exception.BadInputException; + +/** + * The authenticate response produced by the token/key, which is transformed by + * the client into an AuthenticateResponse and sent to the server. + * + * @author Yuriy Movchan Date: 05/14/2015 + */ +public class RawAuthenticateResponse { + public static final byte USER_PRESENT_FLAG = 0x01; + + private final byte userPresence; + private final long counter; + private final byte[] signature; + + public RawAuthenticateResponse(byte userPresence, long counter, byte[] signature) { + this.userPresence = userPresence; + this.counter = counter; + this.signature = signature; + } + + /** + * Bit 0 is set to 1, which means that user presence was verified. (This + * version of the protocol doesn't specify a way to request authentication + * responses without requiring user presence.) A different value of bit 0, + * as well as bits 1 through 7, are reserved for future use. The values of + * bit 1 through 7 SHOULD be 0 + */ + public byte getUserPresence() { + return userPresence; + } + + /** + * This is the big-endian representation of a counter value that the U2F + * device increments every time it performs an authentication operation. + */ + public long getCounter() { + return counter; + } + + /** + * This is a ECDSA signature (on P-256) + */ + public byte[] getSignature() { + return signature; + } + + public void checkUserPresence() throws BadInputException { + if (userPresence != USER_PRESENT_FLAG) { + throw new BadInputException("User presence invalid during authentication"); + } + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/message/RawRegisterResponse.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/message/RawRegisterResponse.java new file mode 100644 index 00000000..9e0771e5 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/message/RawRegisterResponse.java @@ -0,0 +1,59 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.fido.u2f.message; + +import java.security.cert.X509Certificate; + +/** + * The register response produced by the token/key, which is transformed by the + * client into an RegisterResponse and sent to the server. + * + * @author Yuriy Movchan Date: 05/14/2015 + */ +public class RawRegisterResponse { + + /** + * The (uncompressed) x,y-representation of a curve point on the P-256 NIST + * elliptic curve. + */ + private final byte[] userPublicKey; + + /** + * A handle that allows the U2F token to identify the generated key pair. + */ + private final byte[] keyHandle; + private final X509Certificate attestationCertificate; + + /** + * A ECDSA signature (on P-256) + */ + private final byte[] signature; + + public RawRegisterResponse(byte[] userPublicKey, byte[] keyHandle, X509Certificate attestationCertificate, byte[] signature) { + this.userPublicKey = userPublicKey; + this.keyHandle = keyHandle; + this.attestationCertificate = attestationCertificate; + this.signature = signature; + } + + public byte[] getUserPublicKey() { + return userPublicKey; + } + + public byte[] getKeyHandle() { + return keyHandle; + } + + public X509Certificate getAttestationCertificate() { + return attestationCertificate; + } + + public byte[] getSignature() { + return signature; + } + +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/AuthenticateRequest.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/AuthenticateRequest.java new file mode 100644 index 00000000..a28a7266 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/AuthenticateRequest.java @@ -0,0 +1,79 @@ +/* + /* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2015, Gluu + */ + +package org.gluu.oxauth.model.fido.u2f.protocol; + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.gluu.oxauth.model.fido.u2f.U2fConstants; + +/** + * FIDO U2F device authentication request + * + * @author Yuriy Movchan Date: 05/13/2015 + */ +public class AuthenticateRequest implements Serializable { + + private static final long serialVersionUID = 8479635006453668632L; + + /** + * Version of the protocol that the to-be-registered U2F token must speak. + * For the version of the protocol described herein, must be "U2F_V2" + */ + @JsonProperty + private final String version = U2fConstants.U2F_PROTOCOL_VERSION; + + /** + * The websafe-base64-encoded challenge. + */ + @JsonProperty + private final String challenge; + + /** + * The application id that the RP would like to assert. The U2F token will + * enforce that the key handle provided above is associated with this + * application id. The browser enforces that the calling origin belongs to + * the application identified by the application id. + */ + @JsonProperty + private final String appId; + + /** + * websafe-base64 encoding of the key handle obtained from the U2F token + * during registration. + */ + @JsonProperty + private final String keyHandle; + + public AuthenticateRequest(@JsonProperty("challenge") String challenge, @JsonProperty("appId") String appId, @JsonProperty("keyHandle") String keyHandle) { + this.challenge = challenge; + this.appId = appId; + this.keyHandle = keyHandle; + } + + public String getKeyHandle() { + return keyHandle; + } + + public String getChallenge() { + return challenge; + } + + public String getAppId() { + return appId; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("AuthenticateRequest [version=").append(version).append(", challenge=").append(challenge).append(", appId=").append(appId) + .append(", keyHandle=").append(keyHandle).append("]"); + return builder.toString(); + } + +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/AuthenticateRequestMessage.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/AuthenticateRequestMessage.java new file mode 100644 index 00000000..09d76d0c --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/AuthenticateRequestMessage.java @@ -0,0 +1,60 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2015, Gluu + */ + +package org.gluu.oxauth.model.fido.u2f.protocol; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.gluu.oxauth.model.util.Util; + +import java.io.Serializable; +import java.util.Collections; +import java.util.List; + +/** + * FIDO U2F authentication request message + * + * @author Yuriy Movchan Date: 05/15/2015 + */ +public class AuthenticateRequestMessage implements Serializable { + + private static final long serialVersionUID = 5492097239884163697L; + + @JsonProperty + private List authenticateRequests; + + public AuthenticateRequestMessage() {} + + public AuthenticateRequestMessage(@JsonProperty("authenticateRequests") List authenticateRequests) { + this.authenticateRequests = authenticateRequests; + } + + public List getAuthenticateRequests() { + return Collections.unmodifiableList(authenticateRequests); + } + + public void setAuthenticateRequests(List authenticateRequests) { + this.authenticateRequests = authenticateRequests; + } + + @JsonIgnore + public String getRequestId() { + return Util.firstItem(authenticateRequests).getChallenge(); + } + + @JsonIgnore + public String getAppId() { + return Util.firstItem(authenticateRequests).getAppId(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("AuthenticateRequestMessage [authenticateRequests=").append(authenticateRequests).append("]"); + return builder.toString(); + } + +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/AuthenticateResponse.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/AuthenticateResponse.java new file mode 100644 index 00000000..389a9942 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/AuthenticateResponse.java @@ -0,0 +1,96 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2015, Gluu + */ + +package org.gluu.oxauth.model.fido.u2f.protocol; + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.gluu.oxauth.model.fido.u2f.exception.BadInputException; +import org.jboss.resteasy.annotations.providers.jaxb.IgnoreMediaTypes; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +/** + * FIDO U2F device authentication response + * + * @author Yuriy Movchan Date: 05/13/2015 + */ +@IgnoreMediaTypes("application/*+json") +// try to ignore jettison as it's recommended here: +// http://docs.jboss.org/resteasy/docs/2.3.4.Final/userguide/html/json.html +@JsonIgnoreProperties(ignoreUnknown = true) +public class AuthenticateResponse implements Serializable { + + private static final long serialVersionUID = -4854326288654670000L; + + /** + * base64(UTF8(client data)) + */ + @JsonProperty + private final String clientData; + + @JsonIgnore + private transient ClientData clientDataRef; + + /* base64(raw response from U2F device) */ + @JsonProperty + private final String signatureData; + + /* keyHandle originally passed */ + @JsonProperty + private final String keyHandle; + /** + * base64(UTF8(device data)) + */ + @JsonProperty + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private final String deviceData; + + public String getDeviceData() { + return deviceData; + } + + public AuthenticateResponse(@JsonProperty("clientData") String clientData, @JsonProperty("signatureData") String signatureData, + @JsonProperty("keyHandle") String keyHandle, @JsonProperty("deviceData") String deviceData) throws BadInputException { + this.clientData = clientData; + this.signatureData = signatureData; + this.keyHandle = keyHandle; + this.clientDataRef = new ClientData(clientData); + this.deviceData = deviceData; + } + + public ClientData getClientData() { + return clientDataRef; + } + + public String getClientDataRaw() { + return clientData; + } + + public String getSignatureData() { + return signatureData; + } + + public String getKeyHandle() { + return keyHandle; + } + + @JsonIgnore + public String getRequestId() { + return getClientData().getChallenge(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("AuthenticateResponse [clientData=").append(clientData).append(", signatureData=").append(signatureData).append(", keyHandle=") + .append(keyHandle).append(",deviceData=").append("]"); + return builder.toString(); + } + +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/AuthenticateStatus.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/AuthenticateStatus.java new file mode 100644 index 00000000..84f8e52a --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/AuthenticateStatus.java @@ -0,0 +1,55 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2015, Gluu + */ + +package org.gluu.oxauth.model.fido.u2f.protocol; + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.gluu.oxauth.model.fido.u2f.exception.BadInputException; + +/** + * FIDO U2F device authentication status response + * + * @author Yuriy Movchan Date: 05/20/2015 + */ +public class AuthenticateStatus implements Serializable { + + private static final long serialVersionUID = -8287836230637556749L; + + @JsonProperty + private final String status; + + @JsonProperty + private final String challenge; + + public AuthenticateStatus(@JsonProperty("status") String status, @JsonProperty("challenge") String challenge) throws BadInputException { + this.status = status; + this.challenge = challenge; + } + + public String getStatus() { + return status; + } + + public String getChallenge() { + return challenge; + } + + @JsonIgnore + public String getRequestId() { + return challenge; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("RegisterStatus [status=").append(status).append(", challenge=").append(challenge).append("]"); + return builder.toString(); + } + +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/ClientData.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/ClientData.java new file mode 100644 index 00000000..ce6464b3 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/ClientData.java @@ -0,0 +1,73 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2015, Gluu + */ + +package org.gluu.oxauth.model.fido.u2f.protocol; + +import java.io.IOException; +import java.io.Serializable; + +import org.gluu.oxauth.model.fido.u2f.exception.BadInputException; +import org.gluu.oxauth.model.util.Base64Util; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * FIDO U2F client data + * + * @author Yuriy Movchan Date: 05/13/2015 + */ +public class ClientData implements Serializable { + + private static final long serialVersionUID = -1483378146391551962L; + + private static final String TYPE_PARAM = "typ"; + private static final String CHALLENGE_PARAM = "challenge"; + private static final String ORIGIN_PARAM = "origin"; + + private final String typ; + private final String challenge; + private final String origin; + private final String rawClientData; + private final JsonNode data; + + public ClientData(String clientData) throws BadInputException { + this.rawClientData = new String(Base64Util.base64urldecode(clientData)); + try { + this.data = new ObjectMapper().readTree(rawClientData); + this.typ = getString(TYPE_PARAM); + this.challenge = getString(CHALLENGE_PARAM); + this.origin = getString(ORIGIN_PARAM); + } catch (IOException ex) { + throw new BadInputException("Malformed ClientData", ex); + } + } + + public String getTyp() { + return typ; + } + + public String getChallenge() { + return challenge; + } + + public String getOrigin() { + return origin; + } + + public String getString(String key) { + return data.get(key).asText(); + } + + public String getRawClientData() { + return rawClientData; + } + + @Override + public String toString() { + return rawClientData; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/DeviceData.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/DeviceData.java new file mode 100644 index 00000000..80118df2 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/DeviceData.java @@ -0,0 +1,101 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2015, Gluu + */ + +package org.gluu.oxauth.model.fido.u2f.protocol; + +import java.io.Serializable; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * FIDO U2F device data + * + * @author Yuriy Movchan Date: 02/16/2016 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class DeviceData implements Serializable { + + private static final long serialVersionUID = -8173244116167488365L; + + @JsonProperty(value = "uuid") + private final String uuid; + + @JsonProperty(value = "push_token") + private final String pushToken; + + @JsonProperty(value = "type") + private final String type; + + @JsonProperty(value = "platform") + private final String platform; + + @JsonProperty(value = "name") + private final String name; + + @JsonProperty(value = "os_name") + private final String osName; + + @JsonProperty(value = "os_version") + private final String osVersion; + + @JsonProperty(value = "custom_data") + private final Map customData; + + public DeviceData(@JsonProperty(value = "uuid") String uuid, @JsonProperty(value = "token") String pushToken, + @JsonProperty(value = "type") String type, @JsonProperty(value = "platform") String platform, + @JsonProperty(value = "name") String name, @JsonProperty(value = "os_name") String osName, + @JsonProperty(value = "os_version") String osVersion, @JsonProperty(value = "custom_data") Map customData) { + this.uuid = uuid; + this.pushToken = pushToken; + this.type = type; + this.platform = platform; + this.name = name; + this.osName = osName; + this.osVersion = osVersion; + this.customData = customData; + } + + public String getUuid() { + return uuid; + } + + public String getPushToken() { + return pushToken; + } + + public String getType() { + return type; + } + + public String getPlatform() { + return platform; + } + + public String getName() { + return name; + } + + public String getOsName() { + return osName; + } + + public String getOsVersion() { + return osVersion; + } + + public final Map getCustomData() { + return customData; + } + + @Override + public String toString() { + return "DeviceData [uuid=" + uuid + ", pushToken=" + pushToken + ", type=" + type + ", platform=" + platform + ", name=" + name + ", osName=" + + osName + ", osVersion=" + osVersion + ", customData=" + customData + "]"; + } + +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/DeviceNotificationConf.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/DeviceNotificationConf.java new file mode 100644 index 00000000..d9eaf34a --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/DeviceNotificationConf.java @@ -0,0 +1,73 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2015, Gluu + */ + +package org.gluu.oxauth.model.fido.u2f.protocol; + +import java.io.Serializable; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * FIDO2 U2F device notification configuration + * + * @author Yuriy Movchan Date: 03/21/2024 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class DeviceNotificationConf implements Serializable { + + private static final long serialVersionUID = -8173244116167488365L; + + @JsonProperty(value = "sns_endpoint_arn") + private String snsEndpointArn; + + @JsonProperty(value = "sns_endpoint_arn_remove") + private String snsEndpointArnRemove; + + @JsonProperty(value = "sns_endpoint_arn_history") + private List snsEndpointArnHistory; + + public DeviceNotificationConf(@JsonProperty(value = "sns_endpoint_arn") String snsEndpointArn, @JsonProperty(value = "sns_endpoint_arn_remove") String snsEndpointArnRemove, + @JsonProperty(value = "sns_endpoint_arn_history") List snsEndpointArnHistory) { + this.snsEndpointArn = snsEndpointArn; + this.snsEndpointArnRemove = snsEndpointArnRemove; + this.snsEndpointArnHistory = snsEndpointArnHistory; + } + + public String getSnsEndpointArn() { + return snsEndpointArn; + } + + public void setSnsEndpointArn(String snsEndpointArn) { + this.snsEndpointArn = snsEndpointArn; + } + + public String getSnsEndpointArnRemove() { + return snsEndpointArnRemove; + } + + public void setSnsEndpointArnRemove(String snsEndpointArnRemove) { + this.snsEndpointArnRemove = snsEndpointArnRemove; + } + + public List getSnsEndpointArnHistory() { + return snsEndpointArnHistory; + } + + public void setSnsEndpointArnHistory(List snsEndpointArnHistory) { + this.snsEndpointArnHistory = snsEndpointArnHistory; + } + + @Override + public String toString() { + return "Fido2DeviceNotificationConf [snsEndpointArn=" + snsEndpointArn + ", snsEndpointArnRemove=" + + snsEndpointArnRemove + ", snsEndpointArnHistory=" + snsEndpointArnHistory + "]"; + } + +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/RegisterRequest.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/RegisterRequest.java new file mode 100644 index 00000000..6b8312ff --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/RegisterRequest.java @@ -0,0 +1,71 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2015, Gluu + */ + +package org.gluu.oxauth.model.fido.u2f.protocol; + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.gluu.oxauth.model.fido.u2f.U2fConstants; + +/** + * FIDO U2F device registration request + * + * @author Yuriy Movchan Date: 05/13/2015 + */ +public class RegisterRequest implements Serializable { + + private static final long serialVersionUID = -7804531602792040593L; + + /** + * Version of the protocol that the to-be-registered U2F token must speak. + * For the version of the protocol described herein, must be "U2F_V2" + */ + @JsonProperty + private final String version = U2fConstants.U2F_PROTOCOL_VERSION; + + /** + * The websafe-base64-encoded challenge. + */ + @JsonProperty + private final String challenge; + + /** + * The application id that the RP would like to assert. The U2F token will + * enforce that the key handle provided above is associated with this + * application id. The browser enforces that the calling origin belongs to + * the application identified by the application id. + */ + @JsonProperty + private final String appId; + + public RegisterRequest(@JsonProperty("challenge") String challenge, @JsonProperty("appId") String appId) { + this.challenge = challenge; + this.appId = appId; + } + + public String getChallenge() { + return challenge; + } + + public String getAppId() { + return appId; + } + + @JsonIgnore + public String getRequestId() { + return getChallenge(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("RegisterRequest [version=").append(version).append(", challenge=").append(challenge).append(", appId=").append(appId).append("]"); + return builder.toString(); + } + +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/RegisterRequestMessage.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/RegisterRequestMessage.java new file mode 100644 index 00000000..67824669 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/RegisterRequestMessage.java @@ -0,0 +1,64 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2015, Gluu + */ + +package org.gluu.oxauth.model.fido.u2f.protocol; + +import java.io.Serializable; +import java.util.Collections; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.gluu.oxauth.model.util.Util; + +/** + * FIDO U2F registration request message + * + * @author Yuriy Movchan Date: 05/15/2015 + */ +public class RegisterRequestMessage implements Serializable { + + private static final long serialVersionUID = -5554834606247337007L; + + @JsonProperty + private final List authenticateRequests; + + @JsonProperty + private final List registerRequests; + + public RegisterRequestMessage(@JsonProperty("authenticateRequests") List authenticateRequests, + @JsonProperty("registerRequests") List registerRequests) { + this.authenticateRequests = authenticateRequests; + this.registerRequests = registerRequests; + } + + public List getAuthenticateRequests() { + return Collections.unmodifiableList(authenticateRequests); + } + + public List getRegisterRequests() { + return Collections.unmodifiableList(registerRequests); + } + + @JsonIgnore + public RegisterRequest getRegisterRequest() { + return Util.firstItem(registerRequests); + } + + @JsonIgnore + public String getRequestId() { + return Util.firstItem(registerRequests).getChallenge(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("RegisterRequestMessage [authenticateRequests=").append(authenticateRequests).append(", registerRequests=").append(registerRequests) + .append("]"); + return builder.toString(); + } + +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/RegisterResponse.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/RegisterResponse.java new file mode 100644 index 00000000..e64b154f --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/RegisterResponse.java @@ -0,0 +1,83 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2015, Gluu + */ + +package org.gluu.oxauth.model.fido.u2f.protocol; + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.gluu.oxauth.model.fido.u2f.exception.BadInputException; +import org.jboss.resteasy.annotations.providers.jaxb.IgnoreMediaTypes; + +/** + * FIDO U2F device registration response + * + * @author Yuriy Movchan Date: 05/13/2015 + */ +@IgnoreMediaTypes("application/*+json") +// try to ignore jettison as it's recommended here: +// http://docs.jboss.org/resteasy/docs/2.3.4.Final/userguide/html/json.html +@JsonIgnoreProperties(ignoreUnknown = true) +public class RegisterResponse implements Serializable { + + private static final long serialVersionUID = -4192863815075074953L; + + /** + * base64 (raw registration response message) + */ + @JsonProperty + private final String registrationData; + + /** + * base64(UTF8(client data)) + */ + @JsonProperty + private final String clientData; + + /** + * base64(UTF8(device data)) + */ + @JsonProperty + private final String deviceData; + + @JsonIgnore + private transient ClientData clientDataRef; + + public RegisterResponse(@JsonProperty("registrationData") String registrationData, @JsonProperty("clientData") String clientData, @JsonProperty("deviceData") String deviceData) throws BadInputException { + this.registrationData = registrationData; + this.clientData = clientData; + this.clientDataRef = new ClientData(clientData); + this.deviceData = deviceData; + } + + public String getRegistrationData() { + return registrationData; + } + + public ClientData getClientData() { + return clientDataRef; + } + + public String getDeviceData() { + return deviceData; + } + + @JsonIgnore + public String getRequestId() { + return getClientData().getChallenge(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("RegisterResponse [registrationData=").append(registrationData).append(", clientData=").append(clientData).append(", deviceData=") + .append(deviceData).append("]"); + return builder.toString(); + } + +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/RegisterStatus.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/RegisterStatus.java new file mode 100644 index 00000000..d06ba9e3 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/fido/u2f/protocol/RegisterStatus.java @@ -0,0 +1,57 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2015, Gluu + */ + +package org.gluu.oxauth.model.fido.u2f.protocol; + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.gluu.oxauth.model.fido.u2f.exception.BadInputException; + +/** + * FIDO U2F device registration status response + * + * @author Yuriy Movchan Date: 05/20/2015 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class RegisterStatus implements Serializable { + + private static final long serialVersionUID = -1113719742487477953L; + + @JsonProperty + private final String status; + + @JsonProperty + private final String challenge; + + public RegisterStatus(@JsonProperty("status") String status, @JsonProperty("challenge") String challenge) throws BadInputException { + this.status = status; + this.challenge = challenge; + } + + public String getStatus() { + return status; + } + + public String getChallenge() { + return challenge; + } + + @JsonIgnore + public String getRequestId() { + return challenge; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("RegisterStatus [status=").append(status).append(", challenge=").append(challenge).append("]"); + return builder.toString(); + } + +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/gluu/GluuConfiguration.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/gluu/GluuConfiguration.java new file mode 100644 index 00000000..6241f13f --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/gluu/GluuConfiguration.java @@ -0,0 +1,82 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.gluu; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import org.jboss.resteasy.annotations.providers.jaxb.IgnoreMediaTypes; + +import java.util.Map; +import java.util.Set; + +/** + * Created by eugeniuparvan on 8/5/16. + */ +@IgnoreMediaTypes("application/*+json") +@JsonPropertyOrder({ + "id_generation_endpoint", + "introspection_endpoint", + "auth_level_mapping", + "scope_to_claims_mapping" +}) +public class GluuConfiguration { + + @JsonProperty(value = "id_generation_endpoint") + private String idGenerationEndpoint; + + @JsonProperty(value = "introspection_endpoint") + private String introspectionEndpoint; + + @JsonProperty(value = "auth_level_mapping") + private Map> authLevelMapping; + + @JsonProperty(value = "scope_to_claims_mapping") + private Map> scopeToClaimsMapping; + + + public String getIdGenerationEndpoint() { + return idGenerationEndpoint; + } + + public void setIdGenerationEndpoint(String idGenerationEndpoint) { + this.idGenerationEndpoint = idGenerationEndpoint; + } + + public String getIntrospectionEndpoint() { + return introspectionEndpoint; + } + + public void setIntrospectionEndpoint(String introspectionEndpoint) { + this.introspectionEndpoint = introspectionEndpoint; + } + + public Map> getAuthLevelMapping() { + return authLevelMapping; + } + + public void setAuthLevelMapping(Map> authLevelMapping) { + this.authLevelMapping = authLevelMapping; + } + + public Map> getScopeToClaimsMapping() { + return scopeToClaimsMapping; + } + + public void setScopeToClaimsMapping(Map> scopeToClaimsMapping) { + this.scopeToClaimsMapping = scopeToClaimsMapping; + } + + @Override + public String toString() { + return "GluuConfiguration{" + + "idGenerationEndpoint='" + idGenerationEndpoint + '\'' + + ", introspectionEndpoint='" + introspectionEndpoint + '\'' + + ", authLevelMapping=" + authLevelMapping + + ", scopeToClaimsMapping=" + scopeToClaimsMapping + + '}'; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/gluu/GluuErrorResponseType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/gluu/GluuErrorResponseType.java new file mode 100644 index 00000000..e726df02 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/gluu/GluuErrorResponseType.java @@ -0,0 +1,66 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.gluu; + +import java.util.HashMap; +import java.util.Map; + +import org.gluu.oxauth.model.error.IErrorType; + +public enum GluuErrorResponseType implements IErrorType { + + /** + * The server encountered an unexpected condition which + * prevented it from fulfilling the request. + */ + SERVER_ERROR("server_error"); + + + private static Map lookup = new HashMap(); + + static { + for (GluuErrorResponseType enumType : values()) { + lookup.put(enumType.getParameter(), enumType); + } + } + + private final String paramName; + + private GluuErrorResponseType(String paramName) { + this.paramName = paramName; + } + + /** + * Return the corresponding enumeration from a string parameter. + * + * @param param The parameter to be match. + * @return The enumeration if found, otherwise + * null. + */ + public static GluuErrorResponseType fromString(String param) { + return lookup.get(param); + } + + /** + * Returns a string representation of the object. In this case, the lower + * case code of the error. + */ + @Override + public String toString() { + return paramName; + } + + /** + * Gets error parameter. + * + * @return error parameter + */ + @Override + public String getParameter() { + return paramName; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/json/JsonApplier.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/json/JsonApplier.java new file mode 100644 index 00000000..f4938770 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/json/JsonApplier.java @@ -0,0 +1,195 @@ +package org.gluu.oxauth.model.json; + +import org.apache.log4j.Logger; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.lang.reflect.Field; +import java.util.*; + +/** + * @author Yuriy Zabrovarnyy + */ +public class JsonApplier { + + private static final Logger log = Logger.getLogger(JsonApplier.class); + + private static final JsonApplier APPLIER = new JsonApplier(); + + private JsonApplier() { + } + + public static JsonApplier getInstance() { + return APPLIER; + } + + public void apply(Object source, JSONObject target) { + for (PropertyDefinition definition : PropertyDefinition.values()) { + apply(source, target, definition); + } + } + + public void apply(Object source, Map parameters) { + for (PropertyDefinition definition : PropertyDefinition.values()) { + apply(source, parameters, definition); + } + } + + private void apply(Object source, Map target, PropertyDefinition property) { + try { + if (!isAllowed(source, target, property, source.getClass())) { + return; + } + + Field field = source.getClass().getDeclaredField(property.getJavaTargetPropertyName()); + field.setAccessible(true); + Object value = field.get(source); + if (value == null) { + return; + } + + if (String.class.isAssignableFrom(property.getJavaType())) { + + target.put(property.getJsonName(), (String) value); + return; + } + if (Collection.class.isAssignableFrom(property.getJavaType())) { + Collection valueAsCollection = (Collection) field.get(source); + target.put(property.getJsonName(), new JSONArray(valueAsCollection).toString()); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + + private void apply(Object source, JSONObject target, PropertyDefinition property) { + try { + if (!isAllowed(source, target, property, source.getClass())) { + return; + } + + Field field = source.getClass().getDeclaredField(property.getJavaTargetPropertyName()); + field.setAccessible(true); + Object value = field.get(source); + + if (String.class.isAssignableFrom(property.getJavaType())) { + target.put(property.getJsonName(), value); + return; + } + if (Collection.class.isAssignableFrom(property.getJavaType())) { + Collection valueAsCollection = (Collection) field.get(source); + target.put(property.getJsonName(), valueAsCollection); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + + private boolean isAllowed(Object source, Object target, PropertyDefinition property, Class... clazzesToCheck) { + if (source == null || target == null || property == null || clazzesToCheck == null || clazzesToCheck.length == 0) { + return false; + } + + try { + final Set allowedClasses = property.getJavaTargetsClassNamesAsStrings(); + + for (Class clazzToCheck : clazzesToCheck) { + if (!allowedClasses.contains(clazzToCheck.getName())) { + return false; + } + + Field field = clazzToCheck.getDeclaredField(property.getJavaTargetPropertyName()); + + final Class javaType = property.getJavaType(); + if (!field.getType().isAssignableFrom(javaType)) { + return false; + } + } + return true; + } catch (Exception e) { + return false; + } + } + + public void apply(JSONObject source, Object target) { + for (PropertyDefinition definition : PropertyDefinition.values()) { + apply(source, target, definition); + } + } + + public void apply(JSONObject source, Object target, PropertyDefinition property) { + try { + if (!source.has(property.getJsonName())) { + return; + } + if (!isAllowed(source, target, property, target.getClass())) { + return; + } + + Field field = target.getClass().getDeclaredField(property.getJavaTargetPropertyName()); + field.setAccessible(true); + + + Object valueToSet = null; + + if (String.class.isAssignableFrom(property.getJavaType())) { + valueToSet = source.optString(property.getJsonName()); + } + if (Collection.class.isAssignableFrom(property.getJavaType())) { + final JSONArray jsonArray = source.getJSONArray(property.getJsonName()); + valueToSet = jsonArray.toList(); + } + + field.set(target, valueToSet); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + + /** + * Transfer between two java objects + */ + public void transfer(Object source, Object target) { + for (PropertyDefinition definition : PropertyDefinition.values()) { + transfer(source, target, definition); + } + } + + private void transfer(Object source, Object target, PropertyDefinition property) { + try { + if (!isAllowed(source, target, property, source.getClass(), target.getClass())) { + return; + } + + Field sourceField = source.getClass().getDeclaredField(property.getJavaTargetPropertyName()); + sourceField.setAccessible(true); + + Field targetField = target.getClass().getDeclaredField(property.getJavaTargetPropertyName()); + targetField.setAccessible(true); + + + Object valueToSet = null; + + if (String.class.isAssignableFrom(property.getJavaType()) || Collection.class.isAssignableFrom(property.getJavaType())) { + valueToSet = sourceField.get(source); + } + + if (valueToSet != null) { + targetField.set(target, valueToSet); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + + public static List getStringList(JSONArray jsonArray) { + List values = new ArrayList<>(); + for (int i = 0; i < jsonArray.length(); i++) { + String value = jsonArray.optString(i); + if (value != null) { + values.add(value); + } + } + return values; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/json/PropertyDefinition.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/json/PropertyDefinition.java new file mode 100644 index 00000000..a461b74c --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/json/PropertyDefinition.java @@ -0,0 +1,76 @@ +package org.gluu.oxauth.model.json; + +import com.google.common.collect.Sets; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author Yuriy Zabrovarnyy + */ +public enum PropertyDefinition { + + ADDITIONAL_AUDIENCE(Sets.newHashSet(ClassNames.CLIENT_ATTRIBUTES, ClassNames.REGISTER_REQUEST), "additionalAudience", List.class, "additional_audience"); + + private final Set javaTargetsClassNames; + private final Set javaTargetsClassNamesAsStrings; + private final String javaTargetPropertyName; + private final Class javaType; + private final String jsonName; + + + PropertyDefinition(Set javaTargetsClassNames, String javaTargetPropertyName, Class javaType, String jsonName) { + this.javaTargetsClassNames = javaTargetsClassNames; + this.javaTargetsClassNamesAsStrings = javaTargetsClassNames.stream().map(PropertyDefinition.ClassNames::getFullClassName).collect(Collectors.toSet()); + this.javaTargetPropertyName = javaTargetPropertyName; + this.javaType = javaType; + this.jsonName = jsonName; + } + + public Class getJavaType() { + return javaType; + } + + public Set getJavaTargetsClassNames() { + return javaTargetsClassNames; + } + + public Set getJavaTargetsClassNamesAsStrings() { + return javaTargetsClassNamesAsStrings; + } + + public String getJavaTargetPropertyName() { + return javaTargetPropertyName; + } + + public String getJsonName() { + return jsonName; + } + + @Override + public String toString() { + return "PropertyDefinition{" + + "javaTargetsClassNames='" + javaTargetsClassNames + '\'' + + ", javaTargetPropertyName='" + javaTargetPropertyName + '\'' + + ", javaType='" + javaType + '\'' + + ", jsonName='" + jsonName + '\'' + + "} " + super.toString(); + } + + public enum ClassNames { + + CLIENT_ATTRIBUTES("org.oxauth.persistence.model.ClientAttributes"), + REGISTER_REQUEST("org.gluu.oxauth.client.RegisterRequest"); + + private final String fullClassName; + + ClassNames(String fullClassName) { + this.fullClassName = fullClassName; + } + + public String getFullClassName() { + return fullClassName; + } + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/AbstractJweDecrypter.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/AbstractJweDecrypter.java new file mode 100644 index 00000000..8abf8065 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/AbstractJweDecrypter.java @@ -0,0 +1,36 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.jwe; + +import org.gluu.oxauth.model.crypto.encryption.BlockEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.encryption.KeyEncryptionAlgorithm; + +/** + * @author Javier Rojas Blum + * @version November 20, 2018 + */ +public abstract class AbstractJweDecrypter implements JweDecrypter { + + private KeyEncryptionAlgorithm keyEncryptionAlgorithm; + private BlockEncryptionAlgorithm blockEncryptionAlgorithm; + + public KeyEncryptionAlgorithm getKeyEncryptionAlgorithm() { + return keyEncryptionAlgorithm; + } + + public void setKeyEncryptionAlgorithm(KeyEncryptionAlgorithm keyEncryptionAlgorithm) { + this.keyEncryptionAlgorithm = keyEncryptionAlgorithm; + } + + public BlockEncryptionAlgorithm getBlockEncryptionAlgorithm() { + return blockEncryptionAlgorithm; + } + + public void setBlockEncryptionAlgorithm(BlockEncryptionAlgorithm blockEncryptionAlgorithm) { + this.blockEncryptionAlgorithm = blockEncryptionAlgorithm; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/AbstractJweEncrypter.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/AbstractJweEncrypter.java new file mode 100644 index 00000000..232b6506 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/AbstractJweEncrypter.java @@ -0,0 +1,33 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.jwe; + +import org.gluu.oxauth.model.crypto.encryption.BlockEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.encryption.KeyEncryptionAlgorithm; + +/** + * @author Javier Rojas Blum + * @version November 20, 2018 + */ +public abstract class AbstractJweEncrypter implements JweEncrypter { + + private KeyEncryptionAlgorithm keyEncryptionAlgorithm; + private BlockEncryptionAlgorithm blockEncryptionAlgorithm; + + protected AbstractJweEncrypter(KeyEncryptionAlgorithm keyEncryptionAlgorithm, BlockEncryptionAlgorithm blockEncryptionAlgorithm) { + this.keyEncryptionAlgorithm = keyEncryptionAlgorithm; + this.blockEncryptionAlgorithm = blockEncryptionAlgorithm; + } + + public KeyEncryptionAlgorithm getKeyEncryptionAlgorithm() { + return keyEncryptionAlgorithm; + } + + public BlockEncryptionAlgorithm getBlockEncryptionAlgorithm() { + return blockEncryptionAlgorithm; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/Jwe.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/Jwe.java new file mode 100644 index 00000000..11599a9d --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/Jwe.java @@ -0,0 +1,116 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.jwe; + +import java.security.PrivateKey; + +import org.gluu.oxauth.model.exception.InvalidJweException; +import org.gluu.oxauth.model.exception.InvalidJwtException; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.token.JsonWebResponse; + +/** + * @author Javier Rojas Blum + * @version July 29, 2016 + */ +public class Jwe extends JsonWebResponse { + + private String encodedHeader; + private String encodedEncryptedKey; + private String encodedInitializationVector; + private String encodedCiphertext; + private String encodedIntegrityValue; + + private Jwt signedJWTPayload; + + public Jwe() { + encodedHeader = null; + encodedEncryptedKey = null; + encodedInitializationVector = null; + encodedCiphertext = null; + encodedIntegrityValue = null; + } + + public String getEncodedHeader() { + return encodedHeader; + } + + public void setEncodedHeader(String encodedHeader) { + this.encodedHeader = encodedHeader; + } + + public String getEncodedEncryptedKey() { + return encodedEncryptedKey; + } + + public void setEncodedEncryptedKey(String encodedEncryptedKey) { + this.encodedEncryptedKey = encodedEncryptedKey; + } + + public String getEncodedInitializationVector() { + return encodedInitializationVector; + } + + public void setEncodedInitializationVector(String encodedInitializationVector) { + this.encodedInitializationVector = encodedInitializationVector; + } + + public String getEncodedCiphertext() { + return encodedCiphertext; + } + + public void setEncodedCiphertext(String encodedCiphertext) { + this.encodedCiphertext = encodedCiphertext; + } + + public String getEncodedIntegrityValue() { + return encodedIntegrityValue; + } + + public void setEncodedIntegrityValue(String encodedIntegrityValue) { + this.encodedIntegrityValue = encodedIntegrityValue; + } + + public String getAdditionalAuthenticatedData() { + String additionalAuthenticatedData = encodedHeader + "." + + encodedEncryptedKey + "." + + encodedInitializationVector; + + return additionalAuthenticatedData; + } + + public static Jwe parse(String encodedJwe, PrivateKey privateKey, byte[] sharedSymmetricKey) throws InvalidJweException, InvalidJwtException { + Jwe jwe = null; + + if (privateKey != null) { + JweDecrypter jweDecrypter = new JweDecrypterImpl(privateKey); + jwe = jweDecrypter.decrypt(encodedJwe); + } else if (sharedSymmetricKey != null) { + JweDecrypter jweDecrypter = new JweDecrypterImpl(sharedSymmetricKey); + jwe = jweDecrypter.decrypt(encodedJwe); + } + + return jwe; + } + + public Jwt getSignedJWTPayload() { + return signedJWTPayload; + } + + public void setSignedJWTPayload(Jwt signedJWTPayload) { + this.signedJWTPayload = signedJWTPayload; + } + + @Override + public String toString() { + return encodedHeader + "." + + encodedEncryptedKey + "." + + encodedInitializationVector + "." + + encodedCiphertext + "." + + encodedIntegrityValue; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/JweDecrypter.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/JweDecrypter.java new file mode 100644 index 00000000..eaf0cf0a --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/JweDecrypter.java @@ -0,0 +1,27 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.jwe; + +import org.gluu.oxauth.model.crypto.encryption.BlockEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.encryption.KeyEncryptionAlgorithm; +import org.gluu.oxauth.model.exception.InvalidJweException; + +/** + * @author Javier Rojas Blum Date: 12.04.2012 + */ +public interface JweDecrypter { + + public KeyEncryptionAlgorithm getKeyEncryptionAlgorithm(); + + public void setKeyEncryptionAlgorithm(KeyEncryptionAlgorithm keyEncryptionAlgorithm); + + public BlockEncryptionAlgorithm getBlockEncryptionAlgorithm(); + + public void setBlockEncryptionAlgorithm(BlockEncryptionAlgorithm blockEncryptionAlgorithm); + + public Jwe decrypt(String encryptedJwe) throws InvalidJweException; +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/JweDecrypterImpl.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/JweDecrypterImpl.java new file mode 100644 index 00000000..530bc051 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/JweDecrypterImpl.java @@ -0,0 +1,126 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.jwe; + +import com.nimbusds.jose.JWEDecrypter; +import com.nimbusds.jose.crypto.factories.DefaultJWEDecrypterFactory; +import com.nimbusds.jwt.EncryptedJWT; +import com.nimbusds.jwt.SignedJWT; +import org.gluu.oxauth.model.crypto.encryption.BlockEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.encryption.KeyEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.signature.RSAPrivateKey; +import org.gluu.oxauth.model.exception.InvalidJweException; +import org.gluu.oxauth.model.exception.InvalidJwtException; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaims; +import org.gluu.oxauth.model.jwt.JwtHeader; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.util.security.SecurityProviderUtility; + +import javax.crypto.spec.SecretKeySpec; +import java.security.Key; +import java.security.MessageDigest; +import java.security.PrivateKey; +import java.util.Arrays; + +/** + * @author Javier Rojas Blum + * @version November 20, 2018 + */ +public class JweDecrypterImpl extends AbstractJweDecrypter { + + private static final DefaultJWEDecrypterFactory DECRYPTER_FACTORY = new DefaultJWEDecrypterFactory(); + + private PrivateKey privateKey; + private RSAPrivateKey rsaPrivateKey; + private byte[] sharedSymmetricKey; + + public JweDecrypterImpl(byte[] sharedSymmetricKey) { + if (sharedSymmetricKey != null) { + this.sharedSymmetricKey = sharedSymmetricKey.clone(); + } + } + + public JweDecrypterImpl(RSAPrivateKey rsaPrivateKey) { + this.rsaPrivateKey = rsaPrivateKey; + } + + public JweDecrypterImpl(PrivateKey privateKey) { + this.privateKey = privateKey; + } + + @Override + public Jwe decrypt(String encryptedJwe) throws InvalidJweException { + try { + String[] jweParts = encryptedJwe.split("\\."); + if (jweParts.length != 5) { + throw new InvalidJwtException("Invalid JWS format."); + } + + String encodedHeader = jweParts[0]; + String encodedEncryptedKey = jweParts[1]; + String encodedInitializationVector = jweParts[2]; + String encodedCipherText = jweParts[3]; + String encodedIntegrityValue = jweParts[4]; + + Jwe jwe = new Jwe(); + jwe.setEncodedHeader(encodedHeader); + jwe.setEncodedEncryptedKey(encodedEncryptedKey); + jwe.setEncodedInitializationVector(encodedInitializationVector); + jwe.setEncodedCiphertext(encodedCipherText); + jwe.setEncodedIntegrityValue(encodedIntegrityValue); + jwe.setHeader(new JwtHeader(encodedHeader)); + + EncryptedJWT encryptedJwt = EncryptedJWT.parse(encryptedJwe); + + setKeyEncryptionAlgorithm(KeyEncryptionAlgorithm.fromName(jwe.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM))); + setBlockEncryptionAlgorithm(BlockEncryptionAlgorithm.fromName(jwe.getHeader().getClaimAsString(JwtHeaderName.ENCRYPTION_METHOD))); + + final KeyEncryptionAlgorithm keyEncryptionAlgorithm = getKeyEncryptionAlgorithm(); + Key encriptionKey = null; + if (keyEncryptionAlgorithm == KeyEncryptionAlgorithm.RSA1_5 || keyEncryptionAlgorithm == KeyEncryptionAlgorithm.RSA_OAEP) { + encriptionKey = privateKey; + } else if (keyEncryptionAlgorithm == KeyEncryptionAlgorithm.A128KW || keyEncryptionAlgorithm == KeyEncryptionAlgorithm.A256KW) { + if (sharedSymmetricKey == null) { + throw new InvalidJweException("The shared symmetric key is null"); + } + + int keyLength = 16; + if (keyEncryptionAlgorithm == KeyEncryptionAlgorithm.A256KW) { + keyLength = 32; + } + + if (sharedSymmetricKey.length != keyLength) { + MessageDigest sha = MessageDigest.getInstance("SHA-256"); + sharedSymmetricKey = sha.digest(sharedSymmetricKey); + sharedSymmetricKey = Arrays.copyOf(sharedSymmetricKey, keyLength); + } + encriptionKey = new SecretKeySpec(sharedSymmetricKey, 0, sharedSymmetricKey.length, "AES"); + } else { + throw new InvalidJweException("The key encryption algorithm is not supported"); + } + + JWEDecrypter decrypter = DECRYPTER_FACTORY.createJWEDecrypter(encryptedJwt.getHeader(), encriptionKey); + decrypter.getJCAContext().setProvider(SecurityProviderUtility.getBCProvider()); + encryptedJwt.decrypt(decrypter); + + final SignedJWT signedJWT = encryptedJwt.getPayload().toSignedJWT(); + if (signedJWT != null) { + final Jwt jwt = Jwt.parse(signedJWT.serialize()); + jwe.setSignedJWTPayload(jwt); + jwe.setClaims(jwt != null ? jwt.getClaims() : null); + } else { + final String base64encodedPayload = encryptedJwt.getPayload().toString(); + jwe.setClaims(new JwtClaims(base64encodedPayload)); + } + + return jwe; + } catch (Exception e) { + throw new InvalidJweException(e); + } + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/JweEncrypter.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/JweEncrypter.java new file mode 100644 index 00000000..13b7d67a --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/JweEncrypter.java @@ -0,0 +1,17 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.jwe; + +import org.gluu.oxauth.model.exception.InvalidJweException; + +/** + * @author Javier Rojas Blum Date: 12.03.2012 + */ +public interface JweEncrypter { + + public Jwe encrypt(Jwe jwe) throws InvalidJweException; +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/JweEncrypterImpl.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/JweEncrypterImpl.java new file mode 100644 index 00000000..da800e69 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/JweEncrypterImpl.java @@ -0,0 +1,121 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.jwe; + +import com.nimbusds.jose.*; +import com.nimbusds.jose.crypto.AESEncrypter; +import com.nimbusds.jose.crypto.RSAEncrypter; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jwt.SignedJWT; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.interfaces.RSAPublicKey; +import java.text.ParseException; +import java.util.Arrays; + +import org.gluu.oxauth.model.crypto.encryption.BlockEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.encryption.KeyEncryptionAlgorithm; +import org.gluu.oxauth.model.exception.InvalidJweException; +import org.gluu.oxauth.model.exception.InvalidJwtException; +import org.gluu.oxauth.model.jwt.JwtHeader; +import org.gluu.oxauth.model.jwt.JwtType; +import org.gluu.oxauth.model.util.Base64Util; + +/** + * @author Javier Rojas Blum + * @version November 20, 2018 + */ +public class JweEncrypterImpl extends AbstractJweEncrypter { + + private PublicKey publicKey; + private byte[] sharedSymmetricKey; + + public JweEncrypterImpl(KeyEncryptionAlgorithm keyEncryptionAlgorithm, BlockEncryptionAlgorithm blockEncryptionAlgorithm, byte[] sharedSymmetricKey) { + super(keyEncryptionAlgorithm, blockEncryptionAlgorithm); + if (sharedSymmetricKey != null) { + this.sharedSymmetricKey = sharedSymmetricKey.clone(); + } + } + + public JweEncrypterImpl(KeyEncryptionAlgorithm keyEncryptionAlgorithm, BlockEncryptionAlgorithm blockEncryptionAlgorithm, PublicKey publicKey) { + super(keyEncryptionAlgorithm, blockEncryptionAlgorithm); + this.publicKey = publicKey; + } + + public JWEEncrypter createJweEncrypter() throws JOSEException, InvalidJweException, NoSuchAlgorithmException { + final KeyEncryptionAlgorithm keyEncryptionAlgorithm = getKeyEncryptionAlgorithm(); + if (keyEncryptionAlgorithm == KeyEncryptionAlgorithm.RSA1_5 || keyEncryptionAlgorithm == KeyEncryptionAlgorithm.RSA_OAEP) { + return new RSAEncrypter(new RSAKey.Builder((RSAPublicKey) publicKey).build()); + } else if (keyEncryptionAlgorithm == KeyEncryptionAlgorithm.A128KW || keyEncryptionAlgorithm == KeyEncryptionAlgorithm.A256KW) { + if (sharedSymmetricKey == null) { + throw new InvalidJweException("The shared symmetric key is null"); + } + + int keyLength = 16; + if (keyEncryptionAlgorithm == KeyEncryptionAlgorithm.A256KW) { + keyLength = 32; + } + + if (sharedSymmetricKey.length != keyLength) { + MessageDigest sha = MessageDigest.getInstance("SHA-256"); + sharedSymmetricKey = sha.digest(sharedSymmetricKey); + sharedSymmetricKey = Arrays.copyOf(sharedSymmetricKey, keyLength); + } + + return new AESEncrypter(sharedSymmetricKey); + } else { + throw new InvalidJweException("The key encryption algorithm is not supported"); + } + } + + public static Payload createPayload(Jwe jwe) throws ParseException, InvalidJwtException, UnsupportedEncodingException { + if (jwe.getSignedJWTPayload() != null) { + return new Payload(SignedJWT.parse(jwe.getSignedJWTPayload().toString())); + } + return new Payload(Base64Util.base64urlencode(jwe.getClaims().toJsonString().getBytes("UTF-8"))); + } + + @Override + public Jwe encrypt(Jwe jwe) throws InvalidJweException { + try { + JWEEncrypter encrypter = createJweEncrypter(); + + if (jwe.getSignedJWTPayload() != null) { + jwe.getHeader().setContentType(JwtType.JWT); + } + JWEObject jweObject = new JWEObject(JWEHeader.parse(jwe.getHeader().toJsonObject().toString()), createPayload(jwe)); + + jweObject.encrypt(encrypter); + String encryptedJwe = jweObject.serialize(); + + String[] jweParts = encryptedJwe.split("\\."); + if (jweParts.length != 5) { + throw new InvalidJwtException("Invalid JWS format."); + } + + String encodedHeader = jweParts[0]; + String encodedEncryptedKey = jweParts[1]; + String encodedInitializationVector = jweParts[2]; + String encodedCipherText = jweParts[3]; + String encodedIntegrityValue = jweParts[4]; + + jwe.setEncodedHeader(encodedHeader); + jwe.setEncodedEncryptedKey(encodedEncryptedKey); + jwe.setEncodedInitializationVector(encodedInitializationVector); + jwe.setEncodedCiphertext(encodedCipherText); + jwe.setEncodedIntegrityValue(encodedIntegrityValue); + jwe.setHeader(new JwtHeader(encodedHeader)); + + return jwe; + } catch (Exception e) { + throw new InvalidJweException(e); + } + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/KeyDerivationFunction.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/KeyDerivationFunction.java new file mode 100644 index 00000000..b49f24bd --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwe/KeyDerivationFunction.java @@ -0,0 +1,102 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.jwe; + +import org.apache.commons.lang.ArrayUtils; +import org.gluu.oxauth.model.crypto.encryption.BlockEncryptionAlgorithm; +import org.gluu.oxauth.model.exception.InvalidParameterException; +import org.gluu.oxauth.model.util.Base64Util; +import org.gluu.oxauth.model.util.Util; +import org.gluu.util.security.SecurityProviderUtility; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.util.Arrays; + +/** + * @author Javier Rojas Blum + * @version July 31, 2016 + */ +public class KeyDerivationFunction { + + public static byte[] generateCek(byte[] cmk, BlockEncryptionAlgorithm blockEncryptionAlgorithm) + throws UnsupportedEncodingException, NoSuchProviderException, NoSuchAlgorithmException, InvalidParameterException { + if (cmk == null) { + throw new InvalidParameterException("The content master key (CMK) is null"); + } + if (blockEncryptionAlgorithm == null) { + throw new InvalidParameterException("The block encryption algorithm is null"); + } + if (blockEncryptionAlgorithm != BlockEncryptionAlgorithm.A128CBC_PLUS_HS256 + && blockEncryptionAlgorithm != BlockEncryptionAlgorithm.A256CBC_PLUS_HS512) { + throw new InvalidParameterException("The block encryption algorithm is not supported"); + } + + byte[] round1 = Base64Util.unsignedToBytes(new int[]{0, 0, 0, 1}); + byte[] outputBitSize = null; + if (blockEncryptionAlgorithm != BlockEncryptionAlgorithm.A128CBC_PLUS_HS256) { + outputBitSize = Base64Util.unsignedToBytes(new int[]{0, 0, 0, 128}); + } else { //A256CBC_PLUS_HS512 + outputBitSize = Base64Util.unsignedToBytes(new int[]{0, 0, 1, 0}); + } + byte[] encValue = blockEncryptionAlgorithm.getName().getBytes(Util.UTF8_STRING_ENCODING); + byte[] epu = Base64Util.unsignedToBytes(new int[]{0, 0, 0, 0}); + byte[] epv = Base64Util.unsignedToBytes(new int[]{0, 0, 0, 0}); + byte[] label = "Encryption".getBytes(Util.UTF8_STRING_ENCODING); + byte[] round1Input = ArrayUtils.addAll(round1, cmk); + round1Input = ArrayUtils.addAll(round1Input, outputBitSize); + round1Input = ArrayUtils.addAll(round1Input, encValue); + round1Input = ArrayUtils.addAll(round1Input, epu); + round1Input = ArrayUtils.addAll(round1Input, epv); + round1Input = ArrayUtils.addAll(round1Input, label); + + MessageDigest mda = MessageDigest.getInstance(blockEncryptionAlgorithm.getMessageDiggestAlgorithm(), SecurityProviderUtility.getBCProvider()); + byte[] round1Hash = mda.digest(round1Input); + byte[] cek = Arrays.copyOf(round1Hash, blockEncryptionAlgorithm.getCekLength() / 8); + + return cek; + } + + public static byte[] generateCik(byte[] cmk, BlockEncryptionAlgorithm blockEncryptionAlgorithm) + throws UnsupportedEncodingException, NoSuchProviderException, NoSuchAlgorithmException, InvalidParameterException { + if (cmk == null) { + throw new InvalidParameterException("The content master key (CMK) is null"); + } + if (blockEncryptionAlgorithm == null) { + throw new InvalidParameterException("The block encryption algorithm is null"); + } + if (blockEncryptionAlgorithm != BlockEncryptionAlgorithm.A128CBC_PLUS_HS256 + && blockEncryptionAlgorithm != BlockEncryptionAlgorithm.A256CBC_PLUS_HS512) { + throw new InvalidParameterException("The block encryption algorithm is not supported"); + } + + byte[] round1 = Base64Util.unsignedToBytes(new int[]{0, 0, 0, 1}); + byte[] outputBitSize = null; + if (blockEncryptionAlgorithm != BlockEncryptionAlgorithm.A128CBC_PLUS_HS256) { + outputBitSize = Base64Util.unsignedToBytes(new int[]{0, 0, 1, 0}); + } else { //A256CBC_PLUS_HS512 + outputBitSize = Base64Util.unsignedToBytes(new int[]{0, 0, 2, 0}); + } + byte[] encValue = blockEncryptionAlgorithm.getName().getBytes(Util.UTF8_STRING_ENCODING); + byte[] epu = Base64Util.unsignedToBytes(new int[]{0, 0, 0, 0}); + byte[] epv = Base64Util.unsignedToBytes(new int[]{0, 0, 0, 0}); + byte[] label = "Integrity".getBytes(Util.UTF8_STRING_ENCODING); + byte[] round1Input = ArrayUtils.addAll(round1, cmk); + round1Input = ArrayUtils.addAll(round1Input, outputBitSize); + round1Input = ArrayUtils.addAll(round1Input, encValue); + round1Input = ArrayUtils.addAll(round1Input, epu); + round1Input = ArrayUtils.addAll(round1Input, epv); + round1Input = ArrayUtils.addAll(round1Input, label); + + MessageDigest mda = MessageDigest.getInstance(blockEncryptionAlgorithm.getMessageDiggestAlgorithm(), SecurityProviderUtility.getBCProvider()); + byte[] cik = mda.digest(round1Input); + + return cik; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwk/Algorithm.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwk/Algorithm.java new file mode 100644 index 00000000..aed085b2 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwk/Algorithm.java @@ -0,0 +1,115 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.jwk; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import org.gluu.oxauth.model.crypto.signature.AlgorithmFamily; +import org.gluu.oxauth.model.util.StringUtils; +import org.gluu.oxauth.model.crypto.signature.RSAKeyFactory; + +import java.util.ArrayList; +import java.util.List; + +/** + * Identifies the cryptographic algorithm used with the key. + * + * @author Javier Rojas Blum + * @author Sergey Manoylo + * @version December 17, 2021 + */ +@SuppressWarnings("java:S1874") +public enum Algorithm { + + // Signature + RS256("RS256", Use.SIGNATURE, AlgorithmFamily.RSA, RSAKeyFactory.DEF_KEYLENGTH), + RS384("RS384", Use.SIGNATURE, AlgorithmFamily.RSA, RSAKeyFactory.DEF_KEYLENGTH), + RS512("RS512", Use.SIGNATURE, AlgorithmFamily.RSA, RSAKeyFactory.DEF_KEYLENGTH), + ES256("ES256", Use.SIGNATURE, AlgorithmFamily.EC, 256), + ES384("ES384", Use.SIGNATURE, AlgorithmFamily.EC, 384), + ES512("ES512", Use.SIGNATURE, AlgorithmFamily.EC, 528), + PS256("PS256", Use.SIGNATURE, AlgorithmFamily.RSA, RSAKeyFactory.DEF_KEYLENGTH), + PS384("PS384", Use.SIGNATURE, AlgorithmFamily.RSA, RSAKeyFactory.DEF_KEYLENGTH), + PS512("PS512", Use.SIGNATURE, AlgorithmFamily.RSA, RSAKeyFactory.DEF_KEYLENGTH), + + // Encryption + RSA1_5("RSA1_5", Use.ENCRYPTION, AlgorithmFamily.RSA, RSAKeyFactory.DEF_KEYLENGTH), + RSA_OAEP("RSA-OAEP", Use.ENCRYPTION, AlgorithmFamily.RSA, RSAKeyFactory.DEF_KEYLENGTH); + + private final String paramName; + private final Use use; + private final AlgorithmFamily family; + private final int keyLength; + + Algorithm(String paramName, Use use, AlgorithmFamily family, int keyLength) { + this.paramName = paramName; + this.use = use; + this.family = family; + this.keyLength = keyLength; // bits + } + + public String getParamName() { + return paramName; + } + + public Use getUse() { + return use; + } + + public AlgorithmFamily getFamily() { + return family; + } + + public int getKeyLength() { + return keyLength; + } + + /** + * Returns the corresponding {@link Algorithm} for a parameter. + * + * @param param The use parameter. + * @return The corresponding algorithm if found, otherwise null. + */ + @JsonCreator + public static Algorithm fromString(String param) { + if (param != null) { + for (Algorithm algorithm : Algorithm.values()) { + if (param.equals(algorithm.paramName)) { + return algorithm; + } + } + } + return null; + } + + public static List fromString(String[] params, Use use) { + List algorithms = new ArrayList(); + + for (String param : params) { + Algorithm algorithm = Algorithm.fromString(param); + if (algorithm != null && algorithm.use == use) { + algorithms.add(algorithm); + } else if (StringUtils.equals("RSA_OAEP", param)) { + algorithms.add(RSA_OAEP); + } + } + + return algorithms; + } + + + /** + * Returns a string representation of the object. In this case the parameter name. + * + * @return The string representation of the object. + */ + @Override + @JsonValue + public String toString() { + return paramName; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwk/JSONWebKey.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwk/JSONWebKey.java new file mode 100644 index 00000000..15f662f7 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwk/JSONWebKey.java @@ -0,0 +1,281 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.jwk; + +import org.gluu.oxauth.model.crypto.signature.ECEllipticCurve; +import org.gluu.oxauth.model.util.StringUtils; +import org.gluu.oxauth.model.util.Util; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.List; + +import static org.gluu.oxauth.model.jwk.JWKParameter.*; + +/** + * @author Javier Rojas Blum + * @version February 12, 2019 + */ +public class JSONWebKey implements Comparable { + + private String kid; + private KeyType kty; + private Use use; + private Algorithm alg; + private Long exp; + private ECEllipticCurve crv; + private List x5c; + + /** + * Modulus + */ + private String n; + + /** + * Exponent + */ + private String e; + + private String x; + private String y; + + public JSONWebKey() { + } + + /** + * Returns the Key ID. The Key ID member can be used to match a specific key. This can be used, for instance, + * to choose among a set of keys within the JWK during key rollover. + * + * @return The Key ID. + */ + public String getKid() { + return kid; + } + + /** + * Sets the Key ID. + * + * @param kid The Key ID. + */ + public void setKid(String kid) { + this.kid = kid; + } + + public KeyType getKty() { + return kty; + } + + public void setKty(KeyType kty) { + this.kty = kty; + } + + /** + * Returns the intended use of the key: signature or encryption. + * + * @return The intended use of the key. + */ + public Use getUse() { + return use; + } + + /** + * Sets the intended use of the key: signature or encryption. + * + * @param use The intended use of the key. + */ + public void setUse(Use use) { + this.use = use; + } + + public Algorithm getAlg() { + return alg; + } + + public void setAlg(Algorithm alg) { + this.alg = alg; + } + + public Long getExp() { + return exp; + } + + public void setExp(Long exp) { + this.exp = exp; + } + + /** + * Returns the curve member that identifies the cryptographic curve used with the key. + * + * @return The curve member that identifies the cryptographic curve used with the key. + */ + public ECEllipticCurve getCrv() { + return crv; + } + + /** + * Sets the curve member that identifies the cryptographic curve used with the key. + * + * @param crv The curve member that identifies the cryptographic curve used with the key. + */ + public void setCrv(ECEllipticCurve crv) { + this.crv = crv; + } + + public List getX5c() { + return x5c; + } + + public void setX5c(List x5c) { + this.x5c = x5c; + } + + /** + * Returns the modulus value for the RSA public key. It is represented as the base64url encoding of the value's + * representation. + * + * @return The modulus value for the RSA public key. + */ + public String getN() { + return n; + } + + /** + * Sets the modulus value for the RSA public key. + * + * @param n The modulus value for the RSA public key. + */ + public void setN(String n) { + this.n = n; + } + + /** + * Returns the exponent value for the RSA public key. + * + * @return The exponent value for the RSA public key. + */ + public String getE() { + return e; + } + + /** + * Sets the exponent value for the RSA public key. + * + * @param e The exponent value for the RSA public key. + */ + public void setE(String e) { + this.e = e; + } + + /** + * Returns the x member that contains the x coordinate for the elliptic curve point. It is represented as the + * base64url encoding of the coordinate's big endian representation. + * + * @return The x member that contains the x coordinate for the elliptic curve point. + */ + public String getX() { + return x; + } + + /** + * Sets the x member that contains the x coordinate for the elliptic curve point. + * + * @param x The x member that contains the x coordinate for the elliptic curve point. + */ + public void setX(String x) { + this.x = x; + } + + /** + * Returns the y member that contains the x coordinate for the elliptic curve point. It is represented as the + * base64url encoding of the coordinate's big endian representation. + * + * @return The y member that contains the x coordinate for the elliptic curve point. + */ + public String getY() { + return y; + } + + /** + * Sets the y member that contains the y coordinate for the elliptic curve point. + * + * @param y The y member that contains the y coordinate for the elliptic curve point. + */ + public void setY(String y) { + this.y = y; + } + + public JSONObject toJSONObject() throws JSONException { + JSONObject jsonObj = new JSONObject(); + + jsonObj.put(KEY_ID, kid); + jsonObj.put(KEY_TYPE, kty); + if (use != null) { + jsonObj.put(KEY_USE, use.getParamName()); + } + jsonObj.put(ALGORITHM, alg); + jsonObj.put(EXPIRATION_TIME, exp); + if (crv != null) { + jsonObj.put(CURVE, crv.getName()); + } + if (!Util.isNullOrEmpty(n)) { + jsonObj.put(MODULUS, n); + } + if (!Util.isNullOrEmpty(e)) { + jsonObj.put(EXPONENT, e); + } + if (!Util.isNullOrEmpty(x)) { + jsonObj.put(X, x); + } + if (!Util.isNullOrEmpty(y)) { + jsonObj.put(Y, y); + } + if (x5c != null && !x5c.isEmpty()) { + jsonObj.put(CERTIFICATE_CHAIN, StringUtils.toJSONArray(x5c)); + } + + return jsonObj; + } + + @Override + public int compareTo(JSONWebKey o) { + if (this.getExp() == null || o.getExp() == null) { + return 0; + } + + return getExp().compareTo(o.getExp()); + } + + public static JSONWebKey fromJSONObject(JSONObject jwkJSONObject) throws JSONException { + JSONWebKey jwk = new JSONWebKey(); + + jwk.setKid(jwkJSONObject.optString(KEY_ID)); + jwk.setKty(KeyType.fromString(jwkJSONObject.optString(KEY_TYPE))); + jwk.setUse(Use.fromString(jwkJSONObject.optString(KEY_USE))); + jwk.setAlg(Algorithm.fromString(jwkJSONObject.optString(ALGORITHM))); + if (jwkJSONObject.has(EXPIRATION_TIME)) { + jwk.setExp(jwkJSONObject.optLong(EXPIRATION_TIME)); + } + jwk.setCrv(ECEllipticCurve.fromString(jwkJSONObject.optString(CURVE))); + if (jwkJSONObject.has(MODULUS)) { + jwk.setN(jwkJSONObject.optString(MODULUS)); + } + if (jwkJSONObject.has(EXPONENT)) { + jwk.setE(jwkJSONObject.optString(EXPONENT)); + } + if (jwkJSONObject.has(X)) { + jwk.setX(jwkJSONObject.optString(X)); + } + if (jwkJSONObject.has(Y)) { + jwk.setY(jwkJSONObject.optString(Y)); + } + if (jwkJSONObject.has(CERTIFICATE_CHAIN)) { + jwk.setX5c(StringUtils.toList(jwkJSONObject.optJSONArray(CERTIFICATE_CHAIN))); + } + + return jwk; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwk/JSONWebKeySet.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwk/JSONWebKeySet.java new file mode 100644 index 00000000..e9b3a008 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwk/JSONWebKeySet.java @@ -0,0 +1,128 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.jwk; + +import org.apache.log4j.Logger; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsonorg.JsonOrgModule; + +import org.gluu.oxauth.model.crypto.signature.AlgorithmFamily; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; + +import static org.gluu.oxauth.model.jwk.JWKParameter.JSON_WEB_KEY_SET; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * @author Javier Rojas Blum + * @version February 12, 2019 + */ +public class JSONWebKeySet { + + private static final Logger LOG = Logger.getLogger(JSONWebKeySet.class); + + private List keys; + + public JSONWebKeySet() { + keys = new ArrayList(); + } + + public List getKeys() { + return keys; + } + + public void setKeys(List keys) { + this.keys = keys; + } + + public JSONWebKey getKey(String keyId) { + for (JSONWebKey jsonWebKey : keys) { + if (jsonWebKey.getKid().equals(keyId)) { + return jsonWebKey; + } + } + + return null; + } + + @Deprecated + public List getKeys(SignatureAlgorithm algorithm) { + List jsonWebKeys = new ArrayList(); + + if (AlgorithmFamily.RSA.equals(algorithm.getFamily())) { + for (JSONWebKey jsonWebKey : keys) { + if (jsonWebKey.getAlg().equals(algorithm.getName())) { + jsonWebKeys.add(jsonWebKey); + } + } + } else if (AlgorithmFamily.EC.equals(algorithm.getFamily())) { + for (JSONWebKey jsonWebKey : keys) { + if (jsonWebKey.getAlg().equals(algorithm.getName())) { + jsonWebKeys.add(jsonWebKey); + } + } + } + + Collections.sort(jsonWebKeys); + return jsonWebKeys; + } + + public JSONObject toJSONObject() throws JSONException { + JSONObject jsonObj = new JSONObject(); + JSONArray keys = new JSONArray(); + + for (JSONWebKey key : getKeys()) { + JSONObject jsonKeyValue = key.toJSONObject(); + + keys.put(jsonKeyValue); + } + + jsonObj.put(JSON_WEB_KEY_SET, keys); + return jsonObj; + } + + @Override + public String toString() { + try { + JSONObject jwks = toJSONObject(); + return toPrettyJson(jwks).replace("\\/", "/"); + } catch (JSONException e) { + LOG.error(e.getMessage(), e); + return null; + } catch (JsonProcessingException e) { + LOG.error(e.getMessage(), e); + return null; + } + } + + private String toPrettyJson(JSONObject jsonObject) throws JsonProcessingException { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JsonOrgModule()); + return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonObject); + } + + public static JSONWebKeySet fromJSONObject(JSONObject jwksJSONObject) throws JSONException { + JSONWebKeySet jwks = new JSONWebKeySet(); + + JSONArray jwksJsonArray = jwksJSONObject.getJSONArray(JSON_WEB_KEY_SET); + for (int i = 0; i < jwksJsonArray.length(); i++) { + JSONObject jwkJsonObject = jwksJsonArray.getJSONObject(i); + + JSONWebKey jwk = JSONWebKey.fromJSONObject(jwkJsonObject); + jwks.getKeys().add(jwk); + } + + return jwks; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwk/JWKParameter.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwk/JWKParameter.java new file mode 100644 index 00000000..fe222710 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwk/JWKParameter.java @@ -0,0 +1,31 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.jwk; + +/** + * @author Javier Rojas Blum + * @version February 17, 2016 + */ +public interface JWKParameter { + + public static final String JSON_WEB_KEY_SET = "keys"; + public static final String KEY_TYPE = "kty"; + public static final String KEY_USE = "use"; + public static final String ALGORITHM = "alg"; + public static final String KEY_ID = "kid"; + public static final String EXPIRATION_TIME = "exp"; + public static final String MODULUS = "n"; + public static final String EXPONENT = "e"; + public static final String CURVE = "crv"; + public static final String X = "x"; + public static final String Y = "y"; + public static final String D = "d"; + public static final String KEY_VALUE = "k"; + public static final String CERTIFICATE_CHAIN = "x5c"; + public static final String PRIVATE_KEY = "privateKey"; + public static final String PUBLIC_KEY = "publicKey"; +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwk/KeySelectionStrategy.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwk/KeySelectionStrategy.java new file mode 100644 index 00000000..bd7d334f --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwk/KeySelectionStrategy.java @@ -0,0 +1,47 @@ +package org.gluu.oxauth.model.jwk; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.gluu.persist.annotation.AttributeEnum; + +import java.util.Collections; +import java.util.List; + +/** + * @author Yuriy Zabrovarnyy + */ +public enum KeySelectionStrategy implements AttributeEnum { + OLDER, + NEWER, + FIRST; + + @Override + public String getValue() { + return name(); + } + + @Override + public Enum resolveByValue(String s) { + try { + return valueOf(s); + } catch (IllegalArgumentException e) { + return null; + } + } + + @JsonIgnore + public JSONWebKey select(List list) { + if (list == null || list.isEmpty()) + return null; + + if (this == FIRST) + return list.iterator().next(); + + if (this == OLDER) + return Collections.min(list); + + if (this == NEWER) + return Collections.max(list); + + return null; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwk/KeyType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwk/KeyType.java new file mode 100644 index 00000000..7a52f273 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwk/KeyType.java @@ -0,0 +1,64 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.jwk; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Identifies the cryptographic algorithm family used with the key. + * + * @author Javier Rojas Blum + * @version June 15, 2016 + */ +public enum KeyType { + + /** + * The Elliptic Curve Digital Signature Algorithm (ECDSA) is defined by FIPS 186‑3. + */ + EC("EC"), + + /** + * The RSA algorithm is defined by RFC 3447. + */ + RSA("RSA"); + + private final String paramName; + + private KeyType(String paramName) { + this.paramName = paramName; + } + + /** + * Returns the corresponding {@link KeyType} for a parameter use of the JWK endpoint. + * + * @param param The use parameter. + * @return The corresponding algorithm family if found, otherwise null. + */ + @JsonCreator + public static KeyType fromString(String param) { + if (param != null) { + for (KeyType keyType : KeyType.values()) { + if (param.equals(keyType.paramName)) { + return keyType; + } + } + } + return null; + } + + /** + * Returns a string representation of the object. In this case the parameter name. + * + * @return The string representation of the object. + */ + @Override + @JsonValue + public String toString() { + return paramName; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwk/Use.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwk/Use.java new file mode 100644 index 00000000..8c660da5 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwk/Use.java @@ -0,0 +1,65 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.jwk; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * @author Javier Rojas Blum + * @version June 15, 2016 + */ +public enum Use { + + /** + * Use this constant when the key is being used for signature. + */ + SIGNATURE("sig"), + /** + * Use this constant when the key is being used for encryption. + */ + ENCRYPTION("enc"); + + private final String paramName; + + private Use(String paramName) { + this.paramName = paramName; + } + + public String getParamName() { + return paramName; + } + + /** + * Returns the corresponding {@link Use} for a parameter use of the JWK endpoint. + * + * @param param The use parameter. + * @return The corresponding use if found, otherwise null. + */ + @JsonCreator + public static Use fromString(String param) { + if (param != null) { + for (Use use : Use.values()) { + if (param.equals(use.paramName) || param.equalsIgnoreCase(use.name())) { + return use; + } + } + } + return null; + } + + /** + * Returns a string representation of the object. In this case the parameter name. + * + * @return The string representation of the object. + */ + @Override + @JsonValue + public String toString() { + return paramName; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jws/AbstractJwsSigner.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jws/AbstractJwsSigner.java new file mode 100644 index 00000000..c05a95d5 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jws/AbstractJwsSigner.java @@ -0,0 +1,87 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.jws; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.exception.InvalidJwtException; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.util.HashUtil; + +import java.security.SignatureException; + +/** + * @author Javier Rojas Blum + * @version March 14, 2019 + */ +public abstract class AbstractJwsSigner implements JwsSigner { + + private static final Logger LOG = Logger.getLogger(AbstractJwsSigner.class); + + private SignatureAlgorithm signatureAlgorithm; + + public AbstractJwsSigner(SignatureAlgorithm signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + } + + @Override + public SignatureAlgorithm getSignatureAlgorithm() { + return signatureAlgorithm; + } + + @Override + public Jwt sign(Jwt jwt) throws InvalidJwtException, SignatureException { + String signature = generateSignature(jwt.getSigningInput()); + jwt.setEncodedSignature(signature); + return jwt; + } + + @Override + public boolean validate(Jwt jwt) { + try { + String signingInput = jwt.getSigningInput(); + String signature = jwt.getEncodedSignature(); + + return validateSignature(signingInput, signature); + } catch (InvalidJwtException e) { + LOG.error(e.getMessage(), e); + return false; + } catch (SignatureException e) { + LOG.error(e.getMessage(), e); + return false; + } catch (Exception e) { + LOG.error(e.getMessage(), e); + return false; + } + } + + public boolean validateAuthorizationCode(String authorizationCode, Jwt idToken) { + return validateHash(authorizationCode, idToken.getClaims().getClaimAsString(JwtClaimName.CODE_HASH)); + } + + public boolean validateAccessToken(String accessToken, Jwt idToken) { + return validateHash(accessToken, idToken.getClaims().getClaimAsString(JwtClaimName.ACCESS_TOKEN_HASH)); + } + + public boolean validateState(String state, Jwt idToken) { + return validateHash(state, idToken.getClaims().getClaimAsString(JwtClaimName.STATE_HASH)); + } + + private boolean validateHash(String tokenCode, String tokenHash) { + if (StringUtils.isBlank(tokenCode) || StringUtils.isBlank(tokenHash)) { + return false; + } + + return tokenHash.equals(HashUtil.getHash(tokenCode, signatureAlgorithm)); + } + + public abstract String generateSignature(String signingInput) throws SignatureException; + + public abstract boolean validateSignature(String signingInput, String signature) throws SignatureException; +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jws/ECDSASigner.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jws/ECDSASigner.java new file mode 100644 index 00000000..25342204 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jws/ECDSASigner.java @@ -0,0 +1,168 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.jws; + +import java.io.UnsupportedEncodingException; +import java.security.AlgorithmParameters; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPrivateKeySpec; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; + +import org.gluu.oxauth.model.crypto.Certificate; +import org.gluu.oxauth.model.crypto.signature.AlgorithmFamily; +import org.gluu.oxauth.model.crypto.signature.ECDSAPrivateKey; +import org.gluu.oxauth.model.crypto.signature.ECDSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.util.Base64Util; +import org.gluu.oxauth.model.util.Util; +import org.gluu.util.security.SecurityProviderUtility; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.crypto.impl.ECDSA; + +/** + * @author Javier Rojas Blum + * @version July 31, 2016 + */ +public class ECDSASigner extends AbstractJwsSigner { + + private ECDSAPrivateKey ecdsaPrivateKey; + private ECDSAPublicKey ecdsaPublicKey; + + public ECDSASigner(SignatureAlgorithm signatureAlgorithm, ECDSAPrivateKey ecdsaPrivateKey) { + super(signatureAlgorithm); + this.ecdsaPrivateKey = ecdsaPrivateKey; + } + + public ECDSASigner(SignatureAlgorithm signatureAlgorithm, ECDSAPublicKey ecdsaPublicKey) { + super(signatureAlgorithm); + this.ecdsaPublicKey = ecdsaPublicKey; + } + + public ECDSASigner(SignatureAlgorithm signatureAlgorithm, Certificate certificate) { + super(signatureAlgorithm); + this.ecdsaPublicKey = certificate.getEcdsaPublicKey(); + } + + @Override + public String generateSignature(String signingInput) throws SignatureException { + if (getSignatureAlgorithm() == null) { + throw new SignatureException("The signature algorithm is null"); + } + if (ecdsaPrivateKey == null) { + throw new SignatureException("The ECDSA private key is null"); + } + if (signingInput == null) { + throw new SignatureException("The signing input is null"); + } + + try { + AlgorithmParameters parameters = AlgorithmParameters.getInstance("EC", SecurityProviderUtility.getBCProvider()); + parameters.init(new ECGenParameterSpec(getSignatureAlgorithm().getCurve().getName())); + ECParameterSpec ecParameters = parameters.getParameterSpec(ECParameterSpec.class); + + ECPrivateKeySpec privateKeySpec = new ECPrivateKeySpec(ecdsaPrivateKey.getD(), ecParameters); + + KeyFactory keyFactory = KeyFactory.getInstance("EC", SecurityProviderUtility.getBCProvider()); + PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec); + + Signature signer = Signature.getInstance(getSignatureAlgorithm().getAlgorithm(), SecurityProviderUtility.getBCProvider()); + signer.initSign(privateKey); + signer.update(signingInput.getBytes(Util.UTF8_STRING_ENCODING)); + + byte[] signature = signer.sign(); + if (AlgorithmFamily.EC.equals(getSignatureAlgorithm().getFamily())) { + int signatureLenght = ECDSA.getSignatureByteArrayLength(JWSAlgorithm.parse(getSignatureAlgorithm().getName())); + signature = ECDSA.transcodeSignatureToConcat(signature, signatureLenght); + } + + return Base64Util.base64urlencode(signature); + } catch (InvalidKeySpecException e) { + throw new SignatureException(e); + } catch (InvalidKeyException e) { + throw new SignatureException(e); + } catch (NoSuchAlgorithmException e) { + throw new SignatureException(e); + } catch (UnsupportedEncodingException e) { + throw new SignatureException(e); + } catch (Exception e) { + throw new SignatureException(e); + } + } + + @Override + public boolean validateSignature(String signingInput, String signature) throws SignatureException { + if (getSignatureAlgorithm() == null) { + throw new SignatureException("The signature algorithm is null"); + } + if (ecdsaPublicKey == null) { + throw new SignatureException("The ECDSA public key is null"); + } + if (signingInput == null) { + throw new SignatureException("The signing input is null"); + } + + String algorithm; + switch (getSignatureAlgorithm()) { + case ES256: + algorithm = "SHA256WITHECDSA"; + break; + case ES384: + algorithm = "SHA384WITHECDSA"; + break; + case ES512: + algorithm = "SHA512WITHECDSA"; + break; + default: + throw new SignatureException("Unsupported signature algorithm"); + } + + try { + byte[] sigBytes = Base64Util.base64urldecode(signature); + if (AlgorithmFamily.EC.equals(getSignatureAlgorithm().getFamily())) { + sigBytes = ECDSA.transcodeSignatureToDER(sigBytes); + } + byte[] sigInBytes = signingInput.getBytes(Util.UTF8_STRING_ENCODING); + + AlgorithmParameters parameters = AlgorithmParameters.getInstance("EC", SecurityProviderUtility.getBCProvider()); + parameters.init(new ECGenParameterSpec(getSignatureAlgorithm().getCurve().getName())); + ECParameterSpec ecParameters = parameters.getParameterSpec(ECParameterSpec.class); + + ECPoint pubPoint = new ECPoint(ecdsaPublicKey.getX(), ecdsaPublicKey.getY()); + KeySpec publicKeySpec = new ECPublicKeySpec(pubPoint, ecParameters); + + KeyFactory keyFactory = KeyFactory.getInstance("EC", SecurityProviderUtility.getBCProvider()); + PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); + + Signature sig = Signature.getInstance(algorithm, SecurityProviderUtility.getBCProvider()); + sig.initVerify(publicKey); + sig.update(sigInBytes); + return sig.verify(sigBytes); + } catch (InvalidKeySpecException e) { + throw new SignatureException(e); + } catch (InvalidKeyException e) { + throw new SignatureException(e); + } catch (NoSuchAlgorithmException e) { + throw new SignatureException(e); + } catch (UnsupportedEncodingException e) { + throw new SignatureException(e); + } catch (Exception e) { + throw new SignatureException(e); + } + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jws/HMACSigner.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jws/HMACSigner.java new file mode 100644 index 00000000..263ed367 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jws/HMACSigner.java @@ -0,0 +1,85 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.jws; + +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.util.Base64Util; +import org.gluu.oxauth.model.util.StringUtils; +import org.gluu.oxauth.model.util.Util; + +import java.io.UnsupportedEncodingException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; + +/** + * @author Javier Rojas Blum + * @version July 31, 2016 + */ +public class HMACSigner extends AbstractJwsSigner { + + private String sharedSecret; + + public HMACSigner(SignatureAlgorithm signatureAlgorithm, String sharedSecret) { + super(signatureAlgorithm); + this.sharedSecret = sharedSecret; + } + + @Override + public String generateSignature(String signingInput) throws SignatureException { + if (getSignatureAlgorithm() == null) { + throw new SignatureException("The signature algorithm is null"); + } + if (sharedSecret == null) { + throw new SignatureException("The shared secret is null"); + } + if (signingInput == null) { + throw new SignatureException("The signing input is null"); + } + + String algorithm; + switch (getSignatureAlgorithm()) { + case HS256: + algorithm = "HMACSHA256"; + break; + case HS384: + algorithm = "HMACSHA384"; + break; + case HS512: + algorithm = "HMACSHA512"; + break; + default: + throw new SignatureException("Unsupported signature algorithm"); + } + + try { + SecretKey secretKey = new SecretKeySpec(sharedSecret.getBytes(Util.UTF8_STRING_ENCODING), algorithm); + Mac mac = Mac.getInstance(algorithm); + mac.init(secretKey); + byte[] sig = mac.doFinal(signingInput.getBytes(Util.UTF8_STRING_ENCODING)); + return Base64Util.base64urlencode(sig); + } catch (NoSuchAlgorithmException e) { + throw new SignatureException(e); + } catch (InvalidKeyException e) { + throw new SignatureException(e); + } catch (UnsupportedEncodingException e) { + throw new SignatureException(e); + } catch (Exception e) { + throw new SignatureException(e); + } + } + + @Override + public boolean validateSignature(String signingInput, String signature) throws SignatureException { + String expectedSignature = generateSignature(signingInput); + return StringUtils.nullToEmpty(signature).equals(StringUtils.nullToEmpty(expectedSignature)); + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jws/JwsSigner.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jws/JwsSigner.java new file mode 100644 index 00000000..3f8236a0 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jws/JwsSigner.java @@ -0,0 +1,26 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.jws; + +import java.security.SignatureException; + +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.exception.InvalidJwtException; +import org.gluu.oxauth.model.jwt.Jwt; + +/** + * @author Javier Rojas Blum + * @version February 8, 2019 + */ +public interface JwsSigner { + + SignatureAlgorithm getSignatureAlgorithm(); + + Jwt sign(Jwt jwt) throws InvalidJwtException, SignatureException; + + boolean validate(Jwt jwt); +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jws/PlainTextSignature.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jws/PlainTextSignature.java new file mode 100644 index 00000000..2cb2a1b7 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jws/PlainTextSignature.java @@ -0,0 +1,33 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.jws; + +import java.security.SignatureException; + +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.util.StringUtils; + +/** + * @author Javier Rojas Blum + * @version October 26, 2017 + */ +public class PlainTextSignature extends AbstractJwsSigner { + + public PlainTextSignature() { + super(SignatureAlgorithm.NONE); + } + + @Override + public String generateSignature(String signingInput) throws SignatureException { + return StringUtils.EMPTY_STRING; + } + + @Override + public boolean validateSignature(String signingInput, String signature) throws SignatureException { + return StringUtils.EMPTY_STRING.equals(signature); + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jws/RSASigner.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jws/RSASigner.java new file mode 100644 index 00000000..8a149594 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jws/RSASigner.java @@ -0,0 +1,107 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.jws; + +import java.security.*; +import java.security.spec.RSAPrivateKeySpec; +import java.security.spec.RSAPublicKeySpec; + +import org.gluu.oxauth.model.crypto.Certificate; +import org.gluu.oxauth.model.crypto.signature.RSAPrivateKey; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.util.Base64Util; +import org.gluu.oxauth.model.util.Util; +import org.gluu.util.security.SecurityProviderUtility; + +/** + * @author Javier Rojas Blum + * @version February 8, 2019 + */ +public class RSASigner extends AbstractJwsSigner { + + private RSAPrivateKey rsaPrivateKey; + private RSAPublicKey rsaPublicKey; + + public RSASigner(SignatureAlgorithm signatureAlgorithm, RSAPrivateKey rsaPrivateKey) { + super(signatureAlgorithm); + this.rsaPrivateKey = rsaPrivateKey; + } + + public RSASigner(SignatureAlgorithm signatureAlgorithm, RSAPublicKey rsaPublicKey) { + super(signatureAlgorithm); + this.rsaPublicKey = rsaPublicKey; + } + + public RSASigner(SignatureAlgorithm signatureAlgorithm, Certificate certificate) { + super(signatureAlgorithm); + this.rsaPublicKey = certificate.getRsaPublicKey(); + } + + @Override + public String generateSignature(String signingInput) throws SignatureException { + if (getSignatureAlgorithm() == null) { + throw new SignatureException("The signature algorithm is null"); + } + if (rsaPrivateKey == null) { + throw new SignatureException("The RSA private key is null"); + } + if (signingInput == null) { + throw new SignatureException("The signing input is null"); + } + + try { + RSAPrivateKeySpec rsaPrivateKeySpec = new RSAPrivateKeySpec( + rsaPrivateKey.getModulus(), + rsaPrivateKey.getPrivateExponent()); + + KeyFactory keyFactory = KeyFactory.getInstance("RSA", SecurityProviderUtility.getBCProvider()); + PrivateKey privateKey = keyFactory.generatePrivate(rsaPrivateKeySpec); + + Signature signature = Signature.getInstance(getSignatureAlgorithm().getAlgorithm(), SecurityProviderUtility.getBCProvider()); + signature.initSign(privateKey); + signature.update(signingInput.getBytes(Util.UTF8_STRING_ENCODING)); + + return Base64Util.base64urlencode(signature.sign()); + } catch (Exception e) { + throw new SignatureException(e); + } + } + + @Override + public boolean validateSignature(String signingInput, String signature) throws SignatureException { + if (getSignatureAlgorithm() == null) { + throw new SignatureException("The signature algorithm is null"); + } + if (rsaPublicKey == null) { + throw new SignatureException("The RSA public key is null"); + } + if (signingInput == null) { + throw new SignatureException("The signing input is null"); + } + + try { + byte[] sigBytes = Base64Util.base64urldecode(signature); + byte[] sigInBytes = signingInput.getBytes(Util.UTF8_STRING_ENCODING); + + RSAPublicKeySpec rsaPublicKeySpec = new RSAPublicKeySpec( + rsaPublicKey.getModulus(), + rsaPublicKey.getPublicExponent()); + + KeyFactory keyFactory = KeyFactory.getInstance("RSA", SecurityProviderUtility.getBCProvider()); + PublicKey publicKey = keyFactory.generatePublic(rsaPublicKeySpec); + + Signature sign = Signature.getInstance(getSignatureAlgorithm().getAlgorithm(), SecurityProviderUtility.getBCProvider()); + sign.initVerify(publicKey); + sign.update(sigInBytes); + + return sign.verify(sigBytes); + } catch (Exception e) { + throw new SignatureException(e); + } + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/Jwt.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/Jwt.java new file mode 100644 index 00000000..84a32317 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/Jwt.java @@ -0,0 +1,113 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.jwt; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.gluu.oxauth.model.exception.InvalidJwtException; +import org.gluu.oxauth.model.token.JsonWebResponse; + +/** + * JSON Web Token (JWT) is a compact token format intended for space constrained + * environments such as HTTP Authorization headers and URI query parameters. + * JWTs encode claims to be transmitted as a JSON object (as defined in RFC 4627) + * that is base64url encoded and digitally signed. Signing is accomplished using + * a JSON Web Signature (JWS). JWTs may also be optionally encrypted using JSON + * Web Encryption (JWE). + * + * @author Javier Rojas Blum + * @version May 3, 2017 + */ +public class Jwt extends JsonWebResponse { + + private static final Logger log = Logger.getLogger(Jwt.class); + + private String encodedHeader; + private String encodedClaims; + private String encodedSignature; + + private boolean loaded = false; + + public Jwt() { + encodedHeader = null; + encodedClaims = null; + encodedSignature = null; + } + + public String getEncodedSignature() { + return encodedSignature; + } + + public void setEncodedSignature(String encodedSignature) { + this.encodedSignature = encodedSignature; + } + + public String getSigningInput() throws InvalidJwtException { + if (loaded) { + return encodedHeader + "." + encodedClaims; + } else { + return header.toBase64JsonObject() + "." + claims.toBase64JsonObject(); + } + } + + public static Jwt parseSilently(String encodedJwt) { + try { + return parse(encodedJwt); + } catch (Exception e) { + log.trace(e.getMessage(), e); + return null; + } + } + + public static Jwt parse(String encodedJwt) throws InvalidJwtException { + if (StringUtils.isBlank(encodedJwt)) { + return null; + } + + String encodedHeader = null; + String encodedClaims = null; + String encodedSignature = null; + + String[] jwtParts = encodedJwt.split("\\."); + if (jwtParts.length == 2) { // Signature Algorithm NONE + encodedHeader = jwtParts[0]; + encodedClaims = jwtParts[1]; + encodedSignature = ""; + } else if (jwtParts.length == 3) { + encodedHeader = jwtParts[0]; + encodedClaims = jwtParts[1]; + encodedSignature = jwtParts[2]; + } else { + throw new InvalidJwtException("Invalid JWT format."); + } + + Jwt jwt = new Jwt(); + jwt.setHeader(new JwtHeader(encodedHeader)); + jwt.setClaims(new JwtClaims(encodedClaims)); + jwt.setEncodedSignature(encodedSignature); + jwt.encodedHeader = encodedHeader; + jwt.encodedClaims = encodedClaims; + jwt.loaded = true; + + return jwt; + } + + @Override + public String toString() { + try { + if (encodedSignature == null) { + return getSigningInput() + "."; + } else { + return getSigningInput() + "." + encodedSignature; + } + } catch (InvalidJwtException e) { + e.printStackTrace(); + } + + return ""; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtClaimName.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtClaimName.java new file mode 100644 index 00000000..2a0ea954 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtClaimName.java @@ -0,0 +1,237 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.jwt; + +/** + * @author Javier Rojas Blum + * @version September 4, 2019 + */ +public final class JwtClaimName { + + // JWT + /** + * Expiration time on or after which the ID Token must not be accepted for processing. + * The processing of this parameter requires that the current date/time must be before + * the expiration date/time listed in the value. + */ + public static final String EXPIRATION_TIME = "exp"; // ID Token + public static final String NOT_BEFORE = "nbf"; + /** + * Time at which the JWT was issued. Its value is a JSON number representing the number + * of seconds from 1970-01-01T0:0:0Z as measured in UTC until the date/time. + */ + public static final String ISSUED_AT = "iat"; // ID Token + /** + * Issuer Identifier for the Issuer of the response. + * The iss value is a case sensitive URL using the https scheme that contains scheme, + * host, and optionally, port number and path components and no query or fragment components. + */ + public static final String ISSUER = "iss"; // ID Token + /** + * Audience(s) that this ID Token is intended for. + * It must contain the OAuth 2.0 client_id of the Relying Party as an audience value. + * It may also contain identifiers for other audiences. In the general case, the aud + * value is an array of case sensitive strings. + * In the common special case when there is one audience, the aud value may be a single + * case sensitive string. + */ + public static final String AUDIENCE = "aud"; // ID Token + public static final String PRINCIPAL = "prn"; + public static final String JWT_ID = "jti"; + public static final String TYPE = "typ"; + + /** + * Authentication Methods References. + *

+ * JSON array of strings that are identifiers for authentication methods used in the authentication. + * For instance, values might indicate that both password and OTP authentication methods were used. + * The definition of particular values to be used in the amr Claim is beyond the scope of this specification. + * Parties using this claim will need to agree upon the meanings of the values used, which may be context-specific. + * The amr value is an array of case sensitive strings. + */ + public static final String AUTHENTICATION_METHOD_REFERENCES = "amr"; + + /** + * A locally unique and never reassigned identifier within the Issuer for the End-User, + * which is intended to be consumed by the Client. + */ + public static final String SUBJECT_IDENTIFIER = "sub"; // User Info + + public static final String TOKEN_BINDING_HASH = "tbh"; // token binding hash + + public static final String CNF = "cnf"; + /** + * Authorized party - the party to which the ID Token was issued. + * If present, it must contain the OAuth 2.0 Client ID of this party. + * This Claim is only needed when the ID Token has a single audience value and that + * audience is different than the authorized party. + * It may be included even when the authorized party is the same as the sole audience. + */ + public static final String AUTHORIZED_PARTY = "azp"; // ID Token + /** + * Authentication Context Class Reference. + * String specifying an Authentication Context Class Reference value that identifies the + * Authentication Context Class that the authentication performed satisfied. + */ + public static final String AUTHENTICATION_CONTEXT_CLASS_REFERENCE = "acr"; // ID Token + /** + * String value used to associate a Client session with an ID Token, and to mitigate replay attacks. + * The value is passed through unmodified from the Authentication Request to the ID Token. + * If present in the ID Token, Clients must verify that the nonce Claim Value is equal to the value + * of the nonce parameter sent in the Authentication Request. + * If present in the Authentication Request, Authorization Servers must include a nonce Claim in the + * ID Token with the Claim Value being the nonce value sent in the Authentication Request. + * Authorization Servers should perform no other processing on nonce values used. + * The nonce value is a case sensitive string. + */ + public static final String NONCE = "nonce"; + /** + * Time when the End-User authentication occurred. + * Its value is a JSON number representing the number of seconds from 1970-01-01T0:0:0Z + * as measured in UTC until the date/time. + * When a max_age request is made or when auth_time is requested as an Essential Claim, + * then this Claim is required; otherwise, its inclusion is optional. + */ + public static final String AUTHENTICATION_TIME = "auth_time"; + public static final String ACCESS_TOKEN_HASH = "at_hash"; + public static final String CODE_HASH = "c_hash"; + public static final String STATE_HASH = "s_hash"; + + // User Info + /** + * End-User's full name in displayable form including all name parts. + */ + public static final String NAME = "name"; + /** + * Given name or first name of the End-User. + */ + public static final String GIVEN_NAME = "given_name"; + /** + * Surname or last name of the End-User. + */ + public static final String FAMILY_NAME = "family_name"; + /** + * Middle name of the End-User. + */ + public static final String MIDDLE_NAME = "middle_name"; + /** + * Casual name of the End-User. + * For instance, a nickname value of Mike might be returned alongside a given_name value of Michael. + */ + public static final String NICKNAME = "nickname"; + /** + * Shorthand name that the End-User wishes to be referred to at the RP, such as janedoe or j.doe. + */ + public static final String PREFERRED_USERNAME = "preferred_username"; + /** + * URL of the End-User's profile page. + */ + public static final String PROFILE = "profile"; + /** + * URL of the End-User's profile picture. + */ + public static final String PICTURE = "picture"; + /** + * URL of the End-User's web page or blog. + */ + public static final String WEBSITE = "website"; + /** + * The End-User's preferred e-mail address. + */ + public static final String EMAIL = "email"; + /** + * The End-User's preferred userName. + */ + public static final String USER_NAME = "user_name"; + /** + * True if the End-User's e-mail address has been verified; otherwise false. + */ + public static final String EMAIL_VERIFIED = "email_verified"; + /** + * The End-User's gender: Values defined by this specification are female and male. + * Other values MAY be used when neither of the defined values are applicable. + */ + public static final String GENDER = "gender"; + /** + * The End-User's birthday. + */ + public static final String BIRTHDATE = "birthdate"; + /** + * String from zoneinfo time zone database. For example, Europe/Paris or America/Los_Angeles. + */ + public static final String ZONEINFO = "zoneinfo"; + /** + * The End-User's locale, represented as a BCP47 (RFC5646) language tag. + * This is typically an ISO 639-1 Alpha-2 (ISO639‑1) language code in lowercase and an ISO 3166-1 Alpha-2 (ISO3166‑1) + * country code in uppercase, separated by a dash. For example, en-US or fr-CA. + */ + public static final String LOCALE = "locale"; + /** + * The End-User's preferred telephone number. + * E.164 is RECOMMENDED as the format of this Claim. For example, +1 (425) 555-1212 or +56 (2) 687 2400. + */ + public static final String PHONE_NUMBER = "phone_number"; + /** + * True if the End-User's phone number has been verified; otherwise false. When this Claim Value is true, + * this means that the OP took affirmative steps to ensure that this phone number was controlled by the + * End-User at the time the verification was performed. The means by which a phone number is verified is + * context-specific, and dependent upon the trust framework or contractual agreements within which the + * parties are operating. When true, the phone_number Claim MUST be in E.164 format and any extensions + * MUST be represented in RFC 3966 format. + */ + public static final String PHONE_NUMBER_VERIFIED = "phone_number_verified"; + /** + * The End-User's preferred address. + */ + public static final String ADDRESS = "address"; + /** + * Time the End-User's information was last updated. + */ + public static final String UPDATED_AT = "updated_at"; + /** + * The full mailing address, formatted for display or use with a mailing label. + */ + public static final String ADDRESS_FORMATTED = "formatted"; + /** + * The full street address component, which may include house number, street name, PO BOX, + * and multi-line extended street address information. + */ + public static final String ADDRESS_STREET_ADDRESS = "street_address"; + /** + * The city or locality component. + */ + public static final String ADDRESS_LOCALITY = "locality"; + /** + * The state, province, prefecture or region component. + */ + public static final String ADDRESS_REGION = "region"; + /** + * The zip code or postal code component. + */ + public static final String ADDRESS_POSTAL_CODE = "postal_code"; + /** + * The country name component. + */ + public static final String ADDRESS_COUNTRY = "country"; + + // Custom attributes + public static final String OX_OPENID_CONNECT_VERSION = "oxOpenIDConnectVersion"; + + // CIBA + public static final String REFRESH_TOKEN_HASH = "urn:openid:params:jwt:claim:rt_hash"; + public static final String AUTH_REQ_ID = "urn:openid:params:jwt:claim:auth_req_id"; + + /** + * The caller references the constants using JwtClaimName.TYPE, + * and so on. Thus, the caller should be prevented from constructing objects of + * this class, by declaring this private constructor. + */ + private JwtClaimName() { + // this prevents even the native class from calling this constructor as well + throw new AssertionError(); + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtClaimSet.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtClaimSet.java new file mode 100644 index 00000000..af1d9385 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtClaimSet.java @@ -0,0 +1,373 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.jwt; + +import com.google.common.collect.Lists; +import org.gluu.oxauth.model.exception.InvalidJwtException; +import org.gluu.oxauth.model.json.JsonApplier; +import org.gluu.oxauth.model.util.Base64Util; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.*; + +/** + * @author Javier Rojas Blum + * @version January 3, 2018 + */ +public abstract class JwtClaimSet { + + private Map claims; + + protected JwtClaimSet() { + claims = new LinkedHashMap<>(); + } + + protected JwtClaimSet(JSONObject jsonObject) { + this(); + load(jsonObject); + } + + protected JwtClaimSet(String base64JsonObject) throws InvalidJwtException { + this(); + load(base64JsonObject); + } + + public Set keys() { + return claims.keySet(); + } + + public Object getClaim(String key) { + return claims.get(key); + } + + public boolean hasClaim(String key) { + return getClaim(key) != null; + } + + public String getClaimAsString(String key) { + Object claim = getClaim(key); + + if (claim != null) { + return claim.toString(); + } else { + return null; + } + } + + public JSONObject getClaimAsJSON(String key) { + String claim = getClaimAsString(key); + + try { + if (claim != null) { + JSONObject json = null; + json = new JSONObject(claim); + return json; + } + } catch (JSONException e) { + e.printStackTrace(); + } + + return null; + } + + public List getClaimAsStringList(String key) { + List list = new ArrayList<>(); + Object value = getClaim(key); + + try { + if (value instanceof JSONArray) { + JSONArray jsonArray = (JSONArray) value; + for (int i = 0; i < jsonArray.length(); i++) { + list.add(jsonArray.getString(i)); + } + } else { + String claim = getClaimAsString(key); + if (claim != null) { + list.add(claim); + } + } + } catch (JSONException e) { + // ignore + } + + return list; + } + + public Date getClaimAsDate(String key) { + Object claim = getClaim(key); + + if (claim != null) { + if (claim instanceof Date) { + return (Date) claim; + } else if (claim instanceof Integer) { + final long c = (Integer) claim; + final long date = c * 1000; + return new Date(date); + } else if (claim instanceof Long) { + return new Date((Long) claim * 1000); + } else if (claim instanceof Double) { + final double c = (Double) claim; + final BigDecimal bigDecimal = BigDecimal.valueOf(c); + + long claimLong = bigDecimal.longValue(); + claimLong = claimLong * 1000; + + return new Date(claimLong); + } else { + return null; + } + } else { + return null; + } + } + + public Integer getClaimAsInteger(String key) { + Object claim = getClaim(key); + + if (claim != null) { + if (claim instanceof Integer) { + return (Integer) claim; + } else { + return null; + } + } else { + return null; + } + } + + public Long getClaimAsLong(String key) { + Object claim = getClaim(key); + + if (claim != null) { + if (claim instanceof Long) { + return (Long) claim; + } else { + return null; + } + } else { + return null; + } + } + + public Character getClaimAsCharacter(String key) { + Object claim = getClaim(key); + + if (claim != null) { + if (claim instanceof Character) { + return (Character) claim; + } else { + return null; + } + } else { + return null; + } + } + + @SuppressWarnings("java:S3776") + public void setClaimObject(String key, Object value, boolean overrideValue) { + if (value == null) { + setNullClaim(key); + } else if (value instanceof String) { + if (overrideValue) { + setClaim(key, (String) value); + } else { + Object currentValue = getClaim(key); + String valueAsString = (String) value; + + if (currentValue instanceof String) { + if (!currentValue.equals(value)) { + setClaim(key, Lists.newArrayList(currentValue.toString(), valueAsString)); + } else { + setClaim(key, (String) value); + } + } else if (currentValue instanceof List) { + List currentValueAsList = (List) currentValue; + if (!currentValueAsList.contains(valueAsString)) { + currentValueAsList.add(valueAsString); + } + } + } + } else if (value instanceof Date) { + setClaim(key, (Date) value); + } else if (value instanceof Boolean) { + setClaim(key, (Boolean) value); + } else if (value instanceof Integer) { + setClaim(key, (Integer) value); + } else if (value instanceof Long) { + setClaim(key, (Long) value); + } else if (value instanceof Character) { + setClaim(key, (Character) value); + } else if (value instanceof List) { + setClaim(key, (List) value); + } else if (value instanceof JwtSubClaimObject) { + setClaim(key, (JwtSubClaimObject) value); + } else if (value instanceof JSONObject) { + setClaim(key, (JSONObject) value); + } else if (value instanceof JSONArray) { + setClaim(key, (JSONArray) value); + } else { + throw new UnsupportedOperationException("Claim value is not supported, key: " + key + ", value :" + value); + } + } + + public void setNullClaim(String key) { + claims.put(key, null); + } + + public void setClaim(String key, String value) { + claims.put(key, value); + } + + public void setClaim(String key, Date value) { + claims.put(key, value); + } + + public void setClaim(String key, Boolean value) { + claims.put(key, value); + } + + public void setClaim(String key, Integer value) { + claims.put(key, value); + } + + public void setClaim(String key, Long value) { + claims.put(key, value); + } + + public void setClaim(String key, Character value) { + claims.put(key, value); + } + + @SuppressWarnings("java:S3740") + public void setClaim(String key, List values) { + claims.put(key, values); + } + + public void setClaim(String key, JwtSubClaimObject subClaimObject) { + claims.put(key, subClaimObject); + } + + public void setClaim(String key, JSONObject values) { + claims.put(key, values); + } + + public void setClaim(String key, JSONArray values) { + claims.put(key, values); + } + + public void setClaimFromJsonObject(String key, Object attribute) { + if (attribute == null) { + return; + } + + if (attribute instanceof JSONArray) { + claims.put(key, JsonApplier.getStringList((JSONArray) attribute)); + } else { + String value = (String) attribute; + claims.put(key, value); + } + } + + public void removeClaim(String key) { + claims.remove(key); + } + + @SuppressWarnings("java:S3740") + public JSONObject toJsonObject() throws InvalidJwtException { + JSONObject jsonObject = new JSONObject(); + + try { + for (Map.Entry claim : claims.entrySet()) { + if (claim.getValue() instanceof Date) { + Date date = (Date) claim.getValue(); + jsonObject.put(claim.getKey(), date.getTime() / 1000); + } else if (claim.getValue() instanceof JwtSubClaimObject) { + JwtSubClaimObject subClaimObject = (JwtSubClaimObject) claim.getValue(); + jsonObject.put(subClaimObject.getName(), subClaimObject.toJsonObject()); + } else if (claim.getValue() instanceof List) { + List claimObjectList = (List) claim.getValue(); + JSONArray claimsJSONArray = new JSONArray(); + for (Object claimObj : claimObjectList) { + claimsJSONArray.put(claimObj); + } + jsonObject.put(claim.getKey(), claimsJSONArray); + } else { + jsonObject.put(claim.getKey(), claim.getValue()); + } + } + } catch (Exception e) { + throw new InvalidJwtException(e); + } + + return jsonObject; + } + + public String toBase64JsonObject() throws InvalidJwtException { + String jsonObjectString = toJsonString(); + byte[] jsonObjectBytes = jsonObjectString.getBytes(StandardCharsets.UTF_8); + return Base64Util.base64urlencode(jsonObjectBytes); + } + + public String toJsonString() throws InvalidJwtException { + JSONObject jsonObject = toJsonObject(); + String jsonObjectString = jsonObject.toString(); + jsonObjectString = jsonObjectString.replace("\\/", "/"); + + return jsonObjectString; + } + + public Map> toMap() throws InvalidJwtException { + Map> map = new HashMap<>(); + + try { + for (Map.Entry claim : claims.entrySet()) { + String key = claim.getKey(); + Object value = claim.getValue(); + + List values = new ArrayList<>(); + if (value instanceof JSONArray) { + JSONArray jsonArray = (JSONArray) value; + for (int i = 0; i < jsonArray.length(); i++) { + values.add(jsonArray.getString(i)); + } + } else if (value != null) { + values.add(value.toString()); + } + + map.put(key, values); + } + } catch (JSONException e) { + throw new InvalidJwtException(e); + } + + return map; + } + + public void load(JSONObject jsonObject) { + claims.clear(); + + for (Iterator it = jsonObject.keys(); it.hasNext(); ) { + String key = it.next(); + Object value = jsonObject.opt(key); + + claims.put(key, value); + } + } + + public void load(String base64JsonObject) throws InvalidJwtException { + try { + String jsonObjectString = new String(Base64Util.base64urldecode(base64JsonObject), StandardCharsets.UTF_8); + load(new JSONObject(jsonObjectString)); + } catch (Exception e) { + throw new InvalidJwtException(e); + } + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtClaims.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtClaims.java new file mode 100644 index 00000000..8f29858f --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtClaims.java @@ -0,0 +1,205 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.jwt; + +import com.google.common.collect.Lists; +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.exception.InvalidJwtException; +import org.json.JSONObject; + +import java.net.URI; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import static org.gluu.oxauth.model.jwt.JwtClaimName.*; + +/** + * @author Javier Rojas Blum Date: 11.09.2012 + */ +public class JwtClaims extends JwtClaimSet { + + public JwtClaims() { + super(); + } + + public JwtClaims(JSONObject jsonObject) { + super(jsonObject); + } + + public JwtClaims(String base64JsonObject) throws InvalidJwtException { + super(base64JsonObject); + } + + /** + * Identifies the expiration time on or after which the token MUST NOT be accepted for processing. + * + * @param expirationTime The expiration time. + */ + public void setExpirationTime(Date expirationTime) { + setClaim(EXPIRATION_TIME, expirationTime); + } + + /** + * Identifies the time before which the token MUST NOT be accepted for processing. + * The processing of the "nbf" claim requires that the current date/time MUST be after or equal to the not-before + * date/time listed in the "nbf" claim. + * + * @param notBefore The not-before date. + */ + public void setNotBefore(Date notBefore) { + setClaim(NOT_BEFORE, notBefore); + } + + /** + * Identifies the time at which the JWT was issued. + * This claim can be used to determine the age of the token. + * + * @param issuedAt The issue date. + */ + public void setIssuedAt(Date issuedAt) { + setClaim(ISSUED_AT, issuedAt); + } + + /** + * Identifies the principal that issued the JWT. + * + * @param issuer The issuer of the JWT. + */ + public void setIssuer(String issuer) { + setClaim(ISSUER, issuer); + } + + /** + * Identifies the principal that issued the JWT. + * + * @param issuer The issuer of the JWT. + */ + public void setIssuer(URI issuer) { + if (issuer == null) { + setNullClaim(ISSUER); + } else { + setClaim(ISSUER, issuer.toString()); + } + } + + public void addAudience(String audience) { + if (StringUtils.isBlank(audience)) { + return; + } + + if (!hasClaim(AUDIENCE)) { + setAudience(audience); + return; + } + + List value = Lists.newArrayList(); + Object currentAudience = getClaim(AUDIENCE); + if (currentAudience instanceof String) { + value.add((String) currentAudience); + } else if (currentAudience instanceof Collection) { + value.addAll((Collection) currentAudience); + } + if (!value.contains(audience)) { + value.add(audience); + } + + if (value.size() == 1) { + setAudience(value.iterator().next()); + return; + } + + setClaim(AUDIENCE, value); + } + + + /** + * Identifies the audience that the JWT is intended for. + * The principal intended to process the JWT MUST be identified with the value of the audience claim. + * If the principal processing the claim does not identify itself with the identifier in the "aud" claim + * value then the JWT MUST be rejected. + * + * @param audience The audience of the JWT. + */ + public void setAudience(String audience) { + setClaim(AUDIENCE, audience); + } + + /** + * Identifies the audience that the JWT is intended for. + * The principal intended to process the JWT MUST be identified with the value of the audience claim. + * If the principal processing the claim does not identify itself with the identifier in the "aud" claim + * value then the JWT MUST be rejected. + * + * @param audience The audience of the JWT. + */ + public void setAudience(URI audience) { + if (audience == null) { + setNullClaim(AUDIENCE); + } else { + setClaim(AUDIENCE, audience.toString()); + } + } + + /** + * Identifies the subject of the JWT. + * + * @param subjectIdentifier The subject of the JWT. + */ + public void setSubjectIdentifier(String subjectIdentifier) { + setClaim(SUBJECT_IDENTIFIER, subjectIdentifier); + } + + /** + * Identifies the subject of the JWT. + * + * @param subjectIdentifier The subject of the JWT. + */ + public void setSubjectIdentifier(URI subjectIdentifier) { + if (subjectIdentifier == null) { + setNullClaim(SUBJECT_IDENTIFIER); + } else { + setClaim(SUBJECT_IDENTIFIER, subjectIdentifier.toString()); + } + } + + /** + * Provides a unique identifier for the JWT. + * + * @param jwtId Unique identifier for the JWT. + */ + public void setJwtId(String jwtId) { + setClaim(JWT_ID, jwtId); + } + + /** + * Provides a unique identifier for the JWT. + * + * @param jwtId Unique identifier for the JWT. + */ + public void setJwtId(UUID jwtId) { + if (jwtId == null) { + setNullClaim(JWT_ID); + } else { + setClaim(JWT_ID, jwtId.toString()); + } + } + + /** + * Declare a type for the contents of this JWT Claims Set. + * + * @param type The type of the JWT claims set. + */ + public void setType(JwtType type) { + if (type == null) { + setNullClaim(TYPE); + } else { + setClaim(TYPE, type.toString()); + } + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtHeader.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtHeader.java new file mode 100644 index 00000000..c4ab4a96 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtHeader.java @@ -0,0 +1,195 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.jwt; + +import org.gluu.oxauth.model.crypto.encryption.BlockEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.encryption.KeyEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.exception.InvalidJwtException; +import org.json.JSONObject; + +import static org.gluu.oxauth.model.jwt.JwtHeaderName.*; + +/** + * @author Javier Rojas Blum + * @version June 15, 2016 + */ +public class JwtHeader extends JwtClaimSet { + + public JwtHeader() { + super(); + } + + public JwtHeader(JSONObject jsonObject) { + super(jsonObject); + } + + public JwtHeader(String base64JsonObject) throws InvalidJwtException { + super(base64JsonObject); + } + + public static JwtHeader instance() { + return new JwtHeader(); + } + + /** + * Declares the type of this object. + * + * @param type The type of this object. + */ + public JwtHeader setType(JwtType type) { + if (type == null) { + setNullClaim(TYPE); + } else { + setClaim(TYPE, type.toString()); + } + return this; + } + + public SignatureAlgorithm getSignatureAlgorithm() { + String alg = getClaimAsString(ALGORITHM); + return SignatureAlgorithm.fromString(alg); + } + + /** + * Identifies the cryptographic algorithm used to secure the JWS. + * + * @param algorithm The cryptographic algorithm. + */ + public JwtHeader setAlgorithm(SignatureAlgorithm algorithm) { + if (algorithm == null) { + setNullClaim(ALGORITHM); + } else { + setClaim(ALGORITHM, algorithm.toString()); + } + return this; + } + + /** + * Identifies the cryptographic algorithm used to encrypt the JWE. + * + * @param algorithm The cryptographic algorithm. + */ + public JwtHeader setAlgorithm(KeyEncryptionAlgorithm algorithm) { + if (algorithm == null) { + setNullClaim(ALGORITHM); + } else { + setClaim(ALGORITHM, algorithm.toString()); + } + return this; + } + + public String getKeyId() { + String keyId = getClaimAsString(KEY_ID); + return keyId; + } + + /** + * Indicates which key was used to secure/encrypt the JWS/JWE. + * + * @param keyId The key id. + */ + public JwtHeader setKeyId(String keyId) { + setClaim(KEY_ID, keyId); + return this; + } + + /** + * In a JWS it is used to declare the type of the secured content (the Payload). + * In a JWE it is used to declare the type of the encrypted content (the Plaintext). + * + * @param contentType The content type. + */ + public void setContentType(JwtType contentType) { + if (contentType == null) { + setNullClaim(CONTENT_TYPE); + } else { + setClaim(CONTENT_TYPE, contentType.toString()); + } + } + + public JwtType getContentType() { + return JwtType.fromString(getClaimAsString(CONTENT_TYPE)); + } + + /** + * Identifies the block encryption algorithm used to encrypt the Plaintext to produce the Cipher Text. + * + * @param encryptionMethod The JWE Encryption Method + */ + public void setEncryptionMethod(BlockEncryptionAlgorithm encryptionMethod) { + if (encryptionMethod == null) { + setNullClaim(ENCRYPTION_METHOD); + } else { + setClaim(ENCRYPTION_METHOD, encryptionMethod.toString()); + } + } + + public BlockEncryptionAlgorithm getEncryptionMethod() { + return BlockEncryptionAlgorithm.fromName(getClaimAsString(ENCRYPTION_METHOD)); + } + + /** + * Value created by the originator for the use in key agreement algorithms. + * + * @param ephemeralPublicKey The Ephemeral Public Key. + */ + public void setEphemeralPublicKey(String ephemeralPublicKey) { + setClaim(EPHEMERAL_PUBLIC_KEY, ephemeralPublicKey); + } + + /** + * The "zip" (compression algorithm) applied to the Plaintext before encryption, if any. + * If present, the value of the "zip" header parameter MUST be the case sensitive string "DEF". + * Compression is performed with the DEFLATE algorithm. + * + * @param compressionAlgorithm The compression algorithm. + */ + public void setCompressionAlgorithm(String compressionAlgorithm) { + setClaim(COMPRESSION_ALGORITHM, compressionAlgorithm); + } + + /** + * The "apu" (agreement PartyUInfo) value for key agreement algorithms using it (such as "ECDH-ES"), + * represented as a base64url encoded string. + * + * @param agreementPartyUInfo The Agreement PartyUInfo. + */ + public void setAgreementPartyUInfo(String agreementPartyUInfo) { + setClaim(AGREEMENT_PARTY_U_INFO, agreementPartyUInfo); + } + + /** + * The "apv" (agreement PartyVInfo) value for key agreement algorithms using it (such as "ECDH-ES"), + * represented as a base64url encoded string. + * + * @param agreementPartyVInfo The Agreement PartyVInfo. + */ + public void setAgreementPartyVInfo(String agreementPartyVInfo) { + setClaim(AGREEMENT_PARTY_V_INFO, agreementPartyVInfo); + } + + /** + * The "epu" (encryption PartyUInfo) value for plaintext encryption algorithms using it + * (such as "A128CBC+HS256"), represented as a base64url encoded string. + * + * @param encryptionPartyUInfo The Encryption PartyUInfo. + */ + public void setEncryptionPartyUInfo(String encryptionPartyUInfo) { + setClaim(ENCRYPTION_PARTY_U_INFO, encryptionPartyUInfo); + } + + /** + * The "epv" (encryption PartyVInfo) value for plaintext encryption algorithms using it + * (such as "A128CBC+HS256"), represented as a base64url encoded string. + * + * @param encryptionPartyVInfo The Encryption PartyVInfo. + */ + public void setEncryptionPartyVInfo(String encryptionPartyVInfo) { + setClaim(ENCRYPTION_PARTY_V_INFO, encryptionPartyVInfo); + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtHeaderName.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtHeaderName.java new file mode 100644 index 00000000..713d94e7 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtHeaderName.java @@ -0,0 +1,35 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.jwt; + +/** + * @author Javier Rojas Blum Date: 11.09.2012 + */ +public final class JwtHeaderName { + + public static final String TYPE = "typ"; + public static final String ALGORITHM = "alg"; + public static final String KEY_ID = "kid"; + public static final String CONTENT_TYPE = "cty"; + public static final String ENCRYPTION_METHOD = "enc"; + public static final String EPHEMERAL_PUBLIC_KEY = "epk"; + public static final String COMPRESSION_ALGORITHM = "zip"; + public static final String AGREEMENT_PARTY_U_INFO = "apu"; + public static final String AGREEMENT_PARTY_V_INFO = "apv"; + public static final String ENCRYPTION_PARTY_U_INFO = "epu"; + public static final String ENCRYPTION_PARTY_V_INFO = "epv"; + + /** + * The caller references the constants using JwtClaimName.TYPE, + * and so on. Thus, the caller should be prevented from constructing objects of + * this class, by declaring this private constructor. + */ + private JwtHeaderName() { + // this prevents even the native class from calling this constructor as well + throw new AssertionError(); + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtStateClaimName.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtStateClaimName.java new file mode 100644 index 00000000..1277052b --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtStateClaimName.java @@ -0,0 +1,106 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.jwt; + +/** + * @author Javier Rojas Blum + * @version May 3, 2017 + */ +public interface JwtStateClaimName { + + /** + * String containing a verifiable identifier for the browser session, + * that cannot be guessed by a third party. + * The verification of this element by the client protects it from + * accepting authorization responses generated in response to forged + * requests generated by third parties. + */ + public static final String RFP = "rfp"; + + /** + * Identifier of the key used to sign this state token at the issuer. + * Identifier of the key used to encrypt this JWT state token at the issuer. + */ + public static final String KID = "kid"; + + /** + * Timestamp of when this Authorization Request was issued. + */ + public static final String IAT = "iat"; + + /** + * The expiration time claim identifies the expiration time on or after which + * the JWT MUST NOT be accepted for processing. + * The processing of the "exp" claim requires that the current date/time MUST + * be before the expiration date/time listed in the "exp" claim. + * Implementers MAY provide for some small leeway, usually no more than a + * few minutes, to account for clock skew. + * Its value MUST be a number containing an IntDate value. + */ + public static final String EXP = "exp"; + + /** + * String identifying the party that issued this state value. + */ + public static final String ISS = "iss"; + + /** + * String identifying the client that this state value is intended for. + */ + public static final String AUD = "aud"; + + /** + * URI containing the location the user agent is to be redirected to after authorization. + */ + public static final String TARGET_LINK_URI = "target_link_uri"; + + /** + * String identifying the authorization server that this request was sent to. + */ + public static final String AS = "as"; + + /** + * The "jti" (JWT ID) claim provides a unique identifier for the JWT. + * The identifier value MUST be assigned in a manner that ensures that + * there is a negligible probability that the same value will be + * accidentally assigned to a different data object. + * The "jti" claim can be used to prevent the JWT from being replayed. + * The "jti" value is a case-sensitive string. + */ + public static final String JTI = "jti"; + + /** + * Access Token hash value. Its value is the base64url encoding of the left-most half + * of the hash of the octets of the ASCII representation of the "access_token" value, + * where the hash algorithm used is the hash algorithm used in the "alg" parameter of + * the State Token's JWS header. + * For instance, if the "alg" is "RS256", hash the "access_token" value with SHA-256, + * then take the left-most 128 bits and base64url encode them. + * The "at_hash" value is a case sensitive string. + * This is REQUIRED if the JWT [RFC7519] state token is being produced by the AS and + * issued with a "access_token" in the authorization response. + */ + public static final String AT_HASH = "at_hash"; + + /** + * Code hash value. Its value is the base64url encoding of the left-most half of the + * hash of the octets of the ASCII representation of the "code" value, where the hash + * algorithm used is the hash algorithm used in the "alg" header parameter of the + * State Token's JWS [RFC7515] header. + * For instance, if the "alg" is "HS512", hash the "code" value with SHA-512, then + * take the left-most 256 bits and base64url encode them. + * The "c_hash" value is a case sensitive string. + * This is REQUIRED if the JWT [RFC7519] state token is being produced by the AS and + * issued with a "code" in the authorization response. + */ + public static final String C_HASH = "c_hash"; + + /** + * Additional claims + */ + public static final String ADDITIONAL_CLAIMS = "additional_claims"; +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtSubClaimObject.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtSubClaimObject.java new file mode 100644 index 00000000..0ae5d46f --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtSubClaimObject.java @@ -0,0 +1,36 @@ +package org.gluu.oxauth.model.jwt; + +import java.util.Map; + +/** + * @author Javier Rojas Blum + * @version Jun 10, 2015 + */ +public class JwtSubClaimObject extends JwtClaimSet { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public static JwtSubClaimObject fromMap(Map map) { + JwtSubClaimObject result = new JwtSubClaimObject(); + for (Map.Entry entry : map.entrySet()) { + result.setClaim(entry.getKey(), entry.getValue()); + } + return result; + } + + public static JwtSubClaimObject fromBooleanMap(Map map) { + JwtSubClaimObject result = new JwtSubClaimObject(); + for (Map.Entry entry : map.entrySet()) { + result.setClaim(entry.getKey(), entry.getValue()); + } + return result; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtType.java new file mode 100644 index 00000000..ff837a01 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/JwtType.java @@ -0,0 +1,48 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.jwt; + +/** + * @author Javier Rojas Blum + * @version December 17, 2015 + */ +public enum JwtType { + + JWT("JWT"); + + private final String paramName; + + JwtType(String paramName) { + this.paramName = paramName; + } + + /** + * Returns the corresponding {@link JwtType} for a parameter. + * + * @param param The parameter. + * @return The corresponding JWT Type if found, otherwise null. + */ + public static JwtType fromString(String param) { + if (param != null) { + for (JwtType t : JwtType.values()) { + if (param.equals(t.toString())) { + return t; + } + } + } + return null; + } + + public String getParamName() { + return paramName; + } + + @Override + public String toString() { + return paramName; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/PureJwt.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/PureJwt.java new file mode 100644 index 00000000..dd97df7b --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/jwt/PureJwt.java @@ -0,0 +1,110 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.jwt; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.util.Base64Util; +import org.gluu.oxauth.model.util.Util; + +import java.io.UnsupportedEncodingException; + +/** + * @author Yuriy Zabrovarnyy + * @author Javier Rojas Blum + * @version July 31, 2016 + */ + +public class PureJwt { + + private final String m_encodedHeader; + private final String m_encodedPayload; + private final String m_encodedSignature; + private final String m_signingInput; + + private final String m_decodedHeader; + private final String m_decodedPayload; + + public PureJwt(String p_encodedHeader, String p_encodedPayload, String p_encodedSignature) { + + m_encodedHeader = p_encodedHeader; + m_encodedPayload = p_encodedPayload; + m_encodedSignature = p_encodedSignature; + m_signingInput = m_encodedHeader + "." + m_encodedPayload; + + String decodedPayloadTemp = null; + String decodedHeaderTemp = null; + try { + decodedHeaderTemp = new String(Base64Util.base64urldecode(p_encodedHeader), Util.UTF8_STRING_ENCODING); + decodedPayloadTemp = new String(Base64Util.base64urldecode(p_encodedPayload), Util.UTF8_STRING_ENCODING); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + m_decodedHeader = decodedHeaderTemp; + m_decodedPayload = decodedPayloadTemp; + } + + public static PureJwt parse(String p_encodedString) { + if (StringUtils.isNotBlank(p_encodedString)) { + String[] jwtParts = p_encodedString.split("\\."); + if (jwtParts.length == 3) { + return new PureJwt(jwtParts[0], jwtParts[1], jwtParts[2]); + } else if (jwtParts.length == 2) { + return new PureJwt(jwtParts[0], jwtParts[1], ""); + } + } + return null; + } + + public String getDecodedHeader() { + return m_decodedHeader; + } + + public String getDecodedPayload() { + return m_decodedPayload; + } + + public String getSigningInput() { + return m_signingInput; + } + + public String getEncodedHeader() { + return m_encodedHeader; + } + + public String getEncodedPayload() { + return m_encodedPayload; + } + + public String getEncodedSignature() { + return m_encodedSignature; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PureJwt pureJwt = (PureJwt) o; + + if (m_encodedHeader != null ? !m_encodedHeader.equals(pureJwt.m_encodedHeader) : pureJwt.m_encodedHeader != null) + return false; + if (m_encodedPayload != null ? !m_encodedPayload.equals(pureJwt.m_encodedPayload) : pureJwt.m_encodedPayload != null) + return false; + if (m_encodedSignature != null ? !m_encodedSignature.equals(pureJwt.m_encodedSignature) : pureJwt.m_encodedSignature != null) + return false; + + return true; + } + + @Override + public int hashCode() { + int result = m_encodedHeader != null ? m_encodedHeader.hashCode() : 0; + result = 31 * result + (m_encodedPayload != null ? m_encodedPayload.hashCode() : 0); + result = 31 * result + (m_encodedSignature != null ? m_encodedSignature.hashCode() : 0); + return result; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/register/ApplicationType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/register/ApplicationType.java new file mode 100644 index 00000000..5d1339a9 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/register/ApplicationType.java @@ -0,0 +1,68 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.register; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * @author Javier Rojas Blum Date: 01.12.2012 + */ +public enum ApplicationType { + + /** + * Clients incapable of maintaining the confidentiality of their credentials + * (e.g. clients executing on the resource owner's device such as an + * installed native application or a web browser-based application), and + * incapable of secure client authentication via any other mean. + */ + NATIVE("native"), + + /** + * Clients capable of maintaining the confidentiality of their credentials + * (e.g. client implemented on a secure server with restricted access to the + * client credentials), or capable of secure client authentication using + * other means. + */ + WEB("web"); + + private final String paramName; + + private ApplicationType(String paramName) { + this.paramName = paramName; + } + + /** + * Returns the corresponding {@link ApplicationType} from a given string. + * The default if not specified is web. + * + * @param param The string value to convert. + * @return The corresponding {@link ApplicationType}, otherwise null. + */ + @JsonCreator + public static ApplicationType fromString(String param) { + if (param != null) { + for (ApplicationType at : ApplicationType.values()) { + if (param.equals(at.paramName)) { + return at; + } + } + } + return WEB; + } + + /** + * Returns a string representation of the object. In this case the parameter name. + * + * @return The string representation of the object. + */ + @Override + @JsonValue + public String toString() { + return paramName; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/register/RegisterErrorResponseType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/register/RegisterErrorResponseType.java new file mode 100644 index 00000000..f2451806 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/register/RegisterErrorResponseType.java @@ -0,0 +1,97 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.register; + +import org.gluu.oxauth.model.error.IErrorType; + +/** + * Error codes for register error responses. + * + * @author Javier Rojas Blum + * @version 0.9 May 18, 2015 + */ +public enum RegisterErrorResponseType implements IErrorType { + + /** + * Value of one or more redirect_uris is invalid. + */ + INVALID_REDIRECT_URI("invalid_redirect_uri"), + + /** + * Value of one or more claims_redirect_uris is invalid. + */ + INVALID_CLAIMS_REDIRECT_URI("invalid_claims_redirect_uri"), + + /** + * The value of one of the Client Metadata fields is invalid and the server has rejected this request. + * Note that an Authorization Server MAY choose to substitute a valid value for any requested parameter + * of a Client's Metadata. + */ + INVALID_CLIENT_METADATA("invalid_client_metadata"), + /** + * The access token provided is expired, revoked, malformed, or invalid for other reasons. + */ + INVALID_TOKEN("invalid_token"), + + /** + * Value of logout_uri is invalid. + */ + INVALID_LOGOUT_URI("invalid_logout_uri"), + + /** + * Invalid software statement. + */ + INVALID_SOFTWARE_STATEMENT("invalid_software_statement"), + + /** + * The authorization server denied the request. + */ + ACCESS_DENIED("access_denied"); + + private final String paramName; + + private RegisterErrorResponseType(String paramName) { + this.paramName = paramName; + } + + /** + * Return the corresponding enumeration from a string parameter. + * + * @param param The parameter to be match. + * @return The enumeration if found, otherwise + * null. + */ + public static RegisterErrorResponseType fromString(String param) { + if (param != null) { + for (RegisterErrorResponseType err : RegisterErrorResponseType + .values()) { + if (param.equals(err.paramName)) { + return err; + } + } + } + return null; + } + + /** + * Returns a string representation of the object. In this case, the lower case code of the error. + */ + @Override + public String toString() { + return paramName; + } + + /** + * Gets error parameter. + * + * @return error parameter + */ + @Override + public String getParameter() { + return paramName; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/register/RegisterRequestParam.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/register/RegisterRequestParam.java new file mode 100644 index 00000000..a17c13c1 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/register/RegisterRequestParam.java @@ -0,0 +1,391 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.register; + +import org.apache.commons.lang.StringUtils; + +/** + * Listed all standard parameters involved in client registration request. + * + * @author Yuriy Zabrovarnyy + * @author Javier Rojas Blum + * @version August 20, 2019 + */ + +public enum RegisterRequestParam { + + /** + * Array of redirect URIs values used in the Authorization Code and Implicit grant types. One of the these + * registered redirect URI values must match the Scheme, Host, and Path segments of the redirect_uri parameter + * value used in each Authorization Request. + */ + REDIRECT_URIS("redirect_uris"), + + /** + * UMA2 : Array of The Claims Redirect URIs to which the client wishes the authorization server to direct + * the requesting party's user agent after completing its interaction. + * The URI MUST be absolute, MAY contain an application/x-www-form-urlencoded-formatted query parameter component + * that MUST be retained when adding additional parameters, and MUST NOT contain a fragment component. + * The client SHOULD pre-register its claims_redirect_uri with the authorization server, and the authorization server + * SHOULD require all clients to pre-register their claims redirection endpoints. Claims redirection URIs + * are different from the redirection URIs defined in [RFC6749] in that they are intended for the exclusive use + * of requesting parties and not resource owners. Therefore, authorization servers MUST NOT redirect requesting parties + * to pre-registered redirection URIs defined in [RFC6749] unless such URIs are also pre-registered specifically as + * claims redirection URIs. If the URI is pre-registered, this URI MUST exactly match one of the pre-registered claims + * redirection URIs, with the matching performed as described in Section 6.2.1 of [RFC3986] (Simple String Comparison). + */ + CLAIMS_REDIRECT_URIS("claims_redirect_uri"), + + /** + * JSON array containing a list of the OAuth 2.0 response_type values that the Client is declaring that it will + * restrict itself to using. If omitted, the default is that the Client will use only the code response type. + */ + RESPONSE_TYPES("response_types"), + + /** + * JSON array containing a list of the OAuth 2.0 grant types that the Client is declaring that it will restrict + * itself to using. + */ + GRANT_TYPES("grant_types"), + + /** + * Kind of the application. The default if not specified is web. The defined values are native or web. + * Web Clients using the OAuth implicit grant type must only register URLs using the https scheme as redirect_uris; + * they may not use localhost as the hostname. + * Native Clients must only register redirect_uris using custom URI schemes or URLs using the http: scheme with + * localhost as the hostname. + */ + APPLICATION_TYPE("application_type"), + + /** + * Array of e-mail addresses of people responsible for this Client. This may be used by some providers to enable a + * Web user interface to modify the Client information. + */ + CONTACTS("contacts"), + + /** + * Name of the Client to be presented to the user. + */ + CLIENT_NAME("client_name"), + + /** + * URL that references a logo for the Client application. + */ + LOGO_URI("logo_uri"), + + /** + * URL of the home page of the Client. + */ + CLIENT_URI("client_uri"), + + /** + * URL that the Relying Party Client provides to the End-User to read about the how the profile data will be used. + */ + POLICY_URI("policy_uri"), + + /** + * URL that the Relying Party Client provides to the End-User to read about the Relying Party's terms of service. + */ + TOS_URI("tos_uri"), + + /** + * URL for the Client's JSON Web Key Set (JWK) document containing key(s) that are used for signing requests to + * the OP. The JWK Set may also contain the Client's encryption keys(s) that are used by the OP to encrypt the + * responses to the Client. + */ + JWKS_URI("jwks_uri"), + + /** + * Client's JSON Web Key Set (JWK) document, passed by value. The semantics of the jwks parameter are the same as + * the jwks_uri parameter, other than that the JWK Set is passed by value, rather than by reference. + * This parameter is intended only to be used by Clients that, for some reason, are unable to use the jwks_uri + * parameter, for instance, by native applications that might not have a location to host the contents of the JWK + * Set. If a Client can use jwks_uri, it must not use jwks. + * One significant downside of jwks is that it does not enable key rotation (which jwks_uri does, as described in + * Section 10 of OpenID Connect Core 1.0). The jwks_uri and jwks parameters must not be used together. + */ + JWKS("jwks"), + + /** + * URL using the https scheme to be used in calculating Pseudonymous Identifiers by the OP. + * The URL references a file with a single JSON array of redirect_uri values. + */ + SECTOR_IDENTIFIER_URI("sector_identifier_uri"), + + /** + * Subject type requested for the Client ID. Valid types include pairwise and public. + */ + SUBJECT_TYPE("subject_type"), + + /** + * Whether to return RPT as signed JWT + */ + RPT_AS_JWT("rpt_as_jwt"), + + /** + * Whether to return access token as signed JWT + */ + ACCESS_TOKEN_AS_JWT("access_token_as_jwt"), + + /** + * Algorithm used for signing of JWT + */ + ACCESS_TOKEN_SIGNING_ALG("access_token_signing_alg"), + + /** + * JWS alg algorithm (JWA)0 required for the issued ID Token. + */ + ID_TOKEN_SIGNED_RESPONSE_ALG("id_token_signed_response_alg"), + + /** + * JWE alg algorithm (JWA) required for encrypting the ID Token. + */ + ID_TOKEN_ENCRYPTED_RESPONSE_ALG("id_token_encrypted_response_alg"), + + /** + * JWE enc algorithm (JWA) required for symmetric encryption of the ID Token. + */ + ID_TOKEN_ENCRYPTED_RESPONSE_ENC("id_token_encrypted_response_enc"), + + /** + * JWS alg algorithm (JWA) required for UserInfo Responses. + */ + USERINFO_SIGNED_RESPONSE_ALG("userinfo_signed_response_alg"), + + /** + * JWE alg algorithm (JWA) required for encrypting UserInfo Responses. + */ + USERINFO_ENCRYPTED_RESPONSE_ALG("userinfo_encrypted_response_alg"), + + /** + * JWE enc algorithm (JWA) required for symmetric encryption of UserInfo Responses. + */ + USERINFO_ENCRYPTED_RESPONSE_ENC("userinfo_encrypted_response_enc"), + + /** + * JWS alg algorithm (JWA) that must be required by the Authorization Server. + */ + REQUEST_OBJECT_SIGNING_ALG("request_object_signing_alg"), + + /** + * JWS alg algorithm (JWA) that must be used for signing Request Objects sent to the OP. + */ + REQUEST_OBJECT_ENCRYPTION_ALG("request_object_encryption_alg"), + + /** + * JWE enc algorithm (JWA) the RP is declaring that it may use for encrypting Request Objects sent to the OP. + */ + REQUEST_OBJECT_ENCRYPTION_ENC("request_object_encryption_enc"), + + /** + * Requested authentication method for the Token Endpoint. + */ + TOKEN_ENDPOINT_AUTH_METHOD("token_endpoint_auth_method"), + + /** + * JWS alg algorithm (JWA) that MUST be used for signing the JWT used to authenticate the Client at the + * Token Endpoint for the private_key_jwt and client_secret_jwt authentication methods. + */ + TOKEN_ENDPOINT_AUTH_SIGNING_ALG("token_endpoint_auth_signing_alg"), + + /** + * Default Maximum Authentication Age. Specifies that the End-User must be actively authenticated if the End-User + * was authenticated longer ago than the specified number of seconds. The max_age request parameter overrides this + * default value. + */ + DEFAULT_MAX_AGE("default_max_age"), + + /** + * Boolean value specifying whether the auth_time Claim in the ID Token is required. It is required when the value + * is true. The auth_time Claim request in the Request Object overrides this setting. + */ + REQUIRE_AUTH_TIME("require_auth_time"), + + /** + * Default requested Authentication Context Class Reference values. Array of strings that specifies the default acr + * values that the Authorization Server must use for processing requests from the Client. + */ + DEFAULT_ACR_VALUES("default_acr_values"), + + /** + * URI using the https scheme that the Authorization Server can call to initiate a login at the Client. + */ + INITIATE_LOGIN_URI("initiate_login_uri"), + + /** + * URL supplied by the RP to request that the user be redirected to this location after a logout has been performed, + */ + POST_LOGOUT_REDIRECT_URIS("post_logout_redirect_uris"), + + /** + * RP URL that will cause the RP to log itself out when rendered in an iframe by the OP. + * A sid (session ID) query parameter MAY be included by the OP to enable the RP to validate the request and + * to determine which of the potentially multiple sessions is to be logged out. + */ + FRONT_CHANNEL_LOGOUT_URI("frontchannel_logout_uri"), + + /** + * Boolean value specifying whether the RP requires that a sid (session ID) query parameter be included + * to identify the RP session at the OP when the logout_uri is used. If omitted, the default value is false. + */ + FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED("frontchannel_logout_session_required"), + + /** + * RP URL that will cause the RP to log itself out when sent a Logout Token by the OP. + */ + BACKCHANNEL_LOGOUT_URI("backchannel_logout_uri"), + + /** + * Boolean value specifying whether the RP requires that a sid (session ID) Claim be included in the Logout Token to identify the RP session with the OP when the backchannel_logout_uri is used. If omitted, the default value is false. + */ + BACKCHANNEL_LOGOUT_SESSION_REQUIRED("backchannel_logout_session_required"), + + /** + * Array of request_uri values that are pre-registered by the Client for use at the Authorization Server. + */ + REQUEST_URIS("request_uris"), + + /** + * @deprecated This param will be removed in a future version because the correct is 'scope' not 'scopes', see (rfc7591). + */ + SCOPES("scopes"), + + /** + * String containing a space-separated list of claims that can be requested individually. + */ + CLAIMS("claims"), + + /** + * Optional string value specifying the JWT Confirmation Method member name (e.g. tbh) that the Relying Party expects when receiving Token Bound ID Tokens. The presence of this parameter indicates that the Relying Party supports Token Binding of ID Tokens. If omitted, the default is that the Relying Party does not support Token Binding of ID Tokens. + */ + ID_TOKEN_TOKEN_BINDING_CNF("id_token_token_binding_cnf"), + + /** + * string representation of the expected subject + * distinguished name of the certificate, which the OAuth client will + * use in mutual TLS authentication. + */ + TLS_CLIENT_AUTH_SUBJECT_DN("tls_client_auth_subject_dn"), + + /** + * boolean, whether to allow spontaneous scopes for client + */ + ALLOW_SPONTANEOUS_SCOPES("allow_spontaneous_scopes"), + + /** + * list of spontaneous scopes + */ + SPONTANEOUS_SCOPES("spontaneous_scopes"), + + /** + * boolean property which indicates whether to run introspection script and then include claims from result into access_token as JWT + */ + RUN_INTROSPECTION_SCRIPT_BEFORE_ACCESS_TOKEN_CREATION_AS_JWT_AND_INCLUDE_CLAIMS("run_introspection_script_before_access_token_as_jwt_creation_and_include_claims"), + + /** + * boolean property which indicates whether to keep client authorization after expiration + */ + KEEP_CLIENT_AUTHORIZATION_AFTER_EXPIRATION("keep_client_authorization_after_expiration"), + + /** + * String containing a space-separated list of scope values. + */ + SCOPE("scope"), + + /** + * Authorized JavaScript origins. + */ + AUTHORIZED_ORIGINS("authorized_origins"), + + /** + * Client-specific access token expiration. Set this value to null or zero to use the default value. + */ + ACCESS_TOKEN_LIFETIME("access_token_lifetime"), + + /** + * A unique identifier string (UUID) assigned by the client developer or software publisher used by + * registration endpoints to identify the client software to be dynamically registered. + */ + SOFTWARE_ID("software_id"), + + /** + * A version identifier string for the client software identified by "software_id". + * The value of the "software_version" should change on any update to the client software identified by the same + * "software_id". + */ + SOFTWARE_VERSION("software_version"), + + /** + * A software statement containing client metadata values about the client software as claims. + * This is a string value containing the entire signed JWT. + */ + SOFTWARE_STATEMENT("software_statement"), + + BACKCHANNEL_TOKEN_DELIVERY_MODE("backchannel_token_delivery_mode"), + + BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT("backchannel_client_notification_endpoint"), + + BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG("backchannel_authentication_request_signing_alg"), + + BACKCHANNEL_USER_CODE_PARAMETER("backchannel_user_code_parameter"); + + /** + * Parameter name + */ + private final String name; + + /** + * Constructor + * + * @param name parameter name + */ + private RegisterRequestParam(String name) { + this.name = name; + } + + /** + * Gets parameter name. + * + * @return parameter name + */ + public String getName() { + return name; + } + + /** + * Returns whether parameter is standard + * + * @param p_parameterName parameter name + * @return whether parameter is standard + */ + public static boolean isStandard(String p_parameterName) { + if (StringUtils.isNotBlank(p_parameterName)) { + for (RegisterRequestParam t : values()) { + if (t.getName().equalsIgnoreCase(p_parameterName)) { + return true; + } + } + } + return false; + } + + /** + * Returns whether custom parameter is valid. + * + * @param p_parameterName parameter name + * @return whether custom parameter is valid + */ + public static boolean isCustomParameterValid(String p_parameterName) { + return StringUtils.isNotBlank(p_parameterName) && !isStandard(p_parameterName); + } + + + @Override + public String toString() { + return name; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/register/RegisterResponseParam.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/register/RegisterResponseParam.java new file mode 100644 index 00000000..46d0837a --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/register/RegisterResponseParam.java @@ -0,0 +1,67 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.register; + +/** + * Listed all standard parameters involved in client registration response. + * + * @author Javier Rojas Blum + * @version 0.9, 03/23/2013 + */ +public enum RegisterResponseParam { + + /** + * Unique Client identifier. + */ + CLIENT_ID("client_id"), + + /** + * Client secret. + */ + CLIENT_SECRET("client_secret"), + + /** + * Access Token that is used by the Client to perform subsequent operations upon the resulting + * Client registration. + */ + REGISTRATION_ACCESS_TOKEN("registration_access_token"), + + /** + * Location where the Access Token can be used to perform subsequent operations upon the resulting + * Client registration. + */ + REGISTRATION_CLIENT_URI("registration_client_uri"), + + /** + * Time when the Client Identifier was issued. + */ + CLIENT_ID_ISSUED_AT("client_id_issued_at"), + + /** + * Time at which the client_secret will expire or 0 if it will not expire. + */ + CLIENT_SECRET_EXPIRES_AT("client_secret_expires_at"); + + /** + * Parameter name + */ + private final String name; + + /** + * Constructor + * + * @param name parameter name + */ + private RegisterResponseParam(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/session/EndSessionErrorResponseType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/session/EndSessionErrorResponseType.java new file mode 100644 index 00000000..a66bd899 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/session/EndSessionErrorResponseType.java @@ -0,0 +1,91 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.session; + +import org.gluu.oxauth.model.error.IErrorType; + +/** + * Error codes for End Session error responses. + * + * @author Javier Rojas Blum Date: 12.16.2011 + */ +public enum EndSessionErrorResponseType implements IErrorType { + /** + * The provided access token is invalid, or was issued to another client. + */ + INVALID_GRANT("invalid_grant"), + + /** + * The request is missing a required parameter, includes an unsupported parameter or parameter value, repeats a + * parameter, or is otherwise malformed. + */ + INVALID_REQUEST("invalid_request"), + + /** + * The provided access token and session state are invalid or were issued to another client. + */ + INVALID_GRANT_AND_SESSION("invalid_grant_and_session"), + + /** + * The provided session state is empty. + */ + SESSION_NOT_PASSED("session_not_passed"), + + /** + * The provided post logout uri is empty. + */ + POST_LOGOUT_URI_NOT_PASSED("post_logout_uri_not_passed"), + + /** + * The provided post logout uri is not associated with client + */ + POST_LOGOUT_URI_NOT_ASSOCIATED_WITH_CLIENT("post_logout_uri_not_associated_with_client"); + + private final String paramName; + + private EndSessionErrorResponseType(String paramName) { + this.paramName = paramName; + } + + /** + * Returns the corresponding {@link EndSessionErrorResponseType} from a given string. + * + * @param param The string value to convert. + * @return The corresponding {@link EndSessionErrorResponseType}, otherwise null. + */ + public static EndSessionErrorResponseType fromString(String param) { + if (param != null) { + for (EndSessionErrorResponseType err : EndSessionErrorResponseType + .values()) { + if (param.equals(err.paramName)) { + return err; + } + } + } + return null; + } + + /** + * Returns a string representation of the object. In this case the parameter name. + * + * @return The string representation of the object. + */ + @Override + public String toString() { + return paramName; + } + + /** + * Gets error parameter. + * + * @return error parameter + */ + @Override + public String getParameter() { + return paramName; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/session/EndSessionRequestParam.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/session/EndSessionRequestParam.java new file mode 100644 index 00000000..e1edf8da --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/session/EndSessionRequestParam.java @@ -0,0 +1,40 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.session; + +/** + * @author Javier Rojas Blum + * @version August 9, 2017 + */ +public interface EndSessionRequestParam { + + /** + * Previously issued ID Token passed to the logout endpoint as a hint about the End-User's current authenticated + * session with the Client. + */ + public static final String ID_TOKEN_HINT = "id_token_hint"; + + /** + * URL to which the RP is requesting that the End-User's User-Agent be redirected after a logout has been performed. + */ + public static final String POST_LOGOUT_REDIRECT_URI = "post_logout_redirect_uri"; + + + /** + * Opaque value used by the RP to maintain state between the logout request and the callback to the endpoint + * specified by the post_logout_redirect_uri parameter. If included in the logout request, the OP passes this + * value back to the RP using the state query parameter when redirecting the User Agent back to the RP. + */ + public static final String STATE = "state"; + + /** + * String that represents the End-User's login state at the OP. + */ + String SESSION_ID = "session_id"; + + String SID = "sid"; +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/session/EndSessionResponseParam.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/session/EndSessionResponseParam.java new file mode 100644 index 00000000..1a5a8455 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/session/EndSessionResponseParam.java @@ -0,0 +1,15 @@ +package org.gluu.oxauth.model.session; + +/** + * @author Javier Rojas Blum + * @version 0.9 October 30, 2014 + */ +public interface EndSessionResponseParam { + + /** + * Opaque value used by the RP to maintain state between the logout request and the callback to the endpoint + * specified by the post_logout_redirect_uri parameter. If included in the logout request, the OP passes this + * value back to the RP using the state query parameter when redirecting the User Agent back to the RP. + */ + public static final String STATE = "state"; +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/token/ClientAssertionType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/token/ClientAssertionType.java new file mode 100644 index 00000000..c2084db8 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/token/ClientAssertionType.java @@ -0,0 +1,50 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.token; + +/** + * @author Javier Rojas Blum Date: 04.13.2012 + */ +public enum ClientAssertionType { + + JWT_BEARER("urn:ietf:params:oauth:client-assertion-type:jwt-bearer"); + + private final String paramName; + + private ClientAssertionType(String paramName) { + this.paramName = paramName; + } + + /** + * Returns the corresponding {@link ClientAssertionType} for a parameter client_assertion_type. + * + * @param param The client_assertion_type parameter. + * @return The corresponding token type if found, otherwise + * null. + */ + public static ClientAssertionType fromString(String param) { + if (param != null) { + for (ClientAssertionType cat : ClientAssertionType.values()) { + if (param.equals(cat.paramName)) { + return cat; + } + } + } + return null; + } + + /** + * Returns a string representation of the object. In this case the parameter + * name for the client_assertionType parameter. + * + * @return The string representation of the object. + */ + @Override + public String toString() { + return paramName; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/token/JsonWebResponse.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/token/JsonWebResponse.java new file mode 100644 index 00000000..92bd31b1 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/token/JsonWebResponse.java @@ -0,0 +1,70 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2015, Gluu + */ + +package org.gluu.oxauth.model.token; + +import org.gluu.oxauth.model.exception.InvalidJwtException; +import org.gluu.oxauth.model.jwt.JwtClaims; +import org.gluu.oxauth.model.jwt.JwtHeader; + +import java.io.Serializable; + +/** + * JSON Web Token is a compact token format intended for space constrained + * environments such as HTTP Authorization headers and URI query parameters. + * + * @author Yuriy Movchan Date: 06/30/2015 + */ +public class JsonWebResponse implements Serializable { + + private static final long serialVersionUID = -4141298937204111173L; + + protected JwtHeader header; + protected JwtClaims claims; + + public JsonWebResponse() { + this.header = new JwtHeader(); + this.claims = new JwtClaims(); + } + + public JwtHeader getHeader() { + return header; + } + + public void setHeader(JwtHeader header) { + this.header = header; + } + + public JwtClaims getClaims() { + return claims; + } + + public void setClaim(String key, String value) { + if (claims == null) { + return; + } + claims.setClaim(key, value); + } + + public void setClaims(JwtClaims claims) { + this.claims = claims; + } + + public String asString() { + try { + return claims.toJsonString(); + } catch (InvalidJwtException ex) { + ex.printStackTrace(); + } + + return ""; + } + + @Override + public String toString() { + return asString(); + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/token/TokenErrorResponseType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/token/TokenErrorResponseType.java new file mode 100644 index 00000000..1cd07c22 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/token/TokenErrorResponseType.java @@ -0,0 +1,125 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.token; + +import org.gluu.oxauth.model.error.IErrorType; + +/** + * @author Javier Rojas Date: 09.22.2011 + */ +public enum TokenErrorResponseType implements IErrorType { + /** + * The request is missing a required parameter, includes an unsupported + * parameter or parameter value, repeats a parameter, includes multiple + * credentials, utilizes more than one mechanism for authenticating the + * client, or is otherwise malformed. + */ + INVALID_REQUEST("invalid_request"), + /** + * Client authentication failed (e.g. unknown client, no client + * authentication included, or unsupported authentication method). The + * authorization server MAY return an HTTP 401 (Unauthorized) status code to + * indicate which HTTP authentication schemes are supported. If the client + * attempted to authenticate via the Authorization request header field, the + * authorization server MUST respond with an HTTP 401 (Unauthorized) status + * code, and include the WWW-Authenticate response header field matching the + * authentication scheme used by the client. + */ + INVALID_CLIENT("invalid_client"), + + /** + * The client is disabled and can't request an access token using this method. + */ + DISABLED_CLIENT("disabled_client"), + + /** + * The provided authorization grant is invalid, expired, revoked, does not + * match the redirection URI used in the authorization request, or was + * issued to another client. + */ + INVALID_GRANT("invalid_grant"), + /** + * The authenticated client is not authorized to use this authorization + * grant type. + */ + UNAUTHORIZED_CLIENT("unauthorized_client"), + /** + * The authorization grant type is not supported by the authorization + * server. + */ + UNSUPPORTED_GRANT_TYPE("unsupported_grant_type"), + /** + * The requested scope is invalid, unknown, malformed, or exceeds the scope + * granted by the resource owner. + */ + INVALID_SCOPE("invalid_scope"), + + /** + * CIBA. The authorization request is still pending as the end-user hasn't yet been authenticated. + */ + AUTHORIZATION_PENDING("authorization_pending"), + + /** + * CIBA. A variant of "authorization_pending", the authorization request is still pending and + * polling should continue, but the interval MUST be increased by at least 5 seconds + * for this and all subsequent requests. + */ + SLOW_DOWN("slow_down"), + + /** + * CIBA. The auth_req_id has expired. The Client will need to make a new Authentication Request. + */ + EXPIRED_TOKEN("expired_token"), + + /** + * CIBA. The end-user denied the authorization request. + */ + ACCESS_DENIED("access_denied"); + + private final String paramName; + + private TokenErrorResponseType(String paramName) { + this.paramName = paramName; + } + + /** + * Returns the corresponding {@link TokenErrorResponseType} from a given string. + * + * @param param The string value to convert. + * @return The corresponding {@link TokenErrorResponseType}, otherwise null. + */ + public static TokenErrorResponseType fromString(String param) { + if (param != null) { + for (TokenErrorResponseType err : TokenErrorResponseType.values()) { + if (param.equals(err.paramName)) { + return err; + } + } + } + return null; + } + + /** + * Returns a string representation of the object. In this case the parameter name. + * + * @return The string representation of the object. + */ + @Override + public String toString() { + return paramName; + } + + /** + * Gets error parameter. + * + * @return error parameter + */ + @Override + public String getParameter() { + return paramName; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/token/TokenRevocationErrorResponseType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/token/TokenRevocationErrorResponseType.java new file mode 100644 index 00000000..3701d70b --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/token/TokenRevocationErrorResponseType.java @@ -0,0 +1,80 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.token; + +import java.util.HashMap; +import java.util.Map; + +import org.gluu.oxauth.model.error.IErrorType; + +/** + * Error codes for token revocation error responses. + * + * @author Javier Rojas Blum + * @version January 16, 2019 + */ +public enum TokenRevocationErrorResponseType implements IErrorType { + + /** + * The authorization server does not support the revocation of the presented token type. + * That is, the client tried to revoke an access token on a server not supporting this feature. + */ + UNSUPPORTED_TOKEN_TYPE("unsupported_token_type"), + + /** + * The request is missing a required parameter, includes an unsupported + * parameter or parameter value, repeats a parameter, includes multiple + * credentials, utilizes more than one mechanism for authenticating the + * client, or is otherwise malformed. + */ + INVALID_CLIENT("invalid_client"), + + /** + * The request is missing a required parameter, includes an unsupported + * parameter or parameter value, repeats a parameter, includes multiple + * credentials, utilizes more than one mechanism for authenticating the + * client, or is otherwise malformed. + */ + INVALID_REQUEST("invalid_request"); + + private final String paramName; + + private static Map mapByValues = new HashMap(); + + static { + for (TokenRevocationErrorResponseType enumType : values()) { + mapByValues.put(enumType.getParameter(), enumType); + } + } + + TokenRevocationErrorResponseType(String paramName) { + this.paramName = paramName; + } + + public static TokenRevocationErrorResponseType getByValue(String value) { + return mapByValues.get(value); + } + + /** + * Gets error parameter. + * + * @return error parameter + */ + @Override + public String getParameter() { + return paramName; + } + + /** + * Returns a string representation of the object. In this case, the lower + * case code of the error. + */ + @Override + public String toString() { + return paramName; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/token/TokenRevocationRequestParam.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/token/TokenRevocationRequestParam.java new file mode 100644 index 00000000..42e211a5 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/token/TokenRevocationRequestParam.java @@ -0,0 +1,17 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.token; + +/** + * @author Javier Rojas Blum + * @version January 16, 2019 + */ +public interface TokenRevocationRequestParam { + + String TOKEN = "token"; + String TOKEN_TYPE_HINT = "token_type_hint"; +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/ClaimTokenFormatType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/ClaimTokenFormatType.java new file mode 100644 index 00000000..a208af62 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/ClaimTokenFormatType.java @@ -0,0 +1,34 @@ +package org.gluu.oxauth.model.uma; + +/** + * @author yuriyz on 05/30/2017. + */ +public enum ClaimTokenFormatType { + ID_TOKEN("http://openid.net/specs/openid-connect-core-1_0.html#IDToken"); + + private String value; + + ClaimTokenFormatType(String value) { + if (value == null || value.trim().isEmpty()) { + throw new IllegalArgumentException(); + } + this.value = value; + } + + public String getValue() { + return value; + } + + public static ClaimTokenFormatType fromValue(String value) { + for (ClaimTokenFormatType type : values()) { + if (type.getValue().equals(value)) { + return type; + } + } + return null; + } + + public static boolean isValueValid(String value) { + return fromValue(value) != null; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/JsonLogic.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/JsonLogic.java new file mode 100644 index 00000000..8f6ceefc --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/JsonLogic.java @@ -0,0 +1,91 @@ +package org.gluu.oxauth.model.uma; + +import com.google.common.base.Preconditions; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.script.Invocable; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; +import java.io.InputStream; + +/** + * @author yuriyz + */ +public class JsonLogic { + + private final static Logger LOG = LoggerFactory.getLogger(JsonLogic.class); + + private static final JsonLogic INSTANCE = new JsonLogic(); + + private final ScriptEngine engine; + + private JsonLogic() { + ScriptEngineManager factory = new ScriptEngineManager(); + engine = factory.getEngineByName("nashorn"); + + Preconditions.checkNotNull(engine); + + loadScript("json_logic.js"); + } + + private void loadJsonLogicJs() { + loadScript("json_logic.js"); + } + + private void loadScript(String scriptName) { + Preconditions.checkState(StringUtils.isNotBlank(scriptName)); + + InputStream stream = getClass().getClassLoader().getResourceAsStream(scriptName); + Preconditions.checkNotNull(stream); + + try { + String script = IOUtils.toString(stream); + engine.eval(script); + LOG.trace("Loaded script, name: " + scriptName); + } catch (Exception e) { + LOG.error("Failed to load JavaScript script, name: " + scriptName, e); + } finally { + IOUtils.closeQuietly(stream); + } + } + + public static JsonLogic getInstance() { + return INSTANCE; + } + + public ScriptEngine getEngine() { + return engine; + } + + public Invocable getInvocable() { + return (Invocable) engine; + } + + public static Object eval(String script) throws ScriptException { + return getInstance().getEngine().eval(script); + } + + public static Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException { + return getInstance().getInvocable().invokeFunction(name, args); + } + + public static boolean apply(String rule) throws ScriptException { + return applyObject(rule).equals(Boolean.TRUE); + } + + public static boolean apply(String rule, String data) throws ScriptException { + return applyObject(rule, data).equals(Boolean.TRUE); + } + + public static Object applyObject(String rule) throws ScriptException { + return eval("jsonLogic.apply( " + rule + " );"); + } + + public static Object applyObject(String rule, String data) throws ScriptException { + return eval("jsonLogic.apply( " + rule + ", " + data + " );"); + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/JsonLogicNode.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/JsonLogicNode.java new file mode 100644 index 00000000..c86323f7 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/JsonLogicNode.java @@ -0,0 +1,74 @@ +package org.gluu.oxauth.model.uma; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.JsonNode; + +import org.jboss.resteasy.annotations.providers.jaxb.IgnoreMediaTypes; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.ArrayList; +import java.util.List; + +/** + * @author yuriyz + */ +@IgnoreMediaTypes("application/*+json") // try to ignore jettison as it's recommended here: http://docs.jboss.org/resteasy/docs/2.3.4.Final/userguide/html/json.html +@JsonPropertyOrder({ "ticket" }) +@XmlRootElement +public class JsonLogicNode { + + private JsonNode rule; + private List data; + + public JsonLogicNode() { + } + + public JsonLogicNode(JsonNode rule, List data) { + this.rule = rule; + this.data = data; + } + + @JsonIgnore + public boolean isValid() { + return data != null && !data.isEmpty() && rule != null; + } + + @JsonProperty(value = "rule") + @XmlElement(name = "rule") + public JsonNode getRule() { + return rule; + } + + public void setRule(JsonNode rule) { + this.rule = rule; + } + + @JsonProperty(value = "data") + @XmlElement(name = "data") + public List getData() { + return data; + } + + public void setData(List data) { + this.data = data; + } + + @JsonIgnore + public List getDataCopy() { + if (data == null) { + return new ArrayList(); + } + return new ArrayList(data); + } + + @Override + public String toString() { + return "JsonLogicNode{" + + "rule=" + rule + + ", data=" + data + + '}'; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/JsonLogicNodeParser.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/JsonLogicNodeParser.java new file mode 100644 index 00000000..4e1f00eb --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/JsonLogicNodeParser.java @@ -0,0 +1,25 @@ +package org.gluu.oxauth.model.uma; + +import org.gluu.oxauth.model.util.Util; + +/** + * @author yuriyz + */ +public class JsonLogicNodeParser { + + private JsonLogicNodeParser() { + } + + public static JsonLogicNode parseNode(String json) { + try { + return Util.createJsonMapper().readValue(json, JsonLogicNode.class); + } catch (Exception e) { + return null; + } + } + + public static boolean isNodeValid(String json) { + JsonLogicNode node = parseNode(json); + return node != null && node.isValid(); + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/PermissionTicket.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/PermissionTicket.java new file mode 100644 index 00000000..09e7d027 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/PermissionTicket.java @@ -0,0 +1,63 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.uma; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import org.jboss.resteasy.annotations.providers.jaxb.IgnoreMediaTypes; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Resource set permission ticket + * + * @author Yuriy Zabrovarnyy + * @author Yuriy Movchan + */ +@IgnoreMediaTypes("application/*+json") // try to ignore jettison as it's recommended here: http://docs.jboss.org/resteasy/docs/2.3.4.Final/userguide/html/json.html +@JsonPropertyOrder({ "ticket" }) +@XmlRootElement +public class PermissionTicket { + + private String ticket; + + public PermissionTicket() { + } + + public PermissionTicket(String ticket) { + this.ticket = ticket; + } + + @JsonProperty(value = "ticket") + @XmlElement(name = "ticket") + public String getTicket() { + return ticket; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PermissionTicket that = (PermissionTicket) o; + + return !(ticket != null ? !ticket.equals(that.ticket) : that.ticket != null); + + } + + @Override + public int hashCode() { + return ticket != null ? ticket.hashCode() : 0; + } + + @Override + public String toString() { + return "PermissionTiket [ticket=" + ticket + "]"; + } + +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/RPTResponse.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/RPTResponse.java new file mode 100644 index 00000000..a03109ab --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/RPTResponse.java @@ -0,0 +1,54 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.uma; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import org.jboss.resteasy.annotations.providers.jaxb.IgnoreMediaTypes; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Requester permission token + * + * @author Yuriy Movchan + * @author Yuriy Zabrovarnyy + * Date: 10/16/2012 + */ +@IgnoreMediaTypes("application/*+json") // try to ignore jettison as it's recommended here: http://docs.jboss.org/resteasy/docs/2.3.4.Final/userguide/html/json.html +@JsonPropertyOrder({ "rpt" }) +@JsonIgnoreProperties(ignoreUnknown = true) +@XmlRootElement +public class RPTResponse { + + private String rpt; + + public RPTResponse() { + } + + public RPTResponse(String token) { + this.rpt = token; + } + + @JsonProperty(value = "rpt") + @XmlElement(name = "rpt") + public String getRpt() { + return rpt; + } + + public void setRpt(String rpt) { + this.rpt = rpt; + } + + @Override + public String toString() { + return "RPTResponse [rpt=" + rpt + "]"; + } + +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/RptIntrospectionResponse.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/RptIntrospectionResponse.java new file mode 100644 index 00000000..03dc1027 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/RptIntrospectionResponse.java @@ -0,0 +1,181 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.uma; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import org.jboss.resteasy.annotations.providers.jaxb.IgnoreMediaTypes; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.List; +import java.util.Map; + +/** + * Token status response according to RPT introspection profile: + * http://docs.kantarainitiative.org/uma/draft-uma-core.html#uma-bearer-token-profile + * + * @author Yuriy Zabrovarnyy + * @author Yuriy Movchan + * Date: 10/24/2012 + */ + +// ignore jettison as it's recommended here: http://docs.jboss.org/resteasy/docs/2.3.4.Final/userguide/html/json.html +@IgnoreMediaTypes("application/*+json") +@JsonPropertyOrder({"active", "exp", "iat", "nbf", "permissions", "client_id", "sub", "aud", "iss", "jti"}) +@XmlRootElement +@JsonIgnoreProperties(ignoreUnknown = true) +public class RptIntrospectionResponse { + + private boolean active; // according spec, must be "active" http://tools.ietf.org/html/draft-richer-oauth-introspection-03#section-2.2 + private Integer expiresAt; + private Integer issuedAt; + private Integer nbf; + private String clientId; + private String sub; + private String aud; + private String iss; + private String jti; + private List permissions; + private Map> pctClaims; + + public RptIntrospectionResponse() { + } + + public RptIntrospectionResponse(boolean status) { + this.active = status; + } + + @JsonProperty(value = "aud") + @XmlElement(name = "aud") + public String getAud() { + return aud; + } + + public void setAud(String aud) { + this.aud = aud; + } + + @JsonProperty(value = "iss") + @XmlElement(name = "iss") + public String getIss() { + return iss; + } + + public void setIss(String iss) { + this.iss = iss; + } + + @JsonProperty(value = "jti") + @XmlElement(name = "jti") + public String getJti() { + return jti; + } + + public void setJti(String jti) { + this.jti = jti; + } + + @JsonProperty(value = "sub") + @XmlElement(name = "sub") + public String getSub() { + return sub; + } + + public void setSub(String sub) { + this.sub = sub; + } + + @JsonProperty(value = "client_id") + @XmlElement(name = "client_id") + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + @JsonProperty(value = "active") + @XmlElement(name = "active") + public boolean getActive() { + return active; + } + + public void setActive(boolean status) { + this.active = status; + } + + @JsonProperty(value = "nbf") + @XmlElement(name = "nbf") + public Integer getNbf() { + return nbf; + } + + public void setNbf(Integer nbf) { + this.nbf = nbf; + } + + @JsonProperty(value = "exp") + @XmlElement(name = "exp") + public Integer getExpiresAt() { + return expiresAt; + } + + public void setExpiresAt(Integer expiresAt) { + this.expiresAt = expiresAt; + } + + @JsonProperty(value = "iat") + @XmlElement(name = "iat") + public Integer getIssuedAt() { + return issuedAt; + } + + public void setIssuedAt(Integer p_issuedAt) { + issuedAt = p_issuedAt; + } + + @JsonProperty(value = "permissions") + @XmlElement(name = "permissions") + public List getPermissions() { + return permissions; + } + + public void setPermissions(List p_permissions) { + permissions = p_permissions; + } + + @JsonProperty(value = "pct_claims") + @XmlElement(name = "pct_claims") + public Map> getPctClaims() { + return pctClaims; + } + + public void setPctClaims(Map> pctClaims) { + this.pctClaims = pctClaims; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("RptStatusResponse"); + sb.append("{active=").append(active); + sb.append(", expiresAt=").append(expiresAt); + sb.append(", issuedAt=").append(issuedAt); + sb.append(", nbf=").append(nbf); + sb.append(", clientId=").append(clientId); + sb.append(", sub=").append(sub); + sb.append(", aud=").append(aud); + sb.append(", iss=").append(iss); + sb.append(", jti=").append(jti); + sb.append(", permissions=").append(permissions); + sb.append('}'); + return sb.toString(); + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/RptProfiles.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/RptProfiles.java new file mode 100644 index 00000000..2caa5a31 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/RptProfiles.java @@ -0,0 +1,21 @@ +package org.gluu.oxauth.model.uma; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 09/12/2015 + */ + +public enum RptProfiles { + + BEARER("https://docs.kantarainitiative.org/uma/profiles/uma-token-bearer-1.0"); + + private String identifyingUri; + + private RptProfiles(String identifyingUri) { + this.identifyingUri = identifyingUri; + } + + public String getIdentifyingUri() { + return identifyingUri; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaConstants.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaConstants.java new file mode 100644 index 00000000..e279ed38 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaConstants.java @@ -0,0 +1,29 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.uma; + +/** + * @author Yuriy Movchan Date: 10/03/2012 + */ +public class UmaConstants { + + private UmaConstants() { + } + + /* + * yuriyz 01/04/2013 : as it was emailed by Eve: + * We've been removing all the specialized content type extensions, + * and just sticking with application/json. I'll add an issue on our side + * to update the specs to remove those last few instances of application/xxx+json. + */ + public static final String JSON_MEDIA_TYPE = "application/json"; + + public static final String GATHERING_ID = "gathering_id"; + + public static final String NO_SCRIPT = "no_script"; + +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaErrorResponseType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaErrorResponseType.java new file mode 100644 index 00000000..1f310450 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaErrorResponseType.java @@ -0,0 +1,218 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.uma; + +import org.gluu.oxauth.model.error.IErrorType; + +import java.util.HashMap; +import java.util.Map; + +/** + * Error codes for UMA error responses. + * + * @author Yuriy Movchan + * @author Yuriy Zabrovarnyy + * Date: 10.03.2012 + */ +public enum UmaErrorResponseType implements IErrorType { + + /** + * The request is missing a required parameter, includes an unsupported + * parameter or parameter value, or is otherwise malformed. + */ + INVALID_REQUEST("invalid_request"), + + /** + * The client is not authorized to request an access token using this + * method. + */ + UNAUTHORIZED_CLIENT("unauthorized_client"), + + /** + * The client is disabled and can't request an access token using this method. + */ + DISABLED_CLIENT("disabled_client"), + + /** + * The resource owner or AM server denied the request. + */ + ACCESS_DENIED("access_denied"), + + /** + * The AM server does not support obtaining an access token using + * this method. + */ + UNSUPPORTED_RESPONSE_TYPE("unsupported_response_type"), + + /** + * The requested scope is invalid, unknown, or malformed. + */ + INVALID_CLIENT_SCOPE("invalid_client_scope"), + + /** + * The AM server encountered an unexpected condition which + * prevented it from fulfilling the request. + */ + SERVER_ERROR("server_error"), + + /** + * The AM server is currently unable to handle the request due to + * a temporary overloading or maintenance of the server. + */ + TEMPORARILY_UNAVAILABLE("temporarily_unavailable"), + + /** + * The resource set that was requested to be deleted or updated at the AM + * did not match the If-Match value present in the request. + */ + PRECONDITION_FAILED("precondition_failed"), + + /** + * The resource set requested from the AM cannot be found. + */ + NOT_FOUND("not_found"), + + /** + * The host request used an unsupported HTTP method. + */ + UNSUPPORTED_METHOD_TYPE("unsupported_method_type"), + + /** + * Forbidden by policy (policy returned false). + */ + FORBIDDEN_BY_POLICY("forbidden_by_policy"), + + /** + * The access token expired. + */ + INVALID_TOKEN("invalid_token"), + + /** + * Grant type is not urn:ietf:params:oauth:grant-type:uma-ticket (required for UMA 2). + */ + INVALID_GRANT_TYPE("invalid_grant_type"), + + /** + * Invalid permission request. + */ + INVALID_PERMISSION_REQUEST("invalid_permission_request"), + + /** + * The provided resource id was not found at the AS. + */ + INVALID_RESOURCE_ID("invalid_resource_id"), + + /** + * At least one of the scopes included in the request was not registered previously by this host. + */ + INVALID_SCOPE("invalid_scope"), + + /** + * The provided client_id is not valid. + */ + INVALID_CLIENT_ID("invalid_client_id"), + + /** + * The provided invalid_claims_redirect_uri is not valid. + */ + INVALID_CLAIMS_REDIRECT_URI("invalid_claims_redirect_uri"), + + /** + * The provided ticket was not found at the AS. + */ + INVALID_TICKET("invalid_ticket"), + + /** + * The claims-gathering script name is not provided or otherwise failed to load script with this name(s). + */ + INVALID_CLAIMS_GATHERING_SCRIPT_NAME("invalid_claims_gathering_script_name"), + + /** + * The provided ticket has expired. + */ + EXPIRED_TICKET("expired_ticket"), + + /** + * The provided session is invalid. + */ + INVALID_SESSION("invalid_session"), + + /** + * The claim token format is blank or otherwise not supported (supported format is http://openid.net/specs/openid-connect-core-1_0.html#IDToken). + */ + INVALID_CLAIM_TOKEN_FORMAT("invalid_claim_token_format"), + + /** + * The claim token is not valid or unsupported. (If format is http://openid.net/specs/openid-connect-core-1_0.html#IDToken then claim token has to be ID Token). + */ + INVALID_CLAIM_TOKEN("invalid_claim_token"), + + /** + * PCT is invalid (revoked, expired or does not exist anymore on AS) + */ + INVALID_PCT("invalid_pct"), + + /** + * RPT is invalid (revoked, expired or does not exist anymore on AS) + */ + INVALID_RPT("invalid_rpt"), + + /** + * The requester is definitively not authorized for this permission according to user policy. + */ + NOT_AUTHORIZED_PERMISSION("not_authorized_permission"), + + /** + * The AM is unable to determine whether the requester is authorized for this permission without gathering claims from the requesting party. + */ + NEED_CLAIMS("need_claims"); + + private static Map lookup = new HashMap(); + + static { + for (UmaErrorResponseType enumType : values()) { + lookup.put(enumType.getParameter(), enumType); + } + } + + private final String paramName; + + private UmaErrorResponseType(String paramName) { + this.paramName = paramName; + } + + /** + * Return the corresponding enumeration from a string parameter. + * + * @param param + * The parameter to be match. + * @return The enumeration if found, otherwise + * null. + */ + public static UmaErrorResponseType fromString(String param) { + return lookup.get(param); + } + + /** + * Returns a string representation of the object. In this case, the lower + * case code of the error. + */ + @Override + public String toString() { + return paramName; + } + + /** + * Gets error parameter. + * + * @return error parameter + */ + @Override + public String getParameter() { + return paramName; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaMetadata.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaMetadata.java new file mode 100644 index 00000000..783fe35a --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaMetadata.java @@ -0,0 +1,97 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.uma; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.gluu.oxauth.model.discovery.OAuth2Discovery; +import org.jboss.resteasy.annotations.providers.jaxb.IgnoreMediaTypes; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Arrays; + +/** + * UMA2 metadata + */ +@IgnoreMediaTypes("application/*+json") +// try to ignore jettison as it's recommended here: http://docs.jboss.org/resteasy/docs/2.3.4.Final/userguide/html/json.html +@XmlRootElement +@JsonIgnoreProperties(ignoreUnknown = true) +public class UmaMetadata extends OAuth2Discovery { + + @JsonProperty(value = "claims_interaction_endpoint") + @XmlElement(name = "claims_interaction_endpoint") + private String claimsInteractionEndpoint; + + @JsonProperty(value = "uma_profiles_supported") + @XmlElement(name = "uma_profiles_supported") + private String[] umaProfilesSupported; + + @JsonProperty(value = "permission_endpoint") + @XmlElement(name = "permission_endpoint") + private String permissionEndpoint; + + @JsonProperty(value = "resource_registration_endpoint") + @XmlElement(name = "resource_registration_endpoint") + private String resourceRegistrationEndpoint; + + @JsonProperty(value = "scope_endpoint") + @XmlElement(name = "scope_endpoint") + private String scopeEndpoint; + + public String getClaimsInteractionEndpoint() { + return claimsInteractionEndpoint; + } + + public void setClaimsInteractionEndpoint(String claimsInteractionEndpoint) { + this.claimsInteractionEndpoint = claimsInteractionEndpoint; + } + + public String[] getUmaProfilesSupported() { + return umaProfilesSupported; + } + + public void setUmaProfilesSupported(String[] umaProfilesSupported) { + this.umaProfilesSupported = umaProfilesSupported; + } + + public String getPermissionEndpoint() { + return permissionEndpoint; + } + + public void setPermissionEndpoint(String permissionEndpoint) { + this.permissionEndpoint = permissionEndpoint; + } + + public String getResourceRegistrationEndpoint() { + return resourceRegistrationEndpoint; + } + + public void setResourceRegistrationEndpoint(String resourceRegistrationEndpoint) { + this.resourceRegistrationEndpoint = resourceRegistrationEndpoint; + } + + public String getScopeEndpoint() { + return scopeEndpoint; + } + + public void setScopeEndpoint(String scopeEndpoint) { + this.scopeEndpoint = scopeEndpoint; + } + + @Override + public String toString() { + return "UmaConfiguration{" + + "claimsInteractionEndpoint='" + claimsInteractionEndpoint + '\'' + + ", umaProfilesSupported=" + Arrays.toString(umaProfilesSupported) + + ", permissionEndpoint='" + permissionEndpoint + '\'' + + ", resourceRegistrationEndpoint='" + resourceRegistrationEndpoint + '\'' + + ", scopeEndpoint='" + scopeEndpoint + '\'' + + "} " + super.toString(); + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaNeedInfoResponse.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaNeedInfoResponse.java new file mode 100644 index 00000000..e9a6e3a2 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaNeedInfoResponse.java @@ -0,0 +1,74 @@ +package org.gluu.oxauth.model.uma; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.gluu.model.uma.ClaimDefinition; + +import java.io.Serializable; +import java.util.List; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 14/04/2015 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class UmaNeedInfoResponse implements Serializable { + + @JsonProperty(value = "error") + private String error; + @JsonProperty(value = "required_claims") + private List requiredClaims; + @JsonProperty(value = "redirect_user") + private String redirectUser; + @JsonProperty(value = "ticket") + private String ticket; + + public UmaNeedInfoResponse() { + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + public String getRedirectUser() { + return redirectUser; + } + + public void setRedirectUser(String redirectUser) { + this.redirectUser = redirectUser; + } + + public List getRequiredClaims() { + return requiredClaims; + } + + public void setRequiredClaims(List requiredClaims) { + this.requiredClaims = requiredClaims; + } + + public String getTicket() { + return ticket; + } + + public void setTicket(String ticket) { + this.ticket = ticket; + } + + /** + * Builds GET claims-gathering url. Note: it is strictly recommended to use POST http method. + * + * @return claims-gathering url for GET request. + */ + public String buildClaimsGatheringUrl(String clientId, String claimsRedirectUri) { + String result = redirectUser; + result += result.contains("?") ? "&" : "?"; + result += "client_id=" + clientId; + result += "&ticket=" + ticket; + result += "&claims_redirect_uri=" + claimsRedirectUri; + return result; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaPermission.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaPermission.java new file mode 100644 index 00000000..350b4141 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaPermission.java @@ -0,0 +1,102 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.uma; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import org.jboss.resteasy.annotations.providers.jaxb.IgnoreMediaTypes; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * UMA Permission. Used for both: + * 1. register permission ticket + * 2. by introspection RPT endpoint to return RPT status. + * + * @author Yuriy Zabrovarnyy + * @author Yuriy Movchan + */ + +// try to ignore jettison as it's recommended here: http://docs.jboss.org/resteasy/docs/2.3.4.Final/userguide/html/json.html +@IgnoreMediaTypes("application/*+json") +@JsonPropertyOrder({"resource_id", "resource_scopes", "exp"}) +@JsonIgnoreProperties(ignoreUnknown = true) +@XmlRootElement +public class UmaPermission implements Serializable { + + private String resourceId; + private List scopes; + private Integer expiresAt; + + private Map params; + + public UmaPermission() { + } + + public UmaPermission(String resourceId, List scopes) { + this.resourceId = resourceId; + this.scopes = scopes; + } + + @JsonProperty(value = "resource_id") + @XmlElement(name = "resource_id") + public String getResourceId() { + return resourceId; + } + + public void setResourceId(String resourceId) { + this.resourceId = resourceId; + } + + @JsonProperty(value = "resource_scopes") + @XmlElement(name = "resource_scopes") + public List getScopes() { + if (scopes == null) { + scopes = new ArrayList<>(); + } + return scopes; + } + + public void setScopes(List scopes) { + this.scopes = scopes; + } + + @JsonProperty(value = "exp") + @XmlElement(name = "exp") + public Integer getExpiresAt() { + return expiresAt; + } + + public void setExpiresAt(Integer expiresAt) { + this.expiresAt = expiresAt; + } + + @JsonProperty(value = "params") + @XmlElement(name = "params") + public Map getParams() { + return params; + } + + public void setParams(Map params) { + this.params = params; + } + + @Override + public String toString() { + return "UmaPermission{" + + "resourceId='" + resourceId + '\'' + + ", scopes=" + scopes + + ", expiresAt=" + expiresAt + + '}'; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaPermissionList.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaPermissionList.java new file mode 100644 index 00000000..145e6897 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaPermissionList.java @@ -0,0 +1,26 @@ +package org.gluu.oxauth.model.uma; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.jboss.resteasy.annotations.providers.jaxb.IgnoreMediaTypes; + +import java.util.ArrayList; +import java.util.Collections; + +/** + * @author yuriyz on 05/25/2017. + */ +@IgnoreMediaTypes("application/*+json") +public class UmaPermissionList extends ArrayList { + + @JsonIgnore + public UmaPermissionList addPermission(UmaPermission permission) { + add(permission); + return this; + } + + public static UmaPermissionList instance(UmaPermission... permissions) { + UmaPermissionList instance = new UmaPermissionList(); + Collections.addAll(instance, permissions); + return instance; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaResource.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaResource.java new file mode 100644 index 00000000..5d11c3e1 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaResource.java @@ -0,0 +1,158 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.uma; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import org.apache.commons.lang.StringUtils; +import org.jboss.resteasy.annotations.providers.jaxb.IgnoreMediaTypes; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.List; + +/** + * Resource that needs protection by registering a resource description at the AS. + * + * @author Yuriy Zabrovarnyy + * Date: 17/05/2017 + */ + +@IgnoreMediaTypes("application/*+json") +// try to ignore jettison as it's recommended here: http://docs.jboss.org/resteasy/docs/2.3.4.Final/userguide/html/json.html +@JsonPropertyOrder({"name", "uri", "type", "scopes", "scopeExpression", "icon_uri"}) +@JsonIgnoreProperties(ignoreUnknown = true) +@XmlRootElement +public class UmaResource { + + private List scopes; + + private String scopeExpression; + + private String description; + + private String iconUri; + + private String name; + + private String type; + + private Integer iat; + + private Integer exp; + + @JsonProperty(value = "iat") + @XmlElement(name = "iat") + public Integer getIat() { + return iat; + } + + public void setIat(Integer iat) { + this.iat = iat; + } + + @JsonProperty(value = "exp") + @XmlElement(name = "exp") + public Integer getExp() { + return exp; + } + + public void setExp(Integer exp) { + this.exp = exp; + } + + @JsonProperty(value = "description") + @XmlElement(name = "description") + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @JsonProperty(value = "type") + @XmlElement(name = "type") + public String getType() { + return type; + } + + public UmaResource setType(String p_type) { + type = p_type; + return this; + } + + @JsonProperty(value = "name") + @XmlElement(name = "name") + public String getName() { + return name; + } + + public UmaResource setName(String name) { + this.name = name; + return this; + } + + @JsonProperty(value = "icon_uri") + @XmlElement(name = "icon_uri") + public String getIconUri() { + return iconUri; + } + + public UmaResource setIconUri(String iconUri) { + this.iconUri = iconUri; + return this; + } + + @JsonProperty(value = "resource_scopes") + @XmlElement(name = "resource_scopes") + public List getScopes() { + return scopes; + } + + public UmaResource setScopes(List scopes) { + this.scopes = scopes; + return this; + } + + @JsonProperty(value = "scope_expression") + @XmlElement(name = "scope_expression") + public String getScopeExpression() { + return scopeExpression; + } + + public void setScopeExpression(String scopeExpression) { + assertValidExpression(scopeExpression); + this.scopeExpression = scopeExpression; + } + + @JsonIgnore + public static void assertValidExpression(String scopeExpression) { + if (!isValidExpression(scopeExpression)) { + throw new RuntimeException("Scope expression is not valid json logic expression. Expression:" + scopeExpression); + } + } + + @JsonIgnore + public static boolean isValidExpression(String scopeExpression) { + return StringUtils.isBlank(scopeExpression) || JsonLogicNodeParser.isNodeValid(scopeExpression); + } + + @Override + public String toString() { + return "UmaResource{" + + "name='" + name + '\'' + + ", scopes=" + scopes + + ", scopeExpression=" + scopeExpression + + ", description='" + description + '\'' + + ", iconUri='" + iconUri + '\'' + + ", type='" + type + '\'' + + '}'; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaResourceResponse.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaResourceResponse.java new file mode 100644 index 00000000..65160e65 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaResourceResponse.java @@ -0,0 +1,58 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.uma; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import org.jboss.resteasy.annotations.providers.jaxb.IgnoreMediaTypes; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Resource description. + * + * @author Yuriy Movchan + * @author Yuriy Zabrovarnyy + * Date: 10/03/2012 + */ +@IgnoreMediaTypes("application/*+json") // try to ignore jettison as it's recommended here: http://docs.jboss.org/resteasy/docs/2.3.4.Final/userguide/html/json.html +@JsonPropertyOrder({ "_id", "user_access_policy_uri" }) +@JsonIgnoreProperties(ignoreUnknown = true) +@XmlRootElement +public class UmaResourceResponse { + + private String id; + private String userAccessPolicyUri; + + @JsonProperty(value = "_id") + @XmlElement(name = "_id") + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @JsonProperty(value = "user_access_policy_uri") + @XmlElement(name = "user_access_policy_uri") + public String getUserAccessPolicyUri() { + return userAccessPolicyUri; + } + + public void setUserAccessPolicyUri(String userAccessPolicyUri) { + this.userAccessPolicyUri = userAccessPolicyUri; + } + + @Override + public String toString() { + return "UmaResourceResponse [id=" + id + ", user_access_policy_uri=" + userAccessPolicyUri + "]"; + } + +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaResourceWithId.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaResourceWithId.java new file mode 100644 index 00000000..f9abd65c --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaResourceWithId.java @@ -0,0 +1,46 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.uma; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import org.jboss.resteasy.annotations.providers.jaxb.IgnoreMediaTypes; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Resource that needs protection by registering a resource description + * at the AS. + * + * @author Yuriy Zabrovarnyy + * Date: 17/05/2017 + */ +@IgnoreMediaTypes("application/*+json") // try to ignore jettison as it's recommended here: http://docs.jboss.org/resteasy/docs/2.3.4.Final/userguide/html/json.html +@JsonPropertyOrder({ "_id", "_rev", "name", "iconUri", "scopes" }) +@XmlRootElement +public class UmaResourceWithId extends UmaResource { + + private String id; + + @JsonProperty(value = "_id") + @XmlElement(name = "_id") + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @Override + public String toString() { + return "UmaResourceWithId [id=" + id + + ", toString()=" + super.toString() + "]"; + } + +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaScopeDescription.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaScopeDescription.java new file mode 100644 index 00000000..59564966 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaScopeDescription.java @@ -0,0 +1,76 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.uma; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import org.jboss.resteasy.annotations.providers.jaxb.IgnoreMediaTypes; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * A scope is a bounded extent of access that is possible to perform on a + * resource set. + * + * @author Yuriy Movchan + * @author Yuriy Zabrovarnyy + * Date: 10/03/2012 + */ +@IgnoreMediaTypes("application/*+json") // try to ignore jettison as it's recommended here: http://docs.jboss.org/resteasy/docs/2.3.4.Final/userguide/html/json.html +@JsonPropertyOrder({ "name", "icon_uri" }) +@JsonIgnoreProperties(ignoreUnknown = true) +@XmlRootElement() +public class UmaScopeDescription { + + private String description; + + private String iconUri; + + private String name; + + @JsonProperty(value = "description") + @XmlElement(name = "description") + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @JsonProperty(value = "name") + @XmlElement(name = "name") + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @JsonProperty(value = "icon_uri") + @XmlElement(name = "icon_uri") + public String getIconUri() { + return iconUri; + } + + public void setIconUri(String iconUri) { + this.iconUri = iconUri; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("ScopeDescription"); + sb.append("{iconUri='").append(iconUri).append('\''); + sb.append(", name='").append(name).append('\''); + sb.append('}'); + return sb.toString(); + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaScopeType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaScopeType.java new file mode 100644 index 00000000..157589ed --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaScopeType.java @@ -0,0 +1,42 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.uma; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 12/03/2013 + */ + +public enum UmaScopeType { + + PROTECTION("uma_protection"); + + private static Map lookup = new HashMap(); + + static { + for (UmaScopeType enumType : values()) { + lookup.put(enumType.getValue(), enumType); + } + } + + private String m_value; + + private UmaScopeType(String p_value) { + m_value = p_value; + } + + public String getValue() { + return m_value; + } + + public static UmaScopeType fromValue(String p_value) { + return lookup.get(p_value); + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaTokenResponse.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaTokenResponse.java new file mode 100644 index 00000000..6c2ee7b1 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/UmaTokenResponse.java @@ -0,0 +1,77 @@ +package org.gluu.oxauth.model.uma; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.jboss.resteasy.annotations.providers.jaxb.IgnoreMediaTypes; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.io.Serializable; + +/** + * @author yuriyz on 06/04/2017. + */ +@IgnoreMediaTypes("application/*+json") +// try to ignore jettison as it's recommended here: http://docs.jboss.org/resteasy/docs/2.3.4.Final/userguide/html/json.html +@XmlRootElement +@JsonIgnoreProperties(ignoreUnknown = true) +public class UmaTokenResponse implements Serializable { + + @JsonProperty(value = "access_token") + @XmlElement(name = "access_token") + private String accessToken; + + @JsonProperty(value = "token_type") + @XmlElement(name = "token_type") + private String tokenType = "Bearer"; + + @JsonProperty(value = "pct") + @XmlElement(name = "pct") + private String pct; + + @JsonProperty(value = "upgraded") + @XmlElement(name = "upgraded") + private Boolean upgraded =false; + + public String getAccessToken() { + return accessToken; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public String getTokenType() { + return tokenType; + } + + public void setTokenType(String tokenType) { + this.tokenType = tokenType; + } + + public String getPct() { + return pct; + } + + public void setPct(String pct) { + this.pct = pct; + } + + public Boolean getUpgraded() { + return upgraded; + } + + public void setUpgraded(Boolean upgraded) { + this.upgraded = upgraded; + } + + @Override + public String toString() { + return "UmaTokenResponse{" + + "accessToken='" + accessToken + '\'' + + ", tokenType='" + tokenType + '\'' + + ", pct='" + pct + '\'' + + ", upgraded=" + upgraded + + '}'; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/persistence/UmaPermission.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/persistence/UmaPermission.java new file mode 100644 index 00000000..6dc875d1 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/persistence/UmaPermission.java @@ -0,0 +1,205 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.uma.persistence; + +import com.google.common.collect.Maps; +import org.gluu.oxauth.model.util.Pair; +import org.gluu.persist.annotation.*; + +import java.io.Serializable; +import java.time.Duration; +import java.util.*; + +/** + * UMA permission + * + * @author Yuriy Zabrovarnyy + * @version 2.0, date: 17/05/2017 + */ +@DataEntry +@ObjectClass(value = "oxUmaResourcePermission") +public class UmaPermission implements Serializable { + + public static final String PCT = "pct"; + + @DN + private String dn; + @AttributeName(name = "oxStatus") + private String status; + @AttributeName(name = "oxTicket", consistency = true) + private String ticket; + @AttributeName(name = "oxConfigurationCode") + private String configurationCode; + @AttributeName(name = "exp") + private Date expirationDate; + @AttributeName(name = "del") + private boolean deletable = true; + + @AttributeName(name = "oxResourceSetId") + private String resourceId; + @AttributeName(name = "oxAuthUmaScope") + private List scopeDns; + + @JsonObject + @AttributeName(name = "oxAttributes") + private Map attributes; + + @Expiration + private Integer ttl; + + private boolean expired; + + public UmaPermission() { + } + + public UmaPermission(String resourceId, List scopes, String ticket, + String configurationCode, Pair expirationDate) { + this.resourceId = resourceId; + this.scopeDns = scopes; + this.ticket = ticket; + this.configurationCode = configurationCode; + this.expirationDate = expirationDate.getFirst(); + this.ttl = expirationDate.getSecond(); + + checkExpired(); + } + + public Integer getTtl() { + return ttl; + } + + public void setTtl(Integer ttl) { + this.ttl = ttl; + } + + public void resetTtlFromExpirationDate() { + final long ttl = Duration.between(new Date().toInstant(), getExpirationDate().toInstant()).getSeconds(); + setTtl((int) ttl); + } + + public String getDn() { + return dn; + } + + public void setDn(String p_dn) { + dn = p_dn; + } + + public boolean isDeletable() { + return deletable; + } + + public void setDeletable(boolean deletable) { + this.deletable = deletable; + } + + public void checkExpired() { + checkExpired(new Date()); + } + + public void checkExpired(Date now) { + if (now.after(expirationDate) && deletable) { + expired = true; + } + } + + public boolean isValid() { + return !expired; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getConfigurationCode() { + return configurationCode; + } + + public void setConfigurationCode(String configurationCode) { + this.configurationCode = configurationCode; + } + + public String getTicket() { + return ticket; + } + + public void setTicket(String ticket) { + this.ticket = ticket; + } + + public Date getExpirationDate() { + return expirationDate; + } + + public void setExpirationDate(Date expirationDate) { + this.expirationDate = expirationDate; + } + + public String getResourceId() { + return resourceId; + } + + public void setResourceId(String resourceId) { + this.resourceId = resourceId; + } + + public List getScopeDns() { + if (scopeDns == null) { + scopeDns = new ArrayList(); + } + return scopeDns; + } + + public void setScopeDns(List p_scopeDns) { + scopeDns = p_scopeDns; + } + + public Map getAttributes() { + if (attributes == null) { + attributes = Maps.newHashMap(); + } + return attributes; + } + + public void setAttributes(Map attributes) { + this.attributes = attributes != null ? attributes : new HashMap(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + UmaPermission that = (UmaPermission) o; + + return !(ticket != null ? !ticket.equals(that.ticket) : that.ticket != null); + + } + + @Override + public int hashCode() { + return ticket != null ? ticket.hashCode() : 0; + } + + @Override + public String toString() { + return "UmaPermission{" + + "dn='" + dn + '\'' + + ", status='" + status + '\'' + + ", ticket='" + ticket + '\'' + + ", configurationCode='" + configurationCode + '\'' + + ", expirationDate=" + expirationDate + + ", resourceId='" + resourceId + '\'' + + ", scopeDns=" + scopeDns + + ", expired=" + expired + + '}'; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/persistence/UmaResource.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/persistence/UmaResource.java new file mode 100644 index 00000000..c8cf74c0 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/persistence/UmaResource.java @@ -0,0 +1,252 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.uma.persistence; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.google.common.collect.Lists; +import org.gluu.persist.annotation.*; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * Resource description. + * + * @author Yuriy Zabrovarnyy Date: 10/03/2012 + */ +@DataEntry +@ObjectClass(value = "oxUmaResource") +public class UmaResource implements Serializable { + + @DN + private String dn; + + @AttributeName(ignoreDuringUpdate = true) + private String inum; + + @AttributeName(name = "oxId") + private String id; + + @NotNull(message = "Display name should be not empty") + @AttributeName(name = "displayName") + private String name; + + @AttributeName(name = "oxFaviconImage") + private String iconUri; + + @AttributeName(name = "oxAuthUmaScope", consistency = true) + private List scopes; + + @AttributeName(name = "oxScopeExpression", consistency = true) + private String scopeExpression; + + @AttributeName(name = "oxAssociatedClient", consistency = true) + private List clients; + + @AttributeName(name = "oxResource") + private List resources; + + @AttributeName(name = "oxRevision") + private long rev; + + @AttributeName(name = "owner") + private String creator; + + @AttributeName(name = "description") + private String description; + + @AttributeName(name = "oxType") + private String type; + + @AttributeName(name = "iat") + private Date creationDate; + + @AttributeName(name = "exp") + private Date expirationDate; + + @AttributeName(name = "del") + private boolean deletable = true; + + @Expiration + private Integer ttl; + + public Integer getTtl() { + return ttl; + } + + public void setTtl(Integer ttl) { + this.ttl = ttl; + } + + public void resetTtlFromExpirationDate() { + final long ttl = Duration.between(new Date().toInstant(), getExpirationDate().toInstant()).getSeconds(); + setTtl((int) ttl); + } + + public boolean isDeletable() { + return deletable; + } + + public void setDeletable(boolean deletable) { + this.deletable = deletable; + } + + public String getScopeExpression() { + return scopeExpression; + } + + public void setScopeExpression(String scopeExpression) { + this.scopeExpression = scopeExpression; + } + + public String getDn() { + return dn; + } + + public void setDn(String dn) { + this.dn = dn; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getInum() { + return inum; + } + + public void setInum(String inum) { + this.inum = inum; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public List getClients() { + if (clients == null) { + clients = new ArrayList(); + } + return clients; + } + + public void setClients(List p_clients) { + clients = p_clients; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getIconUri() { + return iconUri; + } + + public void setIconUri(String iconUri) { + this.iconUri = iconUri; + } + + public List getScopes() { + if (scopes == null) scopes = Lists.newArrayList(); + return scopes; + } + + public void setScopes(List scopes) { + this.scopes = scopes; + } + + public List getResources() { + return resources; + } + + public void setResources(List resources) { + this.resources = resources; + } + + public long getRev() { + return rev; + } + + public void setRev(long rev) { + this.rev = rev; + } + + public String getCreator() { + return creator; + } + + public void setCreator(String creator) { + this.creator = creator; + } + + public Date getCreationDate() { + return creationDate; + } + + public void setCreationDate(Date creationDate) { + this.creationDate = creationDate; + } + + public Date getExpirationDate() { + return expirationDate; + } + + public void setExpirationDate(Date expirationDate) { + this.expirationDate = expirationDate; + } + + @JsonIgnore + public boolean isExpired() { + return expirationDate != null && new Date().after(expirationDate); + } + + @Override + public String toString() { + return "UmaResource{" + + "dn='" + dn + '\'' + + ", inum='" + inum + '\'' + + ", id='" + id + '\'' + + ", name='" + name + '\'' + + ", iconUri='" + iconUri + '\'' + + ", scopes=" + scopes + + ", scopeExpression='" + scopeExpression + '\'' + + ", clients=" + clients + + ", resources=" + resources + + ", rev='" + rev + '\'' + + ", creator='" + creator + '\'' + + ", description='" + description + '\'' + + ", type='" + type + '\'' + + ", creationDate=" + creationDate + + ", expirationDate=" + expirationDate + + ", deletable=" + deletable + + '}'; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/wrapper/Token.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/wrapper/Token.java new file mode 100644 index 00000000..3a2fa5ce --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/uma/wrapper/Token.java @@ -0,0 +1,89 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.uma.wrapper; + +import java.io.Serializable; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 15/03/2013 + */ + +public class Token implements Serializable { + + private String authorizationCode; + private String scope; + private String accessToken; + private String refreshToken; + private String idToken; + private Integer expiresIn; + + public Token() { + } + + public Token(String authorizationCode, String refreshToken, String accessToken, String scope, Integer expiresIn) { + this.authorizationCode = authorizationCode; + this.refreshToken = refreshToken; + this.accessToken = accessToken; + this.scope = scope; + this.expiresIn = expiresIn; + } + + public String getAuthorizationCode() { + return authorizationCode; + } + + public Token setAuthorizationCode(String p_authorizationCode) { + authorizationCode = p_authorizationCode; + return this; + } + + public String getRefreshToken() { + return refreshToken; + } + + public Token setRefreshToken(String p_refreshToken) { + refreshToken = p_refreshToken; + return this; + } + + public String getAccessToken() { + return accessToken; + } + + public Token setAccessToken(String p_accessToken) { + accessToken = p_accessToken; + return this; + } + + public String getScope() { + return scope; + } + + public Token setScope(String p_scope) { + scope = p_scope; + return this; + } + + public String getIdToken() { + return idToken; + } + + public Token setIdToken(String p_idToken) { + idToken = p_idToken; + return this; + } + + public Integer getExpiresIn() { + return expiresIn; + } + + public void setExpiresIn(Integer expiresIn) { + this.expiresIn = expiresIn; + } + +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/userinfo/Schema.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/userinfo/Schema.java new file mode 100644 index 00000000..6ec795c0 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/userinfo/Schema.java @@ -0,0 +1,46 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.userinfo; + +/** + * @author Javier Rojas Date: 11.28.2011 + */ +public enum Schema { + + OPEN_ID("openid"); + + private final String paramName; + + private Schema(String paramName) { + this.paramName = paramName; + } + + /** + * Returns the corresponding {@link Schema} for a given parameter. + * + * @param param The schema parameter + * @return The corresponding schema if found, otherwise null. + */ + public static Schema fromString(String param) { + if (param != null) { + for (Schema s : Schema.values()) { + if (param.equals(s.paramName)) { + return s; + } + } + } + return null; + } + + /** + * Returns a string representation of the object. In this case the parameter name. + */ + @Override + public String toString() { + return paramName; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/userinfo/UserInfoErrorResponseType.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/userinfo/UserInfoErrorResponseType.java new file mode 100644 index 00000000..f20a2e61 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/userinfo/UserInfoErrorResponseType.java @@ -0,0 +1,63 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.userinfo; + +import org.gluu.oxauth.model.error.IErrorType; + +/** + * @author Javier Rojas Date: 11.30.2011 + */ +public enum UserInfoErrorResponseType implements IErrorType { + + /** + * The request is missing a required parameter, includes an unsupported parameter or parameter value, repeats + * the same parameter, uses more than one method for including an access token, or is otherwise malformed. + */ + INVALID_REQUEST("invalid_request"), + /** + * The access token provided is expired, revoked, malformed, or invalid for other reasons. Try to request a + * new access token and retry the protected resource. + */ + INVALID_TOKEN("invalid_token"), + /** + * The request requires higher privileges than provided by the access token. + */ + INSUFFICIENT_SCOPE("insufficient_scope"); + + private final String paramName; + + private UserInfoErrorResponseType(String paramName) { + this.paramName = paramName; + } + + public static UserInfoErrorResponseType fromString(String param) { + if (param != null) { + for (UserInfoErrorResponseType err : UserInfoErrorResponseType.values()) { + if (param.equals(err.paramName)) { + return err; + } + } + } + + return null; + } + + /** + * Returns a string representation of the object. In this case the parameter name. + * + * @return The string representation of the object. + */ + @Override + public String toString() { + return paramName; + } + + @Override + public String getParameter() { + return paramName; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/Base64Util.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/Base64Util.java new file mode 100644 index 00000000..252daa85 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/Base64Util.java @@ -0,0 +1,90 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.util; + +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; + +import org.apache.commons.codec.binary.Base64; + +/** + * @author Javier Rojas Blum + * @author Sergey Manoylo + * @version December 17, 2021 + */ +public class Base64Util { + + public static String base64urlencode(byte[] arg) { + String s = Base64.encodeBase64String(arg); // Standard base64 encoder + s = s.split("=")[0]; // Remove any trailing '='s + s = s.replace('+', '-'); // 62nd char of encoding + s = s.replace('/', '_'); // 63rd char of encoding + return s; + } + + public static byte[] base64urldecode(String arg) throws IllegalArgumentException { + String s = removePadding(arg); + return Base64.decodeBase64(s); // Standard base64 decoder + } + + public static String base64urldecodeToString(String arg) throws IllegalArgumentException, UnsupportedEncodingException { + byte[] decoded = base64urldecode(arg); + return new String(decoded, "UTF-8"); + } + + public static String removePadding(String base64UrlEncoded) { + String s = base64UrlEncoded; + s = s.replace('-', '+'); // 62nd char of encoding + s = s.replace('_', '/'); // 63rd char of encoding + switch (s.length() % 4) // Pad with trailing '='s + { + case 0: + break; // No pad chars in this case + case 2: + s += "=="; + break; // Two pad chars + case 3: + s += "="; + break; // One pad char + default: + throw new IllegalArgumentException("Illegal base64url string."); + } + return s; + } + + public static String base64urlencodeUnsignedBigInt(final BigInteger bigInteger) { + return Base64Util.base64urlencode(bigIntegerToUnsignedByteArray(bigInteger)); + } + + public static byte[] unsignedToBytes(int[] plaintextUnsignedBytes) { + byte[] bytes = new byte[plaintextUnsignedBytes.length]; + + for (int i = 0; i < plaintextUnsignedBytes.length; i++) { + bytes[i] = (byte) plaintextUnsignedBytes[i]; + } + + return bytes; + } + + public static String bytesToHex(byte[] bytes) { + StringBuilder result = new StringBuilder(); + for (byte aByte : bytes) { + result.append(String.format("%02x", aByte)); + } + return result.toString(); + } + + public static byte[] bigIntegerToUnsignedByteArray(final BigInteger bigInteger) { + byte[] array = bigInteger.toByteArray(); + if (array[0] == 0) { + byte[] tmp = new byte[array.length - 1]; + System.arraycopy(array, 1, tmp, 0, tmp.length); + array = tmp; + } + return array; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/ByteUtils.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/ByteUtils.java new file mode 100644 index 00000000..7ad553a5 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/ByteUtils.java @@ -0,0 +1,22 @@ +package org.gluu.oxauth.model.util; + +/** + * @author Yuriy Zabrovarnyy + */ +public class ByteUtils { + + private ByteUtils() { + } + + public static int twoBytesAsInt(byte one, byte two) { + return (byteAsInt(one) << 8) | byteAsInt(two); + } + + public static int twoIntsAsInt(int one, int two) { + return (one << 8) | two; + } + + public static int byteAsInt(byte value) { + return value & 0xff; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/CertUtils.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/CertUtils.java new file mode 100644 index 00000000..e7d54dc1 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/CertUtils.java @@ -0,0 +1,177 @@ +package org.gluu.oxauth.model.util; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.lang.StringUtils; +import org.bouncycastle.asn1.x500.RDN; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.asn1.x500.style.IETFUtils; +import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; +import org.bouncycastle.util.encoders.Base64; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.security.AlgorithmParameters; +import java.security.Principal; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * @author Yuriy Zabrovarnyy + */ +public class CertUtils { + + private static final Logger log = LoggerFactory.getLogger(CertUtils.class); + + private CertUtils() { + } + + public static SignatureAlgorithm getSignatureAlgorithm(X509Certificate cert) { + String signAlgName = cert.getSigAlgName(); + + for (SignatureAlgorithm sa : SignatureAlgorithm.values()) { + if (signAlgName.equalsIgnoreCase(sa.getAlgorithm())) { + return sa; + } + } + + /* + Ensures that SignatureAlgorithms `PS256`, `PS384`, and `PS512` work properly on JDK 11 and later without the need + for BouncyCastle. Previous releases referenced a BouncyCastle-specific + algorithm name instead of the Java Security Standard Algorithm Name of + [`RSASSA-PSS`](https://docs.oracle.com/en/java/javase/11/docs/specs/security/standard-names.html#signature-algorithms). + This release ensures the standard name is used moving forward. + */ + if ("RSASSA-PSS".equals(signAlgName)) { + AlgorithmParameters algorithmParameters = CertUtils.getAlgorithmParameters(cert); + if (algorithmParameters == null) { + return null; + } + + String algParamString = algorithmParameters.toString(); + if (algParamString.contains("SHA-256")) { + return SignatureAlgorithm.PS256; + } + if (algParamString.contains("SHA-384")) { + return SignatureAlgorithm.PS384; + } + if (algParamString.contains("SHA-512")) { + return SignatureAlgorithm.PS512; + } + } + return null; + } + + public static AlgorithmParameters getAlgorithmParameters(X509Certificate cert) { + try { + AlgorithmParameters result = AlgorithmParameters.getInstance(cert.getSigAlgName()); + result.init(cert.getSigAlgParams()); + return result; + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return null; + } + + public static X509Certificate x509CertificateFromBytes(byte[] cert) { + try { + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + InputStream bais = new ByteArrayInputStream(cert); + + return (X509Certificate) certFactory.generateCertificate(bais); + } catch (Exception ex) { + log.error("Failed to parse X.509 certificates from bytes", ex); + } + + return null; + } + + /** + * + * @param pem (e.g. "-----BEGIN CERTIFICATE-----MIICsDCCAZigAwIBAgIIdF+Wcca7gzkwDQYJKoZIhvcNAQELBQAwGDEWMBQGA1UEAwwNY2FvajdicjRpcHc2dTAeFw0xNzA4MDcxNDMyMzVaFw0xODA4MDcxNDMyMzZaMBgxFjAUBgNVBAMMDWNhb2o3YnI0aXB3NnUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCdrt40Otrveq46K3BzZuds6wDqsP0kZV+C3GdyTQWl53orBRtPIiEh6BauP17Rr19qadh7t4yFBb5thrXwBewseSNEL4j7sB0YoeNwRsmA29Fjfoe0yeNpLixFadL6dz7ej9xW2suPppIO6jA5SYgL6+S42ZlIauCnSQBKFcdP8QRvgDZBZ4A7CmuloRJst7GQzppa+YWR+Zg3V5reV8Ekrkjxhwgd+rMsGahxijY7Juf2zMgLOXwe68y41SGnn+1RwezAhnJgioGiwY2gP7z2m8yNZXhpUiX+KAP2xvYb60wNYOswuqfpya68rSmYT8mQjld1EPR21dBMjRQ8HfUBAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAAIUlqltRlbqiolGETmAUF8AiC008UCUmI+IsnORbHFSaACKW04m1iFH0OlxuAE1ECj1mlTcKb4md6i7n+Fy+fdGXFL73yhlSiBLu7XW5uN1/dAkynA+mXC5BDFijmvkEAgNLKyh40u/U1u75v2SFS+kLyMeqmVxvUHA7qA8VgyHi/FZzXCfEvxK5jye4L8tkAR34x5j5MpPDMfLkwLegUG+ygX+h/f8luKiQAk7eD4C59c/F0PpigvzcMpyg8+SE9loIEuJ9dRaRaTwIzez3QA7PJtrhu9h0TooTtkmF/Zw9HARrO0qXgT8uNtQDcRXZCItt1Qr7cOJyx2IjTFR2rE=-----END CERTIFICATE-----";) + * @return x509 certificate + */ + public static X509Certificate x509CertificateFromPem(String pem) { + pem = StringUtils.remove(pem, "-----BEGIN CERTIFICATE-----"); + pem = StringUtils.remove(pem, "-----END CERTIFICATE-----"); + return x509CertificateFromBytes(Base64.decode(pem)); + } + + public static String confirmationMethodHashS256(String certificateAsPem) { + if (org.apache.commons.lang.StringUtils.isBlank(certificateAsPem)) { + return ""; + } + try { + certificateAsPem = org.apache.commons.lang.StringUtils.remove(certificateAsPem, "-----BEGIN CERTIFICATE-----"); + certificateAsPem = org.apache.commons.lang.StringUtils.remove(certificateAsPem, "-----END CERTIFICATE-----"); + certificateAsPem = StringUtils.replace(certificateAsPem, "\n", ""); + return Base64Util.base64urlencode(DigestUtils.sha256(Base64.decode(certificateAsPem))); + } catch (Exception e) { + log.error("Failed to hash certificate: " + certificateAsPem, e); + return ""; + } + } + + @NotNull + public static String getCN(@Nullable X509Certificate cert) { + try { + if (cert == null) { + return ""; + } + X500Name x500name = new JcaX509CertificateHolder(cert).getSubject(); + final RDN[] rdns = x500name.getRDNs(BCStyle.CN); + if (rdns == null || rdns.length == 0) { + return ""; + } + RDN cn = rdns[0]; + + if (cn != null && cn.getFirst() != null && cn.getFirst().getValue() != null) { + return IETFUtils.valueToString(cn.getFirst().getValue()); + } + } catch (CertificateEncodingException e) { + log.error(e.getMessage(), e); + } + return ""; + } + + public static boolean equalsRdn(String rdn1, String rdn2) { + if (StringUtils.isBlank(rdn1) || StringUtils.isBlank(rdn2)) + return false; + + X500Name n1 = new X500Name (rdn1); + X500Name n2 = new X500Name (rdn2); + + return n1.equals(n2); + } + + public static X509Certificate getIssuer(final X509Certificate certificate, final List issuers) { + + Principal certPrincipal = certificate.getIssuerDN(); + + X509Certificate issuer = null; + + for (int i = 0; i < issuers.size(); i++) { + X509Certificate currIssuer = issuers.get(i); + Principal currIssuerPrincipal = currIssuer.getSubjectDN(); + + if (certPrincipal.equals(currIssuerPrincipal)) { + issuer = currIssuer; + break; + } + } + + if (issuer == null) { + issuer = certificate; + } + + return issuer; + } + +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/HashUtil.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/HashUtil.java new file mode 100644 index 00000000..490fb2e0 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/HashUtil.java @@ -0,0 +1,50 @@ +package org.gluu.oxauth.model.util; + +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Yuriy Zabrovarnyy + */ +public class HashUtil { + + private final static Logger log = LoggerFactory.getLogger(HashUtil.class); + + private HashUtil() { + } + + public static String getHash(String input, SignatureAlgorithm signatureAlgorithm) { + try { + final byte[] digest; + if (signatureAlgorithm == SignatureAlgorithm.HS256 || + signatureAlgorithm == SignatureAlgorithm.RS256 || + signatureAlgorithm == SignatureAlgorithm.PS256 || + signatureAlgorithm == SignatureAlgorithm.ES256) { + digest = JwtUtil.getMessageDigestSHA256(input); + } else if (signatureAlgorithm == SignatureAlgorithm.HS384 || + signatureAlgorithm == SignatureAlgorithm.RS384 || + signatureAlgorithm == SignatureAlgorithm.PS384 || + signatureAlgorithm == SignatureAlgorithm.ES384) { + digest = JwtUtil.getMessageDigestSHA384(input); + } else if (signatureAlgorithm == SignatureAlgorithm.HS512 || + signatureAlgorithm == SignatureAlgorithm.RS512 || + signatureAlgorithm == SignatureAlgorithm.PS512 || + signatureAlgorithm == SignatureAlgorithm.ES512) { + digest = JwtUtil.getMessageDigestSHA512(input); + } else { // Default + digest = JwtUtil.getMessageDigestSHA256(input); + } + + if (digest != null) { + byte[] lefMostHalf = new byte[digest.length / 2]; + System.arraycopy(digest, 0, lefMostHalf, 0, lefMostHalf.length); + return Base64Util.base64urlencode(lefMostHalf); + } + } catch (Exception e) { + log.error("Failed to calculate hash.", e); + } + + return null; + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/JwtUtil.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/JwtUtil.java new file mode 100644 index 00000000..0b04871b --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/JwtUtil.java @@ -0,0 +1,289 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.util; + +import static org.gluu.oxauth.model.jwk.JWKParameter.ALGORITHM; +import static org.gluu.oxauth.model.jwk.JWKParameter.CERTIFICATE_CHAIN; +import static org.gluu.oxauth.model.jwk.JWKParameter.EXPONENT; +import static org.gluu.oxauth.model.jwk.JWKParameter.JSON_WEB_KEY_SET; +import static org.gluu.oxauth.model.jwk.JWKParameter.KEY_ID; +import static org.gluu.oxauth.model.jwk.JWKParameter.MODULUS; +import static org.gluu.oxauth.model.jwk.JWKParameter.PUBLIC_KEY; +import static org.gluu.oxauth.model.jwk.JWKParameter.X; +import static org.gluu.oxauth.model.jwk.JWKParameter.Y; + +import java.io.IOException; +import java.io.StringReader; +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Provider; +import java.security.Security; +import java.security.cert.X509Certificate; +import java.util.Set; + +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.Response; + +import org.apache.log4j.Logger; +import org.bouncycastle.openssl.PEMParser; +import org.gluu.oxauth.model.crypto.Certificate; +import org.gluu.oxauth.model.crypto.PublicKey; +import org.gluu.oxauth.model.crypto.signature.ECDSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.util.StringHelper; +import org.gluu.util.security.SecurityProviderUtility; +import org.jboss.resteasy.client.jaxrs.ClientHttpEngine; +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; +import org.json.JSONArray; +import org.json.JSONObject; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsonorg.JsonOrgModule; + +/** + * @author Javier Rojas Blum + * @author Yuriy Movchan + * @version December 8, 2018 + */ +public class JwtUtil { + + private static final Logger log = Logger.getLogger(JwtUtil.class); + + public static void printAlgorithmsAndProviders() { + Set algorithms = Security.getAlgorithms("Signature"); + for (String algorithm : algorithms) { + log.trace("Algorithm (Signature): " + algorithm); + } + algorithms = Security.getAlgorithms("MessageDigest"); + for (String algorithm : algorithms) { + log.trace("Algorithm (MessageDigest): " + algorithm); + } + algorithms = Security.getAlgorithms("Cipher"); + for (String algorithm : algorithms) { + log.trace("Algorithm (Cipher): " + algorithm); + } + algorithms = Security.getAlgorithms("Mac"); + for (String algorithm : algorithms) { + log.trace("Algorithm (Mac): " + algorithm); + } + algorithms = Security.getAlgorithms("KeyStore"); + for (String algorithm : algorithms) { + log.trace("Algorithm (KeyStore): " + algorithm); + } + Provider[] providers = Security.getProviders(); + for (Provider provider : providers) { + log.trace("Provider: " + provider.getName()); + } + } + + public static String bytesToHex(byte[] bytes) { + final char[] hexArray = "0123456789abcdef".toCharArray(); + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } + + public static byte[] getMessageDigestSHA256(String data) + throws NoSuchProviderException, NoSuchAlgorithmException, UnsupportedEncodingException { + MessageDigest mda = MessageDigest.getInstance("SHA-256", SecurityProviderUtility.getBCProvider()); + return mda.digest(data.getBytes(Util.UTF8_STRING_ENCODING)); + } + + public static byte[] getMessageDigestSHA384(String data) + throws NoSuchProviderException, NoSuchAlgorithmException, UnsupportedEncodingException { + MessageDigest mda = MessageDigest.getInstance("SHA-384",SecurityProviderUtility.getBCProvider()); + return mda.digest(data.getBytes(Util.UTF8_STRING_ENCODING)); + } + + public static byte[] getMessageDigestSHA512(String data) + throws NoSuchProviderException, NoSuchAlgorithmException, UnsupportedEncodingException { + MessageDigest mda = MessageDigest.getInstance("SHA-512", SecurityProviderUtility.getBCProvider()); + return mda.digest(data.getBytes(Util.UTF8_STRING_ENCODING)); + } + + public static PublicKey getPublicKey( + String jwksUri, String jwks, SignatureAlgorithm signatureAlgorithm, String keyId) { + log.debug("Retrieving JWK..."); + + JSONObject jsonKeyValue = getJsonKey(jwksUri, jwks, keyId); + + if (jsonKeyValue == null) { + return null; + } + + org.gluu.oxauth.model.crypto.PublicKey publicKey = null; + + try { + String resultKeyId = jsonKeyValue.getString(KEY_ID); + if (signatureAlgorithm == null) { + signatureAlgorithm = SignatureAlgorithm.fromString(jsonKeyValue.getString(ALGORITHM)); + if (signatureAlgorithm == null) { + log.error(String.format("Failed to determine key '%s' signature algorithm", resultKeyId)); + return null; + } + } + + JSONObject jsonPublicKey = jsonKeyValue; + if (jsonKeyValue.has(PUBLIC_KEY)) { + // Use internal jwks.json format + jsonPublicKey = jsonKeyValue.getJSONObject(PUBLIC_KEY); + } + + if (signatureAlgorithm == SignatureAlgorithm.RS256 || signatureAlgorithm == SignatureAlgorithm.RS384 || signatureAlgorithm == SignatureAlgorithm.RS512) { + //String alg = jsonKeyValue.getString(ALGORITHM); + //String use = jsonKeyValue.getString(KEY_USE); + String exp = jsonPublicKey.getString(EXPONENT); + String mod = jsonPublicKey.getString(MODULUS); + + BigInteger publicExponent = new BigInteger(1, Base64Util.base64urldecode(exp)); + BigInteger modulus = new BigInteger(1, Base64Util.base64urldecode(mod)); + + publicKey = new RSAPublicKey(modulus, publicExponent); + } else if (signatureAlgorithm == SignatureAlgorithm.ES256 || signatureAlgorithm == SignatureAlgorithm.ES384 || signatureAlgorithm == SignatureAlgorithm.ES512) { + //String alg = jsonKeyValue.getString(ALGORITHM); + //String use = jsonKeyValue.getString(KEY_USE); + //String crv = jsonKeyValue.getString(CURVE); + String xx = jsonPublicKey.getString(X); + String yy = jsonPublicKey.getString(Y); + + BigInteger x = new BigInteger(1, Base64Util.base64urldecode(xx)); + BigInteger y = new BigInteger(1, Base64Util.base64urldecode(yy)); + + publicKey = new ECDSAPublicKey(signatureAlgorithm, x, y); + } + + if (publicKey != null && jsonKeyValue.has(CERTIFICATE_CHAIN)) { + final String BEGIN = "-----BEGIN CERTIFICATE-----"; + final String END = "-----END CERTIFICATE-----"; + + JSONArray certChain = jsonKeyValue.getJSONArray(CERTIFICATE_CHAIN); + String certificateString = BEGIN + "\n" + certChain.getString(0) + "\n" + END; + StringReader sr = new StringReader(certificateString); + PEMParser pemReader = new PEMParser(sr); + X509Certificate cert = (X509Certificate) pemReader.readObject(); + Certificate certificate = new Certificate(signatureAlgorithm, cert); + publicKey.setCertificate(certificate); + } + if (publicKey != null) { + publicKey.setKeyId(resultKeyId); + publicKey.setSignatureAlgorithm(signatureAlgorithm); + } + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + } + + return publicKey; + } + + public static JSONObject getJsonKey(String jwksUri, String jwks, String keyId) { + log.debug("Retrieving JWK Key..."); + + JSONObject jsonKey = null; + try { + if (StringHelper.isEmpty(jwks)) { + javax.ws.rs.client.Client clientRequest = ClientBuilder.newClient(); + try { + Response clientResponse = clientRequest.target(jwksUri).request().buildGet().invoke(); + + int status = clientResponse.getStatus(); + log.debug(String.format("Status: %n%d", status)); + + if (status == 200) { + jwks = clientResponse.readEntity(String.class); + log.debug(String.format("JWK: %s", jwks)); + } + } finally { + clientRequest.close(); + } + } + if (StringHelper.isNotEmpty(jwks)) { + JSONObject jsonObject = new JSONObject(jwks); + JSONArray keys = jsonObject.getJSONArray(JSON_WEB_KEY_SET); + if (keys.length() > 0) { + if (StringHelper.isEmpty(keyId)) { + jsonKey = keys.getJSONObject(0); + } else { + for (int i = 0; i < keys.length(); i++) { + JSONObject kv = keys.getJSONObject(i); + if (kv.getString(KEY_ID).equals(keyId)) { + jsonKey = kv; + break; + } + } + } + } + } + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + } + + return jsonKey; + } + + public static JSONObject getJSONWebKeys(String jwksUri) { + return getJSONWebKeys(jwksUri, null); + } + + public static JSONObject getJSONWebKeys(String jwksUri, ClientHttpEngine engine) { + log.debug("Retrieving jwks " + jwksUri + "..."); + + JSONObject jwks = null; + try { + if (!StringHelper.isEmpty(jwksUri)) { + ClientBuilder clientBuilder = ResteasyClientBuilder.newBuilder(); + if (engine != null) { + ((ResteasyClientBuilder) clientBuilder).httpEngine(engine); + } + + javax.ws.rs.client.Client clientRequest = clientBuilder.build(); + try { + Response clientResponse = clientRequest.target(jwksUri).request().buildGet().invoke(); + + int status = clientResponse.getStatus(); + log.debug(String.format("Status: %n%d", status)); + + if (status == 200) { + jwks = fromJson(clientResponse.readEntity(String.class)); + log.debug(String.format("JWK: %s", jwks)); + } + } finally { + clientRequest.close(); + } + } + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + } + + return jwks; + } + + public static JSONObject fromJson(String json) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JsonOrgModule()); + return mapper.readValue(json, JSONObject.class); + } + + public static void transferIntoJwtClaims(JSONObject jsonObject, Jwt jwt) { + if (jsonObject == null || jwt == null) { + return; + } + + for (String key : jsonObject.keySet()) { + final Object value = jsonObject.opt(key); + jwt.getClaims().setClaimObject(key, value, true); + } + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/Pair.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/Pair.java new file mode 100644 index 00000000..c01e16ad --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/Pair.java @@ -0,0 +1,59 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.util; + +/** + * @author Javier Rojas Blum Date: 09.05.2012 + */ +public class Pair { + + private A first; + private B second; + + public Pair(A first, B second) { + this.first = first; + this.second = second; + } + + public A getFirst() { + return first; + } + + public void setFirst(A first) { + this.first = first; + } + + public B getSecond() { + return second; + } + + public void setSecond(B second) { + this.second = second; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Pair pair = (Pair) o; + + return !(first != null ? !first.equals(pair.first) : pair.first != null) && !(second != null ? !second.equals(pair.second) : pair.second != null); + } + + @Override + public int hashCode() { + int result = first != null ? first.hashCode() : 0; + result = 31 * result + (second != null ? second.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "(" + first + ", " + second + ")"; + } +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/QueryBuilder.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/QueryBuilder.java new file mode 100644 index 00000000..c904614c --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/QueryBuilder.java @@ -0,0 +1,66 @@ +package org.gluu.oxauth.model.util; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +/** + * @author Yuriy Zabrovarnyy + */ +public class QueryBuilder { + + private static final Logger LOG = Logger.getLogger(QueryBuilder.class); + + private final StringBuilder builder; + + public QueryBuilder() { + this(new StringBuilder()); + } + + public QueryBuilder(StringBuilder builder) { + this.builder = builder; + } + + public static QueryBuilder instance() { + return new QueryBuilder(); + } + + public String build() { + return builder.toString(); + } + + public StringBuilder getBuilder() { + return builder; + } + + public void appendIfNotNull(String key, Object value) { + if (value != null) { + append(key, value.toString()); + } + } + + public void append(String key, String value) { + try { + if (StringUtils.isNotBlank(value)) { + if (builder.length() > 0) { + appendAmpersand(); + } + builder.append(key).append("=").append(URLEncoder.encode(value, Util.UTF8_STRING_ENCODING)); + } + } catch (UnsupportedEncodingException e) { + LOG.trace(e.getMessage(), e); + } + } + + public void appendAmpersand() { + builder.append("&"); + } + + @Override + public String toString() { + return build(); + } +} + diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/StringUtils.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/StringUtils.java new file mode 100644 index 00000000..e878cf76 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/StringUtils.java @@ -0,0 +1,204 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.util; + +import org.json.JSONArray; +import org.json.JSONException; +import org.gluu.oxauth.model.common.HasParamName; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.security.SecureRandom; +import java.text.SimpleDateFormat; +import java.util.*; + +import static org.apache.commons.lang.StringUtils.isNotBlank; + +/** + * @author Javier Rojas Blum + * @version July 18, 2017 + */ +public class StringUtils { + + public static final String EMPTY_STRING = ""; + public static final String SPACE = " "; + public static final String EASY_TO_READ_CHARACTERS = "BCDFGHJKLMNPQRSTVWXZ"; + + public static String nullToEmpty(String str) { + if (str == null) { + return EMPTY_STRING; + } else { + return str; + } + } + + public static boolean equals(String str1, String str2) { + if (str1 == null && str2 == null) { + return true; + } else if (str1 == null && str2 != null) {//note: str2!=null is always NOT null, see previous 'if' statement*/ + return false; + } else if (str1 != null && str2 == null) { //note: str1!=null is ALWAYS true + return false; + } else { + return str1 != null && str1.equals(str2); + } + } + + /** + * Method to join array elements of type string + * + * @param inputArray Array which contains strings + * @param glueString String between each array element + * @return String containing all array elements separated by glue string. + */ + public static String implode(String[] inputArray, String glueString) { + String output = EMPTY_STRING; + + if (inputArray != null && inputArray.length > 0) { + StringBuilder sb = new StringBuilder(); + sb.append(inputArray[0]); + + for (int i = 1; i < inputArray.length; i++) { + sb.append(glueString); + sb.append(inputArray[i]); + } + + output = sb.toString(); + } + + return output; + } + + /** + * Method to join a list of elements of type string + * + * @param collection List which contains strings + * @param glueString String between each array element + * @return String containing all array elements separated by glue string. + */ + public static String implode(Collection collection, String glueString) { + String output = EMPTY_STRING; + + if (collection != null && !collection.isEmpty()) { + StringJoiner sb = new StringJoiner(glueString); + + for (Object obj : collection) { + sb.add(obj.toString()); + } + + output = sb.toString(); + } + + return output; + } + + public static String implodeEnum(List inputList, String glueString) { + String output = EMPTY_STRING; + + if (inputList != null && !inputList.isEmpty()) { + StringBuilder sb = new StringBuilder(); + sb.append(inputList.get(0)); + + for (int i = 1; i < inputList.size(); i++) { + sb.append(glueString); + sb.append(inputList.get(i).getParamName()); + } + + output = sb.toString(); + } + + return output; + } + + public static List spaceSeparatedToList(String spaceSeparatedString) { + List list = new ArrayList(); + + if (isNotBlank(spaceSeparatedString)) { + list = Arrays.asList(spaceSeparatedString.split(StringUtils.SPACE)); + } + + return list; + } + + public static JSONArray toJSONArray(List inputList) { + JSONArray jsonArray = new JSONArray(); + + if (inputList != null && !inputList.isEmpty()) { + jsonArray = new JSONArray(inputList); + } + + return jsonArray; + } + + public static List toList(JSONArray jsonArray) throws JSONException { + List list = new ArrayList(); + + if (jsonArray != null) { + for (int i = 0; i < jsonArray.length(); i++) { + list.add(jsonArray.getString(i)); + } + } + return list; + } + + public static Date parseSilently(String p_string) { + try { + SimpleDateFormat parser = new SimpleDateFormat("EEE MMM d HH:mm:ss zzz yyyy"); + return parser.parse(p_string); + } catch (Exception e) { + return null; + } + } + + public static void addQueryStringParam(StringBuilder p_queryStringBuilder, String key, Object value) throws UnsupportedEncodingException { + if (p_queryStringBuilder != null && isNotBlank(key) && value != null) { + if (p_queryStringBuilder.toString().length() > 0) { + p_queryStringBuilder.append("&"); + } + p_queryStringBuilder.append(key).append("=") + .append(URLEncoder.encode(value.toString(), Util.UTF8_STRING_ENCODING)); + } + } + + public static void addQueryStringParam(StringBuilder p_queryStringBuilder, String key, Collection value) throws UnsupportedEncodingException { + if (p_queryStringBuilder != null && isNotBlank(key) && value != null && !value.isEmpty()) { + if (p_queryStringBuilder.toString().length() > 0) { + p_queryStringBuilder.append("&"); + } + p_queryStringBuilder.append(key).append("=") + .append(URLEncoder.encode(value.toString(), Util.UTF8_STRING_ENCODING)); + } + } + + /** + * Generates a code using a base of 20 characters easy to read for users, using parametrized + * length separated by dashes with intervals of 4 characters. + */ + public static String generateRandomReadableCode(byte length) { + StringBuilder sb = new StringBuilder(); + SecureRandom sc = new SecureRandom(); + for (int i = 0; i < length; i++) { + if (i % 4 == 0 && i > 0) { + sb.append('-'); + } + char item = EASY_TO_READ_CHARACTERS.charAt(sc.nextInt(EASY_TO_READ_CHARACTERS.length())); + sb.append(item); + } + return sb.toString(); + } + + /** + * Generates a random code using a byte array as its seed. + * @param seedLength Length of the byte array + */ + public static String generateRandomCode(byte seedLength) { + byte[] seed = new byte[seedLength]; + new SecureRandom().nextBytes(seed); + return Util.byteArrayToHexString(seed); + } + +} \ No newline at end of file diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/SubjectIdentifierGenerator.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/SubjectIdentifierGenerator.java new file mode 100644 index 00000000..acae0ed6 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/SubjectIdentifierGenerator.java @@ -0,0 +1,21 @@ +package org.gluu.oxauth.model.util; + +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.crypto.AbstractCryptoProvider; +import org.gluu.oxauth.model.crypto.CryptoProviderFactory; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; + +/** + * @author Javier Rojas Blum + * @version July 31, 2016 + */ +public class SubjectIdentifierGenerator { + + public static String generatePairwiseSubjectIdentifier(String sectorIdentifier, String localAccountId, String key, + String salt, AppConfiguration configuration) throws Exception { + AbstractCryptoProvider cryptoProvider = CryptoProviderFactory.getCryptoProvider(configuration); + + String signingInput = sectorIdentifier + localAccountId + salt; + return cryptoProvider.sign(signingInput, null, key, SignatureAlgorithm.HS256); + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/URLPatternList.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/URLPatternList.java new file mode 100644 index 00000000..ffbeb30d --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/URLPatternList.java @@ -0,0 +1,167 @@ +package org.gluu.oxauth.model.util; + +import org.apache.commons.lang3.StringUtils; +import org.apache.log4j.Logger; + +import java.net.MalformedURLException; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author Javier Rojas Blum + * @version October 10, 2016 + */ +public class URLPatternList { + + private static final Logger LOG = Logger.getLogger(URLPatternList.class); + + private List urlPatternList; + private boolean wildcardEnabled = false; + + public URLPatternList() { + this(new ArrayList()); + } + + public URLPatternList(List urlPatternList) { + this(urlPatternList, false); + } + + public URLPatternList(List urlPatternList, boolean wildcardEnabled) { + this.urlPatternList = new ArrayList<>(); + this.wildcardEnabled = wildcardEnabled; + + if (urlPatternList != null) { + for (String urlPattern : urlPatternList) { + addListEntry(urlPattern); + } + } + } + + public boolean isUrlListed(String uri) { + if (urlPatternList == null) { + return true; + } + + if (wildcardEnabled) { + uri = StringUtils.replace(uri, "*", "a"); + } + + URI parsedUri = URI.create(uri); + + for (URLPattern pattern : urlPatternList) { + if (pattern.matches(parsedUri)) { + return true; + } + } + + return false; + } + + public void addListEntry(String urlPattern) { + if (urlPatternList == null) { + return; + } + + if (urlPattern.compareTo("*") == 0) { + LOG.debug("Unlimited access to network resources"); + urlPatternList = null; + return; + } + + try { + Pattern parts = Pattern.compile("^((\\*|[A-Za-z-]+):(//)?)?(\\*|((\\*\\.)?[^*/:]+))?(:(\\d+))?(/.*)?"); + Matcher m = parts.matcher(urlPattern); + if (m.matches()) { + String scheme = m.group(2); + String host = m.group(4); + // Special case for two urls which are allowed to have empty hosts + if (("file".equals(scheme) || "content".equals(scheme)) && host == null) host = "*"; + String port = m.group(8); + String path = m.group(9); + if (scheme == null) { + urlPatternList.add(new URLPattern("http", host, port, path)); + urlPatternList.add(new URLPattern("https", host, port, path)); + } else { + urlPatternList.add(new URLPattern(scheme, host, port, path)); + } + } + } catch (Exception e) { + LOG.debug("Failed to add origin " + urlPattern); + } + } + + public boolean isWildcardEnabled() { + return wildcardEnabled; + } + + public void setWildcardEnabled(boolean wildcardEnabled) { + this.wildcardEnabled = wildcardEnabled; + } + + private static class URLPattern { + public Pattern scheme; + public Pattern host; + public Integer port; + public Pattern path; + + public URLPattern(String scheme, String host, String port, String path) throws MalformedURLException { + try { + if (scheme == null || "*".equals(scheme)) { + this.scheme = null; + } else { + this.scheme = Pattern.compile(regexFromPattern(scheme, false), Pattern.CASE_INSENSITIVE); + } + if ("*".equals(host)) { + this.host = null; + } else if (host.startsWith("*.")) { + this.host = Pattern.compile("([a-z0-9.-]*\\.)?" + regexFromPattern(host.substring(2), false), Pattern.CASE_INSENSITIVE); + } else { + this.host = Pattern.compile(regexFromPattern(host, false), Pattern.CASE_INSENSITIVE); + } + if (port == null || "*".equals(port)) { + this.port = null; + } else { + this.port = Integer.parseInt(port, 10); + } + if (path == null || "/*".equals(path)) { + this.path = null; + } else { + this.path = Pattern.compile(regexFromPattern(path, true)); + } + } catch (NumberFormatException e) { + throw new MalformedURLException("Port must be a number"); + } + } + + public boolean matches(URI uri) { + try { + final boolean schemaMatches = scheme == null || scheme.matcher(uri.getScheme()).matches(); + final boolean hostMatches = host == null || host.matcher(uri.getHost()).matches(); + final boolean portMatches = port == null || port.equals(uri.getPort()); + final boolean pathMatches = path == null || path.matcher(uri.getPath()).matches(); + return schemaMatches && hostMatches && portMatches && pathMatches; + } catch (Exception e) { + LOG.debug(e.toString()); + return false; + } + } + + private String regexFromPattern(String pattern, boolean allowWildcards) { + final String toReplace = "\\.[]{}()^$?+|"; + StringBuilder regex = new StringBuilder(); + for (int i = 0; i < pattern.length(); i++) { + char c = pattern.charAt(i); + if (c == '*' && allowWildcards) { + regex.append("."); + } else if (toReplace.indexOf(c) > -1) { + regex.append('\\'); + } + regex.append(c); + } + return regex.toString(); + } + } +} diff --git a/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/Util.java b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/Util.java new file mode 100644 index 00000000..00896756 --- /dev/null +++ b/oxAuth/Model/src/main/java/org/gluu/oxauth/model/util/Util.java @@ -0,0 +1,289 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.util; + +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.databind.AnnotationIntrospector; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; +import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector; +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.gluu.oxauth.model.common.HasParamName; +import org.gluu.persist.annotation.AttributeEnum; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.*; + +/** + * @author Yuriy Zabrovarnyy + * @author Javier Rojas Blum + * @version September 4, 2019 + */ + +public class Util { + + private static final Logger LOG = Logger.getLogger(Util.class); + + public static final String UTF8_STRING_ENCODING = "UTF-8"; + + public static ObjectMapper createJsonMapper() { + final AnnotationIntrospector jaxb = new JaxbAnnotationIntrospector(); + final AnnotationIntrospector jackson = new JacksonAnnotationIntrospector(); + + final AnnotationIntrospector pair = AnnotationIntrospector.pair(jackson, jaxb); + + final ObjectMapper mapper = new ObjectMapper(); + mapper.getDeserializationConfig().with(pair); + mapper.getSerializationConfig().with(pair); + return mapper; + } + + public static String asJsonSilently(Object p_object) { + try { + return asJson(p_object); + } catch (IOException e) { + LOG.trace(e.getMessage(), e); + return ""; + } + } + + public static String asPrettyJson(Object p_object) throws IOException { + final ObjectMapper mapper = createJsonMapper().configure(SerializationFeature.WRAP_ROOT_VALUE, false); + mapper.setDefaultPropertyInclusion(Include.NON_EMPTY); + return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(p_object); + } + + public static String asJson(Object p_object) throws IOException { + final ObjectMapper mapper = createJsonMapper().configure(SerializationFeature.WRAP_ROOT_VALUE, false); + mapper.setDefaultPropertyInclusion(Include.NON_EMPTY); + return mapper.writeValueAsString(p_object); + } + + public static byte[] getBytes(String p_str) throws UnsupportedEncodingException { + return p_str.getBytes(UTF8_STRING_ENCODING); + } + + public static List asList(JSONArray p_array) throws JSONException { + final List result = new ArrayList(); + if (p_array != null) { + final int length = p_array.length(); + if (length > 0) { + for (int i = 0; i < length; i++) { + result.add(p_array.getString(i)); + } + } + } + return result; + } + + public static List asEnumList(JSONArray p_array, Class clazz) + throws JSONException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + final List result = new ArrayList(); + if (p_array != null) { + final int length = p_array.length(); + if (length > 0) { + for (int i = 0; i < length; i++) { + Method method = clazz.getMethod("getByValue", String.class); + result.add((T) method.invoke(null, new Object[]{p_array.getString(i)})); + } + } + } + return result; + } + + public static void addToListIfHas(List p_list, JSONObject jsonObj, String p_key) throws JSONException { + if (jsonObj != null && org.apache.commons.lang.StringUtils.isNotBlank(p_key) && jsonObj.has(p_key)) { + JSONArray array = jsonObj.getJSONArray(p_key); + if (p_list != null && array != null) { + p_list.addAll(asList(array)); + } + } + } + + public static void addToJSONObjectIfNotNull(JSONObject p_jsonObject, String key, Object value) throws JSONException { + if (p_jsonObject != null && value != null && StringUtils.isNotBlank(key)) { + p_jsonObject.put(key, value); + } + } + + public static void addToJSONObjectIfNotNull(JSONObject p_jsonObject, String key, AttributeEnum value) throws JSONException { + if (p_jsonObject != null && value != null && StringUtils.isNotBlank(key)) { + p_jsonObject.put(key, value.getValue()); + } + } + + public static void addToJSONObjectIfNotNull(JSONObject p_jsonObject, String key, String[] value) throws JSONException { + if (p_jsonObject != null && value != null && StringUtils.isNotBlank(key)) { + p_jsonObject.put(key, new JSONArray(Arrays.asList(value))); + } + } + + public static String asString(List p_list) { + final StringBuilder sb = new StringBuilder(); + if (p_list != null && !p_list.isEmpty()) { + for (HasParamName p : p_list) { + sb.append(" ").append(p.getParamName()); + } + } + return sb.toString().trim(); + } + + public static String listAsString(List p_list) { + StringBuilder param = new StringBuilder(); + if (p_list != null && !p_list.isEmpty()) { + for (String item : p_list) { + param.append(" ").append(item); + } + } + return param.toString().trim(); + } + + public static String mapAsString(Map p_map) throws JSONException { + if (p_map == null || p_map.size() == 0) { + return null; + } + + JSONArray jsonArray = new JSONArray(); + for (String key : p_map.keySet()) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put(key, p_map.get(key)); + + jsonArray.put(jsonObject); + } + + return jsonArray.toString(); + } + + public static boolean allNotBlank(String... p_strings) { + if (p_strings != null && p_strings.length > 0) { + for (String s : p_strings) { + if (org.apache.commons.lang.StringUtils.isBlank(s)) { + return false; + } + } + return true; + } + return false; + } + + public static List splittedStringAsList(String p_string, String p_delimiter) { + final List result = new ArrayList(); + if (StringUtils.isNotBlank(p_string)) { + final String[] array = p_string.split(p_delimiter); + if (array.length > 0) { + result.addAll(Arrays.asList(array)); + } + } + return result; + } + + public static List jsonArrayStringAsList(String jsonString) throws JSONException { + final List result = new ArrayList(); + if (StringUtils.isNotBlank(jsonString)) { + JSONArray jsonArray = new JSONArray(jsonString); + + return asList(jsonArray); + } + + return result; + } + + public static JSONArray listToJsonArray(Collection list) { + if (list == null) { + return new JSONArray(); + } + + return new JSONArray(list); + } + + /** + * @param jsonString [{"CustomHeader1":"custom_header_value_1"},.....,{"CustomHeaderN":"custom_header_value_N"}] + * @return + */ + public static Map jsonObjectArrayStringAsMap(String jsonString) throws JSONException { + Map result = new HashMap(); + + if (!isNullOrEmpty(jsonString)) { + JSONArray jsonArray = new JSONArray(jsonString); + for (int i = 0; i < jsonArray.length(); i++) { + JSONObject jsonObject = jsonArray.getJSONObject(i); + + Iterator keysIter = jsonObject.keys(); + while (keysIter.hasNext()) { + String key = keysIter.next(); + String value = jsonObject.getString(key); + result.put(key, value); + } + } + } + + return result; + } + + public static T firstItem(List items) { + if (items == null) { + return null; + } + + Iterator iterator = items.iterator(); + if (iterator.hasNext()) { + return iterator.next(); + } + + return null; + } + + public static boolean isNullOrEmpty(String string) { + return string == null || string.length() == 0; + } + + public static int parseIntSilently(String intString) { + return parseIntSilently(intString, -1); + } + + public static int parseIntSilently(String intString, int defaultValue) { + try { + return Integer.parseInt(intString); + } catch (Exception e) { + return defaultValue; + } + } + + // SHA-1 (160 bits) + public static String toSHA1HexString(String input) { + MessageDigest md = null; + try { + md = MessageDigest.getInstance("SHA-1"); + return byteArrayToHexString(md.digest(input.getBytes())); + } + catch(NoSuchAlgorithmException e) { + e.printStackTrace(); + } + + return null; + } + + public static String byteArrayToHexString(byte[] b) { + String result = ""; + for (int i=0; i < b.length; i++) { + result += + Integer.toString( ( b[i] & 0xff ) + 0x100, 16).substring( 1 ); + } + return result; + } + +} diff --git a/oxAuth/Model/src/main/resources/json_logic.js b/oxAuth/Model/src/main/resources/json_logic.js new file mode 100644 index 00000000..085235f1 --- /dev/null +++ b/oxAuth/Model/src/main/resources/json_logic.js @@ -0,0 +1,464 @@ +/* globals define,module */ +/* + Using a Universal Module Loader that should be browser, require, and AMD friendly + http://ricostacruz.com/cheatsheets/umdjs.html + */ +;(function(root, factory) { + if (typeof define === "function" && define.amd) { + define(factory); + } else if (typeof exports === "object") { + module.exports = factory(); + } else { + root.jsonLogic = factory(); + } +}(this, function() { + "use strict"; + /* globals console:false */ + + if ( ! Array.isArray) { + Array.isArray = function(arg) { + return Object.prototype.toString.call(arg) === "[object Array]"; + }; + } + + /** + * Return an array that contains no duplicates (original not modified) + * @param {array} array Original reference array + * @return {array} New array with no duplicates + */ + function arrayUnique(array) { + var a = []; + for (var i=0, l=array.length; i": function(a, b) { + return a > b; + }, + ">=": function(a, b) { + return a >= b; + }, + "<": function(a, b, c) { + return (c === undefined) ? a < b : (a < b) && (b < c); + }, + "<=": function(a, b, c) { + return (c === undefined) ? a <= b : (a <= b) && (b <= c); + }, + "!!": function(a) { + return jsonLogic.truthy(a); + }, + "!": function(a) { + return !jsonLogic.truthy(a); + }, + "%": function(a, b) { + return a % b; + }, + "log": function(a) { + console.log(a); return a; + }, + "in": function(a, b) { + if(typeof b.indexOf === "undefined") return false; + return (b.indexOf(a) !== -1); + }, + "cat": function() { + return Array.prototype.join.call(arguments, ""); + }, + "substr":function(source, start, end) { + if(end < 0){ + // JavaScript doesn't support negative end, this emulates PHP behavior + var temp = String(source).substr(start); + return temp.substr(0, temp.length + end); + } + return String(source).substr(start, end); + }, + "+": function() { + return Array.prototype.reduce.call(arguments, function(a, b) { + return parseFloat(a, 10) + parseFloat(b, 10); + }, 0); + }, + "*": function() { + return Array.prototype.reduce.call(arguments, function(a, b) { + return parseFloat(a, 10) * parseFloat(b, 10); + }); + }, + "-": function(a, b) { + if(b === undefined) { + return -a; + }else{ + return a - b; + } + }, + "/": function(a, b) { + return a / b; + }, + "min": function() { + return Math.min.apply(this, arguments); + }, + "max": function() { + return Math.max.apply(this, arguments); + }, + "merge": function() { + return Array.prototype.reduce.call(arguments, function(a, b) { + return a.concat(b); + }, []); + }, + "var": function(a, b) { + var not_found = (b === undefined) ? null : b; + var data = this; + if(typeof a === "undefined" || a==="" || a===null) { + return data; + } + var sub_props = String(a).split("."); + for(var i = 0; i < sub_props.length; i++) { + if(data === null) { + return not_found; + } + // Descending into data + data = data[sub_props[i]]; + if(data === undefined) { + return not_found; + } + } + return data; + }, + "missing": function() { + /* + Missing can receive many keys as many arguments, like {"missing:[1,2]} + Missing can also receive *one* argument that is an array of keys, + which typically happens if it's actually acting on the output of another command + (like 'if' or 'merge') + */ + + var missing = []; + var keys = Array.isArray(arguments[0]) ? arguments[0] : arguments; + + for(var i = 0; i < keys.length; i++) { + var key = keys[i]; + var value = jsonLogic.apply({"var": key}, this); + if(value === null || value === "") { + missing.push(key); + } + } + + return missing; + }, + "missing_some": function(need_count, options) { + // missing_some takes two arguments, how many (minimum) items must be present, and an array of keys (just like 'missing') to check for presence. + var are_missing = jsonLogic.apply({"missing": options}, this); + + if(options.length - are_missing.length >= need_count) { + return []; + }else{ + return are_missing; + } + }, + "method": function(obj, method, args) { + return obj[method].apply(obj, args); + }, + + }; + + jsonLogic.is_logic = function(logic) { + return ( + typeof logic === "object" && // An object + logic !== null && // but not null + ! Array.isArray(logic) && // and not an array + Object.keys(logic).length === 1 // with exactly one key + ); + }; + + /* + This helper will defer to the JsonLogic spec as a tie-breaker when different language interpreters define different behavior for the truthiness of primitives. E.g., PHP considers empty arrays to be falsy, but Javascript considers them to be truthy. JsonLogic, as an ecosystem, needs one consistent answer. + + Spec and rationale here: http://jsonlogic.com/truthy + */ + jsonLogic.truthy = function(value) { + if(Array.isArray(value) && value.length === 0) { + return false; + } + return !! value; + }; + + + jsonLogic.get_operator = function(logic) { + return Object.keys(logic)[0]; + }; + + jsonLogic.get_values = function(logic) { + return logic[jsonLogic.get_operator(logic)]; + }; + + jsonLogic.apply = function(logic, data) { + // Does this array contain logic? Only one way to find out. + if(Array.isArray(logic)) { + return logic.map(function(l) { + return jsonLogic.apply(l, data); + }); + } + // You've recursed to a primitive, stop! + if( ! jsonLogic.is_logic(logic) ) { + return logic; + } + + data = data || {}; + + var op = jsonLogic.get_operator(logic); + var values = logic[op]; + var i; + var current; + var scopedLogic, scopedData, filtered, initial; + + // easy syntax for unary operators, like {"var" : "x"} instead of strict {"var" : ["x"]} + if( ! Array.isArray(values)) { + values = [values]; + } + + // 'if', 'and', and 'or' violate the normal rule of depth-first calculating consequents, let each manage recursion as needed. + if(op === "if" || op == "?:") { + /* 'if' should be called with a odd number of parameters, 3 or greater + This works on the pattern: + if( 0 ){ 1 }else{ 2 }; + if( 0 ){ 1 }else if( 2 ){ 3 }else{ 4 }; + if( 0 ){ 1 }else if( 2 ){ 3 }else if( 4 ){ 5 }else{ 6 }; + + The implementation is: + For pairs of values (0,1 then 2,3 then 4,5 etc) + If the first evaluates truthy, evaluate and return the second + If the first evaluates falsy, jump to the next pair (e.g, 0,1 to 2,3) + given one parameter, evaluate and return it. (it's an Else and all the If/ElseIf were false) + given 0 parameters, return NULL (not great practice, but there was no Else) + */ + for(i = 0; i < values.length - 1; i += 2) { + if( jsonLogic.truthy( jsonLogic.apply(values[i], data) ) ) { + return jsonLogic.apply(values[i+1], data); + } + } + if(values.length === i+1) return jsonLogic.apply(values[i], data); + return null; + }else if(op === "and") { // Return first falsy, or last + for(i=0; i < values.length; i+=1) { + current = jsonLogic.apply(values[i], data); + if( ! jsonLogic.truthy(current)) { + return current; + } + } + return current; // Last + }else if(op === "or") {// Return first truthy, or last + for(i=0; i < values.length; i+=1) { + current = jsonLogic.apply(values[i], data); + if( jsonLogic.truthy(current) ) { + return current; + } + } + return current; // Last + + + + + }else if(op === 'filter'){ + scopedData = jsonLogic.apply(values[0], data); + scopedLogic = values[1]; + + if ( ! Array.isArray(scopedData)) { + return []; + } + // Return only the elements from the array in the first argument, + // that return truthy when passed to the logic in the second argument. + // For parity with JavaScript, reindex the returned array + return scopedData.filter(function(datum){ + return jsonLogic.truthy( jsonLogic.apply(scopedLogic, datum)); + }); + }else if(op === 'map'){ + scopedData = jsonLogic.apply(values[0], data); + scopedLogic = values[1]; + + if ( ! Array.isArray(scopedData)) { + return []; + } + + return scopedData.map(function(datum){ + return jsonLogic.apply(scopedLogic, datum); + }); + + }else if(op === 'reduce'){ + scopedData = jsonLogic.apply(values[0], data); + scopedLogic = values[1]; + initial = typeof values[2] !== 'undefined' ? values[2] : null; + + if ( ! Array.isArray(scopedData)) { + return initial; + } + + return scopedData.reduce( + function(accumulator, current){ + return jsonLogic.apply( + scopedLogic, + {'current':current, 'accumulator':accumulator} + ); + }, + initial + ); + + }else if(op === "all") { + scopedData = jsonLogic.apply(values[0], data); + scopedLogic = values[1]; + // All of an empty set is false. Note, some and none have correct fallback after the for loop + if( ! scopedData.length) { + return false; + } + for(i=0; i < scopedData.length; i+=1) { + if( ! jsonLogic.truthy( jsonLogic.apply(scopedLogic, scopedData[i]) )) { + return false; // First falsy, short circuit + } + } + return true; // All were truthy + }else if(op === "none") { + filtered = jsonLogic.apply({'filter' : values}, data); + return filtered.length === 0; + + }else if(op === "some") { + filtered = jsonLogic.apply({'filter' : values}, data); + return filtered.length > 0; + } + + // Everyone else gets immediate depth-first recursion + values = values.map(function(val) { + return jsonLogic.apply(val, data); + }); + + + // The operation is called with "data" bound to its "this" and "values" passed as arguments. + // Structured commands like % or > can name formal arguments while flexible commands (like missing or merge) can operate on the pseudo-array arguments + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments + if(typeof operations[op] === "function") { + return operations[op].apply(data, values); + }else if(op.indexOf(".") > 0) { // Contains a dot, and not in the 0th position + var sub_ops = String(op).split("."); + var operation = operations; + for(i = 0; i < sub_ops.length; i++) { + // Descending into operations + operation = operation[sub_ops[i]]; + if(operation === undefined) { + throw new Error("Unrecognized operation " + op + + " (failed at " + sub_ops.slice(0, i+1).join(".") + ")"); + } + } + + return operation.apply(data, values); + } + + throw new Error("Unrecognized operation " + op ); + }; + + jsonLogic.uses_data = function(logic) { + var collection = []; + + if( jsonLogic.is_logic(logic) ) { + var op = jsonLogic.get_operator(logic); + var values = logic[op]; + + if( ! Array.isArray(values)) { + values = [values]; + } + + if(op === "var") { + // This doesn't cover the case where the arg to var is itself a rule. + collection.push(values[0]); + }else{ + // Recursion! + values.map(function(val) { + collection.push.apply(collection, jsonLogic.uses_data(val) ); + }); + } + } + + return arrayUnique(collection); + }; + + jsonLogic.add_operation = function(name, code) { + operations[name] = code; + }; + + jsonLogic.rm_operation = function(name) { + delete operations[name]; + }; + + jsonLogic.rule_like = function(rule, pattern) { + // console.log("Is ". JSON.stringify(rule) . " like " . JSON.stringify(pattern) . "?"); + if(pattern === rule) { + return true; + } // TODO : Deep object equivalency? + if(pattern === "@") { + return true; + } // Wildcard! + if(pattern === "number") { + return (typeof rule === "number"); + } + if(pattern === "string") { + return (typeof rule === "string"); + } + if(pattern === "array") { + // !logic test might be superfluous in JavaScript + return Array.isArray(rule) && ! jsonLogic.is_logic(rule); + } + + if(jsonLogic.is_logic(pattern)) { + if(jsonLogic.is_logic(rule)) { + var pattern_op = jsonLogic.get_operator(pattern); + var rule_op = jsonLogic.get_operator(rule); + + if(pattern_op === "@" || pattern_op === rule_op) { + // echo "\nOperators match, go deeper\n"; + return jsonLogic.rule_like( + jsonLogic.get_values(rule, false), + jsonLogic.get_values(pattern, false) + ); + } + } + return false; // pattern is logic, rule isn't, can't be eq + } + + if(Array.isArray(pattern)) { + if(Array.isArray(rule)) { + if(pattern.length !== rule.length) { + return false; + } + /* + Note, array order MATTERS, because we're using this array test logic to consider arguments, where order can matter. (e.g., + is commutative, but '-' or 'if' or 'var' are NOT) + */ + for(var i = 0; i < pattern.length; i += 1) { + // If any fail, we fail + if( ! jsonLogic.rule_like(rule[i], pattern[i])) { + return false; + } + } + return true; // If they *all* passed, we pass + }else{ + return false; // Pattern is array, rule isn't + } + } + + // Not logic, not array, not a === match for rule. + return false; + }; + + return jsonLogic; +})); \ No newline at end of file diff --git a/oxAuth/Model/src/test/java/org/gluu/oxauth/model/authorize/CodeVerifierTest.java b/oxAuth/Model/src/test/java/org/gluu/oxauth/model/authorize/CodeVerifierTest.java new file mode 100644 index 00000000..b7676e8a --- /dev/null +++ b/oxAuth/Model/src/test/java/org/gluu/oxauth/model/authorize/CodeVerifierTest.java @@ -0,0 +1,56 @@ +package org.gluu.oxauth.model.authorize; + +import org.gluu.oxauth.model.authorize.CodeVerifier; +import org.testng.annotations.Test; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import static org.testng.Assert.*; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 21/03/2016 + */ + +public class CodeVerifierTest { + + @Test + public void verifierAndChallengeMatch() throws NoSuchAlgorithmException, UnsupportedEncodingException { + assertMatch(CodeVerifier.CodeChallengeMethod.PLAIN); + assertMatch(CodeVerifier.CodeChallengeMethod.S256); + + assertFalse(CodeVerifier.matched(null, "", "invalid_code")); + } + + @Test + public void verify() { + String codeChallenge = CodeVerifier.generateCodeChallenge(CodeVerifier.CodeChallengeMethod.S256, "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"); + + assertEquals(codeChallenge, "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"); + } + + @Test + public void codeVerificationGenerator() { + for (int i = 0; i < 10; i++) { + assertTrue(CodeVerifier.isCodeVerifierValid(CodeVerifier.generateCodeVerifier())); + } + } + + private static void assertMatch(CodeVerifier.CodeChallengeMethod type) throws NoSuchAlgorithmException, UnsupportedEncodingException { + CodeVerifier verifier = new CodeVerifier(type); + System.out.println(verifier); + + if (type == CodeVerifier.CodeChallengeMethod.PLAIN) { + assertEquals(verifier.getCodeChallenge(), verifier.getCodeVerifier()); + return; + } + + MessageDigest md = MessageDigest.getInstance(type.getMessageDigestString()); + md.update(verifier.getCodeVerifier().getBytes("UTF-8")); // Change this to "UTF-16" if needed + byte[] digest = md.digest(); + + assertEquals(CodeVerifier.base64UrlEncode(digest), verifier.getCodeChallenge()); + } +} diff --git a/oxAuth/Model/src/test/java/org/gluu/oxauth/model/crypto/binding/TokenBindingParserTest.java b/oxAuth/Model/src/test/java/org/gluu/oxauth/model/crypto/binding/TokenBindingParserTest.java new file mode 100644 index 00000000..5a4f26eb --- /dev/null +++ b/oxAuth/Model/src/test/java/org/gluu/oxauth/model/crypto/binding/TokenBindingParserTest.java @@ -0,0 +1,28 @@ +package org.gluu.oxauth.model.crypto.binding; + +import org.gluu.oxauth.model.crypto.binding.TokenBinding; +import org.gluu.oxauth.model.crypto.binding.TokenBindingMessage; +import org.gluu.oxauth.model.crypto.binding.TokenBindingParseException; +import org.gluu.oxauth.model.crypto.binding.TokenBindingType; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * @author Yuriy Zabrovarnyy + */ +public class TokenBindingParserTest { + + @Test + public void testParsingAndSHA256hashOfTokenBindingId() throws TokenBindingParseException { + // values taken from spec: http://openid.net/specs/openid-connect-token-bound-authentication-1_0-03.html + String encoded = "ARIAAgBBQCfsI1D1sTq5mvT_2H_dihNIvuHJCHGjHPJchPavNbGrOo26-2JgT_IsbvZd4daDFbirYBIwJ-TK1rh8FzrC-psAQO4Au9xPupLSkhwT9Y" + + "n9aSvHXFsMLh4d4cEBKGP1clJtsfUFGDw-8HQSKwgKFN3WfZGq27y8NB3NAM1oNzvqVOIAAAECAEFArPIiuZxj9gK0dWhIcG63r2-sZ8V3LX9gpNl8Um_oGOtmwoP1v0VHNI" + + "HEOzW3BOqcBLvUzVEG6a6KGEj3GrFcqQBA9YxqHPBIuDui_aQ1SoRGKyBEhaG2i-Wke3erRb1YwC7nTgrpqqJG3z1P8bt7cjZN6TpOyktdSSK7OJgiApwG7AAA"; + String expectedIdHash = "suMuxh_IlrP-Zrj33LuQOQ5rX039cmBe-wt2df3BrUQ"; + + TokenBindingMessage message = new TokenBindingMessage(encoded); + TokenBinding referredBinding = message.getFirstTokenBindingByType(TokenBindingType.REFERRED_TOKEN_BINDING); + + Assert.assertEquals(expectedIdHash, referredBinding.getTokenBindingID().sha256base64url()); + } +} diff --git a/oxAuth/Model/src/test/java/org/gluu/oxauth/model/jwt/JwtClaimsTest.java b/oxAuth/Model/src/test/java/org/gluu/oxauth/model/jwt/JwtClaimsTest.java new file mode 100644 index 00000000..6a12b0eb --- /dev/null +++ b/oxAuth/Model/src/test/java/org/gluu/oxauth/model/jwt/JwtClaimsTest.java @@ -0,0 +1,50 @@ +package org.gluu.oxauth.model.jwt; + +import com.google.common.collect.Lists; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; + +/** + * @author Yuriy Z + */ +public class JwtClaimsTest { + + @Test + public void setClaimObject_whenSetSameValue_shouldNotCreateDuplicate() { + JwtClaims claims = new JwtClaims(); + claims.addAudience("client1"); + + claims.setClaimObject("aud", "client1", false); + assertEquals(claims.getClaim("aud"), "client1"); + } + + @Test + public void setClaimObject_whenSetDifferentValues_shouldCreateCorrectArray() { + JwtClaims claims = new JwtClaims(); + claims.addAudience("client1"); + + claims.setClaimObject("aud", "client2", false); + assertEquals(claims.getClaim("aud"), Lists.newArrayList("client1", "client2")); + } + + @Test + public void setClaimObject_whenSetDifferentValue_shouldCreateCorrectArray() { + JwtClaims claims = new JwtClaims(); + claims.addAudience("client1"); + + claims.setClaimObject("aud", "client2", false); + claims.setClaimObject("aud", "client3", false); + assertEquals(claims.getClaim("aud"), Lists.newArrayList("client1", "client2", "client3")); + } + + @Test + public void setClaimObject_whenSetDifferentValueWithOverride_shouldOverrideValue() { + JwtClaims claims = new JwtClaims(); + claims.addAudience("client1"); + + claims.setClaimObject("aud", "client2", false); + claims.setClaimObject("aud", "client3", true); + assertEquals(claims.getClaim("aud"), "client3"); + } +} diff --git a/oxAuth/Model/src/test/java/org/gluu/oxauth/model/jwt/JwtTypeTest.java b/oxAuth/Model/src/test/java/org/gluu/oxauth/model/jwt/JwtTypeTest.java new file mode 100644 index 00000000..3668fcff --- /dev/null +++ b/oxAuth/Model/src/test/java/org/gluu/oxauth/model/jwt/JwtTypeTest.java @@ -0,0 +1,16 @@ +package org.gluu.oxauth.model.jwt; + +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; + +/** + * @author Yuriy Z + */ +public class JwtTypeTest { + + @Test + public void jwtTypeHeader_mustBeUppercased() { + assertEquals(JwtType.JWT.toString(), "JWT"); + } +} diff --git a/oxAuth/Model/src/test/java/org/gluu/oxauth/model/uma/JsonLogicNodeParserTest.java b/oxAuth/Model/src/test/java/org/gluu/oxauth/model/uma/JsonLogicNodeParserTest.java new file mode 100644 index 00000000..2202b812 --- /dev/null +++ b/oxAuth/Model/src/test/java/org/gluu/oxauth/model/uma/JsonLogicNodeParserTest.java @@ -0,0 +1,26 @@ +package org.gluu.oxauth.model.uma; + +import org.apache.commons.io.IOUtils; +import org.gluu.oxauth.model.uma.JsonLogicNode; +import org.gluu.oxauth.model.uma.JsonLogicNodeParser; +import org.testng.annotations.Test; + +import java.io.IOException; + +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +/** + * @author yuriyz + */ +public class JsonLogicNodeParserTest { + + @Test + public void parse() throws IOException { + String json = IOUtils.toString(JsonLogicNodeParserTest.class.getClassLoader().getResourceAsStream("json-logic-node.json")); + JsonLogicNode node = JsonLogicNodeParser.parseNode(json); + + assertNotNull(node); + assertTrue(node.isValid()); + } +} diff --git a/oxAuth/Model/src/test/java/org/gluu/oxauth/model/uma/JsonLogicTest.java b/oxAuth/Model/src/test/java/org/gluu/oxauth/model/uma/JsonLogicTest.java new file mode 100644 index 00000000..f90c8d65 --- /dev/null +++ b/oxAuth/Model/src/test/java/org/gluu/oxauth/model/uma/JsonLogicTest.java @@ -0,0 +1,71 @@ +package org.gluu.oxauth.model.uma; + +import org.gluu.oxauth.model.uma.JsonLogic; +import org.testng.Assert; +import org.testng.annotations.Test; + +import javax.script.ScriptException; + +/** + * @author yuriyz + */ +public class JsonLogicTest { + + @Test + public void testJsEngine() throws ScriptException, NoSuchMethodException { + JsonLogic.eval("var fun1 = function(name) {\n" + + " print('Hi there from Javascript, ' + name);\n" + + " return \"greetings from javascript\";\n" + + "};"); + Object result = JsonLogic.invokeFunction("fun1", ""); + Assert.assertEquals(result, "greetings from javascript"); + } + + @Test + public void testJsonLogic() throws ScriptException, NoSuchMethodException { + assertTrue("jsonLogic.apply( { \"==\" : [1, 1] } );"); + assertFalse("jsonLogic.apply( { \"==\" : [1, 0] } );"); + + Assert.assertTrue(JsonLogic.apply("{ \"==\" : [1, 1] }")); + Assert.assertFalse(JsonLogic.apply("{ \"==\" : [1, 0] }")); + + assertTrue("jsonLogic.apply(\n" + + " {\"and\" : [\n" + + " { \">\" : [3,1] },\n" + + " { \"<\" : [1,3] }\n" + + " ] }\n" + + ");"); + } + + @Test + public void umaSimulation() throws ScriptException { + String rule = "{" + + " \"and\": [ {" + + " \"or\": [" + + " {\"var\": 0 }," + + " {\"var\": 1 }" + + " ]" + + " }," + + " {\"var\": 2 }" + + " ]}"; + Assert.assertTrue(JsonLogic.apply(rule, "[true, true, true]")); + Assert.assertTrue(JsonLogic.apply(rule, "[true, false, true]")); + Assert.assertTrue(JsonLogic.apply(rule, "[false, true, true]")); + + Assert.assertFalse(JsonLogic.apply(rule, "[false, false, false]")); + Assert.assertFalse(JsonLogic.apply(rule, "[false, false, true]")); + Assert.assertFalse(JsonLogic.apply(rule, "[true, true, false]")); + } + + private static void assertResult(String script, Boolean expectedResult) throws ScriptException { + Assert.assertEquals(JsonLogic.eval(script), expectedResult); + } + + private static void assertTrue(String script) throws ScriptException { + assertResult(script, Boolean.TRUE); + } + + private static void assertFalse(String script) throws ScriptException { + assertResult(script, Boolean.FALSE); + } +} diff --git a/oxAuth/Model/src/test/java/org/gluu/oxauth/model/uma/TestUtil.java b/oxAuth/Model/src/test/java/org/gluu/oxauth/model/uma/TestUtil.java new file mode 100644 index 00000000..a54de934 --- /dev/null +++ b/oxAuth/Model/src/test/java/org/gluu/oxauth/model/uma/TestUtil.java @@ -0,0 +1,45 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.uma; + +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 16/10/2013 + */ + +public class TestUtil { + + private static final Logger LOG = Logger.getLogger(TestUtil.class); + + private TestUtil() { + } + + public static void assertNotBlank(String p_str) { + assertTrue(StringUtils.isNotBlank(p_str)); + } + + public static void assertErrorResponse(String p_entity) { + assertNotNull(p_entity, "Unexpected result: " + p_entity); + try { + JSONObject jsonObj = new JSONObject(p_entity); + assertTrue(jsonObj.has("error"), "The error type is null"); + assertTrue(jsonObj.has("error_description"), "The error description is null"); + } catch (JSONException e) { + LOG.error(e.getMessage(), e); + fail(e.getMessage() + "\nResponse was: " + p_entity); + } + } +} diff --git a/oxAuth/Model/src/test/java/org/gluu/oxauth/model/uma/UmaTestUtil.java b/oxAuth/Model/src/test/java/org/gluu/oxauth/model/uma/UmaTestUtil.java new file mode 100644 index 00000000..74c70aa8 --- /dev/null +++ b/oxAuth/Model/src/test/java/org/gluu/oxauth/model/uma/UmaTestUtil.java @@ -0,0 +1,107 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.uma; + +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; + +import javax.ws.rs.core.Response; + +import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.Id; +import org.gluu.oxauth.model.uma.wrapper.Token; + +/** + * @author Yuriy Zabrovarnyy + */ +public class UmaTestUtil { + + private UmaTestUtil() { + } + + public static void assert_(UmaScopeDescription p_scopeDescription) { + assertNotNull(p_scopeDescription, "Scope description is null"); + assertTrue(StringUtils.isNotBlank(p_scopeDescription.getName()), "Scope name is empty"); + } + + public static void assert_(RptIntrospectionResponse p_rptStatus) { + assertNotNull(p_rptStatus, "Token response status is null"); + assertTrue(p_rptStatus.getActive(), "Token is not active"); + assertTrue(p_rptStatus.getPermissions() != null && !p_rptStatus.getPermissions().isEmpty(), "Permissions are empty."); + assertNotNull(p_rptStatus.getExpiresAt(), "Expiration date is null"); + } + + public static void assert_(UmaMetadata metadata) { + assertNotNull(metadata, "Metadata is null"); + assertTrue(ArrayUtils.contains(metadata.getGrantTypesSupported(), GrantType.OXAUTH_UMA_TICKET.getValue())); + assertNotNull(metadata.getIssuer(), "Issuer isn't correct"); + assertNotNull(metadata.getTokenEndpoint(), "Token endpoint isn't correct"); + assertNotNull(metadata.getIntrospectionEndpoint(), "Introspection endpoint isn't correct"); + assertNotNull(metadata.getResourceRegistrationEndpoint(), "Resource registration endpoint isn't correct"); + assertNotNull(metadata.getPermissionEndpoint(), "Permission registration endpoint isn't correct"); + assertNotNull(metadata.getAuthorizationEndpoint(), "Authorization request endpoint isn't correct"); + } + + public static void assert_(Token token) { + assertNotNull(token, "The token object is null"); + assertNotNull(token.getAccessToken(), "The access token is null"); + //assertNotNull(p_token.getRefreshToken(), "The refresh token is null"); + } + + public static void assert_(UmaResourceResponse resourceResponse) { + assertNotNull(resourceResponse, "Resource status is null"); + assertNotNull(resourceResponse.getId(), "Resource description id is null"); + } + + public static UmaResource createResource() { + final UmaResource resource = new UmaResource(); + resource.setName("Server Photo Album"); + resource.setIconUri("http://www.example.com/icons/flower.png"); + resource.setScopes(Arrays.asList("http://photoz.example.com/dev/scopes/view", "http://photoz.example.com/dev/scopes/all")); + resource.setType("myType"); + return resource; + } + + public static void assert_(PermissionTicket ticket) { + assertNotNull(ticket, "Ticket is null"); + assertTrue(StringUtils.isNotBlank(ticket.getTicket()), "Ticket is empty"); + } + + public static void assert_(RPTResponse response) { + assertNotNull(response, "RPT response is null"); + assertNotNull(response.getRpt(), "RPT is null"); + } + + public static void assert_(Response p_response) { + assertNotNull(p_response, "Response is null"); + assertTrue(p_response.getStatus() == Response.Status.OK.getStatusCode(), "Response http code is not OK."); + } + + public static void assert_(Id p_id) { + assertNotNull(p_id, "ID is null"); + assertTrue(StringUtils.isNotBlank(p_id.getId()), "ID is blank"); + } + + public static void assert_(UmaTokenResponse response) { + assertNotNull(response, "UMA Token response is null"); + assertNotNull(response.getAccessToken(), "RPT is null"); + assertNotNull(response.getPct(), "PCT is null"); + } + + public static void assert_(UmaNeedInfoResponse response) { + assertNotNull(response, "UMA Need Info response is null"); + assertTrue(StringUtils.isNotBlank(response.getError()), "need_info error is blank"); + assertTrue(StringUtils.isNotBlank(response.getTicket()), "need_info ticket is blank"); + assertTrue(response.getRequiredClaims() != null && !response.getRequiredClaims().isEmpty(), "need_info required claims are empty"); + assertTrue(StringUtils.isNotBlank(response.getRedirectUser()), "need_info redirect user uri is blank"); + } + +} diff --git a/oxAuth/Model/src/test/java/org/gluu/oxauth/model/util/CertUtilsTest.java b/oxAuth/Model/src/test/java/org/gluu/oxauth/model/util/CertUtilsTest.java new file mode 100644 index 00000000..adf7aef9 --- /dev/null +++ b/oxAuth/Model/src/test/java/org/gluu/oxauth/model/util/CertUtilsTest.java @@ -0,0 +1,49 @@ +package org.gluu.oxauth.model.util; + +import org.gluu.oxauth.model.util.CertUtils; +import org.testng.Assert; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +/** + * @author Yuriy Zabrovarnyy + */ +public class CertUtilsTest { + + public static final String TEST_PEM_1 = "-----BEGIN CERTIFICATE-----\n" + + "MIIBBjCBrAIBAjAKBggqhkjOPQQDAjAPMQ0wCwYDVQQDDARtdGxzMB4XDTE4MTAx\n" + + "ODEyMzcwOVoXDTIyMDUwMjEyMzcwOVowDzENMAsGA1UEAwwEbXRsczBZMBMGByqG\n" + + "SM49AgEGCCqGSM49AwEHA0IABNcnyxwqV6hY8QnhxxzFQ03C7HKW9OylMbnQZjjJ\n" + + "/Au08/coZwxS7LfA4vOLS9WuneIXhbGGWvsDSb0tH6IxLm8wCgYIKoZIzj0EAwID\n" + + "SQAwRgIhAP0RC1E+vwJD/D1AGHGzuri+hlV/PpQEKTWUVeORWz83AiEA5x2eXZOV\n" + + "bUlJSGQgjwD5vaUaKlLR50Q2DmFfQj1L+SY=\n" + + "-----END CERTIFICATE-----"; + + public static final String TEST_PEM_2 = "-----BEGIN CERTIFICATE-----MIIBBjCBrAIBAjAKBggqhkjOPQQDAjAPMQ0wCwYDVQQDDARtdGxzMB4XDTE4MTAxODEyMzcwOVoXDTIyMDUwMjEyMzcwOVowDzENMAsGA1UEAwwEbXRsczBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNcnyxwqV6hY8QnhxxzFQ03C7HKW9OylMbnQZjjJ/Au08/coZwxS7LfA4vOLS9WuneIXhbGGWvsDSb0tH6IxLm8wCgYIKoZIzj0EAwIDSQAwRgIhAP0RC1E+vwJD/D1AGHGzuri+hlV/PpQEKTWUVeORWz83AiEA5x2eXZOVbUlJSGQgjwD5vaUaKlLR50Q2DmFfQj1L+SY=-----END CERTIFICATE-----"; + public static final String TEST_PEM_3 = "MIIBBjCBrAIBAjAKBggqhkjOPQQDAjAPMQ0wCwYDVQQDDARtdGxzMB4XDTE4MTAxODEyMzcwOVoXDTIyMDUwMjEyMzcwOVowDzENMAsGA1UEAwwEbXRsczBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNcnyxwqV6hY8QnhxxzFQ03C7HKW9OylMbnQZjjJ/Au08/coZwxS7LfA4vOLS9WuneIXhbGGWvsDSb0tH6IxLm8wCgYIKoZIzj0EAwIDSQAwRgIhAP0RC1E+vwJD/D1AGHGzuri+hlV/PpQEKTWUVeORWz83AiEA5x2eXZOVbUlJSGQgjwD5vaUaKlLR50Q2DmFfQj1L+SY="; + + @Test + public void s256() { + Assert.assertEquals("A4DtL2JmUMhAsvJj5tKyn64SqzmuXbMrJa0n761y5v0", CertUtils.confirmationMethodHashS256(TEST_PEM_1)); + Assert.assertEquals("A4DtL2JmUMhAsvJj5tKyn64SqzmuXbMrJa0n761y5v0", CertUtils.confirmationMethodHashS256(TEST_PEM_2)); + Assert.assertEquals("A4DtL2JmUMhAsvJj5tKyn64SqzmuXbMrJa0n761y5v0", CertUtils.confirmationMethodHashS256(TEST_PEM_3)); + } + + @Test + public void equalsRdn_withCorrectValues_shouldReturnTrueAndIgnoreOrder() { + String r1 = "C=GB,O=OpenBanking,OU=0015800000jfFGuAAM,CN=1g7yUiOr3p0QFnAB1UvInE"; + String r2 = "CN=1g7yUiOr3p0QFnAB1UvInE, OU=0015800000jfFGuAAM, O=OpenBanking, C=GB"; + + assertTrue(CertUtils.equalsRdn(r1, r2)); + } + + @Test + public void equalsRdn_withWrongValues_shouldReturnFalse() { + String r1 = "C=FAILGB,O=OpenBanking,OU=0015800000jfFGuAAM,CN=1g7yUiOr3p0QFnAB1UvInE"; + String r2 = "CN=1g7yUiOr3p0QFnAB1UvInE, OU=0015800000jfFGuAAM, O=OpenBanking, C=GB"; + + assertFalse(CertUtils.equalsRdn(r1, r2)); + } +} diff --git a/oxAuth/Model/src/test/java/org/gluu/oxauth/model/util/HashUtilTest.java b/oxAuth/Model/src/test/java/org/gluu/oxauth/model/util/HashUtilTest.java new file mode 100644 index 00000000..b35953f1 --- /dev/null +++ b/oxAuth/Model/src/test/java/org/gluu/oxauth/model/util/HashUtilTest.java @@ -0,0 +1,43 @@ +package org.gluu.oxauth.model.util; + +import static org.testng.Assert.assertEquals; + +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.util.security.SecurityProviderUtility; +import org.testng.annotations.Test; + +/** + * @author Yuriy Zabrovarnyy + */ +public class HashUtilTest { + + static { + SecurityProviderUtility.installBCProvider(); + } + + private static final String INPUT = "a308bb8f-25b0-4b1f-85a6-778698a35a43"; + + @Test + public void s256Hash() { + assertEquals("hhNHO19gwnEguTE5SAK-GA", HashUtil.getHash(INPUT, SignatureAlgorithm.ES256)); + assertEquals("hhNHO19gwnEguTE5SAK-GA", HashUtil.getHash(INPUT, SignatureAlgorithm.HS256)); + assertEquals("hhNHO19gwnEguTE5SAK-GA", HashUtil.getHash(INPUT, SignatureAlgorithm.PS256)); + assertEquals("hhNHO19gwnEguTE5SAK-GA", HashUtil.getHash(INPUT, SignatureAlgorithm.RS256)); + } + + @Test + public void s384Hash() { + assertEquals("W-f-EBbMtR-505d5wk4m78wd6qn1vQkZ", HashUtil.getHash(INPUT, SignatureAlgorithm.ES384)); + assertEquals("W-f-EBbMtR-505d5wk4m78wd6qn1vQkZ", HashUtil.getHash(INPUT, SignatureAlgorithm.HS384)); + assertEquals("W-f-EBbMtR-505d5wk4m78wd6qn1vQkZ", HashUtil.getHash(INPUT, SignatureAlgorithm.PS384)); + assertEquals("W-f-EBbMtR-505d5wk4m78wd6qn1vQkZ", HashUtil.getHash(INPUT, SignatureAlgorithm.RS384)); + } + + @Test + public void s512Hash() { + assertEquals("CCmNwrkP_FbnPPpQ5f96xpXTDuzHSeGd3jGZ_JrPJo4", HashUtil.getHash(INPUT, SignatureAlgorithm.ES512)); + assertEquals("CCmNwrkP_FbnPPpQ5f96xpXTDuzHSeGd3jGZ_JrPJo4", HashUtil.getHash(INPUT, SignatureAlgorithm.HS512)); + assertEquals("CCmNwrkP_FbnPPpQ5f96xpXTDuzHSeGd3jGZ_JrPJo4", HashUtil.getHash(INPUT, SignatureAlgorithm.PS512)); + assertEquals("CCmNwrkP_FbnPPpQ5f96xpXTDuzHSeGd3jGZ_JrPJo4", HashUtil.getHash(INPUT, SignatureAlgorithm.RS512)); + } +} diff --git a/oxAuth/Model/src/test/java/org/gluu/oxauth/model/util/JwtUtilTest.java b/oxAuth/Model/src/test/java/org/gluu/oxauth/model/util/JwtUtilTest.java new file mode 100644 index 00000000..6c21a9d6 --- /dev/null +++ b/oxAuth/Model/src/test/java/org/gluu/oxauth/model/util/JwtUtilTest.java @@ -0,0 +1,28 @@ +package org.gluu.oxauth.model.util; + +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaims; +import org.json.JSONObject; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; + +/** + * @author Yuriy Zabrovarnyy + */ +public class JwtUtilTest { + + @Test + public void transferIntoJwtClaimsTest() { + JSONObject json = new JSONObject(); + json.put("active", true); + json.put("key", "valueTest"); + + Jwt jwt = new Jwt(); + JwtUtil.transferIntoJwtClaims(json, jwt); + + final JwtClaims claims = jwt.getClaims(); + assertEquals("true", claims.getClaimAsString("active")); + assertEquals("valueTest", claims.getClaimAsString("key")); + } +} diff --git a/oxAuth/Model/src/test/java/org/gluu/oxauth/model/util/Tester.java b/oxAuth/Model/src/test/java/org/gluu/oxauth/model/util/Tester.java new file mode 100644 index 00000000..77f91018 --- /dev/null +++ b/oxAuth/Model/src/test/java/org/gluu/oxauth/model/util/Tester.java @@ -0,0 +1,17 @@ +package org.gluu.oxauth.model.util; + +/** + * @author Yuriy Z + */ +public class Tester { + private Tester() { + } + + public static void showTitle(String title) { + title = "TEST: " + title; + + System.out.println("#######################################################"); + System.out.println(title); + System.out.println("#######################################################"); + } +} diff --git a/oxAuth/Model/src/test/java/org/gluu/oxauth/model/util/URLPatternListTest.java b/oxAuth/Model/src/test/java/org/gluu/oxauth/model/util/URLPatternListTest.java new file mode 100644 index 00000000..f93bc40b --- /dev/null +++ b/oxAuth/Model/src/test/java/org/gluu/oxauth/model/util/URLPatternListTest.java @@ -0,0 +1,54 @@ +package org.gluu.oxauth.model.util; + +import org.testng.annotations.Test; + +import java.util.Collections; +import java.util.List; + +import static org.gluu.oxauth.model.util.Tester.showTitle; +import static org.testng.Assert.assertTrue; +import static org.testng.AssertJUnit.assertFalse; + +/** + * @author Yuriy Z + */ +public class URLPatternListTest { + + @Test + public void isUrlListed_forUrlWithWildcard_shouldMatch() { + showTitle("isUrlListed_forUrlWithWildcard_shouldMatch"); + + List urlPatterns = Collections.singletonList("*.gluu.org"); + + URLPatternList urlPatternList = new URLPatternList(urlPatterns, true); + assertTrue(urlPatternList.isUrlListed("https://*.gluu.org")); + assertTrue(urlPatternList.isUrlListed("https://abc.gluu.org")); + } + + @Test + public void isUrlListed_forUrlWithoutWildcardSupport_shouldFail() { + showTitle("isUrlListed_forUrlWithoutWildcardSupport_shouldFail"); + + List urlPatterns = Collections.singletonList("*.gluu.org"); + + URLPatternList urlPatternList = new URLPatternList(urlPatterns, false); + assertFalse(urlPatternList.isUrlListed("https://*.gluu.org")); + assertTrue(urlPatternList.isUrlListed("https://abc.gluu.org")); + } + + + @Test + public void isUrlListed_forAllowAll_shouldMatch() { + showTitle("isUrlListed_forAllowAll_shouldMatch"); + + List urlPatterns = Collections.singletonList("*"); + + URLPatternList urlPatternList = new URLPatternList(urlPatterns, true); + assertTrue(urlPatternList.isUrlListed("https://*.gluu.org")); + assertTrue(urlPatternList.isUrlListed("https://abc.gluu.org")); + + urlPatternList = new URLPatternList(urlPatterns, false); + assertTrue(urlPatternList.isUrlListed("https://*.gluu.org")); + assertTrue(urlPatternList.isUrlListed("https://abc.gluu.org")); + } +} diff --git a/oxAuth/Model/src/test/resources/json-logic-node.json b/oxAuth/Model/src/test/resources/json-logic-node.json new file mode 100644 index 00000000..891e96f1 --- /dev/null +++ b/oxAuth/Model/src/test/resources/json-logic-node.json @@ -0,0 +1,18 @@ +{ + "rule": { + "and": [ + { + "or": [ + {"var": 0}, + {"var": 1} + ] + }, + {"var": 2} + ] + }, + "data": [ + "http://photoz.example.com/dev/actions/all", + "http://photoz.example.com/dev/actions/add", + "http://photoz.example.com/dev/actions/internalClient" + ] +} \ No newline at end of file diff --git a/oxAuth/Model/src/test/resources/testng-benchmark.xml b/oxAuth/Model/src/test/resources/testng-benchmark.xml new file mode 100644 index 00000000..b27d76a1 --- /dev/null +++ b/oxAuth/Model/src/test/resources/testng-benchmark.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/oxAuth/Model/src/test/resources/testng-multi-authz.xml b/oxAuth/Model/src/test/resources/testng-multi-authz.xml new file mode 100644 index 00000000..cfe493b5 --- /dev/null +++ b/oxAuth/Model/src/test/resources/testng-multi-authz.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/oxAuth/Model/src/test/resources/testng.xml b/oxAuth/Model/src/test/resources/testng.xml new file mode 100644 index 00000000..50b951f7 --- /dev/null +++ b/oxAuth/Model/src/test/resources/testng.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/oxAuth/README b/oxAuth/README new file mode 100644 index 00000000..7a8df2c3 --- /dev/null +++ b/oxAuth/README @@ -0,0 +1,221 @@ +BUILD + +1. Install maven version 3.0.3 or later (see how to install maven here: + http://maven.apache.org/download.html#Installation) + Use JDK version 6 (not 5) + +2. Download the source code from the subversion repository located at: + https://svn.gluu.info/repository/openxdi/oxAuth + +3. Install gluu-core.jar using the command: + mvn install:install-file -Dfile=gluu-core.jar -DgroupId=org.gluu -DartifactId=gluu-core -Dversion=1.0 -Dpackaging=jar + +4. Configure the file Server/src/test/resources/conf/oxauth-ldap.properties + +5. Go to Client directory of oxAuth Project and run command: mvn clean install + +6. Go to Server directory of oxAuth Project and run command: mvn clean install + + + +DEPLOYMENT + +1. Use Tomcat 6.x or later + +2. Use JDK version 6 (not 5) + +3. Copy and edit the files located at Server/conf to TOMCAT_HOME/conf + +4. Copy the file Server/target/oxauth.war to TOMCAT_HOME/webapps + + +To test the deployment: + +1. Edit the file Client/test/resources/testng.xml, change all the test attributes to enabled=“true†and point the URLs + to your deployment. + +2. Go to Client directory of oxAuth Project and run command: mvn test + + +Testing with SSL and self signed certificate: + +1. openssl s_client -connect localhost:8443 + +2. Cut and paste the certificate (including BEGIN and END lines) into a local file localhost.pem + +3. sudo keytool -import -alias localhost -keystore $JAVA_HOME/jre/lib/security/cacerts -file localhost.pem + +4. The default keystore password is: changeit + + +JAVADOC + +1. Generate the documentation using the command: mvn javadoc:jar + + + +INTEGRATE oxAuth WITH YOUR SYSTEM + +1. Register your Web Application as a client in the file TOMCAT_HOME/conf/oxauth-registration.xml + +2. From step 1, make available in your Web Application the following oxAuth registration values: + +client-identifier +client-secret +redirection-uri + +3. In your web app add a link to the following URL (extra line breaks are for display purposes only): + +http://localhost:8080/oxauth/authorize? +response_type=code +&client_id= +&redirect_uri= +&state= + +Where: +- response_type is mandatory and must be set to "code". +- client_id is mandatory and must be set to the value from step 2. +- redirect_uri is mandatory and must be set to the value from step 2. + It must be URL encoded, for example: https%3A%2F%2Fclient.example.com%2Fcb%3ffoo%3dbar + To encode it you can use: java.net.URLEncoder.encode(redirectUri, "UTF-8") +- state is optional but recommended to prevent cross-site request forgery. + It is an opaque value used by the client to maintain state between the request and callback. + So, you generate a value, send it to oxAuth and the state value returned from oxAuth must be the same you sent. + +CODE: +AuthorizationRequest authorizationRequest = new AuthorizationRequest(ResponseType.CODE, clientId); +authorizationRequest.setRedirectUri(redirecturi); +authorizationRequest.setState(state); +String queryString = "http://localhost:8080/oxauth/authorize?" + authorizationRequest.getQueryString(); +// Put the queryString in a link or redirect to it. + +4. In this step oxAuth will ask the user to login if it is not already logged in, and request its permission. + +5. If the user grants permission, oxAuth will redirect to your redirect_uri and send an authorization code as parameter. + For example (extra line breaks are for display purposes only): + +? +code= +&state= + +If user denies the permission you will receive a response like: + +? +error=access_denied +&error_description= +&state= + +6. Use the authorization code you receive in step 5 to request an access token: + +CODE: +String credentials = clientIdentifier + ":" + clientSecret; +String tokenUrl = "http://localhost:8080/oxauth/restv1/token"; +TokenClient tokenClient = new TokenClient(tokenUrl); +TokenResponse response = tokenClient.execAuthorizationCode(authorizationCode, redirectUri, credentials); +String accessToken = response.getAccessToken(); + +Where: +- authorizationCode Received in step 5 +- redirectUri Your redirect URI +- credentials From step 2 concatenated with a colon in the middle: + credentials = clientIdentifier + ":" + clientSecret; + +7. To extract the information encoded in the accessToken (JWT): + +CODE: +JwtToken jwtToken = new JwtToken(accessToken); + +jwtToken.getType(); +jwtToken.getAlgorithm(); +jwtToken.getJsonWebKeyUrl(); +jwtToken.getKeyId(); +jwtToken.getExpirationTime(); +jwtToken.getIssuedAt(); +jwtToken.getIssuer(); +jwtToken.getUserId(); +jwtToken.getAudience(); +jwtToken.getOxInum(); +jwtToken.getOxOpenIdConnectVersion(); + +jwtToken.validateSignature(credentials)); + +8. To validate your accessToken: + +CODE: +validateUrl = "localhost:8080/oxauth/restv1/validate"; +ValidateTokenClient validateTokenClient = new ValidateTokenClient(validateTokenUrl); +ValidateTokenResponse response = validateTokenClient.execValidateToken(accessToken); + +response.isValid(); +response.getExpiresIn(); // Value in seconds + + + +LOCALHOST TEST URL + +http://localhost:8080/oxauth/authorize?response_type=code&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb%3ffoo%3dbar&state=xyz&client_id=@!1111!0008!FF81!2D39 +http://localhost:8080/oxauth/authorize.seam?response_type=code&client_id=06fe985f-4111-41cf-a16d-434ff48f92a2.localhost&redirect_uri=http%3A%2F%2Flocalhost%2FoxServer&state=xyz + + + +REGISTRATION + +$ cat clients.ldif +dn: ou=clients,o=gluu +objectClass: organizationalUnit +objectClass: top +ou: clients + +$ ldapmodify --defaultAdd --port 1389 --bindDN 'cn=directory manager' --bindPassword secret --filename clients.ldif + + + + +$ cat addClient.ldif +dn: inum=@!1111!0000!6216!CCE6,ou=clients,o=gluu +displayName: oxAuth test app +inum: @!1111!0000!6216!CCE6 +objectClass: oxAuthClient +objectClass: top +oxAuthAppType: web +oxAuthClientExpirationDate: 20120120152419.312Z +oxAuthRedirectURI: https://client.example.com/cb +oxAuthRedirectURI: https://client.example.com/cb1 +oxAuthRedirectURI: https://client.example.com/cb2 +oxAuthScope: openid +oxAuthScope: profile +oxAuthScope: address +oxAuthScope: email +oxAuthClientSecret: 607ae292-c8fe-486e-87d8-c28f84f8c0bd + +$ ldapmodify --defaultAdd --port 1389 --bindDN 'cn=directory manager' --bindPassword secret --filename addClient.ldif + + + +oxTrust +client_id: @!1111!0008!C2EB!75F1 + +oxPlus +client_id: @!1111!0008!2A19!9A70 + +oxServer +client_id: @!1111!0008!7119!0560 + +Gluu IDP +client_id: @!1111!0008!45C0!BE6E + +Test +client_id: @!1111!0008!FF81!2D39 + +oxModel +client_id: @!1111!0008!92C1!D277 + +oxGraph +client_id: @!1111!0008!0336!1008 + +oxTestTool +client_id: @!1111!0008!A64C!475C + +client_id: @!1111!0008!31FD!E7E7 + +client_id: @!1111!0008!2D7F!97C2 diff --git a/oxAuth/README.md b/oxAuth/README.md new file mode 100644 index 00000000..18912eb5 --- /dev/null +++ b/oxAuth/README.md @@ -0,0 +1,9 @@ +## oxAuth + +oxAuth is an open source OpenID Connect Provider (OP) and UMA Authorization Server (AS). The project also includes OpenID Connect Client code which can be used by websites to validate tokens. + +oxAuth currently implements all required aspects of the OpenID Connect stack, including an OAuth 2.0 authorization server, Simple Web Discovery, Dynamic Client Registration, JSON Web Tokens, JSON Web Keys, and User Info Endpoint. + +**oxAuth is tightly coupled with [oxTrust](https://github.com/GluuFederation/oxTrust)**. + +oxAuth configuration is stored in LDAP, and oxTrust is needed to generate the proper configuration. For deployment instructions, use the [Gluu Server documentation](https://gluu.org/docs/ce) diff --git a/oxAuth/Server/.gitignore b/oxAuth/Server/.gitignore new file mode 100644 index 00000000..bb4c543a --- /dev/null +++ b/oxAuth/Server/.gitignore @@ -0,0 +1,4 @@ +/target/ +/test-output/ +/WebContent/ +/weld-validation-report.html diff --git a/oxAuth/Server/.metadata/src/main/webapp/WEB-INF/.gitignore b/oxAuth/Server/.metadata/src/main/webapp/WEB-INF/.gitignore new file mode 100644 index 00000000..4f9590ee --- /dev/null +++ b/oxAuth/Server/.metadata/src/main/webapp/WEB-INF/.gitignore @@ -0,0 +1 @@ +/faces-config.pageflow diff --git a/oxAuth/Server/conf/gluu-couchbase.properties b/oxAuth/Server/conf/gluu-couchbase.properties new file mode 100644 index 00000000..80c9f49b --- /dev/null +++ b/oxAuth/Server/conf/gluu-couchbase.properties @@ -0,0 +1,27 @@ +servers: ${config.couchbase.couchbase_servers} + +# Default scan consistency. Possible values are: not_bounded, request_plus, statement_plus +connection.scan-consistency: not_bounded + +# Enable/disable DNS SRV lookup for the bootstrap nodes +# Default dnsSrvEnabled is true +connection.dns.use-lookup: false + +auth.userName: ${config.couchbase.couchbase_server_user} +auth.userPassword: ${config.couchbase.encoded_couchbase_server_pw} + +buckets: ${config.couchbase.couchbase_buckets} + +bucket.default: ${config.couchbase.default_bucket} +bucket.gluu_user.mapping: ${config.bucket.gluu_user.mapping} +bucket.gluu_cache.mapping: ${config.bucket.gluu_cache.mapping} +bucket.gluu_site.mapping: ${config.bucket.gluu_site.mapping} +bucket.gluu_token.mapping: ${config.bucket.gluu_token.mapping} +bucket.gluu_session.mapping: ${config.bucket.gluu_session.mapping} + +password.encryption.method: ${config.couchbase.encryption_method} + +ssl.trustStore.enable: ${config.couchbase.ssl_enabled} +ssl.trustStore.file: ${config.couchbase.couchbaseTrustStoreFn} +ssl.trustStore.pin: ${config.couchbase.encoded_couchbaseTrustStorePass} +ssl.trustStore.type: pkcs12 diff --git a/oxAuth/Server/conf/gluu-ldap.properties b/oxAuth/Server/conf/gluu-ldap.properties new file mode 100644 index 00000000..e9be44cb --- /dev/null +++ b/oxAuth/Server/conf/gluu-ldap.properties @@ -0,0 +1,5 @@ +bindDN=${config.ldap.bindDN} +bindPassword=${config.ldap.bindPassword} +servers=${config.ldap.servers} +maxconnections=${config.ldap.maxConnections} +useSSL=${config.ldap.useSSL} diff --git a/oxAuth/Server/conf/gluu-spanner.properties b/oxAuth/Server/conf/gluu-spanner.properties new file mode 100644 index 00000000..199cbe8e --- /dev/null +++ b/oxAuth/Server/conf/gluu-spanner.properties @@ -0,0 +1,25 @@ +connection.project=${config.spanner.connection.project} +connection.instance=${config.spanner.connection.instance} +connection.database=${config.spanner.connection.database} + +# Prefix connection.client-property.key=value will be coverterd to key=value +# This is reserved for future usage +#connection.client-property=clientPropertyValue + +# spanner creds or emulator +connection.emulator-host=${config.spanner.connection.emulator-host} + +# Password hash method +password.encryption.method=${config.spanner.password.encryption.method} + +# Max time needed to create connection pool in milliseconds +connection.pool.create-max-wait-time-millis=${config.spanner.connection.pool.create-max-wait-time-millis} + +# Maximum allowed statement result set size +statement.limit.default-maximum-result-size=${config.spanner.statement.limit.default-maximum-result-size} + +# Maximum allowed delete statement result set size +statement.limit.maximum-result-delete-size=${config.spanner.statement.limit.maximum-result-delete-size} + +binaryAttributes=${config.spanner.binaryAttributes} +certificateAttributes=${config.spanner.certificateAttributes} diff --git a/oxAuth/Server/conf/gluu-sql.properties b/oxAuth/Server/conf/gluu-sql.properties new file mode 100644 index 00000000..6fe38fde --- /dev/null +++ b/oxAuth/Server/conf/gluu-sql.properties @@ -0,0 +1,25 @@ +db.schema.name=${config.sql.db.schema.name} + +connection.uri=${config.sql.connection.uri} + +connection.driver-property.serverTimezone=${config.sql.connection.driver-property.serverTimezone} + +auth.userName=${config.sql.auth.userName} +auth.userPassword=${config.sql.auth.userPassword} + +# Password hash method +password.encryption.method=${config.sql.password.encryption.method} + +# Connection pool size +connection.pool.max-total=${config.sql.connection.pool.max-total} +connection.pool.max-idle=${config.sql.connection.pool.max-idle} +connection.pool.min-idle=${config.sql.connection.pool.min-idle} + +# Max time needed to create connection pool in milliseconds +connection.pool.create-max-wait-time-millis=${config.sql.connection.pool.create-max-wait-time-millis} + +# Max wait 20 seconds +connection.pool.max-wait-time-millis=${config.sql.connection.pool.max-wait-time-millis} + +# Allow to evict connection in pool after 30 minutes +connection.pool.min-evictable-idle-time-millis=${config.sql.connection.pool.min-evictable-idle-time-millis} diff --git a/oxAuth/Server/conf/gluu.properties b/oxAuth/Server/conf/gluu.properties new file mode 100644 index 00000000..ce54d449 --- /dev/null +++ b/oxAuth/Server/conf/gluu.properties @@ -0,0 +1,8 @@ +persistence.type: ${config.persistence.type} + +oxauth_ConfigurationEntryDN=${config.generic.configurationEntryDN} + +certsDir=${config.generic.certsDir} + +binaryAttributes=objectGUID +certificateAttributes=userCertificate diff --git a/oxAuth/Server/conf/keystore.jks b/oxAuth/Server/conf/keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..2ca9d51cad25f8b0d39c33352d85e2a66329a6da GIT binary patch literal 22029 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 K#=KSu{r)$)yGY3Z literal 0 HcmV?d00001 diff --git a/oxAuth/Server/conf/oxauth-config.json b/oxAuth/Server/conf/oxauth-config.json new file mode 100644 index 00000000..30816eaf --- /dev/null +++ b/oxAuth/Server/conf/oxauth-config.json @@ -0,0 +1,341 @@ +{ + "issuer":"${config.oxauth.issuer}", + "loginPage":"${config.oxauth.contextPath}/login.htm", + "authorizationPage":"${config.oxauth.contextPath}/authorize.htm", + "baseEndpoint":"${config.oxauth.contextPath}/restv1", + "authorizationEndpoint":"${config.oxauth.contextPath}/restv1/authorize", + "tokenEndpoint":"${config.oxauth.contextPath}/restv1/token", + "tokenRevocationEndpoint": "${config.oxauth.contextPath}/restv1/revoke", + "userInfoEndpoint":"${config.oxauth.contextPath}/restv1/userinfo", + "clientInfoEndpoint":"${config.oxauth.contextPath}/restv1/clientinfo", + "checkSessionIFrame":"${config.oxauth.contextPath}/opiframe.htm", + "endSessionEndpoint":"${config.oxauth.contextPath}/restv1/end_session", + "jwksUri":"${config.oxauth.contextPath}/restv1/jwks", + "registrationEndpoint":"${config.oxauth.contextPath}/restv1/register", + "openIdDiscoveryEndpoint":"${config.oxauth.issuer}/.well-known/webfinger", + "openIdConfigurationEndpoint":"${config.oxauth.issuer}/.well-known/openid-configuration", + "idGenerationEndpoint":"${config.oxauth.contextPath}/restv1/id", + "introspectionEndpoint":"${config.oxauth.contextPath}/restv1/introspection", + "deviceAuthorizationEndpoint":"${config.oxauth.contextPath}/restv1/device_authorization", + "umaConfigurationEndpoint":"${config.oxauth.contextPath}/restv1/uma2-configuration", + "sectorIdentifierEndpoint":"${config.oxauth.contextPath}/sectoridentifier", + "oxElevenGenerateKeyEndpoint":"${config.oxauth.contextPath}/oxeleven/rest/oxeleven/generateKey", + "oxElevenSignEndpoint":"${config.oxauth.contextPath}/oxeleven/rest/oxeleven/sign", + "oxElevenVerifySignatureEndpoint":"${config.oxauth.contextPath}/oxeleven/rest/oxeleven/verifySignature", + "oxElevenDeleteKeyEndpoint":"${config.oxauth.contextPath}/oxeleven/rest/oxeleven/deleteKey", + "backchannelAuthenticationEndpoint":"${config.oxauth.contextPath}/restv1/bc-authorize", + "backchannelDeviceRegistrationEndpoint":"${config.oxauth.contextPath}/restv1/bc-deviceRegistration", + "openidSubAttribute":"inum", + "responseTypesSupported":[ + ["code"], + ["code", "id_token"], + ["token"], + ["token", "id_token"], + ["code", "token"], + ["code", "token", "id_token"], + ["id_token"] + ], + "responseModesSupported":[ + "query", + "fragment", + "form_post" + ], + "grantTypesSupported":[ + "authorization_code", + "implicit", + "password", + "client_credentials", + "refresh_token", + "urn:ietf:params:oauth:grant-type:uma-ticket", + "urn:openid:params:grant-type:ciba", + "urn:ietf:params:oauth:grant-type:device_code" + ], + "subjectTypesSupported":[ + "public", + "pairwise" + ], + "defaultSubjectType": "pairwise", + "userInfoSigningAlgValuesSupported":[ + "HS256", + "HS384", + "HS512", + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "ES512", + "ES512", + "PS256", + "PS384", + "PS512" + ], + "userInfoEncryptionAlgValuesSupported":[ + "RSA1_5", + "RSA-OAEP", + "A128KW", + "A256KW" + ], + "userInfoEncryptionEncValuesSupported":[ + "A128CBC+HS256", + "A256CBC+HS512", + "A128GCM", + "A256GCM" + ], + "idTokenSigningAlgValuesSupported":[ + "none", + "HS256", + "HS384", + "HS512", + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "ES512", + "ES512", + "PS256", + "PS384", + "PS512" + ], + "idTokenEncryptionAlgValuesSupported":[ + "RSA1_5", + "RSA-OAEP", + "A128KW", + "A256KW" + ], + "idTokenEncryptionEncValuesSupported":[ + "A128CBC+HS256", + "A256CBC+HS512", + "A128GCM", + "A256GCM" + ], + "requestObjectSigningAlgValuesSupported":[ + "none", + "HS256", + "HS384", + "HS512", + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "ES512", + "ES512", + "PS256", + "PS384", + "PS512" + ], + "requestObjectEncryptionAlgValuesSupported":[ + "RSA1_5", + "RSA-OAEP", + "A128KW", + "A256KW" + ], + "requestObjectEncryptionEncValuesSupported":[ + "A128CBC+HS256", + "A256CBC+HS512", + "A128GCM", + "A256GCM" + ], + "tokenEndpointAuthMethodsSupported":[ + "client_secret_basic", + "client_secret_post", + "client_secret_jwt", + "private_key_jwt" + ], + "tokenEndpointAuthSigningAlgValuesSupported":[ + "HS256", + "HS384", + "HS512", + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "ES512", + "ES512", + "PS256", + "PS384", + "PS512" + ], + "dynamicRegistrationCustomAttributes":[ + "oxAuthTrustedClient", + "oxIncludeClaimsInIdToken", + "myCustomAttr1", + "myCustomAttr2" + ], + "displayValuesSupported":[ + "page", + "popup" + ], + "dynamicGrantTypeDefault":[ + "authorization_code", + "implicit", + "password", + "client_credentials", + "refresh_token", + "urn:ietf:params:oauth:grant-type:uma-ticket", + "urn:openid:params:grant-type:ciba", + "urn:ietf:params:oauth:grant-type:device_code" + ], + "claimTypesSupported":[ + "normal" + ], + "serviceDocumentation":"http://ox.gluu.org/doku.php?id=oxauth:home", + "claimsLocalesSupported":[ + "en" + ], + "uiLocalesSupported":[ + "en", + "es" + ], + "claimsParameterSupported":true, + "requestParameterSupported":true, + "requestUriParameterSupported":true, + "requireRequestUriRegistration":false, + "rejectJwtWithNoneAlg":false, + "opPolicyUri":"https://www.gluu.org/privacy-policy/", + "opTosUri":"https://www.gluu.org/terms/", + "authorizationCodeLifetime":${config.client.authorization-code-lifetime}, + "refreshTokenLifetime":14400, + "idTokenLifetime":3600, + "accessTokenLifetime":300, + "umaRptLifetime":${config.uma.requester-permission-token-lifetime}, + "umaPctLifetime":${config.uma.requester-permission-token-lifetime}, + "umaAddScopesAutomatically":false, + "umaKeepClientDuringResourceRegistration":true, + "cleanServiceInterval":${config.client.clean-service-interval}, + "keyRegenerationEnabled":false, + "keyRegenerationInterval":48, + "defaultSignatureAlgorithm":"RS256", + "oxOpenIdConnectVersion":"openidconnect-1.0", + "oxId":"https://${server.name}/oxid/service/gluu/inum", + "dynamicRegistrationEnabled":true, + "dynamicRegistrationPasswordGrantTypeEnabled":true, + "dynamicRegistrationExpirationTime":${config.client.dynamic-registration-expiration-time}, + "dynamicRegistrationPersistClientAuthorizations":true, + "trustedClientEnabled":true, + "returnClientSecretOnRead":true, + "skipAuthorizationForOpenIdScopeAndPairwiseId": false, + "dynamicRegistrationScopesParamEnabled":true, + "dynamicRegistrationCustomObjectClass":"oxAuthClientCustomAttributes", + "personCustomObjectClassList":["gluuCustomPerson"], + "authenticationFiltersEnabled":true, + "clientAuthenticationFiltersEnabled":true, + "clientRegDefaultToCodeFlowWithRefresh":true, + "grantTypesAndResponseTypesAutofixEnabled": true, + "authenticationFilters":[ + { + "filter":"(&(mail=*{0}*)(inum={1}))", + "bind":false, + "bindPasswordAttribute":null, + "baseDn":"ou=people,o=gluu" + }, + { + "filter":"uid={0}", + "bind":true, + "bindPasswordAttribute":"pwd", + "baseDn":"ou=people,o=gluu" + } + ], + "clientAuthenticationFilters":[ + { + "filter":"myCustomAttr1={0}", + "bind":false, + "bindPasswordAttribute":"oxAuthClientSecret", + "baseDn":"ou=clients,o=gluu" + } + ], + "applianceInum":"${config.oxauth.appliance}", + "sessionIdUnusedLifetime":86400, + "sessionIdUnauthenticatedUnusedLifetime":60, + "sessionIdEnabled":true, + "sessionIdPersistOnPromptNone":true, + "sessionIdLifetime":86400, + "forceOfflineAccessScopeToEnableRefreshToken":false, + "configurationUpdateInterval":3600, + "cssLocation":"${config.oxauth.contextPath}/stylesheet", + "jsLocation":"${config.oxauth.contextPath}/js", + "imgLocation":"${config.oxauth.contextPath}/img", + "metricReporterInterval":300, + "metricReporterKeepDataDays":15, + "pairwiseIdType":"${config.oxauth.pairwiseIdType}", + "pairwiseCalculationKey":"${config.oxauth.pairwiseCalculationKey}", + "pairwiseCalculationSalt": "${config.oxauth.pairwiseCalculationSalt}", + "shareSubjectIdBetweenClientsWithSameSectorId": true, + "webKeysStorage": "keystore", + "oxElevenTestModeToken": "${config.oxeleven.testModeToken}", + "dnName": "CN=oxAuth CA Certificates", + "keyStoreFile": "./conf/keystore.jks", + "keyStoreSecret": "secret", + "endSessionWithAccessToken":false, + "clientWhiteList": ["*"], + "clientBlackList": ["*.attacker.com/*"], + "legacyIdTokenClaims": false, + "customHeadersWithAuthorizationResponse": true, + "updateUserLastLogonTime": true, + "updateClientAccessTime":true, + "enableClientGrantTypeUpdate": true, + "corsConfigurationFilters": [ + { + "filterName": "CorsFilter", + "corsAllowedOrigins": "*", + "corsAllowedMethods": "GET,POST,HEAD,OPTIONS", + "corsAllowedHeaders": "Origin,Authorization,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers", + "corsExposedHeaders": "", + "corsSupportCredentials": true, + "corsLoggingEnabled": false, + "corsPreflightMaxAge": 1800, + "corsRequestDecorate": true + } + ], + "logClientIdOnClientAuthentication": true, + "logClientNameOnClientAuthentication": false, + "authorizationRequestCustomAllowedParameters" : [ + "customParam1", + "customParam2", + "customParam3" + ], + "legacyDynamicRegistrationScopeParam": false, + "openidScopeBackwardCompatibility": false, + "loggingLevel": "DEBUG", + "backchannelTokenDeliveryModesSupported": ["poll", "ping", "push"], + "backchannelAuthenticationRequestSigningAlgValuesSupported": [ + "RS512", + "ES256", + "ES384", + "ES512", + "ES512", + "PS256", + "PS384", + "PS512" + ], + "backchannelClientId": "123-123-123", + "backchannelRedirectUri": "https://ce.gluu.info:8443/ciba/home.htm", + "backchannelUserCodeParameterSupported": true, + "backchannelBindingMessagePattern": "^[a-zA-Z0-9]{4,8}$", + "backchannelAuthenticationResponseExpiresIn": 3600, + "backchannelAuthenticationResponseInterval": 2, + "backchannelRequestsProcessorJobIntervalSec": 5, + "cibaGrantLifeExtraTimeSec": 180, + "cibaMaxExpirationTimeAllowedSec": 1800, + "backchannelLoginHintClaims": ["inum", "uid", "mail"], + "cibaEndUserNotificationConfig": { + "apiKey": "AIzaSyDwJtxZV-ApPlApt7HdXkleEZhseURgzHI", + "authDomain": "api-project-561176510817.firebaseapp.com", + "databaseURL": "https://api-project-561176510817.firebaseio.com", + "projectId": "api-project-561176510817", + "storageBucket": "api-project-561176510817.appspot.com", + "messagingSenderId": "561176510817", + "appId": "1:561176510817:web:8e327e72cd49e8d5", + "notificationUrl": "https://fcm.googleapis.com/fcm/send", + "notificationKey": "csyBj39m4uPHbs2oHeTw40KfgCiUbgxBKPPz6ZXgkpF4EMQvMOAHAoM7up1UlfHk9GD5QnqdMktzjpmEuKd5xlD2kRDhwMIJ8JYbTA5+Cv39CxuWOiFuuMPJZqg+VqT4X7Ne9sXvm3UtMe8PxmRAoXlSZ1kElT/AuQvC2+YyiqlmLpXoCU01waEtIajltap5TrFuXvAvkmYJjvCkFIdWsg==", + "publicVapidKey": "BOH-FKi3U-7cr5Wv3WeS8RJXXaGpf1R7tlgKSOvYCbFrJRaJER4kI_0xCNG22erHLAmiC78PAW53neERb3eiIO0" + }, + "deviceAuthzRequestExpiresIn": 1800, + "deviceAuthzTokenPollInterval": 5, + "deviceAuthzResponseTypeToProcessAuthz": "code", + "return200OnClientRegistration": true, + "dateFormatterPatterns": {} +} diff --git a/oxAuth/Server/conf/oxauth-errors.json b/oxAuth/Server/conf/oxauth-errors.json new file mode 100644 index 00000000..6a12fe64 --- /dev/null +++ b/oxAuth/Server/conf/oxauth-errors.json @@ -0,0 +1,454 @@ +{ + "authorize":[ + { + "id":"invalid_request", + "description":"The request is missing a required parameter, includes an unsupported parameter or parameter value, or is otherwise malformed.", + "uri":null + }, + { + "id": "disabled_client", + "description": "The client is disabled and can't request an access token using this method.", + "uri":null + }, + { + "id": "unauthorized_client", + "description": "The client is not authorized to request an access token using this method.", + "uri": null + }, + { + "id":"access_denied", + "description":"The resource owner or authorization server denied the request.", + "uri":null + }, + { + "id":"retry", + "description":"The authorization server requires RP to send authorization request again.", + "uri":null + }, + { + "id":"unsupported_response_type", + "description":"The authorization server does not support obtaining an access token using this method.", + "uri":null + }, + { + "id":"invalid_scope", + "description":"The requested scope is invalid, unknown, or malformed.", + "uri":null + }, + { + "id":"server_error", + "description":"The authorization server encountered an unexpected condition which prevented it from fulfilling the request.", + "uri":null + }, + { + "id":"temporarily_unavailable", + "description":"The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.", + "uri":null + }, + { + "id":"invalid_request_redirect_uri", + "description":"The redirect_uri in the Authorization Request does not match any of the Client's pre-registered redirect_uris.", + "uri":null + }, + { + "id":"login_required", + "description":"The Authorization Server requires End-User authentication. This error MAY be returned when the prompt parameter in the Authorization Request is set to none to request that the Authorization Server should not display any user interfaces to the End-User, but the Authorization Request cannot be completed without displaying a user interface for user authentication.", + "uri":null + }, + { + "id":"session_selection_required", + "description":"The End-User is required to select a session at the Authorization Server. The End-User MAY be authenticated at the Authorization Server with different associated accounts, but the End-User did not select a session. This error MAY be returned when the prompt parameter in the Authorization Request is set to none to request that the Authorization Server should not display any user interfaces to the End-User, but the Authorization Request cannot be completed without displaying a user interface to prompt for a session to use.", + "uri":null + }, + { + "id":"consent_required", + "description":"The Authorization Server requires End-User consent. This error MAY be returned when the prompt parameter in the Authorization Request is set to none to request that the Authorization Server should not display any user interfaces to the End-User, but the Authorization Request cannot be completed without displaying a user interface for End-User consent.", + "uri":null + }, + { + "id":"user_mismatched", + "description":"The current logged in End-User at the Authorization Server does not match the requested user. This error MAY be returned when the prompt parameter in the Authorization Request is set to none to request that the Authorization Server should not display any user interfaces to the End-User, but the Authorization Request cannot be completed without displaying a user interface to prompt for the correct End-User authentication.", + "uri":null + }, + { + "id":"request_not_supported", + "description":"The request parameter is not supported.", + "uri":null + }, + { + "id":"request_uri_not_supported", + "description":"The request uri parameter is not supported.", + "uri":null + }, + { + "id":"invalid_request_uri", + "description":"The request_uri in the Authorization Request returns an error or invalid data.", + "uri":null + }, + { + "id":"invalid_request_object", + "description":"The request parameter contains an invalid OpenID Request Object.", + "uri":null + }, + { + "id":"authentication_session_invalid", + "description":"The authorization server can't handle user authentication due to session expiration", + "uri":null + }, + { + "id":"invalid_authentication_method", + "description":"The authorization server can't handle user authentication due to error caused by ACR", + "uri":null + } + ], + "clientInfo":[ + { + "id":"invalid_request", + "description":"The request is missing a required parameter, includes an unsupported parameter or parameter value, repeats the same parameter, uses more than one method for including an access token, or is otherwise malformed.", + "uri":null + }, + { + "id":"invalid_token", + "description":"The access token provided is expired, revoked, malformed, or invalid for other reasons. Try to request a new access token and retry the protected resource.", + "uri":null + } + ], + "endSession":[ + { + "id": "invalid_grant_and_session", + "description": "The provided id token (or access token) or session state are invalid or were issued to another client.", + "uri": null + }, + { + "id": "session_not_passed", + "description": "The provided session state is empty.", + "uri": null + }, + { + "id": "post_logout_uri_not_passed", + "description": "The provided post logout uri is empty.", + "uri": null + }, + { + "id": "post_logout_uri_not_associated_with_client", + "description": "The provided post logout uri is not associated with client.", + "uri": null + }, + { + "id":"invalid_grant", + "description":"The provided access token is invalid, or was issued to another client.", + "uri":null + }, + { + "id":"invalid_logout_uri", + "description":"The provided logout_uri is invalid.", + "uri":null + } + ], + "register":[ + { + "id":"invalid_request", + "description":"The request is missing a required parameter, includes an unsupported parameter or parameter value, or is otherwise malformed.", + "uri":null + }, + { + "id":"invalid_redirect_uri", + "description":"Value of one or more redirect_uris is invalid.", + "uri":null + }, + { + "id":"invalid_client_metadata", + "description":"The value of one of the Client Metadata fields is invalid and the server has rejected this request. Note that an Authorization Server MAY choose to substitute a valid value for any requested parameter of a Client's Metadata.", + "uri":null + }, + { + "id":"invalid_token", + "description":"The access token provided is expired, revoked, malformed, or invalid for other reasons.", + "uri":null + }, + { + "id": "invalid_software_statement", + "description":"The software_statement is malformed or invalid.", + "uri":null + }, + { + "id":"access_denied", + "description":"The authorization server denied the request.", + "uri":null + } + ], + "token":[ + { + "id":"invalid_request", + "description":"The request is missing a required parameter, includes an unsupported parameter or parameter value, repeats a parameter, includes multiple credentials, utilizes more than one mechanism for authenticating the client, or is otherwise malformed.", + "uri":null + }, + { + "id":"invalid_client", + "description":"Client authentication failed (e.g. unknown client, no client authentication included, or unsupported authentication method). The authorization server MAY return an HTTP 401 (Unauthorized) status code to indicate which HTTP authentication schemes are supported. If the client attempted to authenticate via the Authorization request header field, the authorization server MUST respond with an HTTP 401 (Unauthorized) status code, and include the WWW-Authenticate response header field matching the authentication scheme used by the client.", + "uri":null + }, + { + "id": "disabled_client", + "description": "The client is disabled and can't request an access token using this method.", + "uri": null + }, + { + "id":"invalid_grant", + "description":"The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.", + "uri":null + }, + { + "id":"unauthorized_client", + "description":"The authenticated client is not authorized to use this authorization grant type.", + "uri":null + }, + { + "id":"unsupported_grant_type", + "description":"The authorization grant type is not supported by the authorization server.", + "uri":null + }, + { + "id":"invalid_scope", + "description":"The requested scope is invalid, unknown, malformed, or exceeds the scope granted by the resource owner.", + "uri":null + } + ], + "revoke":[ + { + "id":"unsupported_token_type", + "description":"The authorization server does not support the revocation of the presented token type. That is, the client tried to revoke an access token on a server not supporting this feature.", + "uri":null + }, + { + "id":"invalid_request", + "description":"The request is missing a required parameter, includes an unsupported parameter or parameter value, repeats a parameter, includes multiple credentials, utilizes more than one mechanism for authenticating the client, or is otherwise malformed.", + "uri":null + }, + { + "id":"invalid_client", + "description":"Client authentication failed (e.g. unknown client, no client authentication included, or unsupported authentication method). The authorization server MAY return an HTTP 401 (Unauthorized) status code to indicate which HTTP authentication schemes are supported. If the client attempted to authenticate via the Authorization request header field, the authorization server MUST respond with an HTTP 401 (Unauthorized) status code, and include the WWW-Authenticate response header field matching the authentication scheme used by the client.", + "uri":null + } + ], + "uma":[ + { + "id":"invalid_request", + "description":"The request is missing a required parameter, includes an unsupported parameter or parameter value, or is otherwise malformed.", + "uri":null + }, + { + "id":"unauthorized_client", + "description":"The client is not authorized to request an access token using this method.", + "uri":null + }, + { + "id": "disabled_client", + "description": "The client is disabled and can't request an access token using this method.", + "uri": null + }, + { + "id":"access_denied", + "description":"The resource owner or AM server denied the request.", + "uri":null + }, + { + "id":"unsupported_response_type", + "description":"The AM server does not support an access using this method.", + "uri":null + }, + { + "id":"invalid_client_scope", + "description":"The requested scope is invalid, unknown, or malformed.", + "uri":null + }, + { + "id":"server_error", + "description":"The AM server encountered an unexpected condition which prevented it from fulfilling the request.", + "uri":null + }, + { + "id":"temporarily_unavailable", + "description":"The AM server is currently unable to handle the request due to a temporary overloading or maintenance of the server.", + "uri":null + }, + { + "id":"precondition_failed", + "description":"The resource set that was requested to be deleted or updated at the AM did not match the If-Match value present in the request.", + "uri":null + }, + { + "id":"not_found", + "description":"The resource set requested from the AM cannot be found.", + "uri":null + }, + { + "id":"unsupported_method_type", + "description":"The host request used an unsupported HTTP method.", + "uri":null + }, + { + "id":"invalid_token", + "description":"The access token expired.", + "uri":null + }, + { + "id":"invalid_resource_set_id", + "description":"The provided resource set identifier was not found at the AM.", + "uri":null + }, + { + "id":"invalid_scope", + "description":"At least one of the scopes included in the request was not registered previously by this host.", + "uri":null + }, + { + "id":"invalid_requester_ticket", + "description":"The provided ticket was not found at the AM.", + "uri":null + }, + { + "id":"expired_requester_ticket", + "description":"The provided ticket has expired.", + "uri":null + }, + { + "id":"not_authorized_permission", + "description":"The requester is definitively not authorized for this permission according to user policy.", + "uri":null + }, + { + "id":"need_claims", + "description":"The AM is unable to determine whether the requester is authorized for this permission without gathering claims from the requesting party.", + "uri":null + } + ], + "userInfo":[ + { + "id":"invalid_request", + "description":"The request is missing a required parameter, includes an unsupported parameter or parameter value, repeats the same parameter, uses more than one method for including an access token, or is otherwise malformed.", + "uri":null + }, + { + "id":"invalid_token", + "description":"The access token provided is expired, revoked, malformed, or invalid for other reasons. Try to request a new access token and retry the protected resource.", + "uri":null + }, + { + "id":"insufficient_scope", + "description":"The request requires higher privileges than provided by the access token.", + "uri":null + } + ], + "fido":[ + { + "id":"server_error", + "description":"The authorization server encountered an unexpected condition which prevented it from fulfilling the request.", + "uri":null + }, + { + "id":"invalid_request", + "description":"The request is missing a required parameter, includes an unsupported parameter or parameter value, repeats the same parameter, has invalid signature, or is otherwise malformed.", + "uri":null + }, + { + "id":"no_eligable_devices", + "description":"There are no devices registered.", + "uri":null + }, + { + "id":"device_compromised", + "description":"All devices were compromised.", + "uri":null + }, + { + "id":"session_expired", + "description":"The authentication or registration session was expired.", + "uri":null + }, + { + "id":"registration_not_allowed", + "description":"The user has registered device already.", + "uri":null + } + ], + "backchannelAuthentication":[ + { + "id": "invalid_request", + "description": "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, contains more than one of the hints, or is otherwise malformed.", + "uri": null + }, + { + "id": "invalid_scope", + "description": "The requested scope is invalid, unknown, or malformed.", + "uri": null + }, + { + "id": "expired_login_hint_token", + "description": "The login_hint_token provided in the authentication request is not valid because it has expired.", + "uri": null + }, + { + "id": "unknown_user_id", + "description": "The OpenID Provider is not able to identify which end-user the Client wishes to be authenticated by means of the hint provided in the request (login_hint_token, id_token_hint or login_hint).", + "uri": null + }, + { + "id": "unauthorized_client", + "description": "The Client is not authorized to use this authentication flow.", + "uri": null + }, + { + "id": "missing_user_code", + "description": "User code is required but was missing from the request.", + "uri": null + }, + { + "id": "invalid_user_code", + "description": "User code was invalid.", + "uri": null + }, + { + "id": "invalid_binding_message", + "description": "The binding message is invalid or unacceptable for use in the context of the given request.", + "uri": null + }, + { + "id": "invalid_client", + "description": "Client authentication failed (e.g., invalid client credentials, unknown client, no client authentication included, or unsupported authentication method).", + "uri": null + }, + { + "id": "unauthorized_end_user_device", + "description": "The end-user has not registered a device to receive push notifications.", + "uri": null + }, + { + "id": "access_denied", + "description": "The resource owner or OpenID Provider denied the CIBA (Client Initiated Backchannel Authentication) request.", + "uri": null + } + ], + "backchannelDeviceRegistration":[ + { + "id": "invalid_request", + "description": "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.", + "uri": null + }, + { + "id": "unknown_user_id", + "description": "The OpenID Provider is not able to identify the end-user.", + "uri": null + }, + { + "id": "unauthorized_client", + "description": "The Client is not authorized to use this authentication flow.", + "uri": null + }, + { + "id": "access_denied", + "description": "The resource owner or OpenID Provider denied the request.", + "uri": null + } + ] +} \ No newline at end of file diff --git a/oxAuth/Server/conf/oxauth-static-conf.json b/oxAuth/Server/conf/oxauth-static-conf.json new file mode 100644 index 00000000..777c6315 --- /dev/null +++ b/oxAuth/Server/conf/oxauth-static-conf.json @@ -0,0 +1,22 @@ +{ + "baseDn": { + "configuration": "ou=configuration,o=gluu", + "people": "ou=people,o=gluu", + "groups": "ou=groups,o=gluu", + "clients": "ou=clients,o=gluu", + "sessions": "ou=sessions,o=gluu", + "tokens": "ou=tokens,o=gluu", + "authorizations": "ou=authorizations,o=gluu", + "scopes": "ou=scopes,o=gluu", + "attributes": "ou=attributes,o=gluu", + "sessionId": "ou=session,o=gluu", + "scripts": "ou=scripts,o=gluu", + "umaBase": "ou=uma,o=gluu", + "umaPolicy": "ou=policies,ou=uma,o=gluu", + "u2fBase": "ou=u2f,o=gluu", + "metric": "ou=statistic,o=metric", + "sectorIdentifiers": "ou=sector_identifiers,o=gluu", + "ciba": "ou=ciba,o=gluu", + "stat": "ou=stat,o=gluu" + } +} diff --git a/oxAuth/Server/conf/oxauth-web-keys.json b/oxAuth/Server/conf/oxauth-web-keys.json new file mode 100644 index 00000000..2170ddf2 --- /dev/null +++ b/oxAuth/Server/conf/oxauth-web-keys.json @@ -0,0 +1,139 @@ +{ + "keys": [ + { + "kid": "15d79fe5-55de-4e3d-a6dd-9ce15e07b382", + "kty": "RSA", + "use": "sig", + "alg": "RS256", + "exp": 1581477928977, + "n": "oP4Z0lwOinfyWX3-sXCWghuRatQK8yqW63nYRQ_YfWRmIA3pXw4B-XJemhm4xFw9osOPkmv01KYLnzXLdYwGLeab4DS06UtEm8kHAv3WPvBhyEDtHaY_pUdDRo0Dyt4MFV64gAWu1LbM2yESMvzvXZ8H5SIj3o5smneO-r5F197wRSY9wUk6sMr1F7lNpPYO2Luv06RI3tHNzI56l56U9ZxaR8VElDGHqr_e93r3eEV3buYP20yus2u6yoQakiM3_ydYJTTNNAs0CNrYLHoSI3u1QAMgp3mH3TUCGjdMZXxQOeB5iCxvl24FdX3-on-LzrwAgn4-d-C8G-lorOGnsw", + "e": "AQAB", + "x5c": [ + "MIIDAzCCAeugAwIBAgIgELS2fc6fr6GP/ykwfQLWuGDilq2CU2Ny2Ev3AWTM0WwwDQYJKoZIhvcNAQELBQAwITEfMB0GA1UEAwwWb3hBdXRoIENBIENlcnRpZmljYXRlczAeFw0xOTAyMTIwMzI1MTlaFw0yMDAyMTIwMzI1MjhaMCExHzAdBgNVBAMMFm94QXV0aCBDQSBDZXJ0aWZpY2F0ZXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCg/hnSXA6Kd/JZff6xcJaCG5Fq1ArzKpbredhFD9h9ZGYgDelfDgH5cl6aGbjEXD2iw4+Sa/TUpgufNct1jAYt5pvgNLTpS0SbyQcC/dY+8GHIQO0dpj+lR0NGjQPK3gwVXriABa7UtszbIRIy/O9dnwflIiPejmyad476vkXX3vBFJj3BSTqwyvUXuU2k9g7Yu6/TpEje0c3MjnqXnpT1nFpHxUSUMYeqv973evd4RXdu5g/bTK6za7rKhBqSIzf/J1glNM00CzQI2tgsehIje7VAAyCneYfdNQIaN0xlfFA54HmILG+XbgV1ff6if4vOvACCfj534Lwb6Wis4aezAgMBAAGjJzAlMCMGA1UdJQQcMBoGCCsGAQUFBwMBBggrBgEFBQcDAgYEVR0lADANBgkqhkiG9w0BAQsFAAOCAQEAWOG1maZDtXMvvp03s0EDT/Rs0H/q5hLnCg1IUkSBoQoqDt09N4fP5fJ1y02AjJy4fWJu3YNCLlSyFJxMSG7mLS+73TXEbSdlzRGgogVZdP+Hqw25yLbcJKzthv63jtJAoSRSPmjT7pDgIW7Xav+gGeaAbodJftVziJBqakcjTtDI2NdeBKXKkYGWMaWXU74FgNBzvaF8AlBf8bJjCl0EUnOFWiI6O7hJeDw7La6/lekXPo0ijyVsQp0Z2Solec8d5HmynQaZUeb58vkoQdlvZQnTV9xo9U6gad8JJqC/Z5sK61GVi8GNMkxDeYjyTTIgyBoKjxeXb2JRFB9deWm21g==" + ] + }, + { + "kid": "ffb5bb6e-0c86-4ee5-8fb1-1366b6eca189", + "kty": "RSA", + "use": "sig", + "alg": "RS384", + "exp": 1581477928977, + "n": "nxQO7TzhffJDWu4qopRDddk5ovw7JCbDx0MNLNwAdV-RZw-sEPZmcHkg-6W1Yal4G505jpq_afzwJhH89oaxYS9FczkDhZRLLwNnJ6xTcBvwhOYG_bbZlbBdvQVLUfchignboRhCL2c1J8lJnjwQRLdx1ScDKpMShhfNXroeea1t7oNBlU6FrintHQZKBiCbO8VusOBkZjlAp6ED5pbl4IQ6bn-99Ik7pBZTCfCk7-ULhg8mdvG-02-4WVXEBIj76UisJapH7_clpOKOfbrF12uoLKB1W3wyTE3yv_GbwLWUA61fSohZ0kEe_LauBGBSD8TSIanqaxbS9ZioABv4cQ", + "e": "AQAB", + "x5c": [ + "MIIDBDCCAeygAwIBAgIhAJcVOSd8vUbb2/4oDxjDUPpVZfRz6PTVbYbdcoOKBd46MA0GCSqGSIb3DQEBDAUAMCExHzAdBgNVBAMMFm94QXV0aCBDQSBDZXJ0aWZpY2F0ZXMwHhcNMTkwMjEyMDMyNTE5WhcNMjAwMjEyMDMyNTI4WjAhMR8wHQYDVQQDDBZveEF1dGggQ0EgQ2VydGlmaWNhdGVzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnxQO7TzhffJDWu4qopRDddk5ovw7JCbDx0MNLNwAdV+RZw+sEPZmcHkg+6W1Yal4G505jpq/afzwJhH89oaxYS9FczkDhZRLLwNnJ6xTcBvwhOYG/bbZlbBdvQVLUfchignboRhCL2c1J8lJnjwQRLdx1ScDKpMShhfNXroeea1t7oNBlU6FrintHQZKBiCbO8VusOBkZjlAp6ED5pbl4IQ6bn+99Ik7pBZTCfCk7+ULhg8mdvG+02+4WVXEBIj76UisJapH7/clpOKOfbrF12uoLKB1W3wyTE3yv/GbwLWUA61fSohZ0kEe/LauBGBSD8TSIanqaxbS9ZioABv4cQIDAQABoycwJTAjBgNVHSUEHDAaBggrBgEFBQcDAQYIKwYBBQUHAwIGBFUdJQAwDQYJKoZIhvcNAQEMBQADggEBADh/RBrUfDPmw2SZVJnQvm+yqvUGoRPm4Jmv4235k99RwiBqBwXl2EGPPMY+Dr7q+9ZLzIwI/B2ZCnDcN9zvZkym3xD2y9PPaUOSRJ9HtR3AkkeOnaqbo39NLxO88mEM10Wx+uoAbb2gLHu0b+gZp6jWsYTd1mRsSyerzOHS3KWci+goRH8WMMAxEgA3wTXbOhVmYRD8f11TMOa9MydTBcqVCJ/PVVjQyW6zzaLryie71Nb6+8niXuVcXqTyiAHWhdtb/KxPKsnFEFjctk8WSXiM2NaaUmkg8+Vt51VcOHhvKruwoldb+t2Mr5m8H1fAVq88s/Kj9vYHS/YmbeUow6E=" + ] + }, + { + "kid": "13a6a2cb-3bc3-4cae-82e9-e5a516288815", + "kty": "RSA", + "use": "sig", + "alg": "RS512", + "exp": 1581477928977, + "n": "1ZTOQjO3X7Neg2csOriPbtQXsa70-1Ks727stCtJt5IgpCWbXVqX0RYPFIrXmwS4doghTgi8TW7Np8CKlzfAzEZ2bgcTWW0iNkGea2i3puAsn5Kw8FfeBmUMO8hIdWZNyaJ8QO0DHqSYsNP3e3E7j_q0BIi1wUH-eMuMIRUW13JHBQEylLq7TNGcxbATZxfWnSRp51yFDwFx7qXAbFL51GLkkwt95meqlhFsEuUXVtcZ7JP8HTzC6sZBsyxeBICVAkD3bBO7ZVN-dHPmuvvC83peqo8smVo6K_MEXDH_bthDGJNhXFmPzwwH55kaYJCVGwvE5Ps5_aws6CksA1N2uw", + "e": "AQAB", + "x5c": [ + "MIIDAzCCAeugAwIBAgIgPxZvh8tc60WViyopxJufVHytoXC8XhTfDR83gkxzkfYwDQYJKoZIhvcNAQENBQAwITEfMB0GA1UEAwwWb3hBdXRoIENBIENlcnRpZmljYXRlczAeFw0xOTAyMTIwMzI1MjBaFw0yMDAyMTIwMzI1MjhaMCExHzAdBgNVBAMMFm94QXV0aCBDQSBDZXJ0aWZpY2F0ZXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVlM5CM7dfs16DZyw6uI9u1BexrvT7Uqzvbuy0K0m3kiCkJZtdWpfRFg8UitebBLh2iCFOCLxNbs2nwIqXN8DMRnZuBxNZbSI2QZ5raLem4CyfkrDwV94GZQw7yEh1Zk3JonxA7QMepJiw0/d7cTuP+rQEiLXBQf54y4whFRbXckcFATKUurtM0ZzFsBNnF9adJGnnXIUPAXHupcBsUvnUYuSTC33mZ6qWEWwS5RdW1xnsk/wdPMLqxkGzLF4EgJUCQPdsE7tlU350c+a6+8Lzel6qjyyZWjor8wRcMf9u2EMYk2FcWY/PDAfnmRpgkJUbC8Tk+zn9rCzoKSwDU3a7AgMBAAGjJzAlMCMGA1UdJQQcMBoGCCsGAQUFBwMBBggrBgEFBQcDAgYEVR0lADANBgkqhkiG9w0BAQ0FAAOCAQEA0Rjr5HEGJMNo9JiwNtcAI2HUUJ2Ue5xWqeOvMkzb78Lob6wy4JP8qV4OhB3ZheMjOqGiQeOjFj6+uZ+Cd4IGMk3J39pwaUyXN3TUKBpWiLdS+yfkS9ZuBh5XCIe7GWDvsu77Pyg88SfnXHuHS4ShZHeltFHMHfs/XmyCTpMt5OYhlhRPScuLEk9O+ocDZlc/Ei2LJjTTv99PR/E2zKvRQD14acQnNSwi4jW4Mlwdfci97BeJul5pxhhErqxZjU7dllbjAnBrXkDZUmVTA0qMGiESRlFb/YOrw6JvFSmJXNLoTO3izNojt/ENZbpJdThdAOtGfYuaz50rznASFa1EeQ==" + ] + }, + { + "kid": "e98f2a7c-0ff2-4313-939a-0b6f41d9cfd6", + "kty": "EC", + "use": "sig", + "alg": "ES256", + "exp": 1581477928977, + "crv": "P-256", + "x": "3zaKGVOK1x82yg3Dg3i70Ihwl5dB2A5qNjAYvGM6NMk", + "y": "ntwsCcBIGyIq05DAtQg4flnEgG_iQV34AGFED63ZiH0", + "x5c": [ + "MIIBdzCCAR2gAwIBAgIgLukLCPwLSlTOMcmGnqMNsEVCZEmf1GrK6NANj4gHZnAwCgYIKoZIzj0EAwIwITEfMB0GA1UEAwwWb3hBdXRoIENBIENlcnRpZmljYXRlczAeFw0xOTAyMTIwMzI1MjBaFw0yMDAyMTIwMzI1MjhaMCExHzAdBgNVBAMMFm94QXV0aCBDQSBDZXJ0aWZpY2F0ZXMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATfNooZU4rXHzbKDcODeLvQiHCXl0HYDmo2MBi8Yzo0yZ7cLAnASBsiKtOQwLUIOH5ZxIBv4kFd+ABhRA+t2Yh9oycwJTAjBgNVHSUEHDAaBggrBgEFBQcDAQYIKwYBBQUHAwIGBFUdJQAwCgYIKoZIzj0EAwIDSAAwRQIgDoQbhSUrlhGwLdd9oAWSCU0RpU4iONkclZgSRpYymswCIQClIY/LLNzPs7uHuNgL8+oE1MkK99XfyMRLyo9OjqUMkA==" + ] + }, + { + "kid": "bc3dca3f-9358-4fba-968e-fadc5adc5c11", + "kty": "EC", + "use": "sig", + "alg": "ES384", + "exp": 1581477928977, + "crv": "P-384", + "x": "swXs-Fgg1L9QrpM4fOo6xaQYxjJm0mzmO5l1ZIAmQySq6dZW-YnR6CyTDK7r4vPH", + "y": "7GVlwCIeDQ8veFFY49E7uWc6bhrrwICXahdrdn-m5gvxxwMB56LeVeTdeelBySkT", + "x5c": [ + "MIIBtDCCATugAwIBAgIhAPfbY5K51dnRWgUuWj0bKMIGEbQF+uRIUlCikCSjqZmQMAoGCCqGSM49BAMDMCExHzAdBgNVBAMMFm94QXV0aCBDQSBDZXJ0aWZpY2F0ZXMwHhcNMTkwMjEyMDMyNTIwWhcNMjAwMjEyMDMyNTI4WjAhMR8wHQYDVQQDDBZveEF1dGggQ0EgQ2VydGlmaWNhdGVzMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEswXs+Fgg1L9QrpM4fOo6xaQYxjJm0mzmO5l1ZIAmQySq6dZW+YnR6CyTDK7r4vPH7GVlwCIeDQ8veFFY49E7uWc6bhrrwICXahdrdn+m5gvxxwMB56LeVeTdeelBySkToycwJTAjBgNVHSUEHDAaBggrBgEFBQcDAQYIKwYBBQUHAwIGBFUdJQAwCgYIKoZIzj0EAwMDZwAwZAIwJbXvFZjIuKb5CTm67dHtkASnFfbo4i2naUyNuE2tL0Ytk+nNNX4gIOgaz8rACfhFAjBFeF47lNyDYlmwpIwPFPttVRHO/aPNgyFRiZxIUpYPXl0evriqbLa2U0a0vcIQgyo=" + ] + }, + { + "kid": "2f081371-3593-4bd0-87de-4b3b743ec742", + "kty": "EC", + "use": "sig", + "alg": "ES512", + "exp": 1581477928977, + "crv": "P-521", + "x": "AQUib7U9Z9PiPHvwTNK1Crm-QTF8CZhQztcTslxzL6PQeoVaj_VD8z4vSBnTTsbTB-nuetKFQp23i8P19q4-Rvon", + "y": "d1fO6w3pupAWvA8lNkiNiOzSgTfiAApjbxUqx9CBhKbDlP7SHn4fAY-pb5zmwSxwLg9hEJfw-mKM6AVxIxT-uno", + "x5c": [ + "MIIB/jCCAWCgAwIBAgIgCwDaynevgt5tRm5lr/lL0e4czouTAdTNLCamwpHBvhkwCgYIKoZIzj0EAwQwITEfMB0GA1UEAwwWb3hBdXRoIENBIENlcnRpZmljYXRlczAeFw0xOTAyMTIwMzI1MjBaFw0yMDAyMTIwMzI1MjhaMCExHzAdBgNVBAMMFm94QXV0aCBDQSBDZXJ0aWZpY2F0ZXMwgZswEAYHKoZIzj0CAQYFK4EEACMDgYYABAEFIm+1PWfT4jx78EzStQq5vkExfAmYUM7XE7Jccy+j0HqFWo/1Q/M+L0gZ007G0wfp7nrShUKdt4vD9fauPkb6JwB3V87rDem6kBa8DyU2SI2I7NKBN+IACmNvFSrH0IGEpsOU/tIefh8Bj6lvnObBLHAuD2EQl/D6YozoBXEjFP66eqMnMCUwIwYDVR0lBBwwGgYIKwYBBQUHAwEGCCsGAQUFBwMCBgRVHSUAMAoGCCqGSM49BAMEA4GLADCBhwJCAcGtxUENsE6RJ6HdqaoR0BzfrmSuR/yrCFuuTHNeJESiAl1EHT70044AOeQAuLvW6F/rfJLqwGXA06PaMUkb+mymAkFbtBqp7LaRU0NBx71KDCspaeJ9ELRHyRxvLUjXZG3ibc2vQbt7u9ObXvgeqLt/1s3PNbAosH4i2dbPby0GXG2Xpw==" + ] + }, + { + "kid": "017919eb-2117-41af-b471-70ab5843dc44", + "kty": "RSA", + "use": "sig", + "alg": "PS256", + "exp": 1581477928977, + "n": "pHPcKM8W9S4pzmN-yfmvir-ZEVLyZyCSMjcOYehgsY9hlf5SxPcw9r4guAGW_Y2JUrzDXa17dPFS7lr8iDfh2TJeAMgvVixTJUbY-mdgpLzeu0ONsyft60ZZ_zeCsZf_zMQaUAcnxzUhCxe3n21uPwfRIlwQC2y1wb3n5qe-l0q6c3ezy3vrLV7mkDjFAD-X_A67StzMpHd_Px_8eKo8XQXtuuifCdIi5u_r4-ReFHAIA-DDcm_f4sEdjXJ8t7VJjsLqfYkpS3OdkiB4f2IME1DuYUKoXKfb8MAZxg5DQotE0T2XVzKRqrAYTMxgNCUXQ3Ge_3JCryRI7AI1O4whFQ", + "e": "AQAB", + "x5c": [ + "MIIDazCCAh+gAwIBAgIgQ0a0sT+SQeE3WpaBFlIbhYqWX7d0Ww8VTN4u+3DkFeQwQQYJKoZIhvcNAQEKMDSgDzANBglghkgBZQMEAgEFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgEFAKIDAgEgMCExHzAdBgNVBAMMFm94QXV0aCBDQSBDZXJ0aWZpY2F0ZXMwHhcNMTkwMjEyMDMyNTIwWhcNMjAwMjEyMDMyNTI4WjAhMR8wHQYDVQQDDBZveEF1dGggQ0EgQ2VydGlmaWNhdGVzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApHPcKM8W9S4pzmN+yfmvir+ZEVLyZyCSMjcOYehgsY9hlf5SxPcw9r4guAGW/Y2JUrzDXa17dPFS7lr8iDfh2TJeAMgvVixTJUbY+mdgpLzeu0ONsyft60ZZ/zeCsZf/zMQaUAcnxzUhCxe3n21uPwfRIlwQC2y1wb3n5qe+l0q6c3ezy3vrLV7mkDjFAD+X/A67StzMpHd/Px/8eKo8XQXtuuifCdIi5u/r4+ReFHAIA+DDcm/f4sEdjXJ8t7VJjsLqfYkpS3OdkiB4f2IME1DuYUKoXKfb8MAZxg5DQotE0T2XVzKRqrAYTMxgNCUXQ3Ge/3JCryRI7AI1O4whFQIDAQABoycwJTAjBgNVHSUEHDAaBggrBgEFBQcDAQYIKwYBBQUHAwIGBFUdJQAwQQYJKoZIhvcNAQEKMDSgDzANBglghkgBZQMEAgEFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgEFAKIDAgEgA4IBAQCR4IM1U6ya8Ul5cL982FYbgDXbeOSQTbHBZT6rnN12ygSbP5LtcBbnTU2L5N0ghcabtUAntLk79CFkS4tcD3baaB8VQZ9jvMahqs7RJ45ZVK2wGFCimpzatQgxtwxA3ucOFFa6CHN4U9ykgShgXvu4fctOD+II0XGI7+yoL5YLiBQK6KuZ9c4nfZkgxfYUjCqkh6EZj5I9qt3C3p7vEXhFZw5ryGnCmRu9XpCNQz3NvGuU6vCfXhR1kn5zjy+DC8cg1iGeYnongrlvx0gFRzfTmkQhPM3EjeVI6YGPgbHYy5TNriUFGijL9mD205tlP6jNCY7vCf7X6V86Xj1pBqXV" + ] + }, + { + "kid": "79dea39d-a357-4ec1-a68b-73be33ef7ffa", + "kty": "RSA", + "use": "sig", + "alg": "PS384", + "exp": 1581477928977, + "n": "9oGR519MSgBAzHye7De6LB_FjzDFnuWQNuhuxX6vT1xzOysP_i9s63zieOGgqfR8z2KwmI1TA1KqlsW2Bt2kq-z5cXorxvJeVA6AYpbkVpB3DVxD8-VIouR6iPKCgQd83y-j9qwzrFRAb0W9Q96kRIsg-1s82t-Sh9CkAHiJ6ZPRH_X7u8neyt_cpi69mFloIIIar8X1XM9A7akDgSb4Qc_j7hE8KAVBXJbS6D7R2yvh1vYIzmM_oAovnFKHUWfKMWKqe3dis4ZW4VfoA0yz-quEdptYQYQlkv23-9eq-xgyV0YqGBfnrY2Vp-vOVv9Urn3VvlG-iv_zUcWsWbPEWQ", + "e": "AQAB", + "x5c": [ + "MIIDazCCAh+gAwIBAgIgfZsssQOROCaWEHsioRpbIiswdKh2J3k+ck0ociJ/2n4wQQYJKoZIhvcNAQEKMDSgDzANBglghkgBZQMEAgIFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgIFAKIDAgEwMCExHzAdBgNVBAMMFm94QXV0aCBDQSBDZXJ0aWZpY2F0ZXMwHhcNMTkwMjEyMDMyNTIwWhcNMjAwMjEyMDMyNTI4WjAhMR8wHQYDVQQDDBZveEF1dGggQ0EgQ2VydGlmaWNhdGVzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9oGR519MSgBAzHye7De6LB/FjzDFnuWQNuhuxX6vT1xzOysP/i9s63zieOGgqfR8z2KwmI1TA1KqlsW2Bt2kq+z5cXorxvJeVA6AYpbkVpB3DVxD8+VIouR6iPKCgQd83y+j9qwzrFRAb0W9Q96kRIsg+1s82t+Sh9CkAHiJ6ZPRH/X7u8neyt/cpi69mFloIIIar8X1XM9A7akDgSb4Qc/j7hE8KAVBXJbS6D7R2yvh1vYIzmM/oAovnFKHUWfKMWKqe3dis4ZW4VfoA0yz+quEdptYQYQlkv23+9eq+xgyV0YqGBfnrY2Vp+vOVv9Urn3VvlG+iv/zUcWsWbPEWQIDAQABoycwJTAjBgNVHSUEHDAaBggrBgEFBQcDAQYIKwYBBQUHAwIGBFUdJQAwQQYJKoZIhvcNAQEKMDSgDzANBglghkgBZQMEAgIFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgIFAKIDAgEwA4IBAQA5lcwNGZ2LcgRIxevtJt48cIc6LhV6guMZIGczfSNvf7MHWl2Ejd1Stbru4KAu6p3RwntREwzMntZPzAY6ZhPmLjjKXeb9jO457MOx1F4Ctfaj4jBbnSJt8kxnb10sgJXRLy2cs5YyShdmT9fm5OHkdfC2qRQ5ID0pa0PPR8j4PEBvKU10ST+dFErS5pggxM+cYXf0NRRA//DWVlwJMx5B9SPaOaT3ACpREbTKDUe5Z75e2kvEJh5JdCWGL7J+N3pOMc+etheT1LJDh54m/q8RA6EYQGAt0SbT6WmB7Be6kAEO8g1zirOPW4cFl/N2uEKjZe1WD3FWa3uM46xuPwGR" + ] + }, + { + "kid": "995f4140-287e-4ae7-b919-b2e66f9f5b5a", + "kty": "RSA", + "use": "sig", + "alg": "PS512", + "exp": 1581477928977, + "n": "vMU0sknxD-cjUHgZG0G3ulq0vchVA1T2JpxIGp_mLwqCAf6mv63gEqa3-eiwOJa2UYqcG024zGYmB2Ky9zgN5_FG1hxsUPuF0kFkt0HtZNbjMW4WXR1bCZjRpcJ7PR0fvkLrWiHniLDIIZ9u_ECKlkoHUHefhco0N5SBvYedAmVE2J95E-b9h1KL7dl44BXXjic-ieHvrMBLrGcyaEZ0cSpkMR9W3hLqbpskVIhVLClDd2KqaiXBu-8sfqZyl79wm-U4r5QfldvHj5aGUPQTeyMCpNF14GOCicui4G7J5TULPPGoEeLpCgsU90KhWGBzLTojTxkmbMenVywnp7K-lQ", + "e": "AQAB", + "x5c": [ + "MIIDazCCAh+gAwIBAgIgYurz9+Yd7DY5YMgE563SYP5pSRbOp1t9vUoBKaQlkZ8wQQYJKoZIhvcNAQEKMDSgDzANBglghkgBZQMEAgMFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgMFAKIDAgFAMCExHzAdBgNVBAMMFm94QXV0aCBDQSBDZXJ0aWZpY2F0ZXMwHhcNMTkwMjEyMDMyNTIxWhcNMjAwMjEyMDMyNTI4WjAhMR8wHQYDVQQDDBZveEF1dGggQ0EgQ2VydGlmaWNhdGVzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvMU0sknxD+cjUHgZG0G3ulq0vchVA1T2JpxIGp/mLwqCAf6mv63gEqa3+eiwOJa2UYqcG024zGYmB2Ky9zgN5/FG1hxsUPuF0kFkt0HtZNbjMW4WXR1bCZjRpcJ7PR0fvkLrWiHniLDIIZ9u/ECKlkoHUHefhco0N5SBvYedAmVE2J95E+b9h1KL7dl44BXXjic+ieHvrMBLrGcyaEZ0cSpkMR9W3hLqbpskVIhVLClDd2KqaiXBu+8sfqZyl79wm+U4r5QfldvHj5aGUPQTeyMCpNF14GOCicui4G7J5TULPPGoEeLpCgsU90KhWGBzLTojTxkmbMenVywnp7K+lQIDAQABoycwJTAjBgNVHSUEHDAaBggrBgEFBQcDAQYIKwYBBQUHAwIGBFUdJQAwQQYJKoZIhvcNAQEKMDSgDzANBglghkgBZQMEAgMFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgMFAKIDAgFAA4IBAQAKN6gPeiZaJcosj9cRwsdDVs3w30k/gbqlDivCFo+iPX84ojfCPp6O0IzwDtSJrXw3oujda0b03AAXrkGdXeeu6MOIAPGlMJK0eE54rT2RqGK8OFe4q7B00FqY4rFqJ0EvgmFuOqFEhqXClVMwA4Hm7JxrfOJIG3ZWf74+qKgQeG/w0980mPfRCUDF/XsRBN5Pb84HHa7lhQmvyi0ippvBqvTX55uG6nnpteU9+DcOv/i8FkYdZtMrkigHX4+dLQ3zeQTBFKwWUQONdqDM+poYaSXEg4RswAFt4jvGdv/wpGjAUaIMQdxv6Qqxo1EnS4DURK6hQfRUc/zMxvxhSMix" + ] + }, + { + "kid": "6f6ff195-e76c-48ea-b8a8-ffbb45f3a9df", + "kty": "RSA", + "use": "enc", + "alg": "RSA-OAEP", + "exp": 1581477928977, + "n": "mRhr7pyxWj5zRqs1bmweMlx2kAqrwviRU9bbder3zgV7yM7C8z6-l9RkFZstsLJBpJl9r5CHDW8Hae-DKfjN6v3WgRz9LS5UXbKuEVaBw3gByffsl6NUJqEDV7sub3DItt8lbOu98VZFQqEk35e5RK0461URswWP4WWft9s7TuwaC8rBSQPE41B71XyMlm77ZZSmPj9N62Op2-7WodzHcj2N-BpnEl0DTL9-FxK7m44hgWtzuco0xEHHACfXZKrrGGpafRU2NnANIhLORDvuNXZAc0WC1f4YkeyNuuXdkEhe68NkvRu3P8QQhYunnXZiLRlyq0Fy8Zn5hGgJzlhkGw", + "e": "AQAB", + "x5c": [ + "MIIDBDCCAeygAwIBAgIhAMH8ztAdRD/peQs7xsnbfMJods6J16lvTorDo+ZZmdabMA0GCSqGSIb3DQEBCwUAMCExHzAdBgNVBAMMFm94QXV0aCBDQSBDZXJ0aWZpY2F0ZXMwHhcNMTkwMjEyMDMyNTIxWhcNMjAwMjEyMDMyNTI4WjAhMR8wHQYDVQQDDBZveEF1dGggQ0EgQ2VydGlmaWNhdGVzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmRhr7pyxWj5zRqs1bmweMlx2kAqrwviRU9bbder3zgV7yM7C8z6+l9RkFZstsLJBpJl9r5CHDW8Hae+DKfjN6v3WgRz9LS5UXbKuEVaBw3gByffsl6NUJqEDV7sub3DItt8lbOu98VZFQqEk35e5RK0461URswWP4WWft9s7TuwaC8rBSQPE41B71XyMlm77ZZSmPj9N62Op2+7WodzHcj2N+BpnEl0DTL9+FxK7m44hgWtzuco0xEHHACfXZKrrGGpafRU2NnANIhLORDvuNXZAc0WC1f4YkeyNuuXdkEhe68NkvRu3P8QQhYunnXZiLRlyq0Fy8Zn5hGgJzlhkGwIDAQABoycwJTAjBgNVHSUEHDAaBggrBgEFBQcDAQYIKwYBBQUHAwIGBFUdJQAwDQYJKoZIhvcNAQELBQADggEBAAQ7a9iCfIz+tYeaa0/MtDNyt0Ef7YKoKpqmUakP5mjB1rZQeYiiwDuRFL6516tmdzaRcmDduP0PnZtKFiyCj4r21jlzDkltEF+B86gzNLeoPCOqb7gxYbowT/g96h6VQ54sGtGwS6D7mFGnqmS/K4vjceuCItfXPdX/cK2vd2hhDf/YDPBo5DzHviXO5mgSxUpSz7mP8XFdmnYMYW2is2GF3qAq2em4VWKEq2IktbFS/m369ot08fAGci0kH11BRtBQRvouOoyI5ZUj2cyLvOcGAXbXs8ChvPMRsoPMxJxpMxPUDPxnd0S52wxR8xiLWK35cUyDytSRRVFqMi4Va24=" + ] + }, + { + "kid": "ab8ece4b-111e-4a44-8063-8daec44076cc", + "kty": "RSA", + "use": "enc", + "alg": "RSA1_5", + "exp": 1581477928977, + "n": "3Qt0Z0TqgX8nv3gcLafO-o70rYLCzl-8kNr6rW3gWVO7KR0GF60eA1uJv-MNn4S1RlPr5wrhNHKQ6Z62N6hCQHekQOP5aEadb3m8f8L4VQ_P8Jo77uUz8Yngr8c9_G9OY8rNLd_IheS1Jj9qc8fExEVtmlWAMwsfmVxmfIDu9v3Jyd-c3QQLUzSP_zqxb1e4z9AVWGq_w2h5WO4FoexyitHAw3681lb7z3Z68nXtvrtlPvnJdBCTlgxI79mBzkPvbzfrbGdCylC8YRymhX-inHWG4N9ZZfmPUk-fsVabr5C290Fn4KYktLCisRpwxVjOO2etld1KBVwqoX0Opp0w3w", + "e": "AQAB", + "x5c": [ + "MIIDBDCCAeygAwIBAgIhANpzVfkhKxiMb2o4bnlurKABTBfkXN8wdtvaEX/EPaVBMA0GCSqGSIb3DQEBCwUAMCExHzAdBgNVBAMMFm94QXV0aCBDQSBDZXJ0aWZpY2F0ZXMwHhcNMTkwMjEyMDMyNTIxWhcNMjAwMjEyMDMyNTI4WjAhMR8wHQYDVQQDDBZveEF1dGggQ0EgQ2VydGlmaWNhdGVzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3Qt0Z0TqgX8nv3gcLafO+o70rYLCzl+8kNr6rW3gWVO7KR0GF60eA1uJv+MNn4S1RlPr5wrhNHKQ6Z62N6hCQHekQOP5aEadb3m8f8L4VQ/P8Jo77uUz8Yngr8c9/G9OY8rNLd/IheS1Jj9qc8fExEVtmlWAMwsfmVxmfIDu9v3Jyd+c3QQLUzSP/zqxb1e4z9AVWGq/w2h5WO4FoexyitHAw3681lb7z3Z68nXtvrtlPvnJdBCTlgxI79mBzkPvbzfrbGdCylC8YRymhX+inHWG4N9ZZfmPUk+fsVabr5C290Fn4KYktLCisRpwxVjOO2etld1KBVwqoX0Opp0w3wIDAQABoycwJTAjBgNVHSUEHDAaBggrBgEFBQcDAQYIKwYBBQUHAwIGBFUdJQAwDQYJKoZIhvcNAQELBQADggEBAH8xfjYIQcUp/OynpXjmmo52abDUTSWDhG4cpYEjAGieSntNKEMjY0WjCa8/QgVI4wqT8yQzMbD7QcgFvFi8Ehd9BhY/FDh6HAB6uIxXbDG8fQzFFeJ2Jt8EwlWCXNVgY2iMwtYSUAai+SckzX3sdpPh+fviZ6LA83dK2TgaOlSQTAsknV8SsWBemtesqsRVCS6+EsCNtZ4Z3ydbcOtyoJnkZvJFLFuKY2kvlX31IViU1H/sg+tK2G2+F0u1NONRFFiwtlPHam/On4huhSr8am2r7yeCZqQ0LeFSiwH8rrsOKrKrdwjk99rTb45sBs2IHbldpAxJNwBwFfpOd63Fz4M=" + ] + } + ] +} \ No newline at end of file diff --git a/oxAuth/Server/conf/salt b/oxAuth/Server/conf/salt new file mode 100644 index 00000000..b1d581ac --- /dev/null +++ b/oxAuth/Server/conf/salt @@ -0,0 +1 @@ +encodeSalt=${config.oxauth.salt} \ No newline at end of file diff --git a/oxAuth/Server/integrations.deprecatred/inwebo/InWeboExternalAuthenticator.py b/oxAuth/Server/integrations.deprecatred/inwebo/InWeboExternalAuthenticator.py new file mode 100644 index 00000000..f608b9ce --- /dev/null +++ b/oxAuth/Server/integrations.deprecatred/inwebo/InWeboExternalAuthenticator.py @@ -0,0 +1,234 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2016, Gluu +# +# Author: Yuriy Movchan +# + +from org.gluu.oxauth.security import Identity +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.service import UserService, AuthenticationService +from org.gluu.oxauth.service.net import HttpService +from org.gluu.service import XmlService +from org.gluu.oxauth.service import EncryptionService +from org.gluu.util import StringHelper +from org.gluu.util import ArrayHelper +from java.lang import Boolean + +import java +import sys +import json + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + self.client = None + + def init(self, configurationAttributes): + print "InWebo. Initialization" + + iw_cert_store_type = configurationAttributes.get("iw_cert_store_type").getValue2() + iw_cert_path = configurationAttributes.get("iw_cert_path").getValue2() + iw_creds_file = configurationAttributes.get("iw_creds_file").getValue2() + + # Load credentials from file + f = open(iw_creds_file, 'r') + try: + creds = json.loads(f.read()) + except: + return False + finally: + f.close() + + iw_cert_password = creds["CERT_PASSWORD"] + try: + encryptionService = CdiUtil.bean(EncryptionService) + iw_cert_password = encryptionService.decrypt(iw_cert_password) + except: + return False + + httpService = CdiUtil.bean(HttpService) + self.client = httpService.getHttpsClient(None, None, None, iw_cert_store_type, iw_cert_path, iw_cert_password) + print "InWebo. Initialized successfully" + + return True + + def destroy(self, configurationAttributes): + print "InWebo. Destroy" + print "InWebo. Destroyed successfully" + return True + + def getApiVersion(self): + return 1 + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + userService = CdiUtil.bean(UserService) + authenticationService = CdiUtil.bean(AuthenticationService) + + identity = CdiUtil.bean(Identity) + + iw_api_uri = configurationAttributes.get("iw_api_uri").getValue2() + iw_service_id = configurationAttributes.get("iw_service_id").getValue2() + iw_helium_enabled = Boolean(configurationAttributes.get("iw_helium_enabled").getValue2()).booleanValue() + + if (iw_helium_enabled): + identity.setWorkingParameter("iw_count_login_steps", 1) + + credentials = identity.getCredentials() + + user_name = credentials.getUsername() + + if (step == 1): + print "InWebo. Authenticate for step 1" + + print "InWebo. Authenticate for step 1. iw_helium_enabled:", iw_helium_enabled + user_password = credentials.getPassword() + if (iw_helium_enabled): + login_array = requestParameters.get("login") + if ArrayHelper.isEmpty(login_array): + print "InWebo. Authenticate for step 1. login is empty" + return False + + user_name = login_array[0] + + password_array = requestParameters.get("password") + if ArrayHelper.isEmpty(password_array): + print "InWebo. Authenticate for step 1. password is empty" + return False + + user_password = password_array[0] + + response_validation = self.validateInweboToken(iw_api_uri, iw_service_id, user_name, user_password) + if (not response_validation): + return False + + logged_in = False + if (StringHelper.isNotEmptyString(user_name)): + userService = CdiUtil.bean(UserService) + logged_in = authenticationService.authenticate(user_name) + + return logged_in + else: + logged_in = False + if (StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password)): + userService = CdiUtil.bean(UserService) + logged_in = authenticationService.authenticate(user_name, user_password) + + return logged_in + + return True + elif (step == 2): + print "InWebo. Authenticate for step 2" + + passed_step1 = self.isPassedDefaultAuthentication + if (not passed_step1): + return False + + iw_token_array = requestParameters.get("iw_token") + if ArrayHelper.isEmpty(iw_token_array): + print "InWebo. Authenticate for step 2. iw_token is empty" + return False + + iw_token = iw_token_array[0] + + response_validation = self.validateInweboToken(iw_api_uri, iw_service_id, user_name, iw_token) + + return response_validation + else: + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + if (step == 1): + print "InWebo. Prepare for step 1" + identity = CdiUtil.bean(Identity) + + iw_helium_enabled = Boolean(configurationAttributes.get("iw_helium_enabled").getValue2()).booleanValue() + identity.setWorkingParameter("helium_enabled", iw_helium_enabled) + + iw_helium_alias = None + if (iw_helium_enabled): + iw_helium_alias = configurationAttributes.get("iw_helium_alias").getValue2() + identity.setWorkingParameter("helium_alias", iw_helium_alias) + + print "InWebo. Prepare for step 1. Helium status:", iw_helium_enabled + + return True + + def getExtraParametersForStep(self, configurationAttributes, step): + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + identity = CdiUtil.bean(Identity) + if (identity.isSetWorkingParameter("iw_count_login_steps")): + return identity.getWorkingParameter("iw_count_login_steps") + + return 2 + + def getPageForStep(self, configurationAttributes, step): + if (step == 1): + return "/auth/inwebo/iwlogin.xhtml" + if (step == 2): + return "/auth/inwebo/iwauthenticate.xhtml" + else: + return "" + + def isPassedDefaultAuthentication(self): + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + user_name = credentials.getUsername() + passed_step1 = StringHelper.isNotEmptyString(user_name) + + return passed_step1 + + def validateInweboToken(self, iw_api_uri, iw_service_id, user_name, iw_token): + httpService = CdiUtil.bean(HttpService) + xmlService = CdiUtil.bean(XmlService) + + if StringHelper.isEmpty(iw_token): + print "InWebo. Token verification. iw_token is empty" + return False + + request_uri = iw_api_uri + "?action=authenticate" + "&serviceId=" + httpService.encodeUrl(iw_service_id) + "&userId=" + httpService.encodeUrl(user_name) + "&token=" + httpService.encodeUrl(iw_token) + print "InWebo. Token verification. Attempting to send authentication request:", request_uri + # Execute request + http_response = httpService.executeGet(self.client, request_uri) + + # Validate response code + response_validation = httpService.isResponseStastusCodeOk(http_response) + if response_validation == False: + print "InWebo. Token verification. Get unsuccessful response code" + return False + + authentication_response_bytes = httpService.getResponseContent(http_response) + print "InWebo. Token verification. Get response:", httpService.convertEntityToString(authentication_response_bytes) + + # Validate authentication response + response_validation = httpService.isContentTypeXml(http_response) + if response_validation == False: + print "InWebo. Token verification. Get invalid response" + return False + + # Parse XML response + try: + xmlDocument = xmlService.getXmlDocument(authentication_response_bytes) + except Exception, err: + print "InWebo. Token verification. Failed to parse XML response:", err + return False + + result_code = xmlService.getNodeValue(xmlDocument, "/authenticate", None) + print "InWebo. Token verification. Result after parsing XML response:", result_code + + response_validation = StringHelper.equals(result_code, "OK") + print "InWebo. Token verification. Result validation:", response_validation + + return response_validation + + def logout(self, configurationAttributes, requestParameters): + return True diff --git a/oxAuth/Server/integrations.deprecatred/oneid/OneIdExternalAuthenticator.py b/oxAuth/Server/integrations.deprecatred/oneid/OneIdExternalAuthenticator.py new file mode 100644 index 00000000..77a7a278 --- /dev/null +++ b/oxAuth/Server/integrations.deprecatred/oneid/OneIdExternalAuthenticator.py @@ -0,0 +1,236 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2016, Gluu +# +# Author: Yuriy Movchan +# + +import json +from java.util import Arrays +from oneid import OneID +from org.apache.http.entity import ContentType +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.security import Identity +from org.gluu.oxauth.service import UserService, AuthenticationService +from org.gluu.oxauth.service.net import HttpService +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.util import ArrayHelper +from org.gluu.util import StringHelper + + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, configurationAttributes): + print "OneId. Initialization" + print "OneId. Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "OneId. Destroy" + print "OneId. Destroyed successfully" + return True + + def getApiVersion(self): + return 1 + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + identity = CdiUtil.bean(Identity) + + userService = CdiUtil.bean(UserService) + authenticationService = CdiUtil.bean(AuthenticationService) + httpService = CdiUtil.bean(HttpService) + + server_flag = configurationAttributes.get("oneid_server_flag").getValue2() + callback_attrs = configurationAttributes.get("oneid_callback_attrs").getValue2() + creds_file = configurationAttributes.get("oneid_creds_file").getValue2() + + # Create OneID + authn = OneID(server_flag) + + # Set path to credentials file + authn.creds_file = creds_file + + if (step == 1): + print "OneId. Authenticate for step 1" + + # Find OneID request + json_data_array = requestParameters.get("json_data") + if ArrayHelper.isEmpty(json_data_array): + print "OneId. Authenticate for step 1. json_data is empty" + return False + + request = json_data_array[0] + print "OneId. Authenticate for step 1. request: " + request + + if (StringHelper.isEmptyString(request)): + return False + + authn.set_credentials() + + # Validate request + http_client = httpService.getHttpsClientDefaulTrustStore() + auth_data = httpService.encodeBase64(authn.api_id + ":" + authn.api_key) + http_response = httpService.executePost(http_client, authn.helper_server + "/validate", auth_data, request, ContentType.APPLICATION_JSON) + validation_content = httpService.convertEntityToString(httpService.getResponseContent(http_response)) + print "OneId. Authenticate for step 1. validation_content: " + validation_content + + if (StringHelper.isEmptyString(validation_content)): + return False + + validation_resp = json.loads(validation_content) + print "OneId. Authenticate for step 1. validation_resp: " + str(validation_resp) + + if (not authn.success(validation_resp)): + return False + + response = json.loads(request) + for x in validation_resp: + response[x] = validation_resp[x] + + oneid_user_uid = response['uid'] + print "OneId. Authenticate for step 1. oneid_user_uid: " + oneid_user_uid + + # Check if the is user with specified oneid_user_uid + find_user_by_uid = userService.getUserByAttribute("oxExternalUid", "oneid:" + oneid_user_uid) + + if (find_user_by_uid == None): + print "OneId. Authenticate for step 1. Failed to find user" + print "OneId. Authenticate for step 1. Setting count steps to 2" + identity.setWorkingParameter("oneid_count_login_steps", 2) + identity.setWorkingParameter("oneid_user_uid", oneid_user_uid) + return True + + found_user_name = find_user_by_uid.getUserId() + print "OneId. Authenticate for step 1. found_user_name: " + found_user_name + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + credentials.setUsername(found_user_name) + credentials.setUser(find_user_by_uid) + + print "OneId. Authenticate for step 1. Setting count steps to 1" + identity.setWorkingParameter("oneid_count_login_steps", 1) + + return True + elif (step == 2): + print "OneId. Authenticate for step 2" + + sessionAttributes = identity.getSessionId().getSessionAttributes() + if (sessionAttributes == None) or not sessionAttributes.containsKey("oneid_user_uid"): + print "OneId. Authenticate for step 2. oneid_user_uid is empty" + return False + + oneid_user_uid = sessionAttributes.get("oneid_user_uid") + passed_step1 = StringHelper.isNotEmptyString(oneid_user_uid) + if (not passed_step1): + return False + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + user_name = credentials.getUsername() + passed_step1 = StringHelper.isNotEmptyString(user_name) + + if (not passed_step1): + return False + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + user_name = credentials.getUsername() + user_password = credentials.getPassword() + logged_in = False + if (StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password)): + logged_in = authenticationService.authenticate(user_name, user_password) + + if (not logged_in): + return False + + # Check if there is user which has oneid_user_uid + # Avoid mapping OneID account to more than one IDP account + find_user_by_uid = userService.getUserByAttribute("oxExternalUid", "oneid:" + oneid_user_uid) + + if (find_user_by_uid == None): + # Add oneid_user_uid to user one id UIDs + find_user_by_uid = userService.addUserAttribute(user_name, "oxExternalUid", "oneid:" + oneid_user_uid) + if (find_user_by_uid == None): + print "OneId. Authenticate for step 2. Failed to update current user" + return False + + return True + else: + found_user_name = find_user_by_uid.getUserId() + print "OneId. Authenticate for step 2. found_user_name: " + found_user_name + + if StringHelper.equals(user_name, found_user_name): + return True + + return False + else: + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + identity = CdiUtil.bean(Identity) + authenticationService = CdiUtil.bean(AuthenticationService) + + server_flag = configurationAttributes.get("oneid_server_flag").getValue2() + callback_attrs = configurationAttributes.get("oneid_callback_attrs").getValue2() + creds_file = configurationAttributes.get("oneid_creds_file").getValue2() + + # Create OneID + authn = OneID(server_flag) + + # Set path to credentials file + authn.creds_file = creds_file + + if (step == 1): + print "OneId. Prepare for step 1" + + facesContext = CdiUtil.bean(FacesContext) + request = facesContext.getExternalContext().getRequest() + validation_page = request.getContextPath() + "/postlogin.htm?" + "request_uri=&" + authenticationService.parametersAsString() + print "OneId. Prepare for step 1. validation_page: " + validation_page + + oneid_login_button = authn.draw_signin_button(validation_page, callback_attrs, True) + print "OneId. Prepare for step 1. oneid_login_button: " + oneid_login_button + + identity.setWorkingParameter("oneid_login_button", oneid_login_button) + identity.setWorkingParameter("oneid_script_header", authn.script_header) + identity.setWorkingParameter("oneid_form_script", authn.oneid_form_script) + + return True + elif (step == 2): + print "OneId. Prepare for step 2" + + return True + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + if (step == 2): + return Arrays.asList("oneid_user_uid") + + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + identity = CdiUtil.bean(Identity) + if (identity.isSetWorkingParameter("oneid_count_login_steps")): + return identity.getWorkingParameter("oneid_count_login_steps") + + return 2 + + def getPageForStep(self, configurationAttributes, step): + if (step == 1): + return "/auth/oneid/oneidlogin.xhtml" + return "/auth/oneid/oneidpostlogin.xhtml" + + def logout(self, configurationAttributes, requestParameters): + return True diff --git a/oxAuth/Server/integrations.deprecatred/oneid/docs/workflow.png b/oxAuth/Server/integrations.deprecatred/oneid/docs/workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..ff90de95f8f4a209237cf8422a84de18fc3c7189 GIT binary patch literal 37612 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 literal 0 HcmV?d00001 diff --git a/oxAuth/Server/integrations.deprecatred/oneid/docs/workflow.txt b/oxAuth/Server/integrations.deprecatred/oneid/docs/workflow.txt new file mode 100644 index 00000000..fb5180d4 --- /dev/null +++ b/oxAuth/Server/integrations.deprecatred/oneid/docs/workflow.txt @@ -0,0 +1,13 @@ +title OneID external authenticator plugin (enabled) + +Redirect to oxAuth->Basic auth.: Default athentication + +Basic auth.->OneID auth.: OneID authentication + +OneID auth.->LDAP: Try to load user entry from LDAP by OneID UID + +LDAP->Set current user OneID UID: Can't find user by OneID UID + +Set current user OneID UID->LDAP: Load user entry + +LDAP->Validate User: Check if user name in basic auth. equals to user name which found by OneID UID. This step increase security diff --git a/oxAuth/Server/integrations.deprecatred/oneid/lib/oneid.py b/oxAuth/Server/integrations.deprecatred/oneid/lib/oneid.py new file mode 100644 index 00000000..19b7b8e2 --- /dev/null +++ b/oxAuth/Server/integrations.deprecatred/oneid/lib/oneid.py @@ -0,0 +1,145 @@ +#!/usr/bin/python + +# OneID Python API Library +# Copyright 2013 by OneID + +import urllib +import urllib2 +import datetime +import base64 +try: + import json +except ImportError: + import simplejson as json + +#import requests +import StringIO +import os +import random + + + +class OneID: + + def __init__(self, server_flag=""): + """server_flag should be (for example) "-test" when using a non-production server""" + self.helper_server = "https://keychain%s.oneid.com" % server_flag + self.script_header = '' % server_flag + self.oneid_form_script = '' % server_flag + self.creds_file = "api_key"+server_flag+".json" + random.seed() + + + def _call_helper(self, method, data={}): + """Call the OneID Helper Service. """ + url = "%s/%s" % (self.helper_server, method) + r = requests.post(url, json.dumps(data), auth=(self.api_id, self.api_key)) + return r.json + + def set_credentials(self, api_id="", api_key=""): + """Set the credentials used for access to the OneID Helper Service""" + if api_id != "": + self.api_id = api_id + self.api_key = api_key + else: + f = open(self.creds_file,'r') + creds = json.loads(f.read()) + f.close() + self.api_id = creds["API_ID"] + self.api_key = creds["API_KEY"] + + def validate(self,line): + """Validate the data received by a callback""" + resp = json.loads(line) + valdata = dict([("nonces",resp["nonces"]),("uid",resp["uid"])]) + if "attr_claim_tokens" in resp: + valdata["attr_claim_tokens"] = resp["attr_claim_tokens"] + valresp = self._call_helper("validate",valdata) + if (not self.success(valresp)): + valresp["failed"] = "failed" + return valresp + + for x in valresp: + resp[x] = valresp[x] + + return resp + + def draw_signin_button(self, callback_url, attrs="", http_post=False): + """Create a OneID Sign In button on the web page""" + challenge = {"attr" : attrs, + 'auth_level' : "OOB", + "callback" : callback_url} + if http_post: + challenge["request_method"] = "HTTP_POST" + + params = json.dumps({"challenge" : challenge }) + + js = "" + js+= "" + + return js + + def draw_quickfill_button(self, attrs): + """Create a OneID QuickFill button on the web page""" + js = "" + js+= "" + + return js + + def draw_provision_button(self, attrs): + """Create a provision button on the web page""" + js = "

" + js+= "" + + return js + + def redirect(self, page, response, sessionid): + """Create the JSON string that instructs the AJAX code to redirect the browser to the account""" + if self.success(response): + suffix = "?sessionid="+sessionid + else: + suffix = "" + + return json.dumps({"error":response['error'],"errorcode":str(response['errorcode']),\ + "url":page + suffix}) + + def success(self, response): + """Check errorcode in a response""" + return response["errorcode"] == 0 + + def save_session(self, response): + """Save attributes and UID in a temporary file for account page""" + sessionid = str(random.getrandbits(128)) + sessionfile = "/tmp/"+sessionid+".OneID" + f = open(sessionfile, "w") + f.write(json.dumps({"uid":response["uid"], "attr":response["attr"]})) + f.close() + return sessionid; + + def get_session(self, sessionid): + """Retrieve attributes and session ID saved by validation page""" + sessionfile = "/tmp/"+sessionid+".OneID" + f = open(sessionfile, "r") + data = f.read() + f.close() + os.remove(sessionfile) + return json.loads(data) + + def _getnonce(self, response): + """Extract base64-encoded nonce from JWT in a response""" + return response["nonces"]["repo"]["nonce"].split('.')[1] + + + diff --git a/oxAuth/Server/integrations.deprecatred/oneid/lib/stringprep.py b/oxAuth/Server/integrations.deprecatred/oneid/lib/stringprep.py new file mode 100644 index 00000000..955eabc2 --- /dev/null +++ b/oxAuth/Server/integrations.deprecatred/oneid/lib/stringprep.py @@ -0,0 +1,272 @@ +# This file is generated by mkstringprep.py. DO NOT EDIT. +"""Library that exposes various tables found in the StringPrep RFC 3454. + +There are two kinds of tables: sets, for which a member test is provided, +and mappings, for which a mapping function is provided. +""" + +import unicodedata + +#assert unicodedata.unidata_version == '3.2.0' + +def in_table_a1(code): + if unicodedata.category(code) != 'Cn': return False + c = ord(code) + if 0xFDD0 <= c < 0xFDF0: return False + return (c & 0xFFFF) not in (0xFFFE, 0xFFFF) + + +b1_set = set([173, 847, 6150, 6155, 6156, 6157, 8203, 8204, 8205, 8288, 65279] + range(65024,65040)) +def in_table_b1(code): + return ord(code) in b1_set + + +b3_exceptions = { +0xb5:u'\u03bc', 0xdf:u'ss', 0x130:u'i\u0307', 0x149:u'\u02bcn', +0x17f:u's', 0x1f0:u'j\u030c', 0x345:u'\u03b9', 0x37a:u' \u03b9', +0x390:u'\u03b9\u0308\u0301', 0x3b0:u'\u03c5\u0308\u0301', 0x3c2:u'\u03c3', 0x3d0:u'\u03b2', +0x3d1:u'\u03b8', 0x3d2:u'\u03c5', 0x3d3:u'\u03cd', 0x3d4:u'\u03cb', +0x3d5:u'\u03c6', 0x3d6:u'\u03c0', 0x3f0:u'\u03ba', 0x3f1:u'\u03c1', +0x3f2:u'\u03c3', 0x3f5:u'\u03b5', 0x587:u'\u0565\u0582', 0x1e96:u'h\u0331', +0x1e97:u't\u0308', 0x1e98:u'w\u030a', 0x1e99:u'y\u030a', 0x1e9a:u'a\u02be', +0x1e9b:u'\u1e61', 0x1f50:u'\u03c5\u0313', 0x1f52:u'\u03c5\u0313\u0300', 0x1f54:u'\u03c5\u0313\u0301', +0x1f56:u'\u03c5\u0313\u0342', 0x1f80:u'\u1f00\u03b9', 0x1f81:u'\u1f01\u03b9', 0x1f82:u'\u1f02\u03b9', +0x1f83:u'\u1f03\u03b9', 0x1f84:u'\u1f04\u03b9', 0x1f85:u'\u1f05\u03b9', 0x1f86:u'\u1f06\u03b9', +0x1f87:u'\u1f07\u03b9', 0x1f88:u'\u1f00\u03b9', 0x1f89:u'\u1f01\u03b9', 0x1f8a:u'\u1f02\u03b9', +0x1f8b:u'\u1f03\u03b9', 0x1f8c:u'\u1f04\u03b9', 0x1f8d:u'\u1f05\u03b9', 0x1f8e:u'\u1f06\u03b9', +0x1f8f:u'\u1f07\u03b9', 0x1f90:u'\u1f20\u03b9', 0x1f91:u'\u1f21\u03b9', 0x1f92:u'\u1f22\u03b9', +0x1f93:u'\u1f23\u03b9', 0x1f94:u'\u1f24\u03b9', 0x1f95:u'\u1f25\u03b9', 0x1f96:u'\u1f26\u03b9', +0x1f97:u'\u1f27\u03b9', 0x1f98:u'\u1f20\u03b9', 0x1f99:u'\u1f21\u03b9', 0x1f9a:u'\u1f22\u03b9', +0x1f9b:u'\u1f23\u03b9', 0x1f9c:u'\u1f24\u03b9', 0x1f9d:u'\u1f25\u03b9', 0x1f9e:u'\u1f26\u03b9', +0x1f9f:u'\u1f27\u03b9', 0x1fa0:u'\u1f60\u03b9', 0x1fa1:u'\u1f61\u03b9', 0x1fa2:u'\u1f62\u03b9', +0x1fa3:u'\u1f63\u03b9', 0x1fa4:u'\u1f64\u03b9', 0x1fa5:u'\u1f65\u03b9', 0x1fa6:u'\u1f66\u03b9', +0x1fa7:u'\u1f67\u03b9', 0x1fa8:u'\u1f60\u03b9', 0x1fa9:u'\u1f61\u03b9', 0x1faa:u'\u1f62\u03b9', +0x1fab:u'\u1f63\u03b9', 0x1fac:u'\u1f64\u03b9', 0x1fad:u'\u1f65\u03b9', 0x1fae:u'\u1f66\u03b9', +0x1faf:u'\u1f67\u03b9', 0x1fb2:u'\u1f70\u03b9', 0x1fb3:u'\u03b1\u03b9', 0x1fb4:u'\u03ac\u03b9', +0x1fb6:u'\u03b1\u0342', 0x1fb7:u'\u03b1\u0342\u03b9', 0x1fbc:u'\u03b1\u03b9', 0x1fbe:u'\u03b9', +0x1fc2:u'\u1f74\u03b9', 0x1fc3:u'\u03b7\u03b9', 0x1fc4:u'\u03ae\u03b9', 0x1fc6:u'\u03b7\u0342', +0x1fc7:u'\u03b7\u0342\u03b9', 0x1fcc:u'\u03b7\u03b9', 0x1fd2:u'\u03b9\u0308\u0300', 0x1fd3:u'\u03b9\u0308\u0301', +0x1fd6:u'\u03b9\u0342', 0x1fd7:u'\u03b9\u0308\u0342', 0x1fe2:u'\u03c5\u0308\u0300', 0x1fe3:u'\u03c5\u0308\u0301', +0x1fe4:u'\u03c1\u0313', 0x1fe6:u'\u03c5\u0342', 0x1fe7:u'\u03c5\u0308\u0342', 0x1ff2:u'\u1f7c\u03b9', +0x1ff3:u'\u03c9\u03b9', 0x1ff4:u'\u03ce\u03b9', 0x1ff6:u'\u03c9\u0342', 0x1ff7:u'\u03c9\u0342\u03b9', +0x1ffc:u'\u03c9\u03b9', 0x20a8:u'rs', 0x2102:u'c', 0x2103:u'\xb0c', +0x2107:u'\u025b', 0x2109:u'\xb0f', 0x210b:u'h', 0x210c:u'h', +0x210d:u'h', 0x2110:u'i', 0x2111:u'i', 0x2112:u'l', +0x2115:u'n', 0x2116:u'no', 0x2119:u'p', 0x211a:u'q', +0x211b:u'r', 0x211c:u'r', 0x211d:u'r', 0x2120:u'sm', +0x2121:u'tel', 0x2122:u'tm', 0x2124:u'z', 0x2128:u'z', +0x212c:u'b', 0x212d:u'c', 0x2130:u'e', 0x2131:u'f', +0x2133:u'm', 0x213e:u'\u03b3', 0x213f:u'\u03c0', 0x2145:u'd', +0x3371:u'hpa', 0x3373:u'au', 0x3375:u'ov', 0x3380:u'pa', +0x3381:u'na', 0x3382:u'\u03bca', 0x3383:u'ma', 0x3384:u'ka', +0x3385:u'kb', 0x3386:u'mb', 0x3387:u'gb', 0x338a:u'pf', +0x338b:u'nf', 0x338c:u'\u03bcf', 0x3390:u'hz', 0x3391:u'khz', +0x3392:u'mhz', 0x3393:u'ghz', 0x3394:u'thz', 0x33a9:u'pa', +0x33aa:u'kpa', 0x33ab:u'mpa', 0x33ac:u'gpa', 0x33b4:u'pv', +0x33b5:u'nv', 0x33b6:u'\u03bcv', 0x33b7:u'mv', 0x33b8:u'kv', +0x33b9:u'mv', 0x33ba:u'pw', 0x33bb:u'nw', 0x33bc:u'\u03bcw', +0x33bd:u'mw', 0x33be:u'kw', 0x33bf:u'mw', 0x33c0:u'k\u03c9', +0x33c1:u'm\u03c9', 0x33c3:u'bq', 0x33c6:u'c\u2215kg', 0x33c7:u'co.', +0x33c8:u'db', 0x33c9:u'gy', 0x33cb:u'hp', 0x33cd:u'kk', +0x33ce:u'km', 0x33d7:u'ph', 0x33d9:u'ppm', 0x33da:u'pr', +0x33dc:u'sv', 0x33dd:u'wb', 0xfb00:u'ff', 0xfb01:u'fi', +0xfb02:u'fl', 0xfb03:u'ffi', 0xfb04:u'ffl', 0xfb05:u'st', +0xfb06:u'st', 0xfb13:u'\u0574\u0576', 0xfb14:u'\u0574\u0565', 0xfb15:u'\u0574\u056b', +0xfb16:u'\u057e\u0576', 0xfb17:u'\u0574\u056d', 0x1d400:u'a', 0x1d401:u'b', +0x1d402:u'c', 0x1d403:u'd', 0x1d404:u'e', 0x1d405:u'f', +0x1d406:u'g', 0x1d407:u'h', 0x1d408:u'i', 0x1d409:u'j', +0x1d40a:u'k', 0x1d40b:u'l', 0x1d40c:u'm', 0x1d40d:u'n', +0x1d40e:u'o', 0x1d40f:u'p', 0x1d410:u'q', 0x1d411:u'r', +0x1d412:u's', 0x1d413:u't', 0x1d414:u'u', 0x1d415:u'v', +0x1d416:u'w', 0x1d417:u'x', 0x1d418:u'y', 0x1d419:u'z', +0x1d434:u'a', 0x1d435:u'b', 0x1d436:u'c', 0x1d437:u'd', +0x1d438:u'e', 0x1d439:u'f', 0x1d43a:u'g', 0x1d43b:u'h', +0x1d43c:u'i', 0x1d43d:u'j', 0x1d43e:u'k', 0x1d43f:u'l', +0x1d440:u'm', 0x1d441:u'n', 0x1d442:u'o', 0x1d443:u'p', +0x1d444:u'q', 0x1d445:u'r', 0x1d446:u's', 0x1d447:u't', +0x1d448:u'u', 0x1d449:u'v', 0x1d44a:u'w', 0x1d44b:u'x', +0x1d44c:u'y', 0x1d44d:u'z', 0x1d468:u'a', 0x1d469:u'b', +0x1d46a:u'c', 0x1d46b:u'd', 0x1d46c:u'e', 0x1d46d:u'f', +0x1d46e:u'g', 0x1d46f:u'h', 0x1d470:u'i', 0x1d471:u'j', +0x1d472:u'k', 0x1d473:u'l', 0x1d474:u'm', 0x1d475:u'n', +0x1d476:u'o', 0x1d477:u'p', 0x1d478:u'q', 0x1d479:u'r', +0x1d47a:u's', 0x1d47b:u't', 0x1d47c:u'u', 0x1d47d:u'v', +0x1d47e:u'w', 0x1d47f:u'x', 0x1d480:u'y', 0x1d481:u'z', +0x1d49c:u'a', 0x1d49e:u'c', 0x1d49f:u'd', 0x1d4a2:u'g', +0x1d4a5:u'j', 0x1d4a6:u'k', 0x1d4a9:u'n', 0x1d4aa:u'o', +0x1d4ab:u'p', 0x1d4ac:u'q', 0x1d4ae:u's', 0x1d4af:u't', +0x1d4b0:u'u', 0x1d4b1:u'v', 0x1d4b2:u'w', 0x1d4b3:u'x', +0x1d4b4:u'y', 0x1d4b5:u'z', 0x1d4d0:u'a', 0x1d4d1:u'b', +0x1d4d2:u'c', 0x1d4d3:u'd', 0x1d4d4:u'e', 0x1d4d5:u'f', +0x1d4d6:u'g', 0x1d4d7:u'h', 0x1d4d8:u'i', 0x1d4d9:u'j', +0x1d4da:u'k', 0x1d4db:u'l', 0x1d4dc:u'm', 0x1d4dd:u'n', +0x1d4de:u'o', 0x1d4df:u'p', 0x1d4e0:u'q', 0x1d4e1:u'r', +0x1d4e2:u's', 0x1d4e3:u't', 0x1d4e4:u'u', 0x1d4e5:u'v', +0x1d4e6:u'w', 0x1d4e7:u'x', 0x1d4e8:u'y', 0x1d4e9:u'z', +0x1d504:u'a', 0x1d505:u'b', 0x1d507:u'd', 0x1d508:u'e', +0x1d509:u'f', 0x1d50a:u'g', 0x1d50d:u'j', 0x1d50e:u'k', +0x1d50f:u'l', 0x1d510:u'm', 0x1d511:u'n', 0x1d512:u'o', +0x1d513:u'p', 0x1d514:u'q', 0x1d516:u's', 0x1d517:u't', +0x1d518:u'u', 0x1d519:u'v', 0x1d51a:u'w', 0x1d51b:u'x', +0x1d51c:u'y', 0x1d538:u'a', 0x1d539:u'b', 0x1d53b:u'd', +0x1d53c:u'e', 0x1d53d:u'f', 0x1d53e:u'g', 0x1d540:u'i', +0x1d541:u'j', 0x1d542:u'k', 0x1d543:u'l', 0x1d544:u'm', +0x1d546:u'o', 0x1d54a:u's', 0x1d54b:u't', 0x1d54c:u'u', +0x1d54d:u'v', 0x1d54e:u'w', 0x1d54f:u'x', 0x1d550:u'y', +0x1d56c:u'a', 0x1d56d:u'b', 0x1d56e:u'c', 0x1d56f:u'd', +0x1d570:u'e', 0x1d571:u'f', 0x1d572:u'g', 0x1d573:u'h', +0x1d574:u'i', 0x1d575:u'j', 0x1d576:u'k', 0x1d577:u'l', +0x1d578:u'm', 0x1d579:u'n', 0x1d57a:u'o', 0x1d57b:u'p', +0x1d57c:u'q', 0x1d57d:u'r', 0x1d57e:u's', 0x1d57f:u't', +0x1d580:u'u', 0x1d581:u'v', 0x1d582:u'w', 0x1d583:u'x', +0x1d584:u'y', 0x1d585:u'z', 0x1d5a0:u'a', 0x1d5a1:u'b', +0x1d5a2:u'c', 0x1d5a3:u'd', 0x1d5a4:u'e', 0x1d5a5:u'f', +0x1d5a6:u'g', 0x1d5a7:u'h', 0x1d5a8:u'i', 0x1d5a9:u'j', +0x1d5aa:u'k', 0x1d5ab:u'l', 0x1d5ac:u'm', 0x1d5ad:u'n', +0x1d5ae:u'o', 0x1d5af:u'p', 0x1d5b0:u'q', 0x1d5b1:u'r', +0x1d5b2:u's', 0x1d5b3:u't', 0x1d5b4:u'u', 0x1d5b5:u'v', +0x1d5b6:u'w', 0x1d5b7:u'x', 0x1d5b8:u'y', 0x1d5b9:u'z', +0x1d5d4:u'a', 0x1d5d5:u'b', 0x1d5d6:u'c', 0x1d5d7:u'd', +0x1d5d8:u'e', 0x1d5d9:u'f', 0x1d5da:u'g', 0x1d5db:u'h', +0x1d5dc:u'i', 0x1d5dd:u'j', 0x1d5de:u'k', 0x1d5df:u'l', +0x1d5e0:u'm', 0x1d5e1:u'n', 0x1d5e2:u'o', 0x1d5e3:u'p', +0x1d5e4:u'q', 0x1d5e5:u'r', 0x1d5e6:u's', 0x1d5e7:u't', +0x1d5e8:u'u', 0x1d5e9:u'v', 0x1d5ea:u'w', 0x1d5eb:u'x', +0x1d5ec:u'y', 0x1d5ed:u'z', 0x1d608:u'a', 0x1d609:u'b', +0x1d60a:u'c', 0x1d60b:u'd', 0x1d60c:u'e', 0x1d60d:u'f', +0x1d60e:u'g', 0x1d60f:u'h', 0x1d610:u'i', 0x1d611:u'j', +0x1d612:u'k', 0x1d613:u'l', 0x1d614:u'm', 0x1d615:u'n', +0x1d616:u'o', 0x1d617:u'p', 0x1d618:u'q', 0x1d619:u'r', +0x1d61a:u's', 0x1d61b:u't', 0x1d61c:u'u', 0x1d61d:u'v', +0x1d61e:u'w', 0x1d61f:u'x', 0x1d620:u'y', 0x1d621:u'z', +0x1d63c:u'a', 0x1d63d:u'b', 0x1d63e:u'c', 0x1d63f:u'd', +0x1d640:u'e', 0x1d641:u'f', 0x1d642:u'g', 0x1d643:u'h', +0x1d644:u'i', 0x1d645:u'j', 0x1d646:u'k', 0x1d647:u'l', +0x1d648:u'm', 0x1d649:u'n', 0x1d64a:u'o', 0x1d64b:u'p', +0x1d64c:u'q', 0x1d64d:u'r', 0x1d64e:u's', 0x1d64f:u't', +0x1d650:u'u', 0x1d651:u'v', 0x1d652:u'w', 0x1d653:u'x', +0x1d654:u'y', 0x1d655:u'z', 0x1d670:u'a', 0x1d671:u'b', +0x1d672:u'c', 0x1d673:u'd', 0x1d674:u'e', 0x1d675:u'f', +0x1d676:u'g', 0x1d677:u'h', 0x1d678:u'i', 0x1d679:u'j', +0x1d67a:u'k', 0x1d67b:u'l', 0x1d67c:u'm', 0x1d67d:u'n', +0x1d67e:u'o', 0x1d67f:u'p', 0x1d680:u'q', 0x1d681:u'r', +0x1d682:u's', 0x1d683:u't', 0x1d684:u'u', 0x1d685:u'v', +0x1d686:u'w', 0x1d687:u'x', 0x1d688:u'y', 0x1d689:u'z', +0x1d6a8:u'\u03b1', 0x1d6a9:u'\u03b2', 0x1d6aa:u'\u03b3', 0x1d6ab:u'\u03b4', +0x1d6ac:u'\u03b5', 0x1d6ad:u'\u03b6', 0x1d6ae:u'\u03b7', 0x1d6af:u'\u03b8', +0x1d6b0:u'\u03b9', 0x1d6b1:u'\u03ba', 0x1d6b2:u'\u03bb', 0x1d6b3:u'\u03bc', +0x1d6b4:u'\u03bd', 0x1d6b5:u'\u03be', 0x1d6b6:u'\u03bf', 0x1d6b7:u'\u03c0', +0x1d6b8:u'\u03c1', 0x1d6b9:u'\u03b8', 0x1d6ba:u'\u03c3', 0x1d6bb:u'\u03c4', +0x1d6bc:u'\u03c5', 0x1d6bd:u'\u03c6', 0x1d6be:u'\u03c7', 0x1d6bf:u'\u03c8', +0x1d6c0:u'\u03c9', 0x1d6d3:u'\u03c3', 0x1d6e2:u'\u03b1', 0x1d6e3:u'\u03b2', +0x1d6e4:u'\u03b3', 0x1d6e5:u'\u03b4', 0x1d6e6:u'\u03b5', 0x1d6e7:u'\u03b6', +0x1d6e8:u'\u03b7', 0x1d6e9:u'\u03b8', 0x1d6ea:u'\u03b9', 0x1d6eb:u'\u03ba', +0x1d6ec:u'\u03bb', 0x1d6ed:u'\u03bc', 0x1d6ee:u'\u03bd', 0x1d6ef:u'\u03be', +0x1d6f0:u'\u03bf', 0x1d6f1:u'\u03c0', 0x1d6f2:u'\u03c1', 0x1d6f3:u'\u03b8', +0x1d6f4:u'\u03c3', 0x1d6f5:u'\u03c4', 0x1d6f6:u'\u03c5', 0x1d6f7:u'\u03c6', +0x1d6f8:u'\u03c7', 0x1d6f9:u'\u03c8', 0x1d6fa:u'\u03c9', 0x1d70d:u'\u03c3', +0x1d71c:u'\u03b1', 0x1d71d:u'\u03b2', 0x1d71e:u'\u03b3', 0x1d71f:u'\u03b4', +0x1d720:u'\u03b5', 0x1d721:u'\u03b6', 0x1d722:u'\u03b7', 0x1d723:u'\u03b8', +0x1d724:u'\u03b9', 0x1d725:u'\u03ba', 0x1d726:u'\u03bb', 0x1d727:u'\u03bc', +0x1d728:u'\u03bd', 0x1d729:u'\u03be', 0x1d72a:u'\u03bf', 0x1d72b:u'\u03c0', +0x1d72c:u'\u03c1', 0x1d72d:u'\u03b8', 0x1d72e:u'\u03c3', 0x1d72f:u'\u03c4', +0x1d730:u'\u03c5', 0x1d731:u'\u03c6', 0x1d732:u'\u03c7', 0x1d733:u'\u03c8', +0x1d734:u'\u03c9', 0x1d747:u'\u03c3', 0x1d756:u'\u03b1', 0x1d757:u'\u03b2', +0x1d758:u'\u03b3', 0x1d759:u'\u03b4', 0x1d75a:u'\u03b5', 0x1d75b:u'\u03b6', +0x1d75c:u'\u03b7', 0x1d75d:u'\u03b8', 0x1d75e:u'\u03b9', 0x1d75f:u'\u03ba', +0x1d760:u'\u03bb', 0x1d761:u'\u03bc', 0x1d762:u'\u03bd', 0x1d763:u'\u03be', +0x1d764:u'\u03bf', 0x1d765:u'\u03c0', 0x1d766:u'\u03c1', 0x1d767:u'\u03b8', +0x1d768:u'\u03c3', 0x1d769:u'\u03c4', 0x1d76a:u'\u03c5', 0x1d76b:u'\u03c6', +0x1d76c:u'\u03c7', 0x1d76d:u'\u03c8', 0x1d76e:u'\u03c9', 0x1d781:u'\u03c3', +0x1d790:u'\u03b1', 0x1d791:u'\u03b2', 0x1d792:u'\u03b3', 0x1d793:u'\u03b4', +0x1d794:u'\u03b5', 0x1d795:u'\u03b6', 0x1d796:u'\u03b7', 0x1d797:u'\u03b8', +0x1d798:u'\u03b9', 0x1d799:u'\u03ba', 0x1d79a:u'\u03bb', 0x1d79b:u'\u03bc', +0x1d79c:u'\u03bd', 0x1d79d:u'\u03be', 0x1d79e:u'\u03bf', 0x1d79f:u'\u03c0', +0x1d7a0:u'\u03c1', 0x1d7a1:u'\u03b8', 0x1d7a2:u'\u03c3', 0x1d7a3:u'\u03c4', +0x1d7a4:u'\u03c5', 0x1d7a5:u'\u03c6', 0x1d7a6:u'\u03c7', 0x1d7a7:u'\u03c8', +0x1d7a8:u'\u03c9', 0x1d7bb:u'\u03c3', } + +def map_table_b3(code): + r = b3_exceptions.get(ord(code)) + if r is not None: return r + return code.lower() + + +def map_table_b2(a): + al = map_table_b3(a) + b = unicodedata.normalize("NFKC", al) + bl = u"".join([map_table_b3(ch) for ch in b]) + c = unicodedata.normalize("NFKC", bl) + if b != c: + return c + else: + return al + + +def in_table_c11(code): + return code == u" " + + +def in_table_c12(code): + return unicodedata.category(code) == "Zs" and code != u" " + +def in_table_c11_c12(code): + return unicodedata.category(code) == "Zs" + + +def in_table_c21(code): + return ord(code) < 128 and unicodedata.category(code) == "Cc" + +c22_specials = set([1757, 1807, 6158, 8204, 8205, 8232, 8233, 65279] + range(8288,8292) + range(8298,8304) + range(65529,65533) + range(119155,119163)) +def in_table_c22(code): + c = ord(code) + if c < 128: return False + if unicodedata.category(code) == "Cc": return True + return c in c22_specials + +def in_table_c21_c22(code): + return unicodedata.category(code) == "Cc" or \ + ord(code) in c22_specials + + +def in_table_c3(code): + return unicodedata.category(code) == "Co" + + +def in_table_c4(code): + c = ord(code) + if c < 0xFDD0: return False + if c < 0xFDF0: return True + return (ord(code) & 0xFFFF) in (0xFFFE, 0xFFFF) + + +def in_table_c5(code): + return unicodedata.category(code) == "Cs" + + +c6_set = set(range(65529,65534)) +def in_table_c6(code): + return ord(code) in c6_set + + +c7_set = set(range(12272,12284)) +def in_table_c7(code): + return ord(code) in c7_set + + +c8_set = set([832, 833, 8206, 8207] + range(8234,8239) + range(8298,8304)) +def in_table_c8(code): + return ord(code) in c8_set + + +c9_set = set([917505] + range(917536,917632)) +def in_table_c9(code): + return ord(code) in c9_set + + +def in_table_d1(code): + return unicodedata.bidirectional(code) in ("R","AL") + + +def in_table_d2(code): + return unicodedata.bidirectional(code) == "L" diff --git a/oxAuth/Server/integrations.deprecatred/oxpush/oxPushExternalAuthenticator.py b/oxAuth/Server/integrations.deprecatred/oxpush/oxPushExternalAuthenticator.py new file mode 100644 index 00000000..3d83c1da --- /dev/null +++ b/oxAuth/Server/integrations.deprecatred/oxpush/oxPushExternalAuthenticator.py @@ -0,0 +1,307 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2016, Gluu +# +# Author: Yuriy Movchan +# + +import java +from java.util import Arrays +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.security import Identity +from org.gluu.oxauth.service import UserService, AuthenticationService +from org.gluu.oxpush import OxPushClient +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.util import StringHelper + + +class PersonAuthentication(PersonAuthenticationType): + + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, configurationAttributes): + print "oxPush. Initialization" + + oxpush_server_base_uri = configurationAttributes.get("oxpush_server_base_uri").getValue2() + self.oxPushClient = OxPushClient(oxpush_server_base_uri) + print "oxPush. Initialized successfully" + + return True + + def destroy(self, configurationAttributes): + print "oxPush. Destroy" + print "oxPush. Destroyed successfully" + return True + + def getApiVersion(self): + return 1 + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + userService = CdiUtil.bean(UserService) + authenticationService = CdiUtil.bean(AuthenticationService) + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + oxpush_user_timeout = int(configurationAttributes.get("oxpush_user_timeout").getValue2()) + oxpush_application_name = configurationAttributes.get("oxpush_application_name").getValue2() + + user_name = credentials.getUsername() + + if (step == 1): + print "oxPush. Authenticate for step 1" + + user_password = credentials.getPassword() + logged_in = False + if (StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password)): + userService = CdiUtil.bean(UserService) + logged_in = authenticationService.authenticate(user_name, user_password) + + if (not logged_in): + return False + + # Get user entry + userService = CdiUtil.bean(UserService) + find_user_by_uid = authenticationService.getAuthenticatedUser() + if (find_user_by_uid == None): + print "oxPush. Authenticate for step 1. Failed to find user" + return False + + # Check if the user paired account to phone + user_external_uid_attr = userService.getCustomAttribute(find_user_by_uid, "oxExternalUid") + if ((user_external_uid_attr == None) or (user_external_uid_attr.getValues() == None)): + print "oxPush. Authenticate for step 1. There is no external UIDs for user: ", user_name + else: + oxpush_user_uid = None + for ext_uid in user_external_uid_attr.getValues(): + if (ext_uid.startswith('oxpush:')): + oxpush_user_uid = ext_uid[7:len(ext_uid)] + break + + if (oxpush_user_uid == None): + print "oxPush. Authenticate for step 1. There is no oxPush UID for user: ", user_name + else: + # Check deployment status + print "oxPush. Authenticate for step 1. oxpush_user_uid: ", oxpush_user_uid + deployment_status = self.oxPushClient.getDeploymentStatus(oxpush_user_uid) + if (deployment_status.result): + print "oxPush. Authenticate for step 1. Deployment status is valid" + if ("enabled" == deployment_status.status): + print "oxPush. Authenticate for step 1. Deployment is enabled" + identity.setWorkingParameter("oxpush_user_uid", oxpush_user_uid) + else: + print "oxPush. Authenticate for step 1. Deployment is disabled" + return False + else: + print "oxPush. Authenticate for step 1. Deployment status is invalid. Force user to pair again" + # Remove oxpush_user_uid from user entry + find_user_by_uid = userService.removeUserAttribute(user_name, "oxExternalUid", "oxpush:" + oxpush_user_uid) + if (find_user_by_uid == None): + print "oxPush. Authenticate for step 1. Failed to update current user" + return False + + return True + elif (step == 2): + print "oxPush. Authenticate for step 2" + + passed_step1 = self.isPassedDefaultAuthentication + if (not passed_step1): + return False + + sessionAttributes = identity.getSessionId().getSessionAttributes() + if (sessionAttributes == None) or not sessionAttributes.containsKey("oxpush_user_uid"): + print "oxPush. Authenticate for step 2. oxpush_user_uid is empty" + + if (not sessionAttributes.containsKey("oxpush_pairing_uid")): + print "oxPush. Authenticate for step 2. oxpush_pairing_uid is empty" + return False + + oxpush_pairing_uid = sessionAttributes.get("oxpush_pairing_uid") + + # Check pairing status + pairing_status = self.checkStatus("pair", oxpush_pairing_uid, oxpush_user_timeout) + if (pairing_status == None): + print "oxPush. Authenticate for step 2. The pairing has not been authorized by user" + return False + + oxpush_user_uid = pairing_status.deploymentId + + print "oxPush. Authenticate for step 2. Storing oxpush_user_uid in user entry", oxpush_user_uid + + # Store oxpush_user_uid in user entry + find_user_by_uid = userService.addUserAttribute(user_name, "oxExternalUid", "oxpush:" + oxpush_user_uid) + if (find_user_by_uid == None): + print "oxPush. Authenticate for step 2. Failed to update current user" + return False + + identity.setWorkingParameter("oxpush_count_login_steps", 2) + identity.setWorkingParameter("oxpush_user_uid", oxpush_user_uid) + else: + print "oxPush. Authenticate for step 2. Deployment status is valid" + + return True + elif (step == 3): + print "oxPush. Authenticate for step 3" + + passed_step1 = self.isPassedDefaultAuthentication + if (not passed_step1): + return False + + sessionAttributes = identity.getWorkingParameter("oxpush_user_uid") + if (sessionAttributes == None) or not sessionAttributes.containsKey("oxpush_user_uid"): + print "oxPush. Authenticate for step 3. oxpush_user_uid is empty" + return False + + oxpush_user_uid = sessionAttributes.get("oxpush_user_uid") + passed_step1 = StringHelper.isNotEmptyString(oxpush_user_uid) + if (not passed_step1): + return False + + # Initialize authentication process + authentication_request = None + try: + authentication_request = self.oxPushClient.authenticate(oxpush_user_uid, user_name) + except java.lang.Exception, err: + print "oxPush. Authenticate for step 3. Failed to initialize authentication process: ", err + return False + + if (not authentication_request.result): + print "oxPush. Authenticate for step 3. Failed to initialize authentication process" + return False + + # Check authentication status + authentication_status = self.checkStatus("authenticate", authentication_request.authenticationId, oxpush_user_timeout) + if (authentication_status == None): + print "oxPush. Authenticate for step 3. The authentication has not been authorized by user" + return False + + print "oxPush. Authenticate for step 3. The request was granted" + + return True + else: + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + identity = CdiUtil.bean(Identity) + + oxpush_application_name = configurationAttributes.get("oxpush_application_name").getValue2() + + if (step == 1): + print "oxPush. Prepare for step 1" + oxpush_android_download_url = configurationAttributes.get("oxpush_android_download_url").getValue2() + identity.setWorkingParameter("oxpush_android_download_url", oxpush_android_download_url) + elif (step == 2): + print "oxPush. Prepare for step 2" + + passed_step1 = self.isPassedDefaultAuthentication + if (not passed_step1): + return False + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + user_name = credentials.getUsername() + + sessionAttributes = identity.getSessionId().getSessionAttributes() + if (sessionAttributes == None) or not sessionAttributes.containsKey("oxpush_user_uid"): + print "oxPush. Prepare for step 2. oxpush_user_uid is empty" + + # Initialize pairing process + pairing_process = None + try: + pairing_process = self.oxPushClient.pair(oxpush_application_name, user_name) + except java.lang.Exception, err: + print "oxPush. Prepare for step 2. Failed to initialize pairing process: ", err + return False + + if (not pairing_process.result): + print "oxPush. Prepare for step 2. Failed to initialize pairing process" + return False + + pairing_id = pairing_process.pairingId + print "oxPush. Prepare for step 2. Pairing Id: ", pairing_id + + identity.setWorkingParameter("oxpush_pairing_uid", pairing_id) + identity.setWorkingParameter("oxpush_pairing_code", pairing_process.pairingCode) + identity.setWorkingParameter("oxpush_pairing_qr_image", pairing_process.pairingQrImage) + + return True + + def getExtraParametersForStep(self, configurationAttributes, step): + if (step in [2, 3]): + return Arrays.asList("oxpush_user_uid", "oxpush_pairing_uid") + + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + identity = CdiUtil.bean(Identity) + if (identity.isSetWorkingParameter("oxpush_count_login_steps")): + return identity.getWorkingParameter("oxpush_count_login_steps") + + return 3 + + def getPageForStep(self, configurationAttributes, step): + if (step == 1): + return "/auth/oxpush/oxlogin.xhtml" + elif (step == 2): + return "/auth/oxpush/oxpair.xhtml" + elif (step == 3): + return "/auth/oxpush/oxauthenticate.xhtml" + return "" + + def isPassedDefaultAuthentication(): + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + user_name = credentials.getUsername() + passed_step1 = StringHelper.isNotEmptyString(user_name) + + return passed_step1 + + def checkStatus(self, mode, request_id, timeout): + try: + curTime = java.lang.System.currentTimeMillis() + endTime = curTime + timeout * 1000 + while (endTime >= curTime): + response_status = None + if (StringHelper.equals("pair", mode)): + response_status = self.oxPushClient.getPairingStatus(request_id) + else: + response_status = self.oxPushClient.getAuthenticationStatus(request_id) + + if (not response_status.result): + print "oxPush. CheckStatus. Get false result from oxPushServer" + return None + + status = response_status.status + + if ("declined" == status): + print "oxPush. CheckStatus. The process has been cancelled" + return None + + if ("expired" == status): + print "oxPush. CheckStatus. The process has been expired" + return None + + if ("approved" == status): + print "oxPush. CheckStatus. The process was approved" + return response_status + + java.lang.Thread.sleep(2000) + curTime = java.lang.System.currentTimeMillis() + except java.lang.Exception, err: + print "oxPush. CheckStatus. Could not check process status: ", err + return None + + print "oxPush. CheckStatus. The process has not received a response from the phone yet" + + return None + + def logout(self, configurationAttributes, requestParameters): + return True diff --git a/oxAuth/Server/integrations.deprecatred/phonefactor/PhoneFactorExternalAuthenticator.py b/oxAuth/Server/integrations.deprecatred/phonefactor/PhoneFactorExternalAuthenticator.py new file mode 100644 index 00000000..1d434287 --- /dev/null +++ b/oxAuth/Server/integrations.deprecatred/phonefactor/PhoneFactorExternalAuthenticator.py @@ -0,0 +1,200 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2016, Gluu +# +# Author: Yuriy Movchan +# + +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.service import UserService, AuthenticationService +from org.gluu.util import StringHelper, ArrayHelper +from org.gluu.oxauth.service import EncryptionService +from net.phonefactor.pfsdk import PFAuth, PFAuthResult, SecurityException, TimeoutException, PFException +from net.phonefactor.pfsdk import PFAuthResult + +import java +import string + +import json + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + self.pf = PFAuth() + + def init(self, configurationAttributes): + print "PhoneFactor. Initialization" + pf_cert_path = configurationAttributes.get("pf_cert_path").getValue2() + pf_creds_file = configurationAttributes.get("pf_creds_file").getValue2() + + # Load credentials from file + f = open(pf_creds_file, 'r') + try: + creds = json.loads(f.read()) + except: + return False + finally: + f.close() + + certPassword = creds["CERT_PASSWORD"] + try: + encryptionService = CdiUtil.bean(EncryptionService) + certPassword = encryptionService.decrypt(certPassword) + except: + return False + + self.pf.initialize(pf_cert_path, certPassword) + print "PhoneFactor. Initialized successfully" + + return True + + def destroy(self, configurationAttributes): + print "PhoneFactor. Destroy" + print "PhoneFactor. Destroyed successfully" + return True + + def getApiVersion(self): + return 1 + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + authenticationService = CdiUtil.bean(AuthenticationService) + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + user_name = credentials.getUsername() + if (step == 1): + print "PhoneFactor. Authenticate for step 1" + + user_password = credentials.getPassword() + logged_in = False + if (StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password)): + userService = CdiUtil.bean(UserService) + logged_in = authenticationService.authenticate(user_name, user_password) + + if (not logged_in): + return False + + return True + elif (step == 2): + print "PhoneFactor. Authenticate for step 2" + + passed_step1 = self.isPassedDefaultAuthentication + if (not passed_step1): + return False + + pf_phone_number_attr = configurationAttributes.get("pf_phone_number_attr").getValue2() + + # Get user entry from credentials + authenticationService = CdiUtil.bean(AuthenticationService) + credentials_user = authenticationService.getAuthenticatedUser() + + userService = CdiUtil.bean(UserService) + phone_number_with_country_code_attr = userService.getCustomAttribute(credentials_user, pf_phone_number_attr) + if (phone_number_with_country_code_attr == None): + print "PhoneFactor. Authenticate for step 2. There is no phone number: ", user_name + return False + + phone_number_with_country_code = phone_number_with_country_code_attr.getValue() + if (phone_number_with_country_code == None): + print "PhoneFactor. Authenticate for step 2. There is no phone number: ", user_name + return False + + pf_country_delimiter = configurationAttributes.get("pf_country_delimiter").getValue2() + + phone_number_with_country_code_array = string.split(phone_number_with_country_code, pf_country_delimiter, 1) + + phone_number_with_country_code_array_len = len(phone_number_with_country_code_array) + + if (phone_number_with_country_code_array_len == 1): + country_code = "" + phone_number = phone_number_with_country_code_array[0] + else: + country_code = phone_number_with_country_code_array[0] + phone_number = phone_number_with_country_code_array[1] + + print "PhoneFactor. Authenticate for step 2. user_name: ", user_name, ", country_code: ", country_code, ", phone_number: ", phone_number + + pf_auth_result = None + try: + pf_auth_result = self.pf.authenticate(user_name, country_code, phone_number, None, None, None) + except SecurityException, err: + print "PhoneFactor. Authenticate for step 2. BAD AUTH -- Security issue: ", err + except TimeoutException, err: + print "PhoneFactor. Authenticate for step 2. BAD AUTH -- Server timeout: ", err + except PFException, err: + print "PhoneFactor. Authenticate for step 2. BAD AUTH -- PFAuth failed with a PFException: ", err + + if (pf_auth_result == None): + return False + + print "PhoneFactor. Authenticate for step 2. Call Status: ", pf_auth_result.getCallStatusString() + if (pf_auth_result.getAuthenticated()): + print "PhoneFactor. Authenticate for step 2. GOOD AUTH:", user_name + + if (pf_auth_result.getCallStatus() == PFAuthResult.CALL_STATUS_PIN_ENTERED): + print "PhoneFactor. Authenticate for step 2. I have detected that a PIN was entered" + elif (pf_auth_result.getCallStatus() == PFAuthResult.CALL_STATUS_NO_PIN_ENTERED): + print "PhoneFactor. Authenticate for step 2. I have detected that NO PIN was entered" + + return True + else: + print "PhoneFactor. Authenticate for step 2. BAD AUTH:", user_name + + if (pf_auth_result.getCallStatus() == PFAuthResult.CALL_STATUS_USER_HUNG_UP): + print "PhoneFactor. Authenticate for step 2. I have detected that the user hung up" + elif (pf_auth_result.getCallStatus() == PFAuthResult.CALL_STATUS_PHONE_BUSY): + print "PhoneFactor. Authenticate for step 2. I have detected that the phone was busy" + + if (pf_auth_result.getMessageErrorId() != 0): + print "PhoneFactor. Authenticate for step 2. Message Error ID: ", pf_auth_result.getMessageErrorId() + + message_error = pf_auth_result.getMessageError() + if (message_error != null): + print "PhoneFactor. Authenticate for step 2. Message Error: ", message_error + + return False + else: + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + if (step == 1): + print "PhoneFactor. Prepare for step 1" + + return True + elif (step == 2): + print "PhoneFactor. Prepare for step 2" + + return self.isPassedDefaultAuthentication + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + return 2 + + def getPageForStep(self, configurationAttributes, step): + if (step == 2): + return "/auth/phonefactor/pflogin.xhtml" + return "" + + def isPassedDefaultAuthentication(self): + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + user_name = credentials.getUsername() + passed_step1 = StringHelper.isNotEmptyString(user_name) + + return passed_step1 + + def logout(self, configurationAttributes, requestParameters): + return True diff --git a/oxAuth/Server/integrations.deprecatred/phonefactor/repository/com/phonefactor/PhoneFactorSDK/2.13/PhoneFactorSDK-2.13.jar b/oxAuth/Server/integrations.deprecatred/phonefactor/repository/com/phonefactor/PhoneFactorSDK/2.13/PhoneFactorSDK-2.13.jar new file mode 100644 index 0000000000000000000000000000000000000000..cfa470c1c89ad9d29ea4ad6e2078bdaf0e31620e GIT binary patch literal 36294 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 J9pArx`hWUlH%b5i literal 0 HcmV?d00001 diff --git a/oxAuth/Server/integrations.deprecatred/phonefactor/repository/com/phonefactor/PhoneFactorSDK/2.13/PhoneFactorSDK-2.13.pom b/oxAuth/Server/integrations.deprecatred/phonefactor/repository/com/phonefactor/PhoneFactorSDK/2.13/PhoneFactorSDK-2.13.pom new file mode 100644 index 00000000..b4d8c0c8 --- /dev/null +++ b/oxAuth/Server/integrations.deprecatred/phonefactor/repository/com/phonefactor/PhoneFactorSDK/2.13/PhoneFactorSDK-2.13.pom @@ -0,0 +1,9 @@ + + + 4.0.0 + com.phonefactor + PhoneFactorSDK + 2.13 + POM was created from install:install-file + diff --git a/oxAuth/Server/integrations.deprecatred/phonefactor/repository/com/phonefactor/PhoneFactorSDK/maven-metadata-local.xml b/oxAuth/Server/integrations.deprecatred/phonefactor/repository/com/phonefactor/PhoneFactorSDK/maven-metadata-local.xml new file mode 100644 index 00000000..9bd546fb --- /dev/null +++ b/oxAuth/Server/integrations.deprecatred/phonefactor/repository/com/phonefactor/PhoneFactorSDK/maven-metadata-local.xml @@ -0,0 +1,12 @@ + + + com.phonefactor + PhoneFactorSDK + + 2.13 + + 2.13 + + 20130401141314 + + diff --git a/oxAuth/Server/integrations.deprecatred/toopher/ToopherExternalAuthenticator.py b/oxAuth/Server/integrations.deprecatred/toopher/ToopherExternalAuthenticator.py new file mode 100644 index 00000000..5346f3cc --- /dev/null +++ b/oxAuth/Server/integrations.deprecatred/toopher/ToopherExternalAuthenticator.py @@ -0,0 +1,276 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2016, Gluu +# +# Author: Yuriy Movchan +# + +import java +import json +from com.toopher import RequestError +from com.toopher import ToopherAPI +from java.util import Arrays +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.security import Identity +from org.gluu.oxauth.service import EncryptionService +from org.gluu.oxauth.service import UserService, AuthenticationService +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.util import StringHelper, ArrayHelper + + +class PersonAuthentication(PersonAuthenticationType): + + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, configurationAttributes): + print "Toopher. Initialization" + toopher_creds_file = configurationAttributes.get("toopher_creds_file").getValue2() + + # Load credentials from file + f = open(toopher_creds_file, 'r') + try: + creds = json.loads(f.read()) + except: + return False + finally: + f.close() + + consumer_key = creds["CONSUMER_KEY"] + consumer_secret = creds["CONSUMER_SECRET"] + try: + encryptionService = CdiUtil.bean(EncryptionService) + consumer_secret = encryptionService.decrypt(consumer_secret) + except: + return False + + self.tapi = ToopherAPI(consumer_key, consumer_secret) + print "Toopher. Initialized successfully" + + return True + + def destroy(self, configurationAttributes): + print "Toopher. Destroy" + print "Toopher. Destroyed successfully" + return True + + def getApiVersion(self): + return 1 + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + userService = CdiUtil.bean(UserService) + authenticationService = CdiUtil.bean(AuthenticationService) + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + toopher_user_timeout = int(configurationAttributes.get("toopher_user_timeout").getValue2()) + + user_name = credentials.getUsername() + + if (step == 1): + print "Toopher. Authenticate for step 1" + + user_password = credentials.getPassword() + logged_in = False + if (StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password)): + userService = CdiUtil.bean(UserService) + logged_in = authenticationService.authenticate(user_name, user_password) + + if (not logged_in): + return False + + # Get user entry + userService = CdiUtil.bean(UserService) + find_user_by_uid = authenticationService.getAuthenticatedUser() + if (find_user_by_uid == None): + print "Toopher. Authenticate for step 1. Failed to find user" + return False + + # Check if the user paired account to phone + user_external_uid_attr = userService.getCustomAttribute(find_user_by_uid, "oxExternalUid") + if ((user_external_uid_attr == None) or (user_external_uid_attr.getValues() == None)): + print "Toopher. Authenticate for step 1. There is no external UIDs for user: ", user_name + else: + topher_user_uid = None + for ext_uid in user_external_uid_attr.getValues(): + if (ext_uid.startswith('toopher:')): + topher_user_uid = ext_uid[8:len(ext_uid)] + break + + if (topher_user_uid == None): + print "Toopher. Authenticate for step 1. There is no Topher UID for user: ", user_name + else: + identity.setWorkingParameter("toopher_user_uid", topher_user_uid) + + return True + elif (step == 2): + print "Toopher. Authenticate for step 2" + + passed_step1 = self.isPassedDefaultAuthentication + if (not passed_step1): + return False + + sessionAttributes = identity.getSessionId().getSessionAttributes() + if (sessionAttributes == None) or not sessionAttributes.containsKey("toopher_user_uid"): + print "Toopher. Authenticate for step 2. toopher_user_uid is empty" + + # Pair with phone + pairing_phrase_array = requestParameters.get("pairing_phrase") + if ArrayHelper.isEmpty(pairing_phrase_array): + print "Toopher. Authenticate for step 2. pairing_phrase is empty" + return False + + pairing_phrase = pairing_phrase_array[0] + try: + pairing_status = self.tapi.pair(pairing_phrase, user_name) + toopher_user_uid = pairing_status.id + except RequestError, err: + print "Toopher. Authenticate for step 2. Failed pair with phone: ", err + return False + + pairing_result = self.checkPairingStatus(toopher_user_uid, toopher_user_timeout) + + if (not pairing_result): + print "Toopher. Authenticate for step 2. The pairing has not been authorized by the phone yet" + return False + + print "Toopher. Authenticate for step 2. Storing toopher_user_uid in user entry", toopher_user_uid + + # Store toopher_user_uid in user entry + find_user_by_uid = userService.addUserAttribute(user_name, "oxExternalUid", "toopher:" + toopher_user_uid) + if (find_user_by_uid == None): + print "Toopher. Authenticate for step 2. Failed to update current user" + return False + + identity.setWorkingParameter("toopher_user_uid", toopher_user_uid) + else: + toopher_user_uid = sessionAttributes.get("toopher_user_uid") + + # Check pairing stastus + print "Toopher. Authenticate for step 2. toopher_user_uid: ", toopher_user_uid + pairing_result = self.checkPairingStatus(toopher_user_uid, 0) + if (not pairing_result): + print "Toopher. Authenticate for step 2. The pairing has not been authorized by the phone yet" + return False + + return True + elif (step == 3): + print "Toopher. Authenticate for step 3" + + passed_step1 = self.isPassedDefaultAuthentication + if (not passed_step1): + return False + + sessionAttributes = identity.getSessionId().getSessionAttributes() + if (sessionAttributes == None) or not sessionAttributes.containsKey("toopher_user_uid"): + print "Toopher. Authenticate for step 3. toopher_user_uid is empty" + return False + + toopher_user_uid = sessionAttributes.get("toopher_user_uid") + passed_step1 = StringHelper.isNotEmptyString(toopher_user_uid) + if (not passed_step1): + return False + + toopher_terminal_name = configurationAttributes.get("toopher_terminal_name").getValue2() + + try: + request_status = self.tapi.authenticate(toopher_user_uid, toopher_terminal_name) + request_id = request_status.id + except RequestError, err: + print "Toopher. Authenticate for step 3. Failed to send authentication request to phone: ", err + return False + + print "Toopher. Authenticate for step 3. request_id: ", request_id + request_result = self.checkRequestStatus(request_id, toopher_user_timeout) + + if (not request_result): + print "Toopher. Authenticate for step 3. The authentication request has not received a response from the phone yet" + return False + + print "Toopher. Authenticate for step 3. The request was granted" + + return True + else: + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + return True + + def getExtraParametersForStep(self, configurationAttributes, step): + if (step in [2, 3]): + return Arrays.asList("toopher_user_uid") + + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + return 3 + + def getPageForStep(self, configurationAttributes, step): + if (step == 2): + return "/auth/toopher/tppair.xhtml" + elif (step == 3): + return "/auth/toopher/tpauthenticate.xhtml" + return "" + + def isPassedDefaultAuthentication(): + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + user_name = credentials.getUsername() + passed_step1 = StringHelper.isNotEmptyString(user_name) + + return passed_step1 + + def checkPairingStatus(self, pairing_id, timeout): + try: + curTime = java.lang.System.currentTimeMillis() + endTime = curTime + timeout * 1000 + while (endTime >= curTime): + pairing_status = self.tapi.getPairingStatus(pairing_id) + if (pairing_status.enabled): + print "Toopher. Pairing complete" + return True + + java.lang.Thread.sleep(2000) + curTime = java.lang.System.currentTimeMillis() + except java.lang.Exception, err: + print "Toopher. Could not check pairing status: ", err + return False + + print "Toopher. The pairing has not been authorized by the phone yet" + + return False + + def checkRequestStatus(self, request_id, timeout): + try: + curTime = java.lang.System.currentTimeMillis() + endTime = curTime + timeout * 1000 + while (endTime >= curTime): + request_status = self.tapi.getAuthenticationStatus(request_id) + if (request_status.cancelled): + print "Toopher. The authentication request has been cancelled" + return False + + if (not request_status.pending): + if (request_status.granted): + print "Toopher. The request was granted" + return True + + java.lang.Thread.sleep(2000) + curTime = java.lang.System.currentTimeMillis() + except java.lang.Exception, err: + print "Toopher. Could not check authentication status: ", err + return False + + print "Toopher. The authentication request has not received a response from the phone yet" + + return False + + def logout(self, configurationAttributes, requestParameters): + return True diff --git a/oxAuth/Server/integrations.deprecatred/toopher/repository/com/toopher/ToopherSDK/1.0.0/ToopherSDK-1.0.0.jar b/oxAuth/Server/integrations.deprecatred/toopher/repository/com/toopher/ToopherSDK/1.0.0/ToopherSDK-1.0.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..6bf3ad4dfbeeedcac173885b0146bd914809ccdf GIT binary patch literal 8589 zcmb7J1y~hZ*G3eiyAEB_-Q8W%4GMBVI?h45luk)$kdQ{YySpT$yYong@Z;z8>ec)G z&;Rc^GqcY#YrV7f%-(CQS+BDE!$$~E-`aj)h}eHRe;wfO&x(@jqKvXiQq0PKiNQhb z-HWA`si|4tU-rG0Fx}qKYZ6ss>J>E5af~#E52B#dgQ+S3SPJBF7GrYgviR zmQ096Xu1UU0)mu-una!NSiNO5b%lj=Q8nl{=tKQ559&YW-COi`dvxD>VQc;O0REIf z`bz?AYisus=p(hYk45DMxOEEE*gZ<3;p;Fmxfu=xuku(_>`8rTTz2x59+ zWds6+X;|BS(!}z#iRvo0tcw!ucAFO?Rn%^G&apwL$R^;Z*d3_zvkv`^b&%q33fy@pX0LcZaE?KU#5#M zRc{MrQu5r_EsXiEeREtc0z+f(P4D}_-J3ZV&h3ZY5yEcS}iaSG}H zRnZhOE$c6v4Rb3-nR~rFa0EzH_8l7vjpUxZQQzIK&;D# z%C1$B?I2c)>endk5)%3!w1523EHEQIt5X9dG^ zp_ykCw!j3VJms(zsp%Y(D(7l78wA>i`Pp;o>F%0biEZx=){KgWI2;L>h`uN!J~+(v zjh0e*NYZfBS;bfWVu^Lza6HVT6fc)5dCzG#CVyCp7#DD>d$`HE|L9b%X_c>6o&FMe zO4c<0azVYaihD|~>42}XcjxHqwC9KX?Wko`|H!YJ=Li+{_-0ngoxBhGxipW;!&sBc zmuc{{Sl$y3(bssh!)jXJKu5G*>Xsu5ltSnyt#14mt;a<|65&rXu$#ti8gbYO=ZBq{ zf^}a<4>}L&8I+dwvgkUy+Ie80o|lU`aWmEfI>*KR`Tg!}=Uh+whiW{|*}QTq-#dLI zTRZ5)y`Gyow+nfF7A&y99IWf?n~busb|(1MZdUOj@ds4==dbf5NxR0GUh>AtP)Du? zCCl6lQGaab&DVI65D>Lj$)0wcEv)qPGh@`+ zlSWQZqGPJXc*s$IiOg4>Up{2fcM~URQ)T5Ydz1Mwk}PhvwfZBg$X>iDZ0TxZLWdM5 z0p(drcu8G06H%#eLUg}TiEZ{S1q^hurqXos|2u|wr4(m=yeysQJYt$Ug>q$E_ z6jU$_6covCYgF0D+`-(&>>rC$-R9;6}L%RR0A`r}Jv4cJub&GDYj^cxs9Sit0#L>JbHo zlw>HUxPMotI8#?`{96cI1W=$=4LzF81#XsX74U%)2_(o!M*lt_i5*FC5=C4P9i40yg2oUYQ8*`he0-!dnrnq$+&TPS?dY5#;ytq#I6v`_=i2Q?N~@#NLG3UXkn*|la)I!166KwYxPeo@Aqo z#MWHDYxRJZqz+PMFPg;bruEOMfjWGr&@5Z%xGC+ z(D3JWY0Ak<6u|z1#4A`{uTX+o1-@d!_v>h4e4fW}MYe1d?zM z5RAd0hH<&Fop9zY+FM^sLg({l6C7XB-7tPsXgGR&p>@KIH)LbU@G5C*(7ge)k#L^O zzUbxg6?TdCz^z6{gt%H@T+qt3CZ#(?;D9aWg?TI-|5#Vw)@q{`l2zLK&m4TO#OlN0 z9y=U#Pb-yQE^i|GUlL`uR3hNw`Ur42?5apMF-r*c(Dlv-FizFg#>B_ zrf<)WggwRH*9&|aenyStR|U|1)}$$$htu8vg{QVf*+V=#S2}ZVmyNeBnEXIxHm_Ot z3f??dEjFT3AVMZO$B}+DP{_NmnY)Ua@M{42t&^R#uJqOfQnh+06WauKDRT+UnWb%nlog%2GIZ+ z#w>t%0SNgbb-HbADi||;l-67!nonlN)Ms`AHIuT|%U%{p6ewmJkc>`JnS*pH95bvH zvWs#m!cK<55dS$<=k9~AilGs8z$ur5z#Uu1QaF91Oa|2MPx~hjXTF8Lle~aCXX*qGt z-^Q%X&twj(m1SM47k9XISI3s$r_=%kOR?$FHe9EP1~;a!Te|L{CaXh>xX3WR)+S}M zH_0IWZ2C)Z@nN6Y6!fAI8C6mx&-;@x<)hf{@~2_>`%0j*PSD+msJ+YxQzb z39RSZ@=tH)hAh)fOLEc;9u%Ke0_)Bhdb4j(-F{DlxEydIqI zo0}llBAUbPK^_?}%fTD6r^tG;mJ%g>;UPC@Q*D80S&ssI$wkf_K)o}Z@+EzPGs#8g z)Vg$SD`ecuS(42aG{)dsMgP#)SR=QL!NL9Il7N2@)I$=Zq%D>sRw1K!0cbtT4aHO> z_uyf0`Y^J)4>K>rL;ey#ahi7n=^=dG@=WDRktKS6%N)5lGRK+gp0~W@@2qIJ-1D*+ z=Wzl`3k&Nyv#2%3ATdYuP0GDpDN4Gcsh9Av8mcyj=K%b*4IEeuZ8RI zb78~lV~E^Q>DK_tm2J=Q5H!L$K)Z|zNKQ(jeYuqC^ogV67m!M~EoC>PZD1D5*Q|Lg z9HJSi>1vJxP##1H$e-L-S#UZ#BaV)5?g$MTXn*VOS(#5bRmixQO)FSt|AZh$!IA04 zwIZBlI4Ar*{}ikQ)=pF*B{56Yy|E1WpnSH@ebt-c9G~IIE`hv4SXsh!mFv5G2tB9} zHO@bhvhesil?q)yixr11&Dd8e)6`qu`9WR(2r?D@{tfqO177c7kaQqRp^}goZo3ZG zB@q)LX`R7!PHB=6Nh!5S7huxn*84)?1WIt0V$_3@IOCi)^fCMVfZJ(BH(?iS3M zO>C3A=v1;6{85b#^BZ_VVKutuC0I{!*yiX75|dNvxFfB=SE*=>=o^&9Xr^tN-9=_v z5RA@jp-@)+OhPuwk7`*=!EC`;U9@Q&I)2U&aVqi=cG^f9}FhV{r~ z(JeMUJw#l)aY%^*M4>mvr8)6gER4jDbe*Opg+{$jCpjczVs+L1J7eD8k=@%TJH--Cv|=ySQVm13anfr>Cv_E!TCk z2)krt;QEoN0)5et(4u;v@flW108#L6y!`U53jx@bMbehcdb)Z7GrE#~j}%l~M<*A~ ztq-qnbMB_kT+B?llT04!d6s}07S9_Gj~k}W6%DV3kC~ygeZ2hASK@=~#dBLq>8v^g zCu(pH;Z{=U7O*=!$mSv*i#ekmYC=-c@*!^(Ty4<~)gZTMA9_|$A3<8tJ}ket5C}zu zfh?ot5TY(qT;v8ztGhG!!U+*~z()$xz3YDMED%Wj`oeeX&IBzDVOiFlFEB@4fJlrn z9L8StVE7TFa4t>Fp3WCGGA-^pUEkkiEmVIf-&sh4uYKYIX$Zaz*S9N#Zm8hG3U1eA z&B$G;b5s0f7p}w?No-uP&!mrla1)*8DSAkJhsr>8twP=EgBHIVeBq63HW2P_>O1#!D2n@+_}K zwK*C}>kdzU9c6Ga8PnmPnP{8Rv`q#*+#QTDw^c65wP~R4md80%S+ae?4Wq_45Hp$} zPs~ziaOLAmRw~nd+CeH+QgbG}BP`VVgIbgc zK+DakIophvIldoXGbc4hnqneJHVLCtM5_9Pl89wOBf5N0qN05u_$(pwt0PDB`Scf7 z&{PCs{b&0qJsQ3|jv$8BfLB`p>BqZLo$oZ<{UnD7>6UOvJ^TgxSP?RwxVzauRXXiC zCj)=m&9Gjv}SkzawudUtf*P-g~)re%SqQ+|#Vi;~IuZVX0MsAb6&+QBX3qtcZD zP27-<{fiLVY+hiNBO^8vzD-980)F=lcXWb8>a^)MN-O$J! zP?BAKEC$i?Zkwudt^0OacE9!~{3ajU{EIk$+f{x9&G79*g?h^Vxz>>#p^$kM>t`&7 z<0F1HUmzCr1i)%Xg!5qZsVeO_PxYIEbWeuXtjNI~4=HeTD(7lugQ8gK*jGRfcNwzz z1yfVETpV$&aOX7sf;V^mMUy|tQh-ZlrW*ry*b4cVzRbb0F_bqX_M0@`9|tXXg84a2 zL`o2)a3?+c4HCoT^c4euG^nE{wTgY**r+oR5KKC!f<^Sai#({W39orE^Wth46G8~BKkuZ;kA#5+w%?7fG2eY#ZmvPBcIk|Qb3%-uF_h;gJ20b&s;PlK~ zu6EXGKRj}SnTpYxUuhA}y~y+zULMA;T7pIPYJG-19;ZQtsYayNiGNEqq|e$4hGcng z)XmCOWOrB`##O{xfqbDMhxoTXUg5I7kq8a0yxgN>;+ercjwHtp;Hf1W)j2|JhK9B6 zVA?&aw1ZSbJ?^HY+Uv&tMVA=yNzt;g+KU;CqP@8A(upBcjTPnk3*)%Lij?piOqb`n zlO;~&AH0We9seS1x~!r*~o zdKWCrcpu-$o$~9HxIq!7n(>3(J&Sj5%rhtuh=?;m>sfa5zO%0YMLu=vI~Go_7Q9n? zk7#=Lv$Oi(uioq#z2Zwd{g@l0MxiJZrl_FdE zbH_Yiz%}E_g(%yI<@zk+ywm-V z#?@9rTM5}6rGIG94ZFK0@)&=0HU+Mw`+DYzjOMgGcMI_PkmKewXGu6j&NBt(W;6Zl zqOJ8aKs`k?Fgo82KMqNhKTLL*N9q-X4>PubDIjfePpu?MWb$ZHckct(#}eAj4s%Ih zeorJOhgeAC$c5QvIpu8(odtqjA%#FK2u}^}rA4&x3Q1Q*s$Go{oK|w@3Buf(tSJMn z%uahW#Vy%@Req>?YHMM%Vr+Zhi3D8$%ci8Rw*T(-+O!0X^T!NJQR>Sr{ji}F=_#hn z-NdcpFz{!pI{A`nc1p#LV~ahCVnw9xqR)qR`!q+}E!M!SbS50uAycH-ic+BtP1Z5`jl)0OyHf$#WjK zevrdk_Ku^M`&I9R!YD~&InYw4)`mAx+1q;KE+sEl<#0mRp02_SSQB=%t;R1S8%}7) z=pR!?n{vUB`>^NoPGi|c(J%4VMl;n{zSTcfz|3P5K4_yPY2XK$(-&NQtPtLzRHlD&li@|MTvCmg=Asb(x@qfh1hC4;m{fduvm?M*0LJ zmp0c~QizveSa-A{TH?m3TtYb@!fd@{i#96CK+y4ps9k$h+>@Sv`kIO}wUaK0 zZ}$0WxwZ$6r8`eVB}E?os}tcMSOH1Zr{&ejV7STpSBNHcL;PWU%XU~TjYX!Cak{ZCySH32-^d1cJ* zGqY5yyfKB~f?gy=YItlvhJ@T=Dv||Zc@jn9P26T+WfXS>3w(=TQ(|FQ9y9C0TSn}_ z@hojMN)5lo2G5{(FK24(d-@wqAD`G%i`VY19+$V81M2iSu_RRY?iXcyFJ(U6*RHtv@ApD-Li|Aw`6im0ButT90}l9V;?qiOCOt|&)@Qj zGdImekrGOz8gPGL8Zyv5SJxq)M5Z8~qYD*w4zqL{)QRN78nRoZKzKkBwhT6z z8Vg>jBAv10!ymN9uh!lIl@NLgHO}y@eLrKOtC#EW*{go~gmxn&A zFX>OriDhmZjT;KQi`irNV5jr-Ji0JBSjEv6`Cm;!0*5&wF!!Eu( zFzA45@kz%IWNi`HD~`aDxnr4>_(T{URg^Asv-7CowN)7Fp3hB5!BubLn~t0t;Dvfm zT>PXqJW5px=u zH(vT|Ku+x+!ykh?=?Z#+$ot% zY^@x;$qa9>XEw^Jim1ZV^1=1(g4>=Sa$&=WKK_4Vg=37Sl-suR1rUK$NM1{zfs4s{leYBrW? zHZ~PIM`RQg7B&`+jdi-e=uOUX_K|yjQ;xZhnej-WRB>o{g1&c{xkrX^WnGSeu2(@` zf$2eTfJnz{g9ew{d&cqsG}goa&b;0a;l80k?Tdap|LyvIs{K0?`%U;;%krVVKmM)u z3orW<;ZG{`8(I3jeYG$?cW9z>AP2;CO|DpNsWaY5%{IKR+?6?uYX$=D$!uzk~f=N&f}x@jt=-tgHWy^?RxHC)V

CvM_ma#{q(6h|x6u8)p+Vi + + 4.0.0 + com.toopher + ToopherSDK + 1.0.0 + POM was created from install:install-file + diff --git a/oxAuth/Server/integrations.deprecatred/toopher/repository/com/toopher/ToopherSDK/maven-metadata-local.xml b/oxAuth/Server/integrations.deprecatred/toopher/repository/com/toopher/ToopherSDK/maven-metadata-local.xml new file mode 100644 index 00000000..c8c282b6 --- /dev/null +++ b/oxAuth/Server/integrations.deprecatred/toopher/repository/com/toopher/ToopherSDK/maven-metadata-local.xml @@ -0,0 +1,12 @@ + + + com.toopher + ToopherSDK + + 1.0.0 + + 1.0.0 + + 20130409094810 + + diff --git a/oxAuth/Server/integrations.deprecatred/toopher/sdk/pom.xml b/oxAuth/Server/integrations.deprecatred/toopher/sdk/pom.xml new file mode 100644 index 00000000..5d5b5325 --- /dev/null +++ b/oxAuth/Server/integrations.deprecatred/toopher/sdk/pom.xml @@ -0,0 +1,105 @@ + + + 4.0.0 + com.toopher + ToopherSDK + jar + 1.0.0 + + + 3.0.3 + + + + + repository.jboss.org + JBoss Repository + https://repository.jboss.org/nexus/content/groups/public-jboss/ + + + bouncycastle + Bouncy Castle + https://repo2.maven.org/maven2/org/bouncycastle + + + gluu + Gluu repository + https://maven.gluu.org/maven + + + + + package + ${project.artifactId}-${project.version} + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + verify + + jar + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + 1.6 + 1.6 + UTF-8 + + + + + org.apache.maven.plugins + maven-install-plugin + 2.4 + + + + + + + commons-codec + commons-codec + 1.7 + jar + true + + + org.apache.httpcomponents + httpclient + 4.5.13 + + + org.apache.httpcomponents + httpcore + 4.4.5 + + + org.codehaus.jettison + jettison + 1.5.4 + + + stax-api + stax + + + + + oauth.signpost + signpost-commonshttp4 + 1.2.1.2 + + + diff --git a/oxAuth/Server/integrations.deprecatred/toopher/sdk/src/main/java/com/toopher/AuthenticationStatus.java b/oxAuth/Server/integrations.deprecatred/toopher/sdk/src/main/java/com/toopher/AuthenticationStatus.java new file mode 100644 index 00000000..b992baad --- /dev/null +++ b/oxAuth/Server/integrations.deprecatred/toopher/sdk/src/main/java/com/toopher/AuthenticationStatus.java @@ -0,0 +1,72 @@ +package com.toopher; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Provide information about the status of an authentication request + * + */ +public class AuthenticationStatus { + /** + * The unique id for the authentication request + */ + public String id; + + /** + * Indicates if the request is still pending + */ + public boolean pending; + + /** + * Indicates if the request was granted + */ + public boolean granted; + + /** + * Indicates if the request was automated + */ + public boolean automated; + + /** + * Indicates if the request was cancelled + */ + public boolean cancelled; + + /** + * Indicates the reason (if any) for the request's outcome + */ + public String reason; + + /** + * The unique id for the terminal associated with the request + */ + public String terminalId; + + /** + * The descriptive name for the terminal associated with the request + */ + public String terminalName; + + @Override + public String toString() { + return String.format("[AuthenticationStatus: id=%s; pending=%b; granted=%b; automated=%b; cancelled=%d; reason=%s; terminalId=%s; terminalName=%s]", + id, pending, granted, automated, cancelled, reason, terminalId, terminalName); + } + + static AuthenticationStatus fromJSON(JSONObject json) throws JSONException { + AuthenticationStatus as = new AuthenticationStatus(); + as.id = json.getString("id"); + as.pending = json.getBoolean("pending"); + as.granted = json.getBoolean("granted"); + as.automated = json.getBoolean("automated"); + as.cancelled = json.getBoolean("cancelled"); + as.reason = json.getString("reason"); + + JSONObject terminal = json.getJSONObject("terminal"); + as.terminalId = terminal.getString("id"); + as.terminalName = terminal.getString("name"); + + return as; + } +} diff --git a/oxAuth/Server/integrations.deprecatred/toopher/sdk/src/main/java/com/toopher/PairingStatus.java b/oxAuth/Server/integrations.deprecatred/toopher/sdk/src/main/java/com/toopher/PairingStatus.java new file mode 100644 index 00000000..e1d1aa72 --- /dev/null +++ b/oxAuth/Server/integrations.deprecatred/toopher/sdk/src/main/java/com/toopher/PairingStatus.java @@ -0,0 +1,49 @@ +package com.toopher; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Provides information about the status of a pairing request + * + */ +public class PairingStatus { + /** + * The unique id for the pairing request + */ + public String id; + + /** + * The unique id for the user associated with the pairing request + */ + public String userId; + + /** + * The descriptive name for the user associated with the pairing request + */ + public String userName; + + /** + * Indicates if the pairing has been enabled by the user + */ + public boolean enabled; + + @Override + public String toString() { + return String.format("[PairingStatus: id=%s; userId=%s; userName=%s, enabled=%b]", id, + userId, userName, enabled); + } + + static PairingStatus fromJSON(JSONObject json) throws JSONException { + PairingStatus ps = new PairingStatus(); + ps.id = json.getString("id"); + + JSONObject user = json.getJSONObject("user"); + ps.userId = user.getString("id"); + ps.userName = user.getString("name"); + + ps.enabled = json.getBoolean("enabled"); + + return ps; + } +} diff --git a/oxAuth/Server/integrations.deprecatred/toopher/sdk/src/main/java/com/toopher/RequestError.java b/oxAuth/Server/integrations.deprecatred/toopher/sdk/src/main/java/com/toopher/RequestError.java new file mode 100644 index 00000000..5e210408 --- /dev/null +++ b/oxAuth/Server/integrations.deprecatred/toopher/sdk/src/main/java/com/toopher/RequestError.java @@ -0,0 +1,31 @@ +package com.toopher; + +import java.io.IOException; + +import org.apache.http.client.ClientProtocolException; +import org.json.JSONException; + +/** + * Request errors from API calls + * + */ +public class RequestError extends Exception { + + public RequestError(ClientProtocolException e) { + super("Http protocol error", e); + } + + public RequestError(IOException e) { + super("Connection error", e); + } + + public RequestError(JSONException e) { + super("Unexpected response format", e); + } + + public RequestError(Exception e) { + super("Request error", e); + } + + private static final long serialVersionUID = -1479647692976296897L; +} diff --git a/oxAuth/Server/integrations.deprecatred/toopher/sdk/src/main/java/com/toopher/ToopherAPI.java b/oxAuth/Server/integrations.deprecatred/toopher/sdk/src/main/java/com/toopher/ToopherAPI.java new file mode 100644 index 00000000..3e32a052 --- /dev/null +++ b/oxAuth/Server/integrations.deprecatred/toopher/sdk/src/main/java/com/toopher/ToopherAPI.java @@ -0,0 +1,220 @@ +package com.toopher; + +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +import oauth.signpost.OAuthConsumer; +import oauth.signpost.commonshttp.CommonsHttpOAuthConsumer; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.StatusLine; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.HttpClient; +import org.apache.http.client.HttpResponseException; +import org.apache.http.client.ResponseHandler; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.params.HttpProtocolParams; +import org.apache.http.util.EntityUtils; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +/** + * A Java binding for the Toopher API + * + */ +public class ToopherAPI { + /** + * The ToopherJava binding library version + */ + public static final String VERSION = "1.0.0"; + + /** + * Create an API object with the supplied credentials + * + * @param consumerKey + * The consumer key for a requester (obtained from the developer portal) + * @param consumerSecret + * The consumer secret for a requester (obtained from the developer portal) + */ + public ToopherAPI(String consumerKey, String consumerSecret) { + httpClient = new DefaultHttpClient(); + HttpProtocolParams.setUserAgent(httpClient.getParams(), + String.format("ToopherJava/%s", VERSION)); + + consumer = new CommonsHttpOAuthConsumer(consumerKey, consumerSecret); + } + + /** + * Create a pairing + * + * @param pairingPhrase + * The pairing phrase supplied by the user + * @param userName + * A user-facing descriptive name for the user (displayed in requests) + * @return A PairingStatus object + * @throws RequestError + * Thrown when an exceptional condition is encountered + */ + public PairingStatus pair(String pairingPhrase, String userName) throws RequestError { + final String endpoint = "pairings/create"; + + List params = new ArrayList(); + params.add(new BasicNameValuePair("pairing_phrase", pairingPhrase)); + params.add(new BasicNameValuePair("user_name", userName)); + + try { + JSONObject json = post(endpoint, params); + return PairingStatus.fromJSON(json); + } catch (Exception e) { + throw new RequestError(e); + } + } + + /** + * Retrieve the current status of a pairing request + * + * @param pairingRequestId + * The unique id for a pairing request + * @return A PairingStatus object + * @throws RequestError + * Thrown when an exceptional condition is encountered + */ + public PairingStatus getPairingStatus(String pairingRequestId) throws RequestError { + final String endpoint = String.format("pairings/%s", pairingRequestId); + + try { + JSONObject json = get(endpoint); + return PairingStatus.fromJSON(json); + } catch (Exception e) { + throw new RequestError(e); + } + } + + /** + * Initiate a login authentication request + * + * @param pairingId + * The pairing id indicating to whom the request should be sent + * @param terminalName + * The user-facing descriptive name for the terminal from which the request originates + * @return An AuthenticationStatus object + * @throws RequestError + * Thrown when an exceptional condition is encountered + */ + public AuthenticationStatus authenticate(String pairingId, String terminalName) throws RequestError { + return authenticate(pairingId, terminalName, null); + } + + /** + * Initiate an authentication request + * + * @param pairingId + * The pairing id indicating to whom the request should be sent + * @param terminalName + * The user-facing descriptive name for the terminal from which the request originates + * @param actionName + * The user-facing descriptive name for the action which is being authenticated + * @return An AuthenticationStatus object + * @throws RequestError + * Thrown when an exceptional condition is encountered + */ + public AuthenticationStatus authenticate(String pairingId, String terminalName, + String actionName) throws RequestError { + final String endpoint = "authentication_requests/initiate"; + + List params = new ArrayList(); + params.add(new BasicNameValuePair("pairing_id", pairingId)); + params.add(new BasicNameValuePair("terminal_name", terminalName)); + if (actionName != null && actionName.length() > 0) { + params.add(new BasicNameValuePair("action_name", actionName)); + } + + try { + JSONObject json = post(endpoint, params); + return AuthenticationStatus.fromJSON(json); + } catch (Exception e) { + throw new RequestError(e); + } + } + + /** + * Retrieve status information for an authentication request + * + * @param authenticationRequestId + * The authentication request ID + * @return An AuthenticationStatus object + * @throws RequestError + * Thrown when an exceptional condition is encountered + */ + public AuthenticationStatus getAuthenticationStatus(String authenticationRequestId) + throws RequestError { + final String endpoint = String.format("authentication_requests/%s", authenticationRequestId); + + try { + JSONObject json = get(endpoint); + return AuthenticationStatus.fromJSON(json); + } catch (Exception e) { + throw new RequestError(e); + } + } + + private JSONObject get(String endpoint) throws Exception { + URI uri = new URIBuilder().setScheme(URI_SCHEME).setHost(URI_HOST) + .setPath(URI_BASE + endpoint).build(); + HttpGet get = new HttpGet(uri); + consumer.sign(get); + return httpClient.execute(get, jsonHandler); + } + + private JSONObject post(String endpoint, List params) throws Exception { + URI uri = new URIBuilder().setScheme(URI_SCHEME).setHost(URI_HOST) + .setPath(URI_BASE + endpoint).build(); + HttpPost post = new HttpPost(uri); + if (params != null && params.size() > 0) { + post.setEntity(new UrlEncodedFormEntity(params)); + } + + consumer.sign(post); + + return httpClient.execute(post, jsonHandler); + } + + private static ResponseHandler jsonHandler = new ResponseHandler() { + + @Override + public JSONObject handleResponse(HttpResponse response) throws ClientProtocolException, + IOException { + StatusLine statusLine = response.getStatusLine(); + if (statusLine.getStatusCode() >= 300) { + throw new HttpResponseException(statusLine.getStatusCode(), + statusLine.getReasonPhrase()); + } + + HttpEntity entity = response.getEntity(); // TODO: check entity == null + String json = EntityUtils.toString(entity); + + try { + return (JSONObject) new JSONTokener(json).nextValue(); + } catch (JSONException e) { + throw new ClientProtocolException("Could not interpret response as JSON", e); + } + } + }; + + private static final String URI_SCHEME = "https"; + private static final String URI_HOST = "api.toopher.com"; + private static final String URI_BASE = "/v1/"; + + private final HttpClient httpClient; + private final OAuthConsumer consumer; +} diff --git a/oxAuth/Server/integrations.deprecatred/wikid/README.txt b/oxAuth/Server/integrations.deprecatred/wikid/README.txt new file mode 100644 index 00000000..cfa5017e --- /dev/null +++ b/oxAuth/Server/integrations.deprecatred/wikid/README.txt @@ -0,0 +1,37 @@ +This is a person authentication module for oxAuth that enables [Wikid Authentication](http://wikidsystems.com) for user authentication. + +The module has a few properties: + +1) wikid_server_host - It's mandatory property. IP address of WIKID server. + Example: 111.111.111.111 + +2) wikid_server_port - It's mandatory property. TCP port number to connect to (default 8388). + Example: 8388 + +3) wikid_cert_path - It's mandatory property. Path to the PKCS12 certificate file. + Example: /etc/certs/wikid.p12 + +4) wikid_cert_pass - It's mandatory property. Passphrase to open the PKCS12 file. + Example: changeit + +5) wikid_ca_store_path - It's mandatory property. The certificate authority store for validating the WAS server certificat. + Example: /etc/certs/CACertStore.dat + +6) wikid_ca_store_pass - It's mandatory property. The passphrase securing the trust store file. + Example: changeit + +7) wikid_server_code - It's mandatory property. The 12-digit code that represents the server/domain. + Example: 222222222222 + + +This module require few java libraries. Before enabling this module it's mandatory to put next libraries into $TOMCAT_HOME/endorsed +1) https://www.wikidsystems.com/webdemo/wClient-3.5.0.jar +2) http://central.maven.org/maven2/org/jdom/jdom/1.1.3/jdom-1.1.3.jar +3) http://central.maven.org/maven2/log4j/log4j/1.2.17/log4j-1.2.17.jar +4) http://central.maven.org/maven2/com/thoughtworks/xstream/xstream/1.4.8/xstream-1.4.8.jar + +More information about wClient library there is on this page: https://www.wikidsystems.com/downloads/network-clients + +Also this 2F authentication method requires token client: https://www.wikidsystems.com/downloads/token-clients +Hence in order to use this person authentication module user should install and configure it for first time use. This demo explains how to do that: +https://www.wikidsystems.com/demo diff --git a/oxAuth/Server/integrations.deprecatred/wikid/WikidExternalAuthenticator.py b/oxAuth/Server/integrations.deprecatred/wikid/WikidExternalAuthenticator.py new file mode 100644 index 00000000..abb36f93 --- /dev/null +++ b/oxAuth/Server/integrations.deprecatred/wikid/WikidExternalAuthenticator.py @@ -0,0 +1,218 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2016, Gluu +# +# Author: Yuriy Movchan +# + +import java +from com.wikidsystems.client import wClient +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.security import Identity +from org.gluu.oxauth.service import UserService, AuthenticationService +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.util import StringHelper, ArrayHelper + + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, configurationAttributes): + print "Wikid. Initialization" + + if (not (configurationAttributes.containsKey("wikid_server_host") and configurationAttributes.containsKey("wikid_server_port"))): + print "Wikid Initialization. The properties wikid_server_host and wikid_server_port should be not empty" + return False + + if (not (configurationAttributes.containsKey("wikid_cert_path") and configurationAttributes.containsKey("wikid_cert_pass"))): + print "Wikid Initialization. The properties wikid_cert_path and wikid_cert_pass should be not empty" + return False + + if (not (configurationAttributes.containsKey("wikid_ca_store_path") and configurationAttributes.containsKey("wikid_ca_store_pass"))): + print "Wikid Initialization. The properties wikid_ca_store_path and wikid_ca_store_pass should be not empty" + return False + + if (not configurationAttributes.containsKey("wikid_server_code")): + print "Wikid Initialization. The property wikid_server_code should be not empty" + return False + + print "Wikid. Initialization. Creating new client." + wikid_server_host = configurationAttributes.get("wikid_server_host").getValue2() + wikid_server_port = int(configurationAttributes.get("wikid_server_port").getValue2()) + + wikid_cert_path = configurationAttributes.get("wikid_cert_path").getValue2() + wikid_cert_pass = configurationAttributes.get("wikid_cert_pass").getValue2() + wikid_ca_store_path = configurationAttributes.get("wikid_ca_store_path").getValue2() + wikid_ca_store_pass = configurationAttributes.get("wikid_ca_store_pass").getValue2() + + self.wc = wClient(wikid_server_host, wikid_server_port, wikid_cert_path, wikid_cert_pass, wikid_ca_store_path, wikid_ca_store_pass) + + print "Wikid. Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "Wikid. Destroy" + print "Wikid. Destroyed successfully" + return True + + def getApiVersion(self): + return 1 + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + print "Wikid. Authentication. Checking client" + + if (not self.wc.isConnected()): + print "Wikid. Authentication. Wikid client state is invalid" + return False + + authenticationService = CdiUtil.bean(AuthenticationService) + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + is_wikid_registration = False + sessionAttributes = identity.getSessionId().getSessionAttributes() + if (sessionAttributes != None) and sessionAttributes.containsKey("wikid_registration"): + is_wikid_registration = java.lang.Boolean.valueOf(sessionAttributes.get("wikid_registration")) + + wikid_server_code = configurationAttributes.get("wikid_server_code").getValue2() + + user_name = credentials.getUsername() + + if (step == 1): + print "Wikid. Authenticate for step 1" + + user_password = credentials.getPassword() + + logged_in = False + if (StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password)): + userService = CdiUtil.bean(UserService) + logged_in = authenticationService.authenticate(user_name, user_password) + + if (not logged_in): + return False + + print "Wikid. Authenticate for step 1. Attempting to find wikid_user: " + user_name + wc_user = self.wc.findUser(wikid_server_code, user_name) + + if (wc_user == None): + print "Wikid. Authenticate for step 1. There is no associated devices for user: " + user_name + print "Wikid. Authenticate for step 1. Setting count steps to 3" + identity.setWorkingParameter("wikid_count_login_steps", 3) + identity.setWorkingParameter("wikid_registration", True) + else: + identity.setWorkingParameter("wikid_count_login_steps", 2) + + return True + elif (is_wikid_registration): + print "Wikid. Authenticate for step wikid_register_device" + + userService = CdiUtil.bean(UserService) + + wikid_regcode_array = requestParameters.get("regcode") + if ArrayHelper.isEmpty(wikid_regcode_array): + print "Wikid. Authenticate for step wikid_register_device. Regcode is empty" + return False + + wikid_regcode = wikid_regcode_array[0] + + print "Wikid. Authenticate for step wikid_register_device. User: " + user_name + ", regcode: " + wikid_regcode + + register_result = self.wc.registerUsername(user_name, wikid_regcode, wikid_server_code) + + is_valid = register_result == 0 + if is_valid: + print "Wikid. Authenticate for step wikid_register_device. User: " + user_name + " token registered successfully" + + # Add wikid_regcode to user UIDs + find_user_by_uid = userService.addUserAttribute(user_name, "oxExternalUid", "wikid:" + wikid_regcode) + if (find_user_by_uid == None): + print "Wikid. Authenticate for step wikid_register_device. Failed to update user: " + user_name + is_valid = False + else: + identity.setWorkingParameter("wikid_registration", False) + else: + print "Wikid. Authenticate for step wikid_register_device. Failed to register user: " + user_name + " token:" + wikid_regcode + ". Registration result:", register_result + + return is_valid + elif (not is_wikid_registration): + print "Wikid. Authenticate for step wikid_check_passcode" + + wikid_passcode_array = requestParameters.get("passcode") + if ArrayHelper.isEmpty(wikid_passcode_array): + print "Wikid. Authenticate for step wikid_check_passcode. Passcode is empty" + return False + + wikid_passcode = wikid_passcode_array[0] + + print "Wikid. Authenticate for step wikid_check_passcode. wikid_user: " + user_name + + is_valid = self.wc.CheckCredentials(user_name, wikid_passcode, wikid_server_code) + + if is_valid: + print "Wikid. Authenticate for step wikid_check_passcode. wikid_user: " + user_name + " authenticated successfully" + else: + print "Wikid. Authenticate for step wikid_check_passcode. Failed to authenticate. wikid_user: " + user_name + + return is_valid + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + if (step == 2): + return Arrays.asList("wikid_registration", "wikid_count_login_steps") + elif (step == 3): + return Arrays.asList("wikid_registration") + + return None + + def prepareForStep(self, configurationAttributes, requestParameters, step): + if (step == 1): + print "Wikid. Prepare for step 1" + + return True + elif (step == 2): + print "Wikid. Prepare for step 2" + + return True + elif (step == 2): + print "Wikid. Prepare for step 3" + + return True + else: + return False + + def getCountAuthenticationSteps(self, configurationAttributes): + identity = CdiUtil.bean(Identity) + + sessionAttributes = identity.getSessionId().getSessionAttributes() + if (sessionAttributes != None) and sessionAttributes.containsKey("wikid_count_login_steps"): + return java.lang.Integer.valueOf(sessionAttributes.get("wikid_count_login_steps")) + + return 2 + + def getPageForStep(self, configurationAttributes, step): + identity = CdiUtil.bean(Identity) + + is_wikid_registration = False + if (identity.isSetWorkingParameter("wikid_registration")): + is_wikid_registration = identity.getWorkingParameter("wikid_registration") + + if (step == 2): + if (is_wikid_registration): + return "/auth/wikid/wikidregister.xhtml" + else: + return "/auth/wikid/wikidlogin.xhtml" + if (step == 3): + return "/auth/wikid/wikidlogin.xhtml" + + return "" + + def logout(self, configurationAttributes, requestParameters): + return True diff --git a/oxAuth/Server/integrations/Migration_stepts_to_3.1.x.txt b/oxAuth/Server/integrations/Migration_stepts_to_3.1.x.txt new file mode 100644 index 00000000..fbb90bb2 --- /dev/null +++ b/oxAuth/Server/integrations/Migration_stepts_to_3.1.x.txt @@ -0,0 +1,72 @@ +1. Replace: +from org.jboss.seam import Component +from org.jboss.seam.security import Identity + +with: +from org.xdi.service.cdi.util import CdiUtil +from org.xdi.oxauth.security import Identity + +2. Replace: +Component.getInstance(Bean) + +with: +CdiUtil.bean(Bean) + +3. Remove: +from org.jboss.seam.contexts import Contexts + +4. Replace: +FacesContext.getCurrentInstance() + +with: +CdiUtil.bean(FacesContext) + +5. Instead of: +from org.jboss.seam.contexts import Contexts +... +context = Contexts.getEventContext() +context.set(param, value) +context.get(param) + +use: +from org.xdi.oxauth.security import Identity +... +identity = CdiUtil.bean(Identity) +identity.setWorkingParameter(param, value) +identity.getWorkingParameter(param) + +6. Replace: +context = Contexts.getEventContext() +context.get("sessionAttributes") + +with: +identity = CdiUtil.bean(Identity) +identity.getSessionId().getSessionAttributes() + +7. To add faces messages instead of: +from org.jboss.seam.faces import FacesMessages +from org.jboss.seam.international import StatusMessage +... +facesMessages = FacesMessages.instance() +facesMessages.add(StatusMessage.Severity.ERROR, "Error message") +FacesContext.getCurrentInstance().getExternalContext().getExternalContext().getFlash().setKeepMessages(True) + +use: +from org.gluu.jsf2.message import FacesMessages +from javax.faces.application import FacesMessage +... +facesMessages = CdiUtil.bean(FacesMessages) +facesMessages.add(FacesMessage.SEVERITY_ERROR, "Error message") +facesMessages.setKeepMessages() + +8. To authenticate user instead of: +from org.xdi.oxauth.service import UserService +... +userService = CdiUtil.bean(UserService) +logged_in = userService.authenticate(user_name, user_password) + +use +from org.xdi.oxauth.service import AuthenticationService +... +authenticationService = CdiUtil.bean(AuthenticationService) +logged_in = authenticationService.authenticate(user_name, user_password) diff --git a/oxAuth/Server/integrations/Migration_stepts_to_4.0.txt b/oxAuth/Server/integrations/Migration_stepts_to_4.0.txt new file mode 100644 index 00000000..15898b37 --- /dev/null +++ b/oxAuth/Server/integrations/Migration_stepts_to_4.0.txt @@ -0,0 +1,6 @@ +# Steps to upgrade custom scripts to cope with new structure. + +## Packages renamed + 1. All packages starting **org.xdi** has been renamed to start with **org.gluu**. +## Packages moved + 1. The class named **GluuStatus** has been move from **org.gluu.ldap.model** to **org.gluu.model** diff --git a/oxAuth/Server/integrations/ThumbSignIn/README.md b/oxAuth/Server/integrations/ThumbSignIn/README.md new file mode 100644 index 00000000..aba6a825 --- /dev/null +++ b/oxAuth/Server/integrations/ThumbSignIn/README.md @@ -0,0 +1,7 @@ +# ThumbSignIn + +ThumbSignIn can be integrated with Gluu Server to achieve strong authentication for enterprise applications. The administrator of an organization can deploy the ThumbSignIn Java SDK, UI components and custom Jython script in the Gluu server. Here, ThumbSignIn is integrated with Gluu server as a primary authenticator to achieve passwordless login. The user will be able to login to the Gluu server with just his/her biometrics and there is no need for the password. For the first time user, the user can login with his/her LDAP credentials and then can register through ThumbSignIn mobile app. For the subsequent logins, the user can directly login to Gluu server with his/her biometric. + +- [Steps to perform Integration](https://thumbsignin.com/download/thumbsigninGluuIntegrationDoc) + +For more information about ThumbSignIn, see their [website](http://thumbsignin.com) diff --git a/oxAuth/Server/integrations/ThumbSignIn/ThumbSignInExternalAuthenticator.py b/oxAuth/Server/integrations/ThumbSignIn/ThumbSignInExternalAuthenticator.py new file mode 100644 index 00000000..f8ae3700 --- /dev/null +++ b/oxAuth/Server/integrations/ThumbSignIn/ThumbSignInExternalAuthenticator.py @@ -0,0 +1,319 @@ +# Author: ThumbSignIn + +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.service import AuthenticationService +from org.gluu.util import StringHelper +from org.gluu.oxauth.util import ServerUtil +from com.pramati.ts.thumbsignin_java_sdk import ThumbsigninApiController +from org.json import JSONObject +from org.gluu.oxauth.model.util import Base64Util +from java.lang import String + +import java + + +class PersonAuthentication(PersonAuthenticationType): + + def __init__(self, current_time_millis): + self.currentTimeMillis = current_time_millis + self.thumbsigninApiController = ThumbsigninApiController() + + def init(self, customScript, configuration_attributes): + print "ThumbSignIn. Initialization" + + global ts_host + ts_host = configuration_attributes.get("ts_host").getValue2() + print "ThumbSignIn. Initialization. Value of ts_host is %s" % ts_host + + global ts_api_key + ts_api_key = configuration_attributes.get("ts_apiKey").getValue2() + print "ThumbSignIn. Initialization. Value of ts_api_key is %s" % ts_api_key + + global ts_api_secret + ts_api_secret = configuration_attributes.get("ts_apiSecret").getValue2() + + global ts_statusPath + ts_statusPath = "/ts/secure/txn-status/" + + global AUTHENTICATE + AUTHENTICATE = "authenticate" + + global REGISTER + REGISTER = "register" + + global TRANSACTION_ID + TRANSACTION_ID = "transactionId" + + global USER_ID + USER_ID = "userId" + + global USER_LOGIN_FLOW + USER_LOGIN_FLOW = "userLoginFlow" + + global THUMBSIGNIN_AUTHENTICATION + THUMBSIGNIN_AUTHENTICATION = "ThumbSignIn_Authentication" + + global THUMBSIGNIN_REGISTRATION + THUMBSIGNIN_REGISTRATION = "ThumbSignIn_Registration" + + global THUMBSIGNIN_LOGIN_POST_REGISTRATION + THUMBSIGNIN_LOGIN_POST_REGISTRATION = "ThumbSignIn_RegistrationSucess" + + global RELYING_PARTY_ID + RELYING_PARTY_ID = "relyingPartyId" + + global RELYING_PARTY_LOGIN_URL + RELYING_PARTY_LOGIN_URL = "relyingPartyLoginUrl" + + global TSI_LOGIN_PAGE + TSI_LOGIN_PAGE = "/auth/thumbsignin/tsLogin.xhtml" + + global TSI_REGISTER_PAGE + TSI_REGISTER_PAGE = "/auth/thumbsignin/tsRegister.xhtml" + + global TSI_LOGIN_POST_REGISTRATION_PAGE + TSI_LOGIN_POST_REGISTRATION_PAGE = "/auth/thumbsignin/tsRegistrationSuccess.xhtml" + + print "ThumbSignIn. Initialized successfully" + return True + + @staticmethod + def set_relying_party_login_url(identity): + print "ThumbSignIn. Inside set_relying_party_login_url..." + session_id = identity.getSessionId() + session_attribute = session_id.getSessionAttributes() + state_jwt_token = session_attribute.get("state") + print "ThumbSignIn. Value of state_jwt_token is %s" % state_jwt_token + relying_party_login_url = "" + if (state_jwt_token is None) or ("." not in state_jwt_token): + print "ThumbSignIn. Value of state parameter is not in the format of JWT Token" + identity.setWorkingParameter(RELYING_PARTY_LOGIN_URL, relying_party_login_url) + return None + + state_jwt_token_array = String(state_jwt_token).split("\\.") + state_jwt_token_payload = state_jwt_token_array[1] + state_payload_str = String(Base64Util.base64urldecode(state_jwt_token_payload), "UTF-8") + state_payload_json = JSONObject(state_payload_str) + print "ThumbSignIn. Value of state JWT token Payload is %s" % state_payload_json + if state_payload_json.has("additional_claims"): + additional_claims = state_payload_json.get("additional_claims") + relying_party_id = additional_claims.get(RELYING_PARTY_ID) + print "ThumbSignIn. Value of relying_party_id is %s" % relying_party_id + identity.setWorkingParameter(RELYING_PARTY_ID, relying_party_id) + + if String(relying_party_id).startsWith("google.com"): + # google.com/a/unphishableenterprise.com + relying_party_id_array = String(relying_party_id).split("/") + google_domain = relying_party_id_array[2] + print "ThumbSignIn. Value of google_domain is %s" % google_domain + relying_party_login_url = "https://www.google.com/accounts/AccountChooser?hd="+ google_domain + "%26continue=https://apps.google.com/user/hub" + # elif (String(relying_party_id).startsWith("xyz")): + # relying_party_login_url = "xyz.com" + else: + # If relying_party_login_url is empty, Gluu's default login URL will be used + relying_party_login_url = "" + + print "ThumbSignIn. Value of relying_party_login_url is %s" % relying_party_login_url + identity.setWorkingParameter(RELYING_PARTY_LOGIN_URL, relying_party_login_url) + return None + + def initialize_thumbsignin(self, identity, request_path): + # Invoking the authenticate/register ThumbSignIn API via the Java SDK + thumbsignin_response = self.thumbsigninApiController.handleThumbSigninRequest(request_path, ts_api_key, ts_api_secret) + print "ThumbSignIn. Value of thumbsignin_response is %s" % thumbsignin_response + + thumbsignin_response_json = JSONObject(thumbsignin_response) + transaction_id = thumbsignin_response_json.get(TRANSACTION_ID) + status_request_type = "authStatus" if request_path == AUTHENTICATE else "regStatus" + status_request = status_request_type + "/" + transaction_id + print "ThumbSignIn. Value of status_request is %s" % status_request + + authorization_header = self.thumbsigninApiController.getAuthorizationHeaderJsonStr(status_request, ts_api_key, ts_api_secret) + print "ThumbSignIn. Value of authorization_header is %s" % authorization_header + # {"authHeader":"HmacSHA256 Credential=X,SignedHeaders=accept;content-type;x-ts-date,Signature=X","XTsDate":"X"} + authorization_header_json = JSONObject(authorization_header) + auth_header = authorization_header_json.get("authHeader") + x_ts_date = authorization_header_json.get("XTsDate") + + tsi_response_key = "authenticateResponseJsonStr" if request_path == AUTHENTICATE else "registerResponseJsonStr" + identity.setWorkingParameter(tsi_response_key, thumbsignin_response) + identity.setWorkingParameter("authorizationHeader", auth_header) + identity.setWorkingParameter("xTsDate", x_ts_date) + return None + + def prepareForStep(self, configuration_attributes, request_parameters, step): + print "ThumbSignIn. Inside prepareForStep. Step %d" % step + identity = CdiUtil.bean(Identity) + authentication_service = CdiUtil.bean(AuthenticationService) + + identity.setWorkingParameter("ts_host", ts_host) + identity.setWorkingParameter("ts_statusPath", ts_statusPath) + + self.set_relying_party_login_url(identity) + + if step == 1 or step == 3: + print "ThumbSignIn. Prepare for step 1" + self.initialize_thumbsignin(identity, AUTHENTICATE) + return True + + elif step == 2: + print "ThumbSignIn. Prepare for step 2" + if identity.isSetWorkingParameter(USER_LOGIN_FLOW): + user_login_flow = identity.getWorkingParameter(USER_LOGIN_FLOW) + print "ThumbSignIn. Value of user_login_flow is %s" % user_login_flow + user = authentication_service.getAuthenticatedUser() + if user is None: + print "ThumbSignIn. Prepare for step 2. Failed to determine user name" + return False + user_name = user.getUserId() + print "ThumbSignIn. Prepare for step 2. user_name: " + user_name + if user_name is None: + return False + identity.setWorkingParameter(USER_ID, user_name) + self.initialize_thumbsignin(identity, REGISTER + "/" + user_name) + return True + else: + return False + + def get_user_id_from_thumbsignin(self, request_parameters): + transaction_id = ServerUtil.getFirstValue(request_parameters, TRANSACTION_ID) + print "ThumbSignIn. Value of transaction_id is %s" % transaction_id + get_user_request = "getUser/" + transaction_id + print "ThumbSignIn. Value of get_user_request is %s" % get_user_request + + get_user_response = self.thumbsigninApiController.handleThumbSigninRequest(get_user_request, ts_api_key, ts_api_secret) + print "ThumbSignIn. Value of get_user_response is %s" % get_user_response + get_user_response_json = JSONObject(get_user_response) + thumbsignin_user_id = get_user_response_json.get(USER_ID) + print "ThumbSignIn. Value of thumbsignin_user_id is %s" % thumbsignin_user_id + return thumbsignin_user_id + + def authenticate(self, configuration_attributes, request_parameters, step): + print "ThumbSignIn. Inside authenticate. Step %d" % step + authentication_service = CdiUtil.bean(AuthenticationService) + identity = CdiUtil.bean(Identity) + + identity.setWorkingParameter("ts_host", ts_host) + identity.setWorkingParameter("ts_statusPath", ts_statusPath) + + if step == 1 or step == 3: + print "ThumbSignIn. Authenticate for Step %d" % step + + login_flow = ServerUtil.getFirstValue(request_parameters, "login_flow") + print "ThumbSignIn. Value of login_flow parameter is %s" % login_flow + + # Logic for ThumbSignIn Authentication Flow (Either step 1 or step 3) + if login_flow == THUMBSIGNIN_AUTHENTICATION or login_flow == THUMBSIGNIN_LOGIN_POST_REGISTRATION: + identity.setWorkingParameter(USER_LOGIN_FLOW, login_flow) + print "ThumbSignIn. Value of userLoginFlow is %s" % identity.getWorkingParameter(USER_LOGIN_FLOW) + logged_in_status = authentication_service.authenticate(self.get_user_id_from_thumbsignin(request_parameters)) + print "ThumbSignIn. logged_in status : %r" % logged_in_status + return logged_in_status + + # Logic for traditional login flow (step 1) + print "ThumbSignIn. User credentials login flow" + identity.setWorkingParameter(USER_LOGIN_FLOW, THUMBSIGNIN_REGISTRATION) + print "ThumbSignIn. Value of userLoginFlow is %s" % identity.getWorkingParameter(USER_LOGIN_FLOW) + logged_in = self.authenticate_user_credentials(identity, authentication_service) + print "ThumbSignIn. Status of User Credentials based Authentication : %r" % logged_in + + # When the traditional login fails, reinitialize the ThumbSignIn data before sending error response to UI + if not logged_in: + self.initialize_thumbsignin(identity, AUTHENTICATE) + return False + + print "ThumbSignIn. Authenticate successful for step %d" % step + return True + + elif step == 2: + print "ThumbSignIn. Registration flow (step 2)" + self.verify_user_login_flow(identity) + + user = self.get_authenticated_user_from_gluu(authentication_service) + if user is None: + print "ThumbSignIn. Registration flow (step 2). Failed to determine user name" + return False + + user_name = user.getUserId() + print "ThumbSignIn. Registration flow (step 2) successful. user_name: %s" % user_name + return True + + else: + return False + + def authenticate_user_credentials(self, identity, authentication_service): + credentials = identity.getCredentials() + user_name = credentials.getUsername() + user_password = credentials.getPassword() + print "ThumbSignIn. user_name: " + user_name + logged_in = False + if StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password): + logged_in = self.authenticate_user_in_gluu_ldap(authentication_service, user_name, user_password) + return logged_in + + @staticmethod + def authenticate_user_in_gluu_ldap(authentication_service, user_name, user_password): + return authentication_service.authenticate(user_name, user_password) + + @staticmethod + def get_authenticated_user_from_gluu(authentication_service): + return authentication_service.getAuthenticatedUser() + + @staticmethod + def verify_user_login_flow(identity): + if identity.isSetWorkingParameter(USER_LOGIN_FLOW): + user_login_flow = identity.getWorkingParameter(USER_LOGIN_FLOW) + print "ThumbSignIn. Value of user_login_flow is %s" % user_login_flow + else: + identity.setWorkingParameter(USER_LOGIN_FLOW, THUMBSIGNIN_REGISTRATION) + print "ThumbSignIn. Setting the value of user_login_flow to %s" % identity.getWorkingParameter(USER_LOGIN_FLOW) + + def getExtraParametersForStep(self, configuration_attributes, step): + return None + + def getCountAuthenticationSteps(self, configuration_attributes): + print "ThumbSignIn. Inside getCountAuthenticationSteps.." + identity = CdiUtil.bean(Identity) + + user_login_flow = identity.getWorkingParameter(USER_LOGIN_FLOW) + print "ThumbSignIn. Value of userLoginFlow is %s" % user_login_flow + if user_login_flow == THUMBSIGNIN_AUTHENTICATION: + print "ThumbSignIn. Total Authentication Steps is: 1" + return 1 + print "ThumbSignIn. Total Authentication Steps is: 3" + return 3 + + def getPageForStep(self, configuration_attributes, step): + print "ThumbSignIn. Inside getPageForStep. Step %d" % step + if step == 3: + return TSI_LOGIN_POST_REGISTRATION_PAGE + thumbsignin_page = TSI_REGISTER_PAGE if step == 2 else TSI_LOGIN_PAGE + return thumbsignin_page + + def destroy(self, configurationAttributes): + print "ThumbSignIn. Destroy" + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True diff --git a/oxAuth/Server/integrations/acr_router/Readme.txt b/oxAuth/Server/integrations/acr_router/Readme.txt new file mode 100644 index 00000000..05d3d69e --- /dev/null +++ b/oxAuth/Server/integrations/acr_router/Readme.txt @@ -0,0 +1,3 @@ +This is a person authentication module for oxAuth that enables user to forward authorization endpoint with a different acr_values as specified in the Custom property Key. + +1) new_acr_value - It's mandatory property. It's the new acr_value where user will be routed. Please make sure acr_value where user will be redirected is enabled and shown avaialble in acr_values_supported /.well-known/openid-configuration. diff --git a/oxAuth/Server/integrations/acr_router/acr_router_authenticator.py b/oxAuth/Server/integrations/acr_router/acr_router_authenticator.py new file mode 100644 index 00000000..61ef8819 --- /dev/null +++ b/oxAuth/Server/integrations/acr_router/acr_router_authenticator.py @@ -0,0 +1,68 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2016, Gluu +# +# Author: Yuriy Movchan +# + +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType + + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "ACR Router. Initialization" + if not configurationAttributes.containsKey("new_acr_value"): + print "ACR Router. Initialization. Property new_acr_value is mandatory" + return False + print "ACR Router. Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "ACR Router. Destroy" + print "ACR Router. Destroyed successfully" + + return True + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def getApiVersion(self): + return 11 + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return False + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + print "ACR Router. get new acr value" + new_acr_value = configurationAttributes.get("new_acr_value").getValue2() + # Put your custom logic to determine if routing required here... + return new_acr_value + + + def authenticate(self, configurationAttributes, requestParameters, step): + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + return True + + def getExtraParametersForStep(self, configurationAttributes, step): + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + return 1 + + def getPageForStep(self, configurationAttributes, step): + return "" + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + + def logout(self, configurationAttributes, requestParameters): + return True diff --git a/oxAuth/Server/integrations/acr_saml_router/Readme.txt b/oxAuth/Server/integrations/acr_saml_router/Readme.txt new file mode 100644 index 00000000..c326e9d0 --- /dev/null +++ b/oxAuth/Server/integrations/acr_saml_router/Readme.txt @@ -0,0 +1 @@ +In order to pass additional AuthZ parameters to session we need to add to authorizationRequestCustomAllowedParameters oxAuth property issuerId and entityId diff --git a/oxAuth/Server/integrations/acr_saml_router/acr_smal_router_authenticator.py b/oxAuth/Server/integrations/acr_saml_router/acr_smal_router_authenticator.py new file mode 100644 index 00000000..866c2603 --- /dev/null +++ b/oxAuth/Server/integrations/acr_saml_router/acr_smal_router_authenticator.py @@ -0,0 +1,82 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2023, Gluu +# +# Author: Yuriy Movchan +# +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.oxauth.util import ServerUtil +from org.gluu.util import StringHelper + +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType + + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "ACR SAML Router. Initialization" + print "ACR SAML Router. Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "ACR SAML Router. Destroy" + print "ACR SAML Router. Destroyed successfully" + + return True + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def getApiVersion(self): + return 11 + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return False + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + print "ACR SAML Router. Get new acr value" + # !!!Note: oxAuth stores in session only known parameters + # We need to add to authorizationRequestCustomAllowedParameters oxAuth property issuerId and entityId + + identity = CdiUtil.bean(Identity) + identity.getSessionId().getSessionAttributes() + + session_attributes = identity.getSessionId().getSessionAttributes() + if session_attributes.containsKey("issuerId") and session_attributes.containsKey("entityId"): + + issuerId = session_attributes.get("issuerId") + entityId = session_attributes.get("entityId") + redirect_uri = session_attributes.get("redirect_uri") + print "ACR SAML Router. issuerId: %s, entityId: %s, redirect_uri: %s: " % (issuerId, entityId, redirect_uri) + if StringHelper.equalsIgnoreCase(issuerId, "https://samltest.id/saml/sp"): + print "ACR SAML Router. Redirect to super_gluu" + return "super_gluu" + + print "ACR SAML Router. Redirect to default method" + return "basic" + + def authenticate(self, configurationAttributes, requestParameters, step): + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + return True + + def getExtraParametersForStep(self, configurationAttributes, step): + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + return 1 + + def getPageForStep(self, configurationAttributes, step): + return "" + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + return None + + def logout(self, configurationAttributes, requestParameters): + return True diff --git a/oxAuth/Server/integrations/allowed_countries/allowed_countries.py b/oxAuth/Server/integrations/allowed_countries/allowed_countries.py new file mode 100644 index 00000000..9da3a44f --- /dev/null +++ b/oxAuth/Server/integrations/allowed_countries/allowed_countries.py @@ -0,0 +1,155 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2016, Gluu +# +# Author: Yuriy Movchan +# + +from java.util import Arrays +from org.apache.http.params import CoreConnectionPNames +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.model.config import ConfigurationFactory +from org.gluu.oxauth.service import UserService, AuthenticationService, SessionIdService +from org.gluu.oxauth.service.net import HttpService +from org.gluu.oxauth.util import ServerUtil +from org.gluu.util import StringHelper +from org.gluu.oxauth.service.common import EncryptionService +from java.util import Arrays, HashMap, IdentityHashMap + +import java +import datetime +import urllib + +import sys +import json + + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Basic. Initialization" + self.allowedCountries = configurationAttributes.get("allowed_countries").getValue2() + print "Basic. Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "Basic. Destroy" + print "Basic. Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + identity = CdiUtil.bean(Identity) + session_attributes = identity.getSessionId().getSessionAttributes() + authenticationService = CdiUtil.bean(AuthenticationService) + allowedCountriesListArray = StringHelper.split(self.allowedCountries, ",") + if (len(allowedCountriesListArray) > 0 and session_attributes.containsKey("remote_ip")): + remote_ip = session_attributes.get("remote_ip") + remote_loc_dic = self.determineGeolocationData(remote_ip) + if remote_loc_dic == None: + print "Super-Gluu. Prepare for step 2. Failed to determine remote location by remote IP '%s'" % remote_ip + return + remote_loc = "%s" % ( remote_loc_dic['countryCode']) + print "Your remote location is "+remote_loc + if remote_loc in allowedCountriesListArray: + print "you are allowed to access" + else: + return False + + + if (step == 1): + print "Basic. Authenticate for step 1" + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + logged_in = False + if (StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password)): + logged_in = authenticationService.authenticate(user_name, user_password) + + if (not logged_in): + return False + + return True + else: + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + if (step == 1): + print "Basic. Prepare for Step 1" + return True + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + return 1 + + def getPageForStep(self, configurationAttributes, step): + return "" + + def logout(self, configurationAttributes, requestParameters): + return True + + def determineGeolocationData(self, remote_ip): + print "Super-Gluu. Determine remote location. remote_ip: '%s'" % remote_ip + httpService = CdiUtil.bean(HttpService) + http_client = httpService.getHttpsClient() + http_client_params = http_client.getParams() + http_client_params.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 15 * 1000) + geolocation_service_url = "http://ip-api.com/json/%s?fields=520191" % remote_ip + geolocation_service_headers = { "Accept" : "application/json" } + try: + http_service_response = httpService.executeGet(http_client, geolocation_service_url, geolocation_service_headers) + http_response = http_service_response.getHttpResponse() + except: + print "Super-Gluu. Determine remote location. Exception: ", sys.exc_info()[1] + return None + + try: + if not httpService.isResponseStastusCodeOk(http_response): + print "Super-Gluu. Determine remote location. Get invalid response from validation server: ", str(http_response.getStatusLine().getStatusCode()) + httpService.consume(http_response) + return None + response_bytes = httpService.getResponseContent(http_response) + response_string = httpService.convertEntityToString(response_bytes) + httpService.consume(http_response) + finally: + http_service_response.closeConnection() + + if response_string == None: + print "Super-Gluu. Determine remote location. Get empty response from location server" + return None + + + response = json.loads(response_string) + + if not StringHelper.equalsIgnoreCase(response['status'], "success"): + print "Super-Gluu. Determine remote location. Get response with status: '%s'" % response['status'] + return None + + return response + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None \ No newline at end of file diff --git a/oxAuth/Server/integrations/allowed_countries/readme.txt b/oxAuth/Server/integrations/allowed_countries/readme.txt new file mode 100644 index 00000000..3c30d9a4 --- /dev/null +++ b/oxAuth/Server/integrations/allowed_countries/readme.txt @@ -0,0 +1,3 @@ +This is a person authentication module for oxAuth to allow access based on the user location. + +1) allowed_countries = short Country codes (eg US,UK,IN) separated by comma which will be allowed to access diff --git a/oxAuth/Server/integrations/authz/ConsentGatheringSample.py b/oxAuth/Server/integrations/authz/ConsentGatheringSample.py new file mode 100644 index 00000000..378bce80 --- /dev/null +++ b/oxAuth/Server/integrations/authz/ConsentGatheringSample.py @@ -0,0 +1,87 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2017, Gluu +# +# Author: Yuriy Movchan +# + +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.authz import ConsentGatheringType +from org.gluu.util import StringHelper + +import java +import random + +class ConsentGathering(ConsentGatheringType): + + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Consent-Gathering. Initializing ..." + print "Consent-Gathering. Initialized successfully" + + return True + + def destroy(self, configurationAttributes): + print "Consent-Gathering. Destroying ..." + print "Consent-Gathering. Destroyed successfully" + + return True + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def getApiVersion(self): + return 11 + + # Main consent-gather method. Must return True (if gathering performed successfully) or False (if fail). + # All user entered values can be access via Map context.getPageAttributes() + def authorize(self, step, context): # context is reference of org.gluu.oxauth.service.external.context.ConsentGatheringContext + print "Consent-Gathering. Authorizing..." + + if step == 1: + allowButton = context.getRequestParameters().get("authorizeForm:allowButton") + if (allowButton != None) and (len(allowButton) > 0): + print "Consent-Gathering. Authorization success for step 1" + return True + + print "Consent-Gathering. Authorization declined for step 1" + elif step == 2: + allowButton = context.getRequestParameters().get("authorizeForm:allowButton") + if (allowButton != None) and (len(allowButton) > 0): + print "Consent-Gathering. Authorization success for step 2" + return True + + print "Consent-Gathering. Authorization declined for step 2" + + return False + + def getNextStep(self, step, context): + return -1 + + def prepareForStep(self, step, context): + if not context.isAuthenticated(): + print "User is not authenticated. Aborting authorization flow ..." + return False + + if step == 2: + pageAttributes = context.getPageAttributes() + + # Generate random consent gathering request + consentRequest = "Requested transaction #%s approval for the amount of sum $ %s.00" % ( random.randint(100000, 1000000), random.randint(1, 100) ) + pageAttributes.put("consent_request", consentRequest) + return True + + return True + + def getStepsCount(self, context): + return 2 + + def getPageForStep(self, step, context): + if step == 1: + return "/authz/authorize.xhtml" + elif step == 2: + return "/authz/transaction.xhtml" + + return "" diff --git a/oxAuth/Server/integrations/authz/docs/Authz design.dia b/oxAuth/Server/integrations/authz/docs/Authz design.dia new file mode 100644 index 0000000000000000000000000000000000000000..66b1837de3299dfdd5f1c4237c26938f79acb45d GIT binary patch literal 4879 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 + + + + + + oxAuth - New Password + + + + + + +
+
+ + + + + +
+
+ +
+
\ No newline at end of file diff --git a/oxAuth/Server/integrations/basic.client_group/BasicClientGroupExternalAuthenticator.py b/oxAuth/Server/integrations/basic.client_group/BasicClientGroupExternalAuthenticator.py new file mode 100644 index 00000000..8a27a85d --- /dev/null +++ b/oxAuth/Server/integrations/basic.client_group/BasicClientGroupExternalAuthenticator.py @@ -0,0 +1,177 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2019, Gluu +# +# Author: Yuriy Movchan +# + +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.service import UserService, AuthenticationService, AppInitializer +from org.gluu.util import StringHelper +from java.util import Arrays, HashMap + +import java +import json + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Basic (client group). Initialization" + + self.allow_default_login = False + if configurationAttributes.containsKey("allow_default_login"): + self.allow_default_login = StringHelper.toBoolean(configurationAttributes.get("allow_default_login").getValue2(), False) + + if not configurationAttributes.containsKey("configuration_file"): + print "Basic (client group). The property configuration_file is empty" + return False + + configurationFilePath = configurationAttributes.get("configuration_file").getValue2() + self.client_configurations = self.loadClientConfigurations(configurationFilePath) + if self.client_configurations == None: + print "Basic (client group). File with client configuration should be not empty" + return False + + print "Basic (client group). Initialized successfully" + return True + + def destroy(self, clientConfiguration): + print "Basic (client group). Destroy" + + print "Basic (client group). Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + +def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + authenticationService = CdiUtil.bean(AuthenticationService) + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + session_attributes = identity.getSessionId().getSessionAttributes() + + client_id = session_attributes.get("client_id") + print "Basic (client group). Get client_id: '%s' authorization request" % client_id + + user_groups = self.client_configurations.get(client_id) + if user_groups == None: + print "Basic (client group). There is no user groups configuration for client_id '%s'. allow_default_login: %s" % (client_id, self.allow_default_login) + if not self.allow_default_login: + return False + + result = self.authenticateImpl(credentials, authenticationService) + return result + + is_member_client_groups = self.isUserMemberOfGroups(credentials, user_groups) + if not is_member_client_groups: + print "Basic (client group). User '%s' hasn't permissions to log into client_id '%s' application. " % (credentials.getUsername(), client_id) + return False + + result = self.authenticateImpl(credentials, authenticationService) + return result + + def prepareForStep(self, configurationAttributes, requestParameters, step): + if step == 1: + print "Basic (client group). Prepare for Step 1" + return True + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + return 1 + + def getPageForStep(self, configurationAttributes, step): + return "" + + def logout(self, configurationAttributes, requestParameters): + return True + + def authenticateImpl(self, credentials, authenticationService): + print "Basic (client group). Processing user name/password authentication" + + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + logged_in = False + if StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password): + logged_in = authenticationService.authenticate(user_name, user_password) + + if not logged_in: + return False + + return True + + def loadClientConfigurations(self, configurationFile): + clientConfiguration = None + + # Load configuration from file + f = open(configurationFile, 'r') + try: + configurationFileJson = json.loads(f.read()) + except: + print "Basic (client group). Load configuration from file. Failed to load authentication configuration from file:", configurationFile + return None + finally: + f.close() + + clientConfigurations = HashMap() + for client_key in configurationFileJson.keys(): + client_config = configurationFileJson[client_key] + + client_inum = client_config["client_inum"] + user_groups_array = client_config["user_group"] + user_groups = Arrays.asList(user_groups_array) + clientConfigurations.put(client_inum, user_groups) + + print "Basic (client group). Load configuration from file. Loaded '%s' configurations" % clientConfigurations.size() + print clientConfigurations + + return clientConfigurations + + def isUserMemberOfGroups(self, credentials, groups): + userService = CdiUtil.bean(UserService) + + user_name = credentials.getUsername() + if StringHelper.isEmptyString(user_name): + return False + + find_user_by_uid = userService.getUser(user_name) + + is_member = False + member_of_list = find_user_by_uid.getAttributeValues("memberOf") + if member_of_list == None: + return is_member + + print member_of_list + print groups + + for member_of in member_of_list: + for group in groups: + if StringHelper.equalsIgnoreCase(group, member_of) or member_of.endswith(group): + is_member = True + break + + return is_member + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None \ No newline at end of file diff --git a/oxAuth/Server/integrations/basic.client_group/README.txt b/oxAuth/Server/integrations/basic.client_group/README.txt new file mode 100644 index 00000000..9cb52631 --- /dev/null +++ b/oxAuth/Server/integrations/basic.client_group/README.txt @@ -0,0 +1,25 @@ +This person authentication module for oxAuth allows to restrict access to RP for specific user groups. It allows to define configuration for each client. + +This module has one mandatory property `configuration_file`. It's path to JSON configuration file + Example: /etc/certs/client_group.json + Example content of this file: + +{ + "client_1":{ + "client_inum":"client_inum_1", + "user_group":[ + "group_dn_1", + "group_dn_2" + ] + }, + "client_2":{ + "client_inum":"client_inum_2", + "user_group":[ + "group_dn_1", + "group_dn_2" + ] + } +} + +Also it's possible to define how it should work when there is no configuration for specific client. This is controlled via property: +`allow_default_login`: true/false diff --git a/oxAuth/Server/integrations/basic.client_group/client_group.json b/oxAuth/Server/integrations/basic.client_group/client_group.json new file mode 100644 index 00000000..4476826f --- /dev/null +++ b/oxAuth/Server/integrations/basic.client_group/client_group.json @@ -0,0 +1,16 @@ +{ + "client_1":{ + "client_inum":"client_inum_1", + "user_group":[ + "group_dn_1", + "group_dn_2" + ] + }, + "client_2":{ + "client_inum":"client_inum_2", + "user_group":[ + "group_dn_1", + "group_dn_2" + ] + } +} diff --git a/oxAuth/Server/integrations/basic.external_logout/BasicExternalAuthenticatorWithExternalLogout.py b/oxAuth/Server/integrations/basic.external_logout/BasicExternalAuthenticatorWithExternalLogout.py new file mode 100644 index 00000000..8ebfaa3c --- /dev/null +++ b/oxAuth/Server/integrations/basic.external_logout/BasicExternalAuthenticatorWithExternalLogout.py @@ -0,0 +1,91 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2019, Gluu +# +# Author: Yuriy Movchan +# + +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.service import AuthenticationService +from org.gluu.util import StringHelper + +import java + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Basic (with external logout). Initialization" + print "Basic (with external logout). Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "Basic (with external logout). Destroy" + print "Basic (with external logout). Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + authenticationService = CdiUtil.bean(AuthenticationService) + + if (step == 1): + print "Basic (with external logout). Authenticate for step 1" + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + logged_in = False + if (StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password)): + logged_in = authenticationService.authenticate(user_name, user_password) + + if (not logged_in): + return False + + return True + else: + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + if (step == 1): + print "Basic (with external logout). Prepare for Step 1" + return True + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + return 1 + + def getPageForStep(self, configurationAttributes, step): + return "" + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Basic (with external logout). Get external logout URL call" + return "https://www.dummy.org/app/logout.htm" + + # In order to get this method call RP should end_session request to https:///oxauth/logout.htm enpoint + # instead of https:///oxauth/restv1/end_session endpoint + def logout(self, configurationAttributes, requestParameters): + print "Basic (with external logout). Logout call" + return True diff --git a/oxAuth/Server/integrations/basic.external_logout/README.txt b/oxAuth/Server/integrations/basic.external_logout/README.txt new file mode 100644 index 00000000..82ea8046 --- /dev/null +++ b/oxAuth/Server/integrations/basic.external_logout/README.txt @@ -0,0 +1,3 @@ +This is person authentication module for oxAuth which do basic authentication and at logout do redirect to external service. + +This module hasn't properties. \ No newline at end of file diff --git a/oxAuth/Server/integrations/basic.ldap_auth_confs/LdapAuthConfExternalAuthenticator.py b/oxAuth/Server/integrations/basic.ldap_auth_confs/LdapAuthConfExternalAuthenticator.py new file mode 100644 index 00000000..372adbe2 --- /dev/null +++ b/oxAuth/Server/integrations/basic.ldap_auth_confs/LdapAuthConfExternalAuthenticator.py @@ -0,0 +1,125 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2022, Gluu +# +# Author: Yuriy Movchan +# + +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.service import UserService, AuthenticationService, AppInitializer +from org.gluu.oxauth.service.common import ApplicationFactory +from org.gluu.oxauth.service import MetricService +from org.gluu.oxauth.service.common import EncryptionService +from org.gluu.model.metric import MetricType +from org.gluu.util import StringHelper +from org.gluu.util import ArrayHelper +from org.gluu.persist.service import PersistanceFactoryService +from org.gluu.persist.ldap.impl import LdapEntryManagerFactory +from org.gluu.model.ldap import GluuLdapConfiguration +from java.util import List, Arrays, Properties + +import java + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Basic (ldap auth conf). Initialization" + + authenticationService = CdiUtil.bean(AuthenticationService) + self.ldapAuthConfigs = authenticationService.getLdapAuthConfigs() + self.ldapAuthEntryManagers = authenticationService.getLdapAuthEntryManagers() + if self.ldapAuthEntryManagers == None: + print "Basic (ldap auth conf). At least one LDAP authentication configuration should be defined" + return False + + print "Basic (ldap auth conf). Found %s LDAP Authentication entry managers" % self.ldapAuthEntryManagers.size() + + print "Basic (ldap auth conf). Initialized successfully" + return True + + def destroy(self, authConfiguration): + print "Basic (ldap auth conf). Destroy" + + print "Basic (ldap auth conf). Destroyed successfully" + + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + authenticationService = CdiUtil.bean(AuthenticationService) + + if (step == 1): + print "Basic (ldap auth conf). Authenticate for step 1" + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + metricService = CdiUtil.bean(MetricService) + timerContext = metricService.getTimer(MetricType.OXAUTH_USER_AUTHENTICATION_RATE).time() + try: + keyValue = credentials.getUsername() + userPassword = credentials.getPassword() + + if (StringHelper.isNotEmptyString(keyValue) and StringHelper.isNotEmptyString(userPassword)): + for i in range(self.ldapAuthEntryManagers.size()): + ldapConfiguration = self.ldapAuthConfigs.get(i) + ldapEntryManager = self.ldapAuthEntryManagers.get(i) + + primaryKey = ldapConfiguration.getPrimaryKey() + localPrimaryKey = ldapConfiguration.getLocalPrimaryKey() + + print "Basic (ldap auth conf). Authenticate for step 1. Using configuration: " + ldapConfiguration.getConfigId() + + + loggedIn = authenticationService.authenticate(ldapConfiguration, ldapEntryManager, keyValue, userPassword, primaryKey, localPrimaryKey) + if (loggedIn): + metricService.incCounter(MetricType.OXAUTH_USER_AUTHENTICATION_SUCCESS) + return True + finally: + timerContext.stop() + + metricService.incCounter(MetricType.OXAUTH_USER_AUTHENTICATION_FAILURES) + + return False + else: + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + if (step == 1): + print "Basic (ldap auth conf). Prepare for Step 1" + return True + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + return 1 + + def getPageForStep(self, configurationAttributes, step): + return "" + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True diff --git a/oxAuth/Server/integrations/basic.ldap_auth_confs/README.txt b/oxAuth/Server/integrations/basic.ldap_auth_confs/README.txt new file mode 100644 index 00000000..6279ee99 --- /dev/null +++ b/oxAuth/Server/integrations/basic.ldap_auth_confs/README.txt @@ -0,0 +1,2 @@ +This is person authentication module for oxAuth which allows to specify multiple authentication configurations. +It's uses configurations defined on `Manage Authentication` page. diff --git a/oxAuth/Server/integrations/basic.lock.account/BasicLockAccountExternalAuthenticator.py b/oxAuth/Server/integrations/basic.lock.account/BasicLockAccountExternalAuthenticator.py new file mode 100644 index 00000000..240603e3 --- /dev/null +++ b/oxAuth/Server/integrations/basic.lock.account/BasicLockAccountExternalAuthenticator.py @@ -0,0 +1,271 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2016, Gluu +# +# Author: Yuriy Movchan +# Author: Gasmyr Mougang +# + +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.service import AuthenticationService +from org.gluu.oxauth.service.common import UserService +from org.gluu.service import CacheService +from org.gluu.util import StringHelper +from org.gluu.persist.exception import AuthenticationException +from javax.faces.application import FacesMessage +from org.gluu.jsf2.message import FacesMessages +from java.time import LocalDateTime, Duration +from java.time.format import DateTimeFormatter + +import java +import datetime +import json + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Basic (lock account). Initialization" + + self.invalidLoginCountAttribute = "oxCountInvalidLogin" + if configurationAttributes.containsKey("invalid_login_count_attribute"): + self.invalidLoginCountAttribute = configurationAttributes.get("invalid_login_count_attribute").getValue2() + else: + print "Basic (lock account). Initialization. Using default attribute" + + self.maximumInvalidLoginAttemps = 3 + if configurationAttributes.containsKey("maximum_invalid_login_attemps"): + self.maximumInvalidLoginAttemps = StringHelper.toInteger(configurationAttributes.get("maximum_invalid_login_attemps").getValue2()) + else: + print "Basic (lock account). Initialization. Using default number attempts" + + self.lockExpirationTime = 180 + if configurationAttributes.containsKey("lock_expiration_time"): + self.lockExpirationTime = StringHelper.toInteger(configurationAttributes.get("lock_expiration_time").getValue2()) + else: + print "Basic (lock account). Initialization. Using default lock expiration time" + + + print "Basic (lock account). Initialized successfully. invalid_login_count_attribute: '%s', maximum_invalid_login_attemps: '%s', lock_expiration_time: '%s'" % (self.invalidLoginCountAttribute, self.maximumInvalidLoginAttemps, self.lockExpirationTime) + + return True + + def destroy(self, configurationAttributes): + print "Basic (lock account). Destroy" + print "Basic (lock account). Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + authenticationService = CdiUtil.bean(AuthenticationService) + + if step == 1: + print "Basic (lock account). Authenticate for step 1" + facesMessages = CdiUtil.bean(FacesMessages) + facesMessages.setKeepMessages() + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + user_name = credentials.getUsername() + user_password = credentials.getPassword() + cacheService = CdiUtil.bean(CacheService) + userService = CdiUtil.bean(UserService) + + + logged_in = False + if (StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password)): + try: + logged_in = authenticationService.authenticate(user_name, user_password) + except AuthenticationException: + print "Basic (lock account). Authenticate. Failed to authenticate user '%s'" % user_name + + if logged_in: + self.setUserAttributeValue(user_name, self.invalidLoginCountAttribute, StringHelper.toString(0)) + else: + countInvalidLoginArributeValue = self.getUserAttributeValue(user_name, self.invalidLoginCountAttribute) + userSatus = self.getUserAttributeValue(user_name, "gluuStatus") + print "Current user '%s' status is '%s'" % ( user_name, userSatus ) + + countInvalidLogin = StringHelper.toInteger(countInvalidLoginArributeValue, 0) + + if countInvalidLogin < self.maximumInvalidLoginAttemps: + countInvalidLogin = countInvalidLogin + 1 + remainingAttempts = self.maximumInvalidLoginAttemps - countInvalidLogin + + print "Remaining login count attempts '%s' for user '%s'" % ( remainingAttempts, user_name ) + + self.setUserAttributeValue(user_name, self.invalidLoginCountAttribute, StringHelper.toString(countInvalidLogin)) + if remainingAttempts > 0 and userSatus == "active": + facesMessages.add(FacesMessage.SEVERITY_INFO, StringHelper.toString(remainingAttempts)+" more attempt(s) before account is LOCKED!") + + if (countInvalidLogin >= self.maximumInvalidLoginAttemps) and ((userSatus == None) or (userSatus == "active")): + print "Basic (lock account). Locking '%s' for '%s' seconds" % ( user_name, self.lockExpirationTime) + self.lockUser(user_name) + return False + + if (countInvalidLogin >= self.maximumInvalidLoginAttemps) and userSatus == "inactive": + print "Basic (lock account). User '%s' is locked. Checking if we can unlock him" % user_name + + unlock_and_authenticate = False + + object_from_store = cacheService.get(None, "lock_user_" + user_name) + if object_from_store == None: + # Object in cache was expired. We need to unlock user + print "Basic (lock account). User locking details for user '%s' not exists" % user_name + unlock_and_authenticate = True + else: + # Analyze object from cache + user_lock_details = json.loads(object_from_store) + + user_lock_details_locked = user_lock_details['locked'] + user_lock_details_created = user_lock_details['created'] + user_lock_details_created_date = LocalDateTime.parse(user_lock_details_created, DateTimeFormatter.ISO_LOCAL_DATE_TIME) + user_lock_details_created_diff = Duration.between(user_lock_details_created_date, LocalDateTime.now()).getSeconds() + print "Basic (lock account). Get user '%s' locking details. locked: '%s', Created: '%s', Difference in seconds: '%s'" % ( user_name, user_lock_details_locked, user_lock_details_created, user_lock_details_created_diff ) + + if user_lock_details_locked and user_lock_details_created_diff >= self.lockExpirationTime: + print "Basic (lock account). Unlocking user '%s' after lock expiration" % user_name + unlock_and_authenticate = True + + if unlock_and_authenticate: + self.unLockUser(user_name) + self.setUserAttributeValue(user_name, self.invalidLoginCountAttribute, StringHelper.toString(0)) + logged_in = authenticationService.authenticate(user_name, user_password) + if not logged_in: + # Update number of attempts + self.setUserAttributeValue(user_name, self.invalidLoginCountAttribute, StringHelper.toString(1)) + if self.maximumInvalidLoginAttemps == 1: + # Lock user if maximum count login attempts is 1 + self.lockUser(user_name) + return False + + + return logged_in + else: + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + if step == 1: + print "Basic (lock account). Prepare for Step 1" + return True + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + return 1 + + def getPageForStep(self, configurationAttributes, step): + return "" + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True + + def getUserAttributeValue(self, user_name, attribute_name): + if StringHelper.isEmpty(user_name): + return None + + userService = CdiUtil.bean(UserService) + + find_user_by_uid = userService.getUser(user_name, attribute_name) + if find_user_by_uid == None: + return None + + custom_attribute_value = userService.getCustomAttribute(find_user_by_uid, attribute_name) + if custom_attribute_value == None: + return None + + attribute_value = custom_attribute_value.getValue() + + print "Basic (lock account). Get user attribute. User's '%s' attribute '%s' value is '%s'" % (user_name, attribute_name, attribute_value) + + return attribute_value + + def setUserAttributeValue(self, user_name, attribute_name, attribute_value): + if StringHelper.isEmpty(user_name): + return None + + userService = CdiUtil.bean(UserService) + + find_user_by_uid = userService.getUser(user_name) + if find_user_by_uid == None: + return None + + userService.setCustomAttribute(find_user_by_uid, attribute_name, attribute_value) + updated_user = userService.updateUser(find_user_by_uid) + + print "Basic (lock account). Set user attribute. User's '%s' attribute '%s' value is '%s'" % (user_name, attribute_name, attribute_value) + + return updated_user + + def lockUser(self, user_name): + if StringHelper.isEmpty(user_name): + return None + + userService = CdiUtil.bean(UserService) + cacheService= CdiUtil.bean(CacheService) + facesMessages = CdiUtil.bean(FacesMessages) + facesMessages.setKeepMessages() + + find_user_by_uid = userService.getUser(user_name) + if (find_user_by_uid == None): + return None + + status_attribute_value = userService.getCustomAttribute(find_user_by_uid, "gluuStatus") + if status_attribute_value != None: + user_status = status_attribute_value.getValue() + if StringHelper.equals(user_status, "inactive"): + print "Basic (lock account). Lock user. User '%s' locked already" % user_name + return + + userService.setCustomAttribute(find_user_by_uid, "gluuStatus", "inactive") + updated_user = userService.updateUser(find_user_by_uid) + + object_to_store = json.dumps({'locked': True, 'created': LocalDateTime.now().toString()}, separators=(',',':')) + + cacheService.put(StringHelper.toString(self.lockExpirationTime), "lock_user_"+user_name, object_to_store); + facesMessages.add(FacesMessage.SEVERITY_ERROR, "Your account is locked. Please try again after " + StringHelper.toString(self.lockExpirationTime) + " secs") + + print "Basic (lock account). Lock user. User '%s' locked" % user_name + + def unLockUser(self, user_name): + if StringHelper.isEmpty(user_name): + return None + + userService = CdiUtil.bean(UserService) + cacheService= CdiUtil.bean(CacheService) + + find_user_by_uid = userService.getUser(user_name) + if (find_user_by_uid == None): + return None + + object_to_store = json.dumps({'locked': False, 'created': LocalDateTime.now().toString()}, separators=(',',':')) + cacheService.put(StringHelper.toString(self.lockExpirationTime), "lock_user_"+user_name, object_to_store); + + userService.setCustomAttribute(find_user_by_uid, "gluuStatus", "active") + userService.setCustomAttribute(find_user_by_uid, self.invalidLoginCountAttribute, None) + updated_user = userService.updateUser(find_user_by_uid) + + + print "Basic (lock account). Lock user. User '%s' unlocked" % user_name diff --git a/oxAuth/Server/integrations/basic.lock.account/README.txt b/oxAuth/Server/integrations/basic.lock.account/README.txt new file mode 100644 index 00000000..e9c9cab7 --- /dev/null +++ b/oxAuth/Server/integrations/basic.lock.account/README.txt @@ -0,0 +1,15 @@ + +This is person authentication module for oxAuth which do basic authentication. +It looks user account after specified number of unsuccessful login attempts. + +This module has 2 properties: + +1) invalid_login_count_attribute - Specify attribute where script stores count of invalid number of login attemps + Default value: oxCountInvalidLogin + +2) maximum_invalid_login_attemps - Specify how many times user can enter invalid password before application will lock account + Allowed values: integer value greater that 0 + Example: 3 + Default value: 3 +3) lock_expiration_time - Specify the time in seconds when lock will be expired + Default value: 180 diff --git a/oxAuth/Server/integrations/basic.multi_auth_conf/BasicMultiAuthConfExternalAuthenticator.py b/oxAuth/Server/integrations/basic.multi_auth_conf/BasicMultiAuthConfExternalAuthenticator.py new file mode 100644 index 00000000..91908e74 --- /dev/null +++ b/oxAuth/Server/integrations/basic.multi_auth_conf/BasicMultiAuthConfExternalAuthenticator.py @@ -0,0 +1,293 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2016, Gluu +# +# Author: Yuriy Movchan +# + +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.service import UserService, AuthenticationService, AppInitializer +from org.gluu.oxauth.service import MetricService +from org.gluu.oxauth.service.common import EncryptionService +from org.gluu.model.metric import MetricType +from org.gluu.util import StringHelper +from org.gluu.util import ArrayHelper +from org.gluu.persist.service import PersistanceFactoryService +from org.gluu.persist.ldap.impl import LdapEntryManagerFactory +from org.gluu.model.ldap import GluuLdapConfiguration +from java.util import Arrays, Properties + +import java +import json + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Basic (multi auth conf). Initialization" + + if (not configurationAttributes.containsKey("auth_configuration_file")): + print "Basic (multi auth conf). The property auth_configuration_file is empty" + return False + + authConfigurationFile = configurationAttributes.get("auth_configuration_file").getValue2() + authConfiguration = self.loadAuthConfiguration(authConfigurationFile) + if (authConfiguration == None): + print "Basic (multi auth conf). File with authentication configuration should be not empty" + return False + + validationResult = self.validateAuthConfiguration(authConfiguration) + if (not validationResult): + return False + + ldapExtendedEntryManagers = self.createLdapExtendedEntryManagers(authConfiguration) + if (ldapExtendedEntryManagers == None): + return False + + self.ldapExtendedEntryManagers = ldapExtendedEntryManagers + + print "Basic (multi auth conf). Initialized successfully" + return True + + def destroy(self, authConfiguration): + print "Basic (multi auth conf). Destroy" + + result = True + for ldapExtendedEntryManager in self.ldapExtendedEntryManagers: + ldapConfiguration = ldapExtendedEntryManager["ldapConfiguration"] + ldapEntryManager = ldapExtendedEntryManager["ldapEntryManager"] + + destoryResult = ldapEntryManager.destroy() + result = result and destoryResult + print "Basic (multi auth conf). Destroyed: " + ldapConfiguration.getConfigId() + ". Result: " + str(destoryResult) + + print "Basic (multi auth conf). Destroyed successfully" + + return result + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + authenticationService = CdiUtil.bean(AuthenticationService) + + if (step == 1): + print "Basic (multi auth conf). Authenticate for step 1" + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + metricService = CdiUtil.bean(MetricService) + timerContext = metricService.getTimer(MetricType.OXAUTH_USER_AUTHENTICATION_RATE).time() + try: + keyValue = credentials.getUsername() + userPassword = credentials.getPassword() + + if (StringHelper.isNotEmptyString(keyValue) and StringHelper.isNotEmptyString(userPassword)): + for ldapExtendedEntryManager in self.ldapExtendedEntryManagers: + ldapConfiguration = ldapExtendedEntryManager["ldapConfiguration"] + ldapEntryManager = ldapExtendedEntryManager["ldapEntryManager"] + loginAttributes = ldapExtendedEntryManager["loginAttributes"] + localLoginAttributes = ldapExtendedEntryManager["localLoginAttributes"] + + print "Basic (multi auth conf). Authenticate for step 1. Using configuration: " + ldapConfiguration.getConfigId() + + idx = 0 + count = len(loginAttributes) + while (idx < count): + primaryKey = loginAttributes[idx] + localPrimaryKey = localLoginAttributes[idx] + + loggedIn = authenticationService.authenticate(ldapConfiguration, ldapEntryManager, keyValue, userPassword, primaryKey, localPrimaryKey) + if (loggedIn): + metricService.incCounter(MetricType.OXAUTH_USER_AUTHENTICATION_SUCCESS) + return True + idx += 1 + finally: + timerContext.stop() + + metricService.incCounter(MetricType.OXAUTH_USER_AUTHENTICATION_FAILURES) + + return False + else: + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + if (step == 1): + print "Basic (multi auth conf). Prepare for Step 1" + return True + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + return 1 + + def getPageForStep(self, configurationAttributes, step): + return "" + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True + + def loadAuthConfiguration(self, authConfigurationFile): + authConfiguration = None + + # Load authentication configuration from file + f = open(authConfigurationFile, 'r') + try: + authConfiguration = json.loads(f.read()) + except: + print "Basic (multi auth conf). Load auth configuration. Failed to load authentication configuration from file:", authConfigurationFile + return None + finally: + f.close() + + return authConfiguration + + def validateAuthConfiguration(self, authConfiguration): + isValid = True + + if (not ("ldap_configuration" in authConfiguration)): + print "Basic (multi auth conf). Validate auth configuration. There is no ldap_configuration section in configuration" + return False + + idx = 1 + for ldapConfiguration in authConfiguration["ldap_configuration"]: + if (not self.containsAttributeString(ldapConfiguration, "configId")): + print "Basic (multi auth conf). Validate auth configuration. There is no 'configId' attribute in ldap_configuration section #" + str(idx) + return False + + configId = ldapConfiguration["configId"] + + if (not self.containsAttributeArray(ldapConfiguration, "servers")): + print "Basic (multi auth conf). Validate auth configuration. Property 'servers' in configuration '" + configId + "' is invalid" + return False + + if (self.containsAttributeString(ldapConfiguration, "bindDN")): + if (not self.containsAttributeString(ldapConfiguration, "bindPassword")): + print "Basic (multi auth conf). Validate auth configuration. Property 'bindPassword' in configuration '" + configId + "' is invalid" + return False + + if (not self.containsAttributeString(ldapConfiguration, "useSSL")): + print "Basic (multi auth conf). Validate auth configuration. Property 'useSSL' in configuration '" + configId + "' is invalid" + return False + + if (not self.containsAttributeString(ldapConfiguration, "maxConnections")): + print "Basic (multi auth conf). Validate auth configuration. Property 'maxConnections' in configuration '" + configId + "' is invalid" + return False + + if (not self.containsAttributeArray(ldapConfiguration, "baseDNs")): + print "Basic (multi auth conf). Validate auth configuration. Property 'baseDNs' in configuration '" + configId + "' is invalid" + return False + + if (not self.containsAttributeArray(ldapConfiguration, "loginAttributes")): + print "Basic (multi auth conf). Validate auth configuration. Property 'loginAttributes' in configuration '" + configId + "' is invalid" + return False + + if (not self.containsAttributeArray(ldapConfiguration, "localLoginAttributes")): + print "Basic (multi auth conf). Validate auth configuration. Property 'localLoginAttributes' in configuration '" + configId + "' is invalid" + return False + + if (len(ldapConfiguration["loginAttributes"]) != len(ldapConfiguration["localLoginAttributes"])): + print "Basic (multi auth conf). Validate auth configuration. The number of attributes in 'loginAttributes' and 'localLoginAttributes' isn't equal in configuration '" + configId + "'" + return False + + idx += 1 + + return True + + def createLdapExtendedEntryManagers(self, authConfiguration): + ldapExtendedConfigurations = self.createLdapExtendedConfigurations(authConfiguration) + + appInitializer = CdiUtil.bean(AppInitializer) + persistanceFactoryService = CdiUtil.bean(PersistanceFactoryService) + ldapEntryManagerFactory = persistanceFactoryService.getPersistenceEntryManagerFactory(LdapEntryManagerFactory) + persistenceType = ldapEntryManagerFactory.getPersistenceType() + + ldapExtendedEntryManagers = [] + for ldapExtendedConfiguration in ldapExtendedConfigurations: + connectionConfiguration = ldapExtendedConfiguration["connectionConfiguration"] + ldapConfiguration = ldapExtendedConfiguration["ldapConfiguration"] + + ldapProperties = Properties() + for key, value in connectionConfiguration.items(): + value_string = value + if isinstance(value_string, list): + value_string = ", ".join(value) + else: + value_string = str(value) + + ldapProperties.setProperty(persistenceType + "#" + key, value_string) + + if StringHelper.isNotEmptyString(ldapConfiguration.getBindPassword()): + ldapProperties.setProperty(persistenceType + "#bindPassword", ldapConfiguration.getBindPassword()) + + ldapEntryManager = ldapEntryManagerFactory.createEntryManager(ldapProperties) + + ldapExtendedEntryManagers.append({ "ldapConfiguration" : ldapConfiguration, "ldapProperties" : ldapProperties, "loginAttributes" : ldapExtendedConfiguration["loginAttributes"], "localLoginAttributes" : ldapExtendedConfiguration["localLoginAttributes"], "ldapEntryManager" : ldapEntryManager }) + + return ldapExtendedEntryManagers + + def createLdapExtendedConfigurations(self, authConfiguration): + ldapExtendedConfigurations = [] + + for connectionConfiguration in authConfiguration["ldap_configuration"]: + configId = connectionConfiguration["configId"] + + servers = connectionConfiguration["servers"] + + bindDN = None + bindPassword = None + useAnonymousBind = True + if (self.containsAttributeString(connectionConfiguration, "bindDN")): + useAnonymousBind = False + bindDN = connectionConfiguration["bindDN"] + bindPassword = CdiUtil.bean(EncryptionService).decrypt(connectionConfiguration["bindPassword"]) + + useSSL = connectionConfiguration["useSSL"] + maxConnections = connectionConfiguration["maxConnections"] + baseDNs = connectionConfiguration["baseDNs"] + loginAttributes = connectionConfiguration["loginAttributes"] + localLoginAttributes = connectionConfiguration["localLoginAttributes"] + + ldapConfiguration = GluuLdapConfiguration() + ldapConfiguration.setConfigId(configId) + ldapConfiguration.setBindDN(bindDN) + ldapConfiguration.setBindPassword(bindPassword) + ldapConfiguration.setServers(Arrays.asList(servers)) + ldapConfiguration.setMaxConnections(maxConnections) + ldapConfiguration.setUseSSL(useSSL) + ldapConfiguration.setBaseDNs(Arrays.asList(baseDNs)) + ldapConfiguration.setPrimaryKey(loginAttributes[0]) + ldapConfiguration.setLocalPrimaryKey(localLoginAttributes[0]) + ldapConfiguration.setUseAnonymousBind(useAnonymousBind) + + ldapExtendedConfigurations.append({ "ldapConfiguration" : ldapConfiguration, "connectionConfiguration" : connectionConfiguration, "loginAttributes" : loginAttributes, "localLoginAttributes" : localLoginAttributes }) + + return ldapExtendedConfigurations + + def containsAttributeString(self, dictionary, attribute): + return ((attribute in dictionary) and StringHelper.isNotEmptyString(dictionary[attribute])) + + def containsAttributeArray(self, dictionary, attribute): + return ((attribute in dictionary) and (len(dictionary[attribute]) > 0)) diff --git a/oxAuth/Server/integrations/basic.multi_auth_conf/INSTALLATION.txt b/oxAuth/Server/integrations/basic.multi_auth_conf/INSTALLATION.txt new file mode 100644 index 00000000..a390abdf --- /dev/null +++ b/oxAuth/Server/integrations/basic.multi_auth_conf/INSTALLATION.txt @@ -0,0 +1,35 @@ +This list of steps needed to do to enable Basic Multi person authentication module. + +1. This module depends on python libraries. In order to use it we need to install Jython. Please use next articles to proper Jython installation: + - Installation notest: http://ox.gluu.org/doku.php?id=oxtauth:customauthscript#jython_installation_optional + - Jython integration: http://ox.gluu.org/doku.php?id=oxtauth:customauthscript#jython_python_integration + +2. Copy shared required python libraries from ../shared_libs folder to $CATALINA_HOME/conf/python folder. + +3. Prepare authentication configuration file /etc/certs/multi_auth_conf.json. There is format description and sample configuration in README.txt. + +4. Confire new custom module in oxTrust: + - Log into oxTrust with administrative permissions. + - Open "Configuration→Manage Custom Scripts" page. + - Select "Person Authentication" tab. + - Click on "Add custom script configuration" link. + - Enter name = basic_multi_auth_conf + - Enter level = 0-100 (priority of this method). + - Select usage type "Interactive". + - Add custom required and optional properties which specified in README.txt + - Copy/paste script from BasicMultiAuthConfExternalAuthenticator.py. + - Activate it via "Enabled" checkbox. + - Click "Update" button at the bottom of this page. + +5. Configure oxAuth to use Basic Multi authentication by default: + - Log into oxTrust with administrative permissions. + - Open "Configuration→Manage Authentication" page. + - Scroll to "Default Authentication Method" panel. Select "basic_multi_auth_conf" authentication mode. + - Click "Update" button at the bottom of this page. + +6. Try to log in using Basic Multi authentication method: + - Wait 30 seconds and try to log in again. During this time oxAuth reload list of available person authentication modules. + - Open second browser or second browsing session and try to log in again. It's better to try to do that from another browser session because we can return back to previous authentication method if something will go wrong. + +There are log messages in this custom authentication script. In order to debug this module we can use command like this: +tail -f /opt/tomcat/logs/wrapper.log | grep "Basic (multi auth conf)" diff --git a/oxAuth/Server/integrations/basic.multi_auth_conf/README.txt b/oxAuth/Server/integrations/basic.multi_auth_conf/README.txt new file mode 100644 index 00000000..f13a5219 --- /dev/null +++ b/oxAuth/Server/integrations/basic.multi_auth_conf/README.txt @@ -0,0 +1,38 @@ +This is person authentication module for oxAuth which allows to specify multiple authentication configurations. + +This module has only one property: +1) auth_configuration_file - It's path to file which contains AD LDAP authentication connection details and list of attributes which user can use in order to log in. + Example: /etc/certs/multi_auth_conf.json + Example content of this file [ 'bindPassword' should be the base64 encoded of password text. You can take the advantage of 'encode.py' script to encode/decode your password. 'encode.py' is available inside Gluu server container ( location: /opt/gluu/bin/ ) ]: + + +{ + "ldap_configuration": + [ + { + "configId":"ad_1", + "servers":["localhost:1389"], + "bindDN":"cn=directory manager", + "bindPassword":"encoded_pass", + "useSSL":false, + "maxConnections":3, + "baseDNs":["ou=people,o=gluu"], + "loginAttributes":["uid"], + "localLoginAttributes":["uid"] + }, + { + "configId":"ad_2", + "servers":["localhost:2389"], + "bindDN":"cn=directory manager", + "bindPassword":"encoded_pass", + "useSSL":false, + "maxConnections":3, + "baseDNs":["ou=people,o=gluu"], + "loginAttributes":["mail"], + "localLoginAttributes":["mail"] + } + ] +} + + + The names/values of properties are similar to oxAuth/oxTrust ldap configuration files. diff --git a/oxAuth/Server/integrations/basic.multi_login/BasicMultiLoginExternalAuthenticator.py b/oxAuth/Server/integrations/basic.multi_login/BasicMultiLoginExternalAuthenticator.py new file mode 100644 index 00000000..0a35f991 --- /dev/null +++ b/oxAuth/Server/integrations/basic.multi_login/BasicMultiLoginExternalAuthenticator.py @@ -0,0 +1,122 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2016, Gluu +# +# Author: Yuriy Movchan +# + +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.service import UserService, AuthenticationService +from org.gluu.util import StringHelper, ArrayHelper + +import java + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Basic (multi login). Initialization" + + login_attributes_list_object = configurationAttributes.get("login_attributes_list") + if (login_attributes_list_object == None): + print "Basic (multi login). Initialization. There is no property login_attributes_list" + return False + + login_attributes_list = login_attributes_list_object.getValue2() + if (StringHelper.isEmpty(login_attributes_list)): + print "Basic (multi login). Initialization. There is no attributes specified in login_attributes property" + return False + + login_attributes_list_array = StringHelper.split(login_attributes_list, ",") + if (ArrayHelper.isEmpty(login_attributes_list_array)): + print "Basic (multi login). Initialization. There is no attributes specified in login_attributes property" + return False + + if (configurationAttributes.containsKey("local_login_attributes_list")): + local_login_attributes_list = configurationAttributes.get("local_login_attributes_list").getValue2() + local_login_attributes_list_array = StringHelper.split(local_login_attributes_list, ",") + else: + print "Basic (multi login). Initialization. There is no property local_login_attributes_list. Assuming that login attributes are equal to local login attributes." + local_login_attributes_list_array = login_attributes_list_array + + if (len(login_attributes_list_array) != len(local_login_attributes_list_array)): + print "Basic (multi login). Initialization. The number of attributes in login_attributes_list and local_login_attributes_list isn't equal" + return False + + self.login_attributes_list_array = login_attributes_list_array + self.local_login_attributes_list_array = local_login_attributes_list_array + + print "Basic (multi login). Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "Basic (multi login). Destroy" + print "Basic (multi login). Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + authenticationService = CdiUtil.bean(AuthenticationService) + + if (step == 1): + print "Basic (multi login). Authenticate for step 1" + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + key_value = credentials.getUsername() + user_password = credentials.getPassword() + + logged_in = False + if (StringHelper.isNotEmptyString(key_value) and StringHelper.isNotEmptyString(user_password)): + i = 0 + count = len(self.login_attributes_list_array) + while (i < count): + primary_key = self.login_attributes_list_array[i] + local_primary_key = self.local_login_attributes_list_array[i] + logged_in = authenticationService.authenticate(key_value, user_password, primary_key, local_primary_key) + if (logged_in): + return True + i += 1 + + return False + else: + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + if (step == 1): + print "Basic (multi login). Prepare for Step 1" + return True + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + return 1 + + def getPageForStep(self, configurationAttributes, step): + return "" + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True diff --git a/oxAuth/Server/integrations/basic.multi_login/INSTALLATION.txt b/oxAuth/Server/integrations/basic.multi_login/INSTALLATION.txt new file mode 100644 index 00000000..13ea87d7 --- /dev/null +++ b/oxAuth/Server/integrations/basic.multi_login/INSTALLATION.txt @@ -0,0 +1,33 @@ +This list of steps needed to do to enable Basic Multi Login person authentication module. + +1. This module depends on python libraries. In order to use it we need to install Jython. Please use next articles to proper Jython installation: + - Installation notest: http://ox.gluu.org/doku.php?id=oxtauth:customauthscript#jython_installation_optional + - Jython integration: http://ox.gluu.org/doku.php?id=oxtauth:customauthscript#jython_python_integration + +2. Copy shared required python libraries from ../shared_libs folder to $CATALINA_HOME/conf/python folder. + +3. Confire new custom module in oxTrust: + - Log into oxTrust with administrative permissions. + - Open "Configuration→Manage Custom Scripts" page. + - Select "Person Authentication" tab. + - Click on "Add custom script configuration" link. + - Enter name = basic_multi_login + - Enter level = 0-100 (priority of this method). + - Select usage type "Interactive". + - Add custom required and optional properties which specified in README.txt. + - Copy/paste script from BasicMultiLoginExternalAuthenticator.py. + - Activate it via "Enabled" checkbox. + - Click "Update" button at the bottom of this page. + +4. Configure oxAuth to use Basic Multi Login authentication by default: + - Log into oxTrust with administrative permissions. + - Open "Configuration→Manage Authentication" page. + - Scroll to "Default Authentication Method" panel. Select "basic_multi_login" authentication mode. + - Click "Update" button at the bottom of this page. + +5. Try to log in using Basic Multi Login authentication method: + - Wait 30 seconds and try to log in again. During this time oxAuth reload list of available person authentication modules. + - Open second browser or second browsing session and try to log in again. It's better to try to do that from another browser session because we can return back to previous authentication method if something will go wrong. + +There are log messages in this custom authentication script. In order to debug this module we can use command like this: +tail -f /opt/tomcat/logs/wrapper.log | grep "Basic (multi login)" diff --git a/oxAuth/Server/integrations/basic.multi_login/README.txt b/oxAuth/Server/integrations/basic.multi_login/README.txt new file mode 100644 index 00000000..4b1567ff --- /dev/null +++ b/oxAuth/Server/integrations/basic.multi_login/README.txt @@ -0,0 +1,13 @@ +This is person authentication modules for oxAuth which allows to use several attributes as user name. + +This module has next properties: + +1) login_attributes_list - Comma separated list of attribute names. Specify list of IdP attributes which this module should use to map to local attributes. + It's optional property. + The count of attributes in this property should be equal to count attributes in local_login_attributes_list property. + Example: uid, mail + +2) local_login_attributes_list - Comma separated list of attribute names. Specify list of local attributes mapped from IdP attributes. + It's optional property. + The count of attributes in this property should be equal to count attributes in login_attributes_list property. + Example: uid, mail diff --git a/oxAuth/Server/integrations/basic.multiple_test_email_addresses/BasicMultipleTestEmailAddressesExternalAuthenticator.py b/oxAuth/Server/integrations/basic.multiple_test_email_addresses/BasicMultipleTestEmailAddressesExternalAuthenticator.py new file mode 100644 index 00000000..8974ef75 --- /dev/null +++ b/oxAuth/Server/integrations/basic.multiple_test_email_addresses/BasicMultipleTestEmailAddressesExternalAuthenticator.py @@ -0,0 +1,97 @@ + + +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.service import AuthenticationService +from org.gluu.util import StringHelper + +import java + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Basic. Initialization" + print "Basic. Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "Basic. Destroy" + print "Basic. Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + authenticationService = CdiUtil.bean(AuthenticationService) + + if (step == 1): + print "Basic. Authenticate for step 1" + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + user_name_array = StringHelper.split(credentials.getUsername(),"+") + + user_name = None + + if len(user_name_array) == 2: + + email_id_array = StringHelper.split(user_name_array[1],"@") + user_name = user_name_array[0] + "@"+ email_id_array[1] + else: + + user_name = user_name_array[0] + + print "Username for authentication is: %s " % user_name + user_password = credentials.getPassword() + + logged_in = False + if (StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password)): + + logged_in = authenticationService.authenticate(user_name, user_password,"mail","mail") + + if (not logged_in): + return False + + return True + else: + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + if (step == 1): + print "Basic. Prepare for Step 1" + return True + else: + return False + + def getNextStep(self, step, context): + return -1 + + def getExtraParametersForStep(self, configurationAttributes, step): + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + return 1 + + def getPageForStep(self, configurationAttributes, step): + return "" + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True diff --git a/oxAuth/Server/integrations/basic.multiple_test_email_addresses/README.txt b/oxAuth/Server/integrations/basic.multiple_test_email_addresses/README.txt new file mode 100644 index 00000000..4449f768 --- /dev/null +++ b/oxAuth/Server/integrations/basic.multiple_test_email_addresses/README.txt @@ -0,0 +1,5 @@ +This is person authentication modules for oxAuth which allows the use of multiple test addresses that lead to a single email account. +e.g. Everything addressed to myaccount+.*@gmail.com goes to myaccount@gmail.com. They are all the same account. +Read this blog for understanding the feature better - http://www.codestore.net/store.nsf/unid/BLOG-20111201-0411 + +oxAuth issue - https://github.com/GluuFederation/oxAuth/issues/1220 diff --git a/oxAuth/Server/integrations/basic.one_session/BasicOneSessionExternalAuthenticator.py b/oxAuth/Server/integrations/basic.one_session/BasicOneSessionExternalAuthenticator.py new file mode 100644 index 00000000..453a2085 --- /dev/null +++ b/oxAuth/Server/integrations/basic.one_session/BasicOneSessionExternalAuthenticator.py @@ -0,0 +1,118 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2016, Gluu +# +# Author: Yuriy Movchan +# + +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.service import AuthenticationService +from org.gluu.persist import PersistenceEntryManager +from org.gluu.oxauth.model.ldap import TokenLdap +from org.gluu.util import StringHelper +from javax.faces.application import FacesMessage +from org.gluu.jsf2.message import FacesMessages +from org.gluu.oxauth.model.config import StaticConfiguration + +import java + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Basic (one session). Initialization" + self.entryManager = CdiUtil.bean(PersistenceEntryManager) + self.staticConfiguration = CdiUtil.bean(StaticConfiguration) + + print "Basic (one session). Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "Basic (one session). Destroy" + print "Basic (one session). Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + authenticationService = CdiUtil.bean(AuthenticationService) + + if step == 1: + print "Basic (one session). Authenticate for step 1" + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + logged_in = False + if StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password): + logged_in = authenticationService.authenticate(user_name, user_password) + + if not logged_in: + return False + + logged_in = self.isFirstSession(user_name) + if not logged_in: + facesMessages = CdiUtil.bean(FacesMessages) + facesMessages.add(FacesMessage.SEVERITY_ERROR, "Please, end active session first!") + return False + + + return True + else: + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + if step == 1: + print "Basic (one session). Prepare for Step 1" + return True + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + return 1 + + def getPageForStep(self, configurationAttributes, step): + return "" + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True + + def isFirstSession(self, user_name): + tokenLdap = TokenLdap() + tokenLdap.setDn(self.staticConfiguration.getBaseDn().getClients()) + tokenLdap.setUserId(user_name) + + tokenLdapList = self.entryManager.findEntries(tokenLdap, 1) + print "Basic (one session). isFirstSession. Get result: '%s'" % tokenLdapList + + if (tokenLdapList != None) and (tokenLdapList.size() > 0): + print "Basic (one session). isFirstSession: False" + return False + + print "Basic (one session). isFirstSession: True" + return True diff --git a/oxAuth/Server/integrations/basic.one_session/README.txt b/oxAuth/Server/integrations/basic.one_session/README.txt new file mode 100644 index 00000000..c61795de --- /dev/null +++ b/oxAuth/Server/integrations/basic.one_session/README.txt @@ -0,0 +1 @@ +This person authentication module for oxAuth which allows to restrict access to RP for one user session. diff --git a/oxAuth/Server/integrations/basic.password_expiration/PasswordExpiration.py b/oxAuth/Server/integrations/basic.password_expiration/PasswordExpiration.py new file mode 100644 index 00000000..1b181157 --- /dev/null +++ b/oxAuth/Server/integrations/basic.password_expiration/PasswordExpiration.py @@ -0,0 +1,149 @@ +import datetime + +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.service import UserService, AuthenticationService +from org.gluu.util import StringHelper, ArrayHelper +from com.unboundid.util import StaticUtils +from java.util import GregorianCalendar, TimeZone +from java.util import Arrays + + +# This script expect that user has attribute oxPasswordExpirationDate with valid expiration date +class PersonAuthentication(PersonAuthenticationType): + + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Basic (with password update). Initialization" + print "Basic (with password update). Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "Basic (with password update). Destroy" + print "Basic (with password update). Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + authenticationService = CdiUtil.bean(AuthenticationService) + userService = CdiUtil.bean(UserService) + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + if step == 1: + print "Basic (with password update). Authenticate for step 1" + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + logged_in = False + if StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password): + logged_in = authenticationService.authenticate(user_name, user_password) + + if not logged_in: + return False + + find_user_by_uid = authenticationService.getAuthenticatedUser() + user_expDate = find_user_by_uid.getAttribute("oxPasswordExpirationDate", False) + + if user_expDate == None: + print "Basic (with password update). Authenticate for step 1. User has no oxPasswordExpirationDate date" + return False + + dt = StaticUtils.decodeGeneralizedTime(user_expDate) + + # Get Current Date + calendar = GregorianCalendar(TimeZone.getTimeZone("UTC")); + now = calendar.getTime() + if now.compareTo(dt) > 0: + # Add 90 Days to current date + calendar.setTime(now) + calendar.add(calendar.DATE, 1) + dt_plus_90 = calendar.getTime() + expDate = StaticUtils.encodeGeneralizedTime(dt_plus_90) + identity.setWorkingParameter("expDate", expDate) + + return True + elif step == 2: + print "Basic (with password update). Authenticate for step 2" + user = authenticationService.getAuthenticatedUser() + if user == None: + print "Basic (with password update). Authenticate for step 2. Failed to determine user name" + return False + + user_name = user.getUserId() + find_user_by_uid = userService.getUser(user_name) + newExpDate = identity.getWorkingParameter("expDate") + if find_user_by_uid == None: + print "Basic (with password update). Authenticate for step 2. Failed to find user" + return False + print "Basic (with password update). Authenticate for step 2" + update_button = requestParameters.get("loginForm:updateButton") + + if ArrayHelper.isEmpty(update_button): + return True + + find_user_by_uid.setAttribute("oxPasswordExpirationDate", newExpDate) + new_password_array = requestParameters.get("loginForm:password") + if ArrayHelper.isEmpty(new_password_array) or StringHelper.isEmpty(new_password_array[0]): + print "Basic (with password update). Authenticate for step 2. New password is empty" + return False + + new_password = new_password_array[0] + find_user_by_uid.setAttribute("userPassword", new_password) + print "Basic (with password update). Authenticate for step 2. Attempting to set new user '%s' password" % user_name + + userService.updateUser(find_user_by_uid) + print "Basic (with password update). Authenticate for step 2. Password updated successfully" + + return True + else: + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + if step == 1: + print "Basic (with password update). Prepare for Step 1" + return True + elif step == 2: + print "Basic (with password update). Prepare for Step 2" + return True + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + return Arrays.asList("expDate") + + def getCountAuthenticationSteps(self, configurationAttributes): + identity = CdiUtil.bean(Identity) + if identity.isSetWorkingParameter("expDate"): + return 2 + else: + return 1 + + def getPageForStep(self, configurationAttributes, step): + if step == 2: + return "/auth/pwd/newpassword.xhtml" + return "" + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True diff --git a/oxAuth/Server/integrations/basic.password_expiration/README.md b/oxAuth/Server/integrations/basic.password_expiration/README.md new file mode 100644 index 00000000..bf878afb --- /dev/null +++ b/oxAuth/Server/integrations/basic.password_expiration/README.md @@ -0,0 +1,2 @@ +# Password Expiration +The script would ask the user to update password after 90 days. diff --git a/oxAuth/Server/integrations/basic.passwordchangewithvalidations/INSTALLATION.txt b/oxAuth/Server/integrations/basic.passwordchangewithvalidations/INSTALLATION.txt new file mode 100644 index 00000000..0bec2cd1 --- /dev/null +++ b/oxAuth/Server/integrations/basic.passwordchangewithvalidations/INSTALLATION.txt @@ -0,0 +1,53 @@ +This is a person authentication script with validations for the following: + 1. It checks for the empty password and gives a message to the user for the empty password. + 2. It detects if the user is disabled/ inactive in the LDAP/ AD and gives a message that your account is disabled. + 3. It also has an option to change password. During password update it checks for the password complexity and gives a message to the user if the password is not upto the expected complexity + level. If a user input a complex password then it updates the password. +Following are the steps to install this script and associated library. + +1. Install zxcvbn python library for complexity checking + a. Notedown the Jython version. To find Jythonversion, inside the chroot change the directory to /opt directory and run ls command. There should be a directory named jython-, it should be + similar to jython-2.7.2 and here 2.7.2 is the Jython version. + b. Now, open the file /etc/gluu/conf/gluu.properties and look for the line starting with pythonModulesDir=. Append the value /opt/jython-/Lib/site-packages to any existing value. Each + value is separated by a colon (:). It should be similar to pythonModulesDir=/opt/gluu/python/libs:/opt/jython-2.7.2/Lib/site-packages + c. If this is the first additional library then run the following command /opt/jython-/bin/jython -m ensurepip. This will create pip executables like pip, pip2, pip2.7. + d. Now, install zxcvbn library with + /opt/jython-/bin/pip install zxcvbn. + e. After successful installation, make sure that there should be read permission to group and others (rw-r--r--) for all the *.class files, as shown below + root@test1:~# cd /opt/jython-2.7.2/Lib/site-packages/zxcvbn + root@test1:/opt/jython-2.7.2/Lib/site-packages/zxcvbn# ls -lrt + -rw-r--r-- 1 root root 3085 Mar 28 2019 time_estimates.py + -rw-r--r-- 1 root root 14200 Mar 28 2019 scoring.py + -rw-r--r-- 1 root root 21797 Mar 28 2019 matching.py + -rw-r--r-- 1 root root 792962 Mar 28 2019 frequency_lists.py + -rw-r--r-- 1 root root 4719 Mar 28 2019 feedback.py + -rw-r--r-- 1 root root 9800 Mar 28 2019 adjacency_graphs.py + -rw-r--r-- 1 root root 1132 Mar 28 2019 __main__.py + -rw-r--r-- 1 root root 1171 Mar 28 2019 __init__.py + -rw-r--r-- 1 root root 796337 Dec 18 09:19 'frequency_lists$py.class' + -rw-r--r-- 1 root root 9764 Dec 18 09:19 'feedback$py.class' + -rw-r--r-- 1 root root 24510 Dec 18 09:19 'scoring$py.class' + -rw-r--r-- 1 root root 5775 Dec 18 09:19 '__init__$py.class' + -rw-r--r-- 1 root root 35478 Dec 18 09:19 'matching$py.class' + -rw-r--r-- 1 root root 20699 Dec 18 09:19 'adjacency_graphs$py.class' + -rw-r--r-- 1 root root 6962 Dec 18 09:19 '__main__$py.class' + -rw-r--r-- 1 root root 8813 Dec 18 09:19 'time_estimates$py.class' + f. If not then run “chmod 644 *.class†command in the /opt/jython-/Lib/site-packages/zxcvbn/ directory + g. Restart the oxauth service. +2. Configure this new script in oxTrust: + - Log into oxTrust with administrative permissions. + - Open "Configuration→Manage Custom Scripts" page. + - Select "Person Authentication" tab. + - Click on "Add custom script configuration" link. + - Enter name = basic_password_change_validations (or any suitable name) + - Enter level = 0-100 (priority of this method). + - Select usage type "Interactive". + - Copy/paste script from PasswordChangeWithValidations.py. + - Activate it via "Enabled" checkbox. + - Click "Update" button at the bottom of this page. + +3. Configure oxAuth to use Basic Multi authentication by default: + - Log into oxTrust with administrative permissions. + - Open "Configuration→Manage Authentication" page. + - Navigate to "Default Authentication Method" panel. Select "basic_password_change_validations" authentication mode. + - Click "Update" button at the bottom of this page. diff --git a/oxAuth/Server/integrations/basic.passwordchangewithvalidations/PasswordChangeWithValidations.py b/oxAuth/Server/integrations/basic.passwordchangewithvalidations/PasswordChangeWithValidations.py new file mode 100644 index 00000000..771ec253 --- /dev/null +++ b/oxAuth/Server/integrations/basic.passwordchangewithvalidations/PasswordChangeWithValidations.py @@ -0,0 +1,166 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2016, Gluu +# +# Author: Yuriy Movchan +# Author: Hemant Mehta +# + +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.service import UserService, AuthenticationService +from org.gluu.service import CacheService +from org.gluu.util import StringHelper, ArrayHelper +from org.gluu.oxauth.util import ServerUtil + +from javax.faces.application import FacesMessage +from org.gluu.jsf2.message import FacesMessages + +from zxcvbn import zxcvbn + +import java + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Basic (with password update). Initialization" + print "Basic (with password update). Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "Basic (with password update). Destroy" + print "Basic (with password update). Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + userService = CdiUtil.bean(UserService) + authenticationService = CdiUtil.bean(AuthenticationService) + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + user_name = credentials.getUsername() + + if (step == 1): + print "Basic (with password update). Authenticate for step 1" + facesMessages = CdiUtil.bean(FacesMessages) + facesMessages.setKeepMessages() + user_password = credentials.getPassword() + + logged_in = False + if (StringHelper.isNotEmptyString(user_name) and StringHelper.isEmptyString(user_password)): + facesMessages.add(FacesMessage.SEVERITY_INFO, "Password is empty! Enter your password") + print "Basic. Authenticate: Password is empty! Enter your password" + return False + + if (StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password)): + find_user = userService.getUser(user_name) + if (find_user == None): + facesMessages.add(FacesMessage.SEVERITY_INFO, "User doesn't Exist") + return False + else: + print "Basic . Authenticate for step 1-4-disablechecking" + user_status = userService.getCustomAttribute(find_user, "gluuStatus") + print "Basic . Authenticate for step 1-5-disablechecking" + if (user_status != None): + user_status_value = user_status.getValue() + if (StringHelper.equals(user_status_value, "inactive")): + facesMessages.add(FacesMessage.SEVERITY_INFO, "User is Disabled") + return False + if (StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password)): + logged_in = authenticationService.authenticate(user_name, user_password) + + if (not logged_in): + return False + + return True + elif (step == 2): + print "Basic (with password update). Authenticate for step 2-see" + user = authenticationService.getAuthenticatedUser() + facesMessages = CdiUtil.bean(FacesMessages) + facesMessages.setKeepMessages() + if user == None: + print "Basic (with password update). Authenticate for step 2. Failed to determine user name" + return False + + user_name = user.getUserId() + find_user_by_uid = userService.getUser(user_name) + + update_button = requestParameters.get("loginForm:updateButton") + + if ArrayHelper.isEmpty(update_button): + return True + + new_password_array = requestParameters.get("loginForm:password") + new_password = new_password_array[0] + if ArrayHelper.isEmpty(new_password_array) or StringHelper.isEmpty(new_password): + print "Basic (with password update). Authenticate for step 2. New password is empty" + return False + + + print "Basic (with password update). Authenticate for step 2. see the new password ================'%s'" % new_password + + results = zxcvbn(new_password) + if results['score'] <2: + print 'Its a weak Password, please increase the complexity.' + facesMessages.add(FacesMessage.SEVERITY_INFO, "Its weak password, please increase the complexity.") + return False + + find_user_by_uid.setAttribute("userPassword", new_password) + + + print "Basic (with password update). Authenticate for step 2. Attempting to set new user '%s' password " % user_name + + + userService.updateUser(find_user_by_uid) + print "Basic (with password update). Authenticate for step 2. Password updated successfully" + + return True + else: + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + if (step == 1): + print "Basic (with password update). Prepare for Step 1" + return True + elif (step == 2): + print "Basic (with password update). Prepare for Step 2-confirm" + return True + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + return 2 + + def getPageForStep(self, configurationAttributes, step): + if (step == 2): + print "Basic (with password update). Redirecting to password change page" + return "/auth/pwd/newpassword.xhtml" + + return "" + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True diff --git a/oxAuth/Server/integrations/basic.passwordchangewithvalidations/README.md b/oxAuth/Server/integrations/basic.passwordchangewithvalidations/README.md new file mode 100644 index 00000000..dc16d631 --- /dev/null +++ b/oxAuth/Server/integrations/basic.passwordchangewithvalidations/README.md @@ -0,0 +1,4 @@ +This is a person authentication script with validations for the following: +- It checks for the empty password and gives a message to the user for the empty password. +- It detects if the user is disabled/ inactive in the LDAP/ AD and gives a message that your account is disabled. +- It also has an option to change password. During password update it checks for the password complexity and gives a message to the user if the password is not upto the expected complexity level. If a user input a complex password then it updates the password. diff --git a/oxAuth/Server/integrations/basic.passwordchangewithvalidations/pwd/newpassword.xhtml b/oxAuth/Server/integrations/basic.passwordchangewithvalidations/pwd/newpassword.xhtml new file mode 100644 index 00000000..4e962828 --- /dev/null +++ b/oxAuth/Server/integrations/basic.passwordchangewithvalidations/pwd/newpassword.xhtml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + +

+
+
+ +
+ + + + +
+ +
+

+
+ +
+
+ + +
+ +
+
+ + + + diff --git a/oxAuth/Server/integrations/basic.recaptcha/BasicRecaptchaExternalAuthenticator.py b/oxAuth/Server/integrations/basic.recaptcha/BasicRecaptchaExternalAuthenticator.py new file mode 100644 index 00000000..3f52df46 --- /dev/null +++ b/oxAuth/Server/integrations/basic.recaptcha/BasicRecaptchaExternalAuthenticator.py @@ -0,0 +1,219 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2016, Gluu +# +# Author: Yuriy Movchan +# Modified: Mobarak Hosen Shakil +# + +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.service import AuthenticationService +from org.gluu.util import StringHelper +from javax.faces.context import FacesContext +from org.gluu.oxauth.security import Identity +from org.gluu.oxauth.service.common import UserService +from org.gluu.oxauth.util import ServerUtil +from org.gluu.oxauth.service.common import EncryptionService +from java.util import Arrays +from org.gluu.oxauth.util import CertUtil +from org.gluu.oxauth.model.util import CertUtils +from org.gluu.oxauth.service.net import HttpService +from org.apache.http.params import CoreConnectionPNames + +import sys +import base64 +import urllib +import json +import java + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Basic. Initialized successfully" + + self.enabled_recaptcha = self.initRecaptcha(configurationAttributes) + print "Basic. Initialization. enabled_recaptcha: '%s'" % self.enabled_recaptcha + + print "Basic. Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "Basic. Destroy" + print "Basic. Destroyed successfully" + return True + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def getApiVersion(self): + return 11 + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + print "Basic. Authenticate for step 1" + + authenticationService = CdiUtil.bean(AuthenticationService) + + if step == 1: + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + logged_in = False + #self.enabled_recaptcha = self.initRecaptcha(configurationAttributes) + #self.prepareForStep(configurationAttributes, requestParameters, step) + if (StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password)): + if self.enabled_recaptcha: + print "Authentication for step 1. Validating recaptcha response." + recaptcha_response = requestParameters.get("g-recaptcha-response") + print "Printed recaptcha response: %s" % (recaptcha_response[0]) + recaptcha_result = self.validateRecaptcha(recaptcha_response[0]) + if recaptcha_result: + logged_in = authenticationService.authenticate(user_name, user_password) + else: + self.enabled_recaptcha = self.initRecaptcha(configurationAttributes) + print "Basic Recaptcha. Authentication for step 1. recaptcha_result: '%s'" % recaptcha_result + print "login failed..." + print "captcha option: %s" % self.enabled_recaptcha + self.prepareForStep(configurationAttributes, requestParameters, step) + return False + + else: + logged_in = authenticationService.authenticate(user_name, user_password) + + if (not logged_in): + print "login failed for all step" + self.enabled_recaptcha = self.initRecaptcha(configurationAttributes) + self.prepareForStep(configurationAttributes, requestParameters, step) + return False + + return True + else: + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + identity = CdiUtil.bean(Identity) + if step == 1: + print "Basic. Prepare for Step 1" + if self.enabled_recaptcha: + print "Identity parameter has been set..." + identity.setWorkingParameter("recaptcha_site_key", self.recaptcha_creds['site_key']) + print "Identity parameter has been set...%s"%self.recaptcha_creds['site_key'] + return True + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + return Arrays.asList(self.recaptcha_creds['site_key']) + + def getCountAuthenticationSteps(self, configurationAttributes): + return 1 + + def getPageForStep(self, configurationAttributes, step): + if step == 1: + return "/auth/recaptcha/login.xhtml" + return "" + + def initRecaptcha(self, configurationAttributes): + print "Basic. Initialize recaptcha" + if not configurationAttributes.containsKey("credentials_file"): + return False + + cert_creds_file = configurationAttributes.get("credentials_file").getValue2() + + print "Load credentials from file" + f = open(cert_creds_file, 'r') + try: + creds = json.loads(f.read()) + except: + print "Basic. Initialize recaptcha. Failed to load credentials from file: %s" % cert_creds_file + return False + finally: + f.close() + + try: + recaptcha_creds = creds["recaptcha"] + except: + print "Basic. Initialize recaptcha. Invalid credentials file '%s' format:" % cert_creds_file + return False + + self.recaptcha_creds = None + if recaptcha_creds["enabled"]: + print "Basic. Initialize recaptcha. Recaptcha is enabled" + site_key = recaptcha_creds["site_key"] + secret_key = recaptcha_creds["secret_key"] + self.recaptcha_creds = { 'site_key' : site_key, "secret_key" : secret_key } + print "Basic. Initialize recaptcha. Recaptcha is configured correctly" + + return True + else: + print "Basic. Initialize recaptcha. Recaptcha is disabled" + + return False + + def validateRecaptcha(self, recaptcha_response): + print "Basic. Validate recaptcha response" + + facesContext = CdiUtil.bean(FacesContext) + request = facesContext.getExternalContext().getRequest() + + remoteip = ServerUtil.getIpAddress(request) + print "Basic. Validate recaptcha response. remoteip: '%s'" % remoteip + + httpService = CdiUtil.bean(HttpService) + + http_client = httpService.getHttpsClient() + http_client_params = http_client.getParams() + http_client_params.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 15 * 1000) + + recaptcha_validation_url = "https://www.google.com/recaptcha/api/siteverify" + recaptcha_validation_request = urllib.urlencode({ "secret" : self.recaptcha_creds['secret_key'], "response" : recaptcha_response, "remoteip" : remoteip }) + recaptcha_validation_headers = { "Content-type" : "application/x-www-form-urlencoded", "Accept" : "application/json" } + + try: + http_service_response = httpService.executePost(http_client, recaptcha_validation_url, None, recaptcha_validation_headers, recaptcha_validation_request) + http_response = http_service_response.getHttpResponse() + except: + print "Basic. Validate recaptcha response. Exception: ", sys.exc_info()[1] + return False + + try: + if not httpService.isResponseStastusCodeOk(http_response): + print "Basic. Validate recaptcha response. Get invalid response from validation server: ", str(http_response.getStatusLine().getStatusCode()) + httpService.consume(http_response) + return False + + response_bytes = httpService.getResponseContent(http_response) + response_string = httpService.convertEntityToString(response_bytes) + httpService.consume(http_response) + finally: + http_service_response.closeConnection() + + if response_string == None: + print "Basic. Validate recaptcha response. Get empty response from validation server" + return False + print "printed: %s" % (response_string) + response = json.loads(response_string) + print "printed: %s" % (response) + return response["success"] + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True diff --git a/oxAuth/Server/integrations/basic.recaptcha/README.md b/oxAuth/Server/integrations/basic.recaptcha/README.md new file mode 100644 index 00000000..ff7616f3 --- /dev/null +++ b/oxAuth/Server/integrations/basic.recaptcha/README.md @@ -0,0 +1,15 @@ +# Basic login with Google Recaptcha Validation + +1. Add [this](BasicRecaptchaExternalAuthenticator.py) custom script in `Person Authentication` section + +2. Add `credentials_file` key - Patch to file with reCAPTCHA credentials. + + Example: `/etc/certs/cert_creds.json` + + See demo [file](cert_creds.json) + +3. Add [this](../../src/main/webapp/auth/recaptcha/login.xhtml) html file inside the custom page directory. + + Example: `/opt/gluu/jetty/oxauth/custom/pages/auth/recaptcha/login.xhtml` + +Enable the script and check through login with this custom authentication method. diff --git a/oxAuth/Server/integrations/basic.recaptcha/cert_creds.json b/oxAuth/Server/integrations/basic.recaptcha/cert_creds.json new file mode 100644 index 00000000..a266a0a0 --- /dev/null +++ b/oxAuth/Server/integrations/basic.recaptcha/cert_creds.json @@ -0,0 +1,7 @@ +{ + "recaptcha":{ + "enabled":true, + "site_key":"", + "secret_key":"" + } +} diff --git a/oxAuth/Server/integrations/basic.recaptcha/login-page.png b/oxAuth/Server/integrations/basic.recaptcha/login-page.png new file mode 100644 index 0000000000000000000000000000000000000000..9bf89b0421b0e76afe26b442c736f5479b3c4ce5 GIT binary patch literal 227809 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
Ih$;HB?cTVG4k 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%0AYW@abN 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 1 : + return "/auth/bioid/bioid.xhtml" + else: + return "" + + def getNextStep(self, configurationAttributes, requestParameters, step): + print "BioID. getNextStep called. %s" % str(step) + + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True + + # Get a BWS token to be used for authorization. + # bcid - The Biometric Class ID (BCID) of the person + # forTask - The task for which the issued token shall be used. + # A string containing the issued BWS token. + def getAccessToken(self, bcid, forTask): + + httpService = CdiUtil.bean(HttpService) + + http_client = httpService.getHttpsClient() + http_client_params = http_client.getParams() + + bioID_service_url = self.ENDPOINT + "token?id="+self.APP_IDENTIFIER+"&bcid="+bcid+"&task="+forTask+"&livedetection=true" + encodedString = base64.b64encode((self.APP_IDENTIFIER+":"+self.APP_SECRET).encode('utf-8')) + bioID_service_headers = {"Authorization": "Basic "+encodedString} + + try: + http_service_response = httpService.executeGet(http_client, bioID_service_url, bioID_service_headers) + http_response = http_service_response.getHttpResponse() + except: + print "BioID. Unable to obtain access token. Exception: ", sys.exc_info()[1] + return None + + try: + if not httpService.isResponseStastusCodeOk(http_response): + print "BioID. Unable to obtain access token. Get non 200 OK response from server:", str(http_response.getStatusLine().getStatusCode()) + httpService.consume(http_response) + return None + + response_bytes = httpService.getResponseContent(http_response) + response_string = httpService.convertEntityToString(response_bytes, Charset.forName("UTF-8")) + httpService.consume(http_response) + return response_string + finally: + http_service_response.closeConnection() + + def isenrolled(self, bcid): + httpService = CdiUtil.bean(HttpService) + + http_client = httpService.getHttpsClient() + http_client_params = http_client.getParams() + + bioID_service_url = self.ENDPOINT + "isenrolled?bcid="+bcid+"&trait=Face" + print "BioID. isenrolled URL - %s" %bioID_service_url + encodedString = base64.b64encode((self.APP_IDENTIFIER+":"+self.APP_SECRET).encode('utf-8')) + bioID_service_headers = {"Authorization": "Basic "+encodedString} + + try: + http_service_response = httpService.executeGet(http_client, bioID_service_url, bioID_service_headers) + http_response = http_service_response.getHttpResponse() + except: + print "BioID. failed to invoke isenrolled API: ", sys.exc_info()[1] + return None + + try: + if not httpService.isResponseStastusCodeOk(http_response): + print "BioID. Face,Periocular not enrolled. Get non 200 OK response from server:", str(http_response.getStatusLine().getStatusCode()) + httpService.consume(http_response) + return False + + else: + return True + finally: + http_service_response.closeConnection() + + def processBasicAuthentication(self, credentials): + userService = CdiUtil.bean(UserService) + authenticationService = CdiUtil.bean(AuthenticationService) + + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + logged_in = False + if StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password): + logged_in = authenticationService.authenticate(user_name, user_password) + + if not logged_in: + print "OTP. Process basic authentication. Failed to find user '%s'" % user_name + return None + + find_user_by_uid = authenticationService.getAuthenticatedUser() + if find_user_by_uid == None: + print "OTP. Process basic authentication. Failed to find user '%s'" % user_name + return None + + return find_user_by_uid + + + def performBiometricOperation(self, token, task): + httpService = CdiUtil.bean(HttpService) + http_client = httpService.getHttpsClient() + http_client_params = http_client.getParams() + bioID_service_url = self.ENDPOINT + task+"?livedetection=true" + bioID_service_headers = {"Authorization": "Bearer "+token} + + try: + http_service_response = httpService.executeGet(http_client, bioID_service_url, bioID_service_headers) + http_response = http_service_response.getHttpResponse() + response_bytes = httpService.getResponseContent(http_response) + response_string = httpService.convertEntityToString(response_bytes, Charset.forName("UTF-8")) + json_response = JSONObject(response_string) + httpService.consume(http_response) + if json_response.get("Success") == True: + return True + else: + print "BioID. Reason for failure : %s " % json_response.get("Error") + return False + except: + print "BioID. failed to invoke %s API: %s" %(task,sys.exc_info()[1]) + return None + + finally: + http_service_response.closeConnection() + + + \ No newline at end of file diff --git a/oxAuth/Server/integrations/bioid/README.md b/oxAuth/Server/integrations/bioid/README.md new file mode 100644 index 00000000..6d988c5d --- /dev/null +++ b/oxAuth/Server/integrations/bioid/README.md @@ -0,0 +1,72 @@ +# BioID Web Service +## Overview +[BioID Web Service](https://www.bioid.com) is a "Biometrics as a service" provider. This document will explain how to use Gluu's [BioID interception script](https://github.com/GluuFederation/oxAuth/blob/master/Server/integrations/bioID/BioIDExternalAuthenticator.py) to configure the Gluu Server for a two-step authentication process with username and password as the first step, and BioID's biometric authentication as the second step. + +In order to use this authentication mechanism your organization will need to register for a BioID account. + +## Prerequisitesm +- A Gluu Server ([installation instructions](../installation-guide/index.md)); +- [BioID interception script](https://github.com/GluuFederation/oxAuth/blob/master/Server/integrations/bioID/BioIDExternalAuthenticator.py) (included in the default Gluu Server distribution); +- An account with [BioID](https://bwsportal.bioid.com/register). + +## Properties +The mandatory properties in the BioID authentication script are as follows +| Property | Description | Example | +|-----------------------|-------------------------------|---------------| +|ENDPOINT |URL of the BioID Web Service|`https://bws.bioid.com/extension/`| +|APP_IDENTIFIER |API key |`c20b04cc-776a-45ed-7a1f-06347f8edf6c`| +|APP_SECRET |API secret |`sTGB4n4HAkvc2BnJp6KeNUTk`| +|STORAGE |The storage name assigned by BioID depending on the type of contract you have. |`bws`| +|PARTITION |A number assigned to your company by BioID. |`12345`| + + + +## Configure BioID Account + +1. [Sign up](https://bwsportal.bioid.com/register) for a BioID account. + +2. Upon registration, you will recieve an email with the instance name (listed as STORAGE in Gluu's BioID authentication script), partition number(listed as PARTITION in Gluu's BioID authentication script). + +3. As the owner of this instance, you are entitled to access BWS Portal at https://bwsportal.bioid.com using the account associated with your email. +With the BWS Portal, you can do the following: +a. View your trial information such as your credentials (e.g. your client certificate), enrolled classes, BWS logs and more. +b. Create your App ID and App secret, under "Web API keys". + +## BioID Documentation + +You can find all API reference at https://developer.bioid.com/bwsreference. +Lots of useful information about BWS is available at https://developer.bioid.com/blog. +If you intend to use liveness detection, you will find information about motion trigger helpful: https://developer.bioid.com/app-developer-guide/bioid-motion-detection + +## Configure oxTrust + +Follow the steps below to configure the BioID module in the oxTrust Admin GUI. + +1. Navigate to `Configuration` > `Person Authentication Scripts`. +1. Scroll down to the BioID authentication script +![bioid-script](../img/admin-guide/multi-factor/bioid-script.png) + +1. Configure the properties, all of which are mandatory, according to your API + +1. Enable the script by ticking the check box +![enable](../img/admin-guide/enable.png) + +Now BioID's biometric authentication is available as an authentication mechanism for your Gluu Server. This means that, using OpenID Connect `acr_values`, applications can now request BioID biometric authentication for users. + +!!! Note + To make sure BioID has been enabled successfully, you can check your Gluu Server's OpenID Connect configuration by navigating to the following URL: `https:///.well-known/openid-configuration`. Find `"acr_values_supported":` and you should see `"bioid"`. + +## Make BioID the Default Authentication Mechanism + +Now applications can request BioID's biometric authentication. To make BioID biometic authentication your default authentication mechanism, follow these instructions: + +1. Navigate to `Configuration` > `Manage Authentication`. +2. Select the `Default Authentication Method` tab. +3. In the Default Authentication Method window you will see two options: `Default acr` and `oxTrust acr`. + + - The `oxTrust acr` field controls the authentication mechanism that is presented to access the oxTrust dashboard GUI (the application you are in). + - The `Default acr` field controls the default authentication mechanism that is presented to users from all applications that leverage your Gluu Server for authentication. + +You can change one or both fields to BioID authentication as you see fit. If you want BioID to be the default authentication mechanism for access to oxTrust and all other applications that leverage your Gluu Server, change both fields to bioid. + +![BioID](../img/admin-guide/multi-factor/bioID.png) diff --git a/oxAuth/Server/integrations/bioid/README.txt b/oxAuth/Server/integrations/bioid/README.txt new file mode 100644 index 00000000..6d988c5d --- /dev/null +++ b/oxAuth/Server/integrations/bioid/README.txt @@ -0,0 +1,72 @@ +# BioID Web Service +## Overview +[BioID Web Service](https://www.bioid.com) is a "Biometrics as a service" provider. This document will explain how to use Gluu's [BioID interception script](https://github.com/GluuFederation/oxAuth/blob/master/Server/integrations/bioID/BioIDExternalAuthenticator.py) to configure the Gluu Server for a two-step authentication process with username and password as the first step, and BioID's biometric authentication as the second step. + +In order to use this authentication mechanism your organization will need to register for a BioID account. + +## Prerequisitesm +- A Gluu Server ([installation instructions](../installation-guide/index.md)); +- [BioID interception script](https://github.com/GluuFederation/oxAuth/blob/master/Server/integrations/bioID/BioIDExternalAuthenticator.py) (included in the default Gluu Server distribution); +- An account with [BioID](https://bwsportal.bioid.com/register). + +## Properties +The mandatory properties in the BioID authentication script are as follows +| Property | Description | Example | +|-----------------------|-------------------------------|---------------| +|ENDPOINT |URL of the BioID Web Service|`https://bws.bioid.com/extension/`| +|APP_IDENTIFIER |API key |`c20b04cc-776a-45ed-7a1f-06347f8edf6c`| +|APP_SECRET |API secret |`sTGB4n4HAkvc2BnJp6KeNUTk`| +|STORAGE |The storage name assigned by BioID depending on the type of contract you have. |`bws`| +|PARTITION |A number assigned to your company by BioID. |`12345`| + + + +## Configure BioID Account + +1. [Sign up](https://bwsportal.bioid.com/register) for a BioID account. + +2. Upon registration, you will recieve an email with the instance name (listed as STORAGE in Gluu's BioID authentication script), partition number(listed as PARTITION in Gluu's BioID authentication script). + +3. As the owner of this instance, you are entitled to access BWS Portal at https://bwsportal.bioid.com using the account associated with your email. +With the BWS Portal, you can do the following: +a. View your trial information such as your credentials (e.g. your client certificate), enrolled classes, BWS logs and more. +b. Create your App ID and App secret, under "Web API keys". + +## BioID Documentation + +You can find all API reference at https://developer.bioid.com/bwsreference. +Lots of useful information about BWS is available at https://developer.bioid.com/blog. +If you intend to use liveness detection, you will find information about motion trigger helpful: https://developer.bioid.com/app-developer-guide/bioid-motion-detection + +## Configure oxTrust + +Follow the steps below to configure the BioID module in the oxTrust Admin GUI. + +1. Navigate to `Configuration` > `Person Authentication Scripts`. +1. Scroll down to the BioID authentication script +![bioid-script](../img/admin-guide/multi-factor/bioid-script.png) + +1. Configure the properties, all of which are mandatory, according to your API + +1. Enable the script by ticking the check box +![enable](../img/admin-guide/enable.png) + +Now BioID's biometric authentication is available as an authentication mechanism for your Gluu Server. This means that, using OpenID Connect `acr_values`, applications can now request BioID biometric authentication for users. + +!!! Note + To make sure BioID has been enabled successfully, you can check your Gluu Server's OpenID Connect configuration by navigating to the following URL: `https:///.well-known/openid-configuration`. Find `"acr_values_supported":` and you should see `"bioid"`. + +## Make BioID the Default Authentication Mechanism + +Now applications can request BioID's biometric authentication. To make BioID biometic authentication your default authentication mechanism, follow these instructions: + +1. Navigate to `Configuration` > `Manage Authentication`. +2. Select the `Default Authentication Method` tab. +3. In the Default Authentication Method window you will see two options: `Default acr` and `oxTrust acr`. + + - The `oxTrust acr` field controls the authentication mechanism that is presented to access the oxTrust dashboard GUI (the application you are in). + - The `Default acr` field controls the default authentication mechanism that is presented to users from all applications that leverage your Gluu Server for authentication. + +You can change one or both fields to BioID authentication as you see fit. If you want BioID to be the default authentication mechanism for access to oxTrust and all other applications that leverage your Gluu Server, change both fields to bioid. + +![BioID](../img/admin-guide/multi-factor/bioID.png) diff --git a/oxAuth/Server/integrations/cas2/Cas2ExternalAuthenticator.py b/oxAuth/Server/integrations/cas2/Cas2ExternalAuthenticator.py new file mode 100644 index 00000000..54cbcd4f --- /dev/null +++ b/oxAuth/Server/integrations/cas2/Cas2ExternalAuthenticator.py @@ -0,0 +1,338 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2016, Gluu +# +# Author: Yuriy Movchan +# + +import sys +from java.util import Arrays +from java.util import HashMap +from javax.faces.context import FacesContext +from org.apache.http.params import CoreConnectionPNames +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.security import Identity +from org.gluu.oxauth.service import UserService, AuthenticationService, RequestParameterService +from org.gluu.oxauth.service.net import HttpService +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.util import StringHelper, ArrayHelper +from org.gluu.jsf2.service import FacesService + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "CAS2. Initialization" + + if not configurationAttributes.containsKey("cas_host"): + print "CAS2. Initialization. Parameter 'cas_host' is missing" + return False + + self.cas_host = configurationAttributes.get("cas_host").getValue2() + + self.cas_extra_opts = None + if configurationAttributes.containsKey("cas_extra_opts"): + self.cas_extra_opts = configurationAttributes.get("cas_extra_opts").getValue2() + + + self.cas_renew_opt = False + if configurationAttributes.containsKey("cas_renew_opt"): + self.cas_renew_opt = StringHelper.toBoolean(configurationAttributes.get("cas_renew_opt").getValue2(), False) + + self.cas_map_user = False + if configurationAttributes.containsKey("cas_map_user"): + self.cas_map_user = StringHelper.toBoolean(configurationAttributes.get("cas_map_user").getValue2(), False) + + self.cas_enable_server_validation = False + if (configurationAttributes.containsKey("cas_validation_uri") and + configurationAttributes.containsKey("cas_validation_pattern") and + configurationAttributes.containsKey("cas_validation_timeout")): + + print "CAS2. Initialization. Configuring checker client" + self.cas_enable_server_validation = True + + self.cas_validation_uri = configurationAttributes.get("cas_validation_uri").getValue2() + self.cas_validation_pattern = configurationAttributes.get("cas_validation_pattern").getValue2() + cas_validation_timeout = int(configurationAttributes.get("cas_validation_timeout").getValue2()) * 1000 + + httpService = CdiUtil.bean(HttpService) + + self.http_client = httpService.getHttpsClient() + self.http_client_params = self.http_client.getParams() + self.http_client_params.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, cas_validation_timeout) + + self.cas_alt_auth_mode = None + if configurationAttributes.containsKey("cas_alt_auth_mode"): + self.cas_alt_auth_mode = configurationAttributes.get("cas_alt_auth_mode").getValue2() + + print "CAS2. Initialized successfully" + + return True + + def destroy(self, configurationAttributes): + print "CAS2. Destroy" + if self.cas_enable_server_validation: + print "CAS2. CDestory. Destorying checker client" + self.http_client = None + + print "CAS2. Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + if not self.cas_enable_server_validation: + return True + + print "CAS2. isValidAuthenticationMethod" + + httpService = CdiUtil.bean(HttpService) + + try: + http_service_response = httpService.executeGet(self.http_client, self.cas_validation_uri) + except: + print "CAS2. isValidAuthenticationMethod. Exception: ", sys.exc_info()[1] + return False + + try: + http_response = http_service_response.getHttpResponse() + if http_response.getStatusLine().getStatusCode() != 200: + print "CAS2. isValidAuthenticationMethod. Get invalid response from CAS2 server: ", str(http_response.getStatusLine().getStatusCode()) + httpService.consume(http_response) + return False + + validation_response_bytes = httpService.getResponseContent(http_response) + validation_response_string = httpService.convertEntityToString(validation_response_bytes) + httpService.consume(http_response) + finally: + http_service_response.closeConnection() + + if (validation_response_string == None) or (validation_response_string.find(self.cas_validation_pattern) == -1): + print "CAS2. isValidAuthenticationMethod. Get invalid login page from CAS2 server:" + return False + + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return self.cas_alt_auth_mode + + def authenticate(self, configurationAttributes, requestParameters, step): + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + userService = CdiUtil.bean(UserService) + requestParameterService = CdiUtil.bean(RequestParameterService) + authenticationService = CdiUtil.bean(AuthenticationService) + httpService = CdiUtil.bean(HttpService) + + if step == 1: + print "CAS2. Authenticate for step 1" + ticket_array = requestParameters.get("ticket") + if ArrayHelper.isEmpty(ticket_array): + print "CAS2. Authenticate for step 1. ticket is empty" + return False + + ticket = ticket_array[0] + print "CAS2. Authenticate for step 1. ticket: " + ticket + + if StringHelper.isEmptyString(ticket): + print "CAS2. Authenticate for step 1. ticket is invalid" + return False + + # Validate ticket + facesContext = CdiUtil.bean(FacesContext) + request = facesContext.getExternalContext().getRequest() + + parametersMap = HashMap() + parametersMap.put("service", httpService.constructServerUrl(request) + "/postlogin.htm") + if self.cas_renew_opt: + parametersMap.put("renew", "true") + parametersMap.put("ticket", ticket) + cas_service_request_uri = requestParameterService.parametersAsString(parametersMap) + cas_service_request_uri = self.cas_host + "/serviceValidate?" + cas_service_request_uri + if self.cas_extra_opts != None: + cas_service_request_uri = cas_service_request_uri + "&" + self.cas_extra_opts + + print "CAS2. Authenticate for step 1. cas_service_request_uri: " + cas_service_request_uri + + http_client = httpService.getHttpsClient() + http_service_response = httpService.executeGet(http_client, cas_service_request_uri) + try: + validation_content = httpService.convertEntityToString(httpService.getResponseContent(http_service_response.getHttpResponse())) + finally: + http_service_response.closeConnection() + + print "CAS2. Authenticate for step 1. validation_content: " + validation_content + if StringHelper.isEmpty(validation_content): + print "CAS2. Authenticate for step 1. Ticket validation response is invalid" + return False + + cas2_auth_failure = self.parse_tag(validation_content, "cas:authenticationFailure") + print "CAS2. Authenticate for step 1. cas2_auth_failure: ", cas2_auth_failure + + cas2_user_uid = self.parse_tag(validation_content, "cas:user") + print "CAS2. Authenticate for step 1. cas2_user_uid: ", cas2_user_uid + + if (cas2_auth_failure != None) or (cas2_user_uid == None): + print "CAS2. Authenticate for step 1. Ticket is invalid" + return False + + if self.cas_map_user: + print "CAS2. Authenticate for step 1. Attempting to find user by oxExternalUid: cas2:" + cas2_user_uid + + # Check if the is user with specified cas2_user_uid + find_user_by_uid = userService.getUserByAttribute("oxExternalUid", "cas2:" + cas2_user_uid) + + if find_user_by_uid == None: + print "CAS2. Authenticate for step 1. Failed to find user" + print "CAS2. Authenticate for step 1. Setting count steps to 2" + identity.setWorkingParameter("cas2_count_login_steps", 2) + identity.setWorkingParameter("cas2_user_uid", cas2_user_uid) + return True + + found_user_name = find_user_by_uid.getUserId() + print "CAS2. Authenticate for step 1. found_user_name: " + found_user_name + + authenticationService.authenticate(found_user_name) + + print "CAS2. Authenticate for step 1. Setting count steps to 1" + identity.setWorkingParameter("cas2_count_login_steps", 1) + + return True + else: + print "CAS2. Authenticate for step 1. Attempting to find user by uid:" + cas2_user_uid + + # Check if there is user with specified cas2_user_uid + find_user_by_uid = userService.getUser(cas2_user_uid) + if find_user_by_uid == None: + print "CAS2. Authenticate for step 1. Failed to find user" + return False + + found_user_name = find_user_by_uid.getUserId() + print "CAS2. Authenticate for step 1. found_user_name: " + found_user_name + + authenticationService.authenticate(found_user_name) + + print "CAS2. Authenticate for step 1. Setting count steps to 1" + identity.setWorkingParameter("cas2_count_login_steps", 1) + + return True + elif step == 2: + print "CAS2. Authenticate for step 2" + + if identity.isSetWorkingParameter("cas2_user_uid"): + print "CAS2. Authenticate for step 2. cas2_user_uid is empty" + return False + + cas2_user_uid = identity.getWorkingParameter("cas2_user_uid") + passed_step1 = StringHelper.isNotEmptyString(cas2_user_uid) + if not passed_step1: + return False + + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + logged_in = False + if StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password): + logged_in = authenticationService.authenticate(user_name, user_password) + + if not logged_in: + return False + + # Check if there is user which has cas2_user_uid + # Avoid mapping CAS2 account to more than one IDP account + find_user_by_uid = userService.getUserByAttribute("oxExternalUid", "cas2:" + cas2_user_uid) + + if find_user_by_uid == None: + # Add cas2_user_uid to user one id UIDs + find_user_by_uid = userService.addUserAttribute(user_name, "oxExternalUid", "cas2:" + cas2_user_uid) + if find_user_by_uid == None: + print "CAS2. Authenticate for step 2. Failed to update current user" + return False + + return True + else: + found_user_name = find_user_by_uid.getUserId() + print "CAS2. Authenticate for step 2. found_user_name: " + found_user_name + + if StringHelper.equals(user_name, found_user_name): + return True + + return False + else: + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + if step == 1: + print "CAS2. Prepare for step 1" + + requestParameterService = CdiUtil.bean(RequestParameterService) + httpService = CdiUtil.bean(HttpService) + + facesContext = CdiUtil.bean(FacesContext) + request = facesContext.getExternalContext().getRequest() + + parametersMap = HashMap() + parametersMap.put("service", httpService.constructServerUrl(request) + "/postlogin.htm") + if self.cas_renew_opt: + parametersMap.put("renew", "true") + cas_service_request_uri = requestParameterService.parametersAsString(parametersMap) + cas_service_request_uri = self.cas_host + "/login?" + cas_service_request_uri + if self.cas_extra_opts != None: + cas_service_request_uri = cas_service_request_uri + "&" + self.cas_extra_opts + + print "CAS2. Prepare for step 1. cas_service_request_uri: " + cas_service_request_uri + facesService = CdiUtil.bean(FacesService) + facesService.redirectToExternalURL(cas_service_request_uri) + + return True + elif step == 2: + print "CAS2. Prepare for step 2" + + return True + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + if step == 2: + return Arrays.asList("cas2_count_login_steps", "cas2_user_uid") + + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + identity = CdiUtil.bean(Identity) + if identity.isSetWorkingParameter("cas2_count_login_steps"): + return int(identity.getWorkingParameter("cas2_count_login_steps")) + + return 2 + + def getPageForStep(self, configurationAttributes, step): + identity = CdiUtil.bean(Identity) + if step == 1: + return "/auth/cas2/cas2login.xhtml" + return "/auth/cas2/cas2postlogin.xhtml" + + def parse_tag(self, str, tag): + tag1_pos1 = str.find("<" + tag) + # No tag found, return empty string. + if tag1_pos1 == -1: return None + tag1_pos2 = str.find(">", tag1_pos1) + if tag1_pos2 == -1: return None + tag2_pos1 = str.find("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 MxnWAAKKuVS0np_cb^rhX literal 0 HcmV?d00001 diff --git a/oxAuth/Server/integrations/casa/Casa.py b/oxAuth/Server/integrations/casa/Casa.py new file mode 100644 index 00000000..5c776c54 --- /dev/null +++ b/oxAuth/Server/integrations/casa/Casa.py @@ -0,0 +1,673 @@ +# Author: Jose Gonzalez + +from java.lang import Integer +from java.util import Collections, HashMap, HashSet, ArrayList, Arrays, Date +from java.nio.charset import Charset + +from org.apache.http.params import CoreConnectionPNames + +from org.oxauth.persistence.model.configuration import GluuConfiguration +from org.gluu.oxauth.security import Identity +from org.gluu.oxauth.service import AuthenticationService, UserService +from org.gluu.oxauth.service.common import EncryptionService +from org.gluu.oxauth.service.custom import CustomScriptService +from org.gluu.oxauth.service.net import HttpService +from org.gluu.oxauth.util import ServerUtil +from org.gluu.model import SimpleCustomProperty +from org.gluu.model.casa import ApplicationConfiguration +from org.gluu.model.custom.script import CustomScriptType +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.persist import PersistenceEntryManager +from org.gluu.service import CacheService +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.util import StringHelper + +try: + import json +except ImportError: + import simplejson as json +import sys + +class PersonAuthentication(PersonAuthenticationType): + + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + self.ACR_SG = "super_gluu" + self.ACR_U2F = "u2f" + + self.modulePrefix = "casa-external_" + + def init(self, customScript, configurationAttributes): + + print "Casa. init called" + self.authenticators = {} + self.uid_attr = self.getLocalPrimaryKey() + + custScriptService = CdiUtil.bean(CustomScriptService) + self.scriptsList = custScriptService.findCustomScripts(Collections.singletonList(CustomScriptType.PERSON_AUTHENTICATION), "oxConfigurationProperty", "displayName", "oxEnabled", "oxLevel") + dynamicMethods = self.computeMethods(self.scriptsList) + + if len(dynamicMethods) > 0: + print "Casa. init. Loading scripts for dynamic modules: %s" % dynamicMethods + + for acr in dynamicMethods: + moduleName = self.modulePrefix + acr + try: + external = __import__(moduleName, globals(), locals(), ["PersonAuthentication"], -1) + module = external.PersonAuthentication(self.currentTimeMillis) + + print "Casa. init. Got dynamic module for acr %s" % acr + configAttrs = self.getConfigurationAttributes(acr, self.scriptsList) + + if acr == self.ACR_U2F: + u2f_application_id = configurationAttributes.get("u2f_app_id").getValue2() + configAttrs.put("u2f_application_id", SimpleCustomProperty("u2f_application_id", u2f_application_id)) + elif acr == self.ACR_SG: + application_id = configurationAttributes.get("supergluu_app_id").getValue2() + configAttrs.put("application_id", SimpleCustomProperty("application_id", application_id)) + + if module.init(None, configAttrs): + module.configAttrs = configAttrs + self.authenticators[acr] = module + else: + print "Casa. init. Call to init in module '%s' returned False" % moduleName + except: + print "Casa. init. Failed to load module %s" % moduleName + print "Exception: ", sys.exc_info()[1] + + mobile_methods = configurationAttributes.get("mobile_methods") + self.mobile_methods = [] if mobile_methods == None else StringHelper.split(mobile_methods.getValue2(), ",") + + print "Casa. init. Initialized successfully" + return True + + + def destroy(self, configurationAttributes): + print "Casa. Destroyed called" + return True + + + def getApiVersion(self): + return 11 + + + def getAuthenticationMethodClaims(self, configurationAttributes): + return None + + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + print "Casa. isValidAuthenticationMethod called" + return True + + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + + def authenticate(self, configurationAttributes, requestParameters, step): + print "Casa. authenticate for step %s" % str(step) + + userService = CdiUtil.bean(UserService) + authenticationService = CdiUtil.bean(AuthenticationService) + identity = CdiUtil.bean(Identity) + + if step == 1: + credentials = identity.getCredentials() + user_name = credentials.getUsername() + user_password = credentials.getPassword() + identity.setWorkingParameter("platformAuthenticatorAvailable",ServerUtil.getFirstValue(requestParameters, "loginForm:platformAuthenticator")) + + if StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password): + + foundUser = userService.getUserByAttribute(self.uid_attr, user_name) + #foundUser = userService.getUser(user_name) + if foundUser == None: + print "Casa. authenticate for step 1. Unknown username" + else: + platform_data = self.parsePlatformData(requestParameters) + preferred = foundUser.getAttribute("oxPreferredMethod") + mfaOff = preferred == None + logged_in = False + + if mfaOff: + logged_in = authenticationService.authenticate(user_name, user_password) + else: + acr = self.getSuitableAcr(foundUser, platform_data, preferred) + if acr != None: + module = self.authenticators[acr] + logged_in = module.authenticate(module.configAttrs, requestParameters, step) + + if logged_in: + foundUser = authenticationService.getAuthenticatedUser() + + if foundUser == None: + print "Casa. authenticate for step 1. Cannot retrieve logged user" + else: + if mfaOff: + identity.setWorkingParameter("skip2FA", True) + else: + #Determine whether to skip 2FA based on policy defined (global or user custom) + skip2FA = self.determineSkip2FA(userService, identity, foundUser, platform_data) + identity.setWorkingParameter("skip2FA", skip2FA) + identity.setWorkingParameter("ACR", acr) + + return True + + else: + print "Casa. authenticate for step 1 was not successful" + return False + + else: + user = authenticationService.getAuthenticatedUser() + if user == None: + print "Casa. authenticate for step 2. Cannot retrieve logged user" + return False + + #see casa.xhtml + alter = ServerUtil.getFirstValue(requestParameters, "alternativeMethod") + if alter != None: + #bypass the rest of this step if an alternative method was provided. Current step will be retried (see getNextStep) + self.simulateFirstStep(requestParameters, alter) + return True + + session_attributes = identity.getSessionId().getSessionAttributes() + acr = session_attributes.get("ACR") + #this working parameter is used in casa.xhtml + identity.setWorkingParameter("methods", ArrayList(self.getAvailMethodsUser(user, acr))) + + success = False + if acr in self.authenticators: + module = self.authenticators[acr] + success = module.authenticate(module.configAttrs, requestParameters, step) + + #Update the list of trusted devices if 2fa passed + if success: + print "Casa. authenticate. 2FA authentication was successful" + tdi = session_attributes.get("trustedDevicesInfo") + if tdi == None: + print "Casa. authenticate. List of user's trusted devices was not updated" + else: + user.setAttribute("oxTrustedDevicesInfo", tdi) + userService.updateUser(user) + else: + print "Casa. authenticate. 2FA authentication failed" + + return success + + return False + + + def prepareForStep(self, configurationAttributes, requestParameters, step): + print "Casa. prepareForStep %s" % str(step) + identity = CdiUtil.bean(Identity) + + if step == 1: + self.prepareUIParams(identity) + return True + else: + session_attributes = identity.getSessionId().getSessionAttributes() + + authenticationService = CdiUtil.bean(AuthenticationService) + user = authenticationService.getAuthenticatedUser() + + if user == None: + print "Casa. prepareForStep. Cannot retrieve logged user" + return False + + acr = session_attributes.get("ACR") + print "Casa. prepareForStep. ACR = %s" % acr + identity.setWorkingParameter("methods", ArrayList(self.getAvailMethodsUser(user, acr))) + + if acr in self.authenticators: + module = self.authenticators[acr] + return module.prepareForStep(module.configAttrs, requestParameters, step) + else: + return False + + + def getExtraParametersForStep(self, configurationAttributes, step): + print "Casa. getExtraParametersForStep %s" % str(step) + list = ArrayList() + + if step > 1: + acr = CdiUtil.bean(Identity).getWorkingParameter("ACR") + + if acr in self.authenticators: + module = self.authenticators[acr] + params = module.getExtraParametersForStep(module.configAttrs, step) + if params != None: + list.addAll(params) + + list.addAll(Arrays.asList("ACR", "methods", "trustedDevicesInfo")) + + list.addAll(Arrays.asList("casa_contextPath", "casa_prefix", "casa_faviconUrl", "casa_extraCss", "casa_logoUrl","platformAuthenticatorAvailable")) + print "extras are %s" % list + return list + + + def getCountAuthenticationSteps(self, configurationAttributes): + print "Casa. getCountAuthenticationSteps called" + + if CdiUtil.bean(Identity).getWorkingParameter("skip2FA"): + return 1 + + acr = CdiUtil.bean(Identity).getWorkingParameter("ACR") + if acr in self.authenticators: + module = self.authenticators[acr] + return module.getCountAuthenticationSteps(module.configAttrs) + else: + return 2 + + print "Casa. getCountAuthenticationSteps. Could not determine the step count for acr %s" % acr + + + def getPageForStep(self, configurationAttributes, step): + print "Casa. getPageForStep called %s" % str(step) + + if step > 1: + acr = CdiUtil.bean(Identity).getWorkingParameter("ACR") + if acr in self.authenticators: + module = self.authenticators[acr] + page = module.getPageForStep(module.configAttrs, step) + else: + page=None + + return page + + return "/casa/login.xhtml" + + + def getNextStep(self, configurationAttributes, requestParameters, step): + + print "Casa. getNextStep called %s" % str(step) + if step > 1: + acr = ServerUtil.getFirstValue(requestParameters, "alternativeMethod") + if acr != None: + print "Casa. getNextStep. Use alternative method %s" % acr + CdiUtil.bean(Identity).setWorkingParameter("ACR", acr) + #retry step with different acr + return 2 + + return -1 + + + def logout(self, configurationAttributes, requestParameters): + print "Casa. logout called" + return True + +# Miscelaneous + + def getLocalPrimaryKey(self): + entryManager = CdiUtil.bean(PersistenceEntryManager) + config = GluuConfiguration() + config = entryManager.find(config.getClass(), "ou=configuration,o=gluu") + #Pick (one) attribute where user id is stored (e.g. uid/mail) + # primary_key is the primary attribute of backend AD / LDAP ( i.e. for all Active directories, primary_key is samAccountName ). + # local_primary_key is Gluu’s OpenDJ primary key ( which is UID ) + uid_attr = config.getOxIDPAuthentication().get(0).getConfig().getLocalPrimaryKey() + print "Casa. init. uid attribute is '%s'" % uid_attr + return uid_attr + + + def getSettings(self): + entryManager = CdiUtil.bean(PersistenceEntryManager) + config = ApplicationConfiguration() + config = entryManager.find(config.getClass(), "ou=casa,ou=configuration,o=gluu") + settings = None + try: + settings = json.loads(config.getSettings()) + except: + print "Casa. getSettings. Failed to parse casa settings from DB" + return settings + + + def computeMethods(self, scriptList): + + methods = [] + mapping = {} + cmConfigs = self.getSettings() + + if cmConfigs != None and 'acr_plugin_mapping' in cmConfigs: + mapping = cmConfigs['acr_plugin_mapping'] + + for m in mapping: + for customScript in scriptList: + if customScript.getName() == m and customScript.isEnabled(): + methods.append(m) + + print "Casa. computeMethods. %s" % methods + return methods + + + def getConfigurationAttributes(self, acr, scriptsList): + + configMap = HashMap() + for customScript in scriptsList: + if customScript.getName() == acr and customScript.isEnabled(): + for prop in customScript.getConfigurationProperties(): + configMap.put(prop.getValue1(), SimpleCustomProperty(prop.getValue1(), prop.getValue2())) + + print "Casa. getConfigurationAttributes. %d configuration properties were found for %s" % (configMap.size(), acr) + return configMap + + + def getAvailMethodsUser(self, user, skip=None): + methods = HashSet() + + for method in self.authenticators: + try: + module = self.authenticators[method] + if module.hasEnrollments(module.configAttrs, user): + methods.add(method) + except: + print "Casa. getAvailMethodsUser. hasEnrollments call could not be issued for %s module" % method + print "Exception: ", sys.exc_info()[1] + + try: + if skip != None: + # skip is guaranteed to be a member of methods (if hasEnrollments routines are properly implemented). + # A call to remove strangely crashes when skip is absent + methods.remove(skip) + except: + print "Casa. getAvailMethodsUser. methods list does not contain %s" % skip + + print "Casa. getAvailMethodsUser %s" % methods.toString() + return methods + + + def prepareUIParams(self, identity): + + print "Casa. prepareUIParams. Reading UI branding params" + cacheService = CdiUtil.bean(CacheService) + casaAssets = cacheService.get("casa_assets") + + if casaAssets == None: + #This may happen when cache type is IN_MEMORY, where actual cache is merely a local variable + #(a expiring map) living inside Casa webapp, not oxAuth webapp + + sets = self.getSettings() + + custPrefix = "/custom" + logoUrl = "/images/logo.png" + faviconUrl = "/images/favicon.ico" + if ("extra_css" in sets and sets["extra_css"] != None) or sets["use_branding"]: + logoUrl = custPrefix + logoUrl + faviconUrl = custPrefix + faviconUrl + + prefix = custPrefix if sets["use_branding"] else "" + + casaAssets = { + "contextPath": "/casa", + "prefix" : prefix, + "faviconUrl" : faviconUrl, + "extraCss": sets["extra_css"] if "extra_css" in sets else None, + "logoUrl": logoUrl + } + + #Setting a single variable with the whole map does not work... + identity.setWorkingParameter("casa_contextPath", casaAssets['contextPath']) + identity.setWorkingParameter("casa_prefix", casaAssets['prefix']) + identity.setWorkingParameter("casa_faviconUrl", casaAssets['contextPath'] + casaAssets['faviconUrl']) + identity.setWorkingParameter("casa_extraCss", casaAssets['extraCss']) + identity.setWorkingParameter("casa_logoUrl", casaAssets['contextPath'] + casaAssets['logoUrl']) + + + def simulateFirstStep(self, requestParameters, acr): + #To simulate 1st step, there is no need to call: + # getPageforstep (no need as user/pwd won't be shown again) + # isValidAuthenticationMethod (by restriction, it returns True) + # prepareForStep (by restriction, it returns True) + # getExtraParametersForStep (by restriction, it returns None) + print "Casa. simulateFirstStep. Calling authenticate (step 1) for %s module" % acr + if acr in self.authenticators: + module = self.authenticators[acr] + auth = module.authenticate(module.configAttrs, requestParameters, 1) + print "Casa. simulateFirstStep. returned value was %s" % auth + + +# 2FA policy enforcement + + def parsePlatformData(self, requestParameters): + try: + #Find device info passed in HTTP request params (see index.xhtml) + platform = ServerUtil.getFirstValue(requestParameters, "loginForm:platform") + deviceInf = json.loads(platform) + except: + print "Casa. parsePlatformData. Error parsing platform data" + deviceInf = None + + return deviceInf + + + def getSuitableAcr(self, user, deviceInf, preferred): + + onMobile = deviceInf != None and 'isMobile' in deviceInf and deviceInf['isMobile'] + id = user.getUserId() + strongest = -1 + acr = None + user_methods = self.getAvailMethodsUser(user) + + for s in self.scriptsList: + name = s.getName() + level = Integer.MAX_VALUE if name == preferred else s.getLevel() + if user_methods.contains(name) and level > strongest and (not onMobile or name in self.mobile_methods): + acr = name + strongest = level + + print "Casa. getSuitableAcr. On mobile = %s" % onMobile + if acr == None and onMobile: + print "Casa. getSuitableAcr. No mobile-friendly authentication method available for user %s" % id + # user_methods is not empty when this function is called, so just pick any + acr = user_methods.stream().findFirst().get() + + print "Casa. getSuitableAcr. %s was selected for user %s" % (acr, id) + return acr + + + def determineSkip2FA(self, userService, identity, foundUser, deviceInf): + + cmConfigs = self.getSettings() + + if cmConfigs == None: + print "Casa. determineSkip2FA. Failed to read policy_2fa" + return False + + missing = False + if not 'plugins_settings' in cmConfigs: + missing = True + elif not 'strong-authn-settings' in cmConfigs['plugins_settings']: + missing = True + else: + cmConfigs = cmConfigs['plugins_settings']['strong-authn-settings'] + + policy2FA = 'EVERY_LOGIN' + if not missing and 'policy_2fa' in cmConfigs: + policy2FA = ','.join(cmConfigs['policy_2fa']) + + print "Casa. determineSkip2FA with general policy %s" % policy2FA + policy2FA += ',' + skip2FA = False + + if 'CUSTOM,' in policy2FA: + #read setting from user profile + policy = foundUser.getAttribute("oxStrongAuthPolicy") + if policy == None: + policy = 'EVERY_LOGIN,' + else: + policy = policy.upper() + ',' + print "Casa. determineSkip2FA. Using user's enforcement policy %s" % policy + + else: + #If it's not custom, then apply the global setting admin defined + policy = policy2FA + + if not 'EVERY_LOGIN,' in policy: + locationCriterion = 'LOCATION_UNKNOWN,' in policy + deviceCriterion = 'DEVICE_UNKNOWN,' in policy + + if locationCriterion or deviceCriterion: + if deviceInf == None: + print "Casa. determineSkip2FA. No user device data. Forcing 2FA to take place..." + else: + skip2FA = self.process2FAPolicy(identity, foundUser, deviceInf, locationCriterion, deviceCriterion) + + if skip2FA: + print "Casa. determineSkip2FA. Second factor is skipped" + #Update attribute if authentication will not have second step + devInf = identity.getWorkingParameter("trustedDevicesInfo") + if devInf != None: + foundUser.setAttribute("oxTrustedDevicesInfo", devInf) + userService.updateUser(foundUser) + else: + print "Casa. determineSkip2FA. Unknown %s policy: cannot skip 2FA" % policy + + return skip2FA + + + def process2FAPolicy(self, identity, foundUser, deviceInf, locationCriterion, deviceCriterion): + + skip2FA = False + #Retrieve user's devices info + devicesInfo = foundUser.getAttribute("oxTrustedDevicesInfo") + + #do geolocation + geodata = self.getGeolocation(identity) + if geodata == None: + print "Casa. process2FAPolicy: Geolocation data not obtained. 2FA skipping based on location cannot take place" + + try: + encService = CdiUtil.bean(EncryptionService) + + if devicesInfo == None: + print "Casa. process2FAPolicy: There are no trusted devices for user yet" + #Simulate empty list + devicesInfo = "[]" + else: + devicesInfo = encService.decrypt(devicesInfo) + + devicesInfo = json.loads(devicesInfo) + + partialMatch = False + idx = 0 + #Try to find a match for device only + for device in devicesInfo: + partialMatch = device['browser']['name']==deviceInf['name'] and device['os']['version']==deviceInf['os']['version'] and device['os']['family']==deviceInf['os']['family'] + if partialMatch: + break + idx+=1 + + matchFound = False + + #At least one of locationCriterion or deviceCriterion is True + if locationCriterion and not deviceCriterion: + #this check makes sense if there is city data only + if geodata!=None: + for device in devicesInfo: + #Search all registered cities that are found in trusted devices + for origin in device['origins']: + matchFound = matchFound or origin['city']==geodata['city'] + + elif partialMatch: + #In this branch deviceCriterion is True + if not locationCriterion: + matchFound = True + elif geodata!=None: + for origin in devicesInfo[idx]['origins']: + matchFound = matchFound or origin['city']==geodata['city'] + + skip2FA = matchFound + now = Date().getTime() + + #Update attribute oxTrustedDevicesInfo accordingly + if partialMatch: + #Update an existing record (update timestamp in city, or else add it) + if geodata != None: + partialMatch = False + idxCity = 0 + + for origin in devicesInfo[idx]['origins']: + partialMatch = origin['city']==geodata['city'] + if partialMatch: + break; + idxCity+=1 + + if partialMatch: + devicesInfo[idx]['origins'][idxCity]['timestamp'] = now + else: + devicesInfo[idx]['origins'].append({"city": geodata['city'], "country": geodata['country'], "timestamp": now}) + else: + #Create a new entry + browser = {"name": deviceInf['name'], "version": deviceInf['version']} + os = {"family": deviceInf['os']['family'], "version": deviceInf['os']['version']} + + if geodata == None: + origins = [] + else: + origins = [{"city": geodata['city'], "country": geodata['country'], "timestamp": now}] + + obj = {"browser": browser, "os": os, "addedOn": now, "origins": origins} + devicesInfo.append(obj) + + enc = json.dumps(devicesInfo, separators=(',',':')) + enc = encService.encrypt(enc) + identity.setWorkingParameter("trustedDevicesInfo", enc) + + except: + print "Casa. process2FAPolicy. Error!", sys.exc_info()[1] + + return skip2FA + + + def getGeolocation(self, identity): + + session_attributes = identity.getSessionId().getSessionAttributes() + if session_attributes.containsKey("remote_ip"): + remote_ip = session_attributes.get("remote_ip").split(",", 2)[0].strip() + if StringHelper.isNotEmpty(remote_ip): + + httpService = CdiUtil.bean(HttpService) + + http_client = httpService.getHttpsClient() + http_client_params = http_client.getParams() + http_client_params.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 4 * 1000) + + geolocation_service_url = "http://ip-api.com/json/%s?fields=country,city,status,message" % remote_ip + geolocation_service_headers = { "Accept" : "application/json" } + + try: + http_service_response = httpService.executeGet(http_client, geolocation_service_url, geolocation_service_headers) + http_response = http_service_response.getHttpResponse() + except: + print "Casa. Determine remote location. Exception: ", sys.exc_info()[1] + return None + + try: + if not httpService.isResponseStastusCodeOk(http_response): + print "Casa. Determine remote location. Get non 200 OK response from server:", str(http_response.getStatusLine().getStatusCode()) + httpService.consume(http_response) + return None + + response_bytes = httpService.getResponseContent(http_response) + response_string = httpService.convertEntityToString(response_bytes, Charset.forName("UTF-8")) + httpService.consume(http_response) + finally: + http_service_response.closeConnection() + + if response_string == None: + print "Casa. Determine remote location. Get empty response from location server" + return None + + response = json.loads(response_string) + + if not StringHelper.equalsIgnoreCase(response['status'], "success"): + print "Casa. Determine remote location. Get response with status: '%s'" % response['status'] + return None + + return response + + return None + + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None diff --git a/oxAuth/Server/integrations/casa/README.md b/oxAuth/Server/integrations/casa/README.md new file mode 100644 index 00000000..5b4b0e9a --- /dev/null +++ b/oxAuth/Server/integrations/casa/README.md @@ -0,0 +1,52 @@ +# Casa authentication script + +Gluu Casa is a self-service web portal for end-users to manage authentication and authorization preferences for their account in Gluu Server. Click [here](https://casa.gluu.org) to learn more about Casa. + +Specifically, Casa features a custom script that is aligned with how the application is configured by the administrator. This means the real potential of the script is perceived in the context of an actual casa deployment. The behavior of the script depends on a variety of settings (specially 2FA-related) that can be tweaked using Casa's administration console or via the configuration API. + +Among others, Casa performs actions such as: + +- Identification of user device +- Geolocation of user IP +- Determine whether 2FA should take place +- Compute suitable 2FA mechanisms the user can be prompted depending on the context + +## Required files + +The following are the assets involved in casa authentication script: + +- Main script: `https://github.com/GluuFederation/community-edition-setup/blob/version_/static/extension/person_authentication/Casa.py` +- Dependant scripts: `https://github.com/GluuFederation/community-edition-setup/tree/version_/static/casa/scripts`. These are bundled with a default installation; more scripts may be required depending on the authentication mechanisms to support. Gluu installer already copies the default scripts in their destination: `/opt/gluu/python/libs` +- XHTML templates: `https://github.com/GluuFederation/oxAuth/tree/version_/Server/src/main/webapp/casa`. More files may be required depending on the authentication mechanisms to support. These files are already hosted by oxAuth web application. + +Note: to locate the files that match your Gluu installation replace `` with the (semantic) version of your Server. + +## Configuration properties + +For the main script: + +|Name|Description|Sample value| +|-|-|-| +|`mobile_methods`|Optional. Click [here]( https://www.gluu.org/docs/casa/administration/2fa-basics/#associated-strength-of-credentials)|otp, twilio_sms, super_gluu| +|`2fa_requisite`|Optional. Click [here]( https://gluu.org/docs/casa/administration/2fa-basics/#forcing-users-to-enroll-a-specific-credential-before-2fa-is-available)|`true`| +|`supergluu_app_id`|U2F application ID used by SuperGluu enrollments made using Casa, if any|`https:///casa`| +|`u2f_app_id`|U2F application ID used by FIDO (u2f) enrollments made using Casa, if any|`https://`| + +Auxiliary scripts require properties on their own. You can visit [this](https://www.gluu.org/docs/gluu-server/authn-guide/intro/) page to locate specific pages for every authentication method. + +## About the authentication flow + +Casa script orchestrates a 2FA flow by delegating specific implementation details of authentication methods to other scripts. This allows the flow to present users with alternatives in case some credential is not working as expected or is lost. Specific behavior depends on how Casa application is parameterized, please see ["About Two-Factor Authentication"](https://gluu.org/docs/casa/administration/2fa-basics/) for an introduction. + +An important restriction to account is that users must present a username and password combination before any form of strong authentication can take place in the flow. + +### Adding authentication mechanisms (new factors) + +If the method you want to add is already supported out-of-the-box, it is a matter of enabling it: Casa's admin console [doc page](https://gluu.org/docs/casa/administration/admin-console/#enabled-methods) has the required steps. If you are planning to onboard a different mechanism more work is required. In that case, we suggest reading [this page](https://gluu.org/docs/casa/developer/authn-methods/) of Casa's developer guide. + +## Flow look&feel + +Casa flow pages inherit many of the design elements already set in the [custom branding](https://gluu.org/docs/casa/plugins/custom-branding/) plugin. Changes in design elements such as color scheme or custom CSS rules should take effect in flow pages immediately. + +If you require a full customization of the look and feel you have to modify the flow pages. Follow [this](https://gluu.org/docs/gluu-server/operation/custom-design/) as a guide. Account relevant pages are located in `casa` folder of oxAuth war. + diff --git a/oxAuth/Server/integrations/casa_external_user/casa_external.py b/oxAuth/Server/integrations/casa_external_user/casa_external.py new file mode 100644 index 00000000..a781af87 --- /dev/null +++ b/oxAuth/Server/integrations/casa_external_user/casa_external.py @@ -0,0 +1,896 @@ +# Author: Jose Gonzalez + +from java.lang import Integer +from java.util import Collections, HashMap, HashSet, ArrayList, Arrays, Date, Properties +from java.nio.charset import Charset + +from org.apache.http.params import CoreConnectionPNames + +from org.oxauth.persistence.model.configuration import GluuConfiguration +from org.gluu.oxauth.security import Identity +from org.gluu.oxauth.service import AppInitializer, AuthenticationService, MetricService, UserService +from org.gluu.oxauth.service.common import EncryptionService +from org.gluu.oxauth.service.custom import CustomScriptService +from org.gluu.oxauth.service.net import HttpService +from org.gluu.oxauth.util import ServerUtil +from org.gluu.model import SimpleCustomProperty +from org.gluu.model.ldap import GluuLdapConfiguration +from org.gluu.model.casa import ApplicationConfiguration +from org.gluu.model.custom.script import CustomScriptType +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.model.metric import MetricType +from org.gluu.persist import PersistenceEntryManager +from org.gluu.persist.service import PersistanceFactoryService +from org.gluu.persist.ldap.impl import LdapEntryManagerFactory +from org.gluu.service import CacheService +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.util import StringHelper + +try: + import json +except ImportError: + import simplejson as json +import sys + +class PersonAuthentication(PersonAuthenticationType): + + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + self.ACR_SG = "super_gluu" + self.ACR_U2F = "u2f" + + self.modulePrefix = "casa-external_" + + def init(self, customScript, configurationAttributes): + + print "Casa. init called" + self.authenticators = {} + self.uid_attr = self.getLocalPrimaryKey() + + custScriptService = CdiUtil.bean(CustomScriptService) + self.scriptsList = custScriptService.findCustomScripts(Collections.singletonList(CustomScriptType.PERSON_AUTHENTICATION), "oxConfigurationProperty", "displayName", "oxEnabled", "oxLevel") + dynamicMethods = self.computeMethods(self.scriptsList) + + if len(dynamicMethods) > 0: + print "Casa. init. Loading scripts for dynamic modules: %s" % dynamicMethods + + for acr in dynamicMethods: + moduleName = self.modulePrefix + acr + try: + external = __import__(moduleName, globals(), locals(), ["PersonAuthentication"], -1) + module = external.PersonAuthentication(self.currentTimeMillis) + + print "Casa. init. Got dynamic module for acr %s" % acr + configAttrs = self.getConfigurationAttributes(acr, self.scriptsList) + + if acr == self.ACR_U2F: + u2f_application_id = configurationAttributes.get("u2f_app_id").getValue2() + configAttrs.put("u2f_application_id", SimpleCustomProperty("u2f_application_id", u2f_application_id)) + elif acr == self.ACR_SG: + application_id = configurationAttributes.get("supergluu_app_id").getValue2() + configAttrs.put("application_id", SimpleCustomProperty("application_id", application_id)) + + if module.init(None, configAttrs): + module.configAttrs = configAttrs + self.authenticators[acr] = module + else: + print "Casa. init. Call to init in module '%s' returned False" % moduleName + except: + print "Casa. init. Failed to load module %s" % moduleName + print "Exception: ", sys.exc_info()[1] + + mobile_methods = configurationAttributes.get("mobile_methods") + self.mobile_methods = [] if mobile_methods == None else StringHelper.split(mobile_methods.getValue2(), ",") + + + if configurationAttributes.containsKey("auth_configuration_file"): + print "Casa. External LDAP configuration provided" + + authConfigurationFile = configurationAttributes.get("auth_configuration_file").getValue2() + authConfiguration = self.loadAuthConfiguration(authConfigurationFile) + + if authConfiguration != None and self.validateAuthConfiguration(authConfiguration): + try: + self.ldapExtendedEntryManagers = None + ldapExtendedEntryManagers = self.createLdapExtendedEntryManagers(authConfiguration) + + ll = len(ldapExtendedEntryManagers) + print "%s LDAP configurations processed" % ll + + if ll > 0: + self.ldapExtendedEntryManagers = ldapExtendedEntryManagers + except: + print "Casa. Error creating LdapExtendedEntryManagers:", sys.exc_info()[1] + + print "Casa. init. Initialized successfully" + return True + + + def destroy(self, configurationAttributes): + print "Casa. Destroyed called" + + if self.ldapExtendedEntryManagers == None: + return True + + result = True + for ldapExtendedEntryManager in self.ldapExtendedEntryManagers: + ldapConfiguration = ldapExtendedEntryManager["ldapConfiguration"] + ldapEntryManager = ldapExtendedEntryManager["ldapEntryManager"] + + destoryResult = ldapEntryManager.destroy() + result = result and destoryResult + print "Basic (multi auth conf). Destroyed: " + ldapConfiguration.getConfigId() + ". Result: " + str(destoryResult) + + print "Basic (multi auth conf). Destroyed successfully" + + return result + + + def getApiVersion(self): + return 11 + + + def getAuthenticationMethodClaims(self, configurationAttributes): + return None + + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + print "Casa. isValidAuthenticationMethod called" + return True + + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + + def authenticate(self, configurationAttributes, requestParameters, step): + print "Casa. authenticate for step %s" % str(step) + + userService = CdiUtil.bean(UserService) + authenticationService = CdiUtil.bean(AuthenticationService) + identity = CdiUtil.bean(Identity) + + if step == 1: + credentials = identity.getCredentials() + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + if StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password): + + localAuth = self.ldapExtendedEntryManagers == None + print "Casa. authenticate for step 1. Local authentication is %s" % localAuth + + uid_attr = self.uid_attr if localAuth else self.authenticateLdapGetPK(user_name, user_password) + print "Casa. authenticate for step 1. authn attribute is %s" % uid_attr + + foundUser = None if uid_attr == None else userService.getUserByAttribute(uid_attr, user_name) + + #foundUser = userService.getUser(user_name) + if foundUser == None: + print "Casa. authenticate for step 1. Unknown username %s" % user_name + else: + platform_data = self.parsePlatformData(requestParameters) + preferred = foundUser.getAttribute("oxPreferredMethod") + mfaOff = preferred == None + logged_in = False + + if mfaOff: + logged_in = authenticationService.authenticate(user_name, user_password) if localAuth else True + else: + print "Casa. authenticate for step 1. User has 2FA on" + acr = self.getSuitableAcr(foundUser, platform_data, preferred) + if acr != None: + module = self.authenticators[acr] + logged_in = module.authenticate(module.configAttrs, requestParameters, step) + + if logged_in: + foundUser = authenticationService.getAuthenticatedUser() + + if foundUser == None: + print "Casa. authenticate for step 1. Cannot retrieve logged user" + else: + if mfaOff: + identity.setWorkingParameter("skip2FA", True) + else: + #Determine whether to skip 2FA based on policy defined (global or user custom) + skip2FA = self.determineSkip2FA(userService, identity, foundUser, platform_data) + identity.setWorkingParameter("skip2FA", skip2FA) + identity.setWorkingParameter("ACR", acr) + + return True + + else: + print "Casa. authenticate for step 1 was not successful" + return False + + else: + user = authenticationService.getAuthenticatedUser() + if user == None: + print "Casa. authenticate for step 2. Cannot retrieve logged user" + return False + + #see casa.xhtml + alter = ServerUtil.getFirstValue(requestParameters, "alternativeMethod") + if alter != None: + #bypass the rest of this step if an alternative method was provided. Current step will be retried (see getNextStep) + self.simulateFirstStep(requestParameters, alter) + return True + + session_attributes = identity.getSessionId().getSessionAttributes() + acr = session_attributes.get("ACR") + #this working parameter is used in casa.xhtml + identity.setWorkingParameter("methods", ArrayList(self.getAvailMethodsUser(user, acr))) + + success = False + if acr in self.authenticators: + module = self.authenticators[acr] + success = module.authenticate(module.configAttrs, requestParameters, step) + + #Update the list of trusted devices if 2fa passed + if success: + print "Casa. authenticate. 2FA authentication was successful" + tdi = session_attributes.get("trustedDevicesInfo") + if tdi == None: + print "Casa. authenticate. List of user's trusted devices was not updated" + else: + user.setAttribute("oxTrustedDevicesInfo", tdi) + userService.updateUser(user) + else: + print "Casa. authenticate. 2FA authentication failed" + + return success + + return False + + + def prepareForStep(self, configurationAttributes, requestParameters, step): + print "Casa. prepareForStep %s" % str(step) + identity = CdiUtil.bean(Identity) + + if step == 1: + self.prepareUIParams(identity) + return True + else: + session_attributes = identity.getSessionId().getSessionAttributes() + + authenticationService = CdiUtil.bean(AuthenticationService) + user = authenticationService.getAuthenticatedUser() + + if user == None: + print "Casa. prepareForStep. Cannot retrieve logged user" + return False + + acr = session_attributes.get("ACR") + print "Casa. prepareForStep. ACR = %s" % acr + identity.setWorkingParameter("methods", ArrayList(self.getAvailMethodsUser(user, acr))) + + if acr in self.authenticators: + module = self.authenticators[acr] + return module.prepareForStep(module.configAttrs, requestParameters, step) + else: + return False + + + def getExtraParametersForStep(self, configurationAttributes, step): + print "Casa. getExtraParametersForStep %s" % str(step) + list = ArrayList() + + if step > 1: + acr = CdiUtil.bean(Identity).getWorkingParameter("ACR") + + if acr in self.authenticators: + module = self.authenticators[acr] + params = module.getExtraParametersForStep(module.configAttrs, step) + if params != None: + list.addAll(params) + + list.addAll(Arrays.asList("ACR", "methods", "trustedDevicesInfo")) + + list.addAll(Arrays.asList("casa_contextPath", "casa_prefix", "casa_faviconUrl", "casa_extraCss", "casa_logoUrl")) + print "extras are %s" % list + return list + + + def getCountAuthenticationSteps(self, configurationAttributes): + print "Casa. getCountAuthenticationSteps called" + + if CdiUtil.bean(Identity).getWorkingParameter("skip2FA"): + return 1 + + acr = CdiUtil.bean(Identity).getWorkingParameter("ACR") + if acr in self.authenticators: + module = self.authenticators[acr] + return module.getCountAuthenticationSteps(module.configAttrs) + else: + return 2 + + print "Casa. getCountAuthenticationSteps. Could not determine the step count for acr %s" % acr + + + def getPageForStep(self, configurationAttributes, step): + print "Casa. getPageForStep called %s" % str(step) + + if step > 1: + acr = CdiUtil.bean(Identity).getWorkingParameter("ACR") + if acr in self.authenticators: + module = self.authenticators[acr] + page = module.getPageForStep(module.configAttrs, step) + else: + page=None + + return page + + return "/casa/login.xhtml" + + + def getNextStep(self, configurationAttributes, requestParameters, step): + + print "Casa. getNextStep called %s" % str(step) + if step > 1: + acr = ServerUtil.getFirstValue(requestParameters, "alternativeMethod") + if acr != None: + print "Casa. getNextStep. Use alternative method %s" % acr + CdiUtil.bean(Identity).setWorkingParameter("ACR", acr) + #retry step with different acr + return 2 + + return -1 + + + def logout(self, configurationAttributes, requestParameters): + print "Casa. logout called" + return True + +# Miscelaneous + + def getLocalPrimaryKey(self): + entryManager = CdiUtil.bean(PersistenceEntryManager) + config = GluuConfiguration() + config = entryManager.find(config.getClass(), "ou=configuration,o=gluu") + #Pick (one) attribute where user id is stored (e.g. uid/mail) + uid_attr = config.getOxIDPAuthentication().get(0).getConfig().getPrimaryKey() + print "Casa. init. uid attribute is '%s'" % uid_attr + return uid_attr + + + def getSettings(self): + entryManager = CdiUtil.bean(PersistenceEntryManager) + config = ApplicationConfiguration() + config = entryManager.find(config.getClass(), "ou=casa,ou=configuration,o=gluu") + settings = None + try: + settings = json.loads(config.getSettings()) + except: + print "Casa. getSettings. Failed to parse casa settings from DB" + return settings + + + def computeMethods(self, scriptList): + + methods = [] + mapping = {} + cmConfigs = self.getSettings() + + if cmConfigs != None and 'acr_plugin_mapping' in cmConfigs: + mapping = cmConfigs['acr_plugin_mapping'] + + for m in mapping: + for customScript in scriptList: + if customScript.getName() == m and customScript.isEnabled(): + methods.append(m) + + print "Casa. computeMethods. %s" % methods + return methods + + + def getConfigurationAttributes(self, acr, scriptsList): + + configMap = HashMap() + for customScript in scriptsList: + if customScript.getName() == acr and customScript.isEnabled(): + for prop in customScript.getConfigurationProperties(): + configMap.put(prop.getValue1(), SimpleCustomProperty(prop.getValue1(), prop.getValue2())) + + print "Casa. getConfigurationAttributes. %d configuration properties were found for %s" % (configMap.size(), acr) + return configMap + + + def getAvailMethodsUser(self, user, skip=None): + methods = HashSet() + + for method in self.authenticators: + try: + module = self.authenticators[method] + if module.hasEnrollments(module.configAttrs, user): + methods.add(method) + except: + print "Casa. getAvailMethodsUser. hasEnrollments call could not be issued for %s module" % method + print "Exception: ", sys.exc_info()[1] + + try: + if skip != None: + # skip is guaranteed to be a member of methods (if hasEnrollments routines are properly implemented). + # A call to remove strangely crashes when skip is absent + methods.remove(skip) + except: + print "Casa. getAvailMethodsUser. methods list does not contain %s" % skip + + print "Casa. getAvailMethodsUser %s" % methods.toString() + return methods + + + def prepareUIParams(self, identity): + + print "Casa. prepareUIParams. Reading UI branding params" + cacheService = CdiUtil.bean(CacheService) + casaAssets = cacheService.get("casa_assets") + + if casaAssets == None: + #This may happen when cache type is IN_MEMORY, where actual cache is merely a local variable + #(a expiring map) living inside Casa webapp, not oxAuth webapp + + sets = self.getSettings() + + custPrefix = "/custom" + logoUrl = "/images/logo.png" + faviconUrl = "/images/favicon.ico" + if ("extra_css" in sets and sets["extra_css"] != None) or sets["use_branding"]: + logoUrl = custPrefix + logoUrl + faviconUrl = custPrefix + faviconUrl + + prefix = custPrefix if sets["use_branding"] else "" + + casaAssets = { + "contextPath": "/casa", + "prefix" : prefix, + "faviconUrl" : faviconUrl, + "extraCss": sets["extra_css"] if "extra_css" in sets else None, + "logoUrl": logoUrl + } + + #Setting a single variable with the whole map does not work... + identity.setWorkingParameter("casa_contextPath", casaAssets['contextPath']) + identity.setWorkingParameter("casa_prefix", casaAssets['prefix']) + identity.setWorkingParameter("casa_faviconUrl", casaAssets['contextPath'] + casaAssets['faviconUrl']) + identity.setWorkingParameter("casa_extraCss", casaAssets['extraCss']) + identity.setWorkingParameter("casa_logoUrl", casaAssets['contextPath'] + casaAssets['logoUrl']) + + + def simulateFirstStep(self, requestParameters, acr): + #To simulate 1st step, there is no need to call: + # getPageforstep (no need as user/pwd won't be shown again) + # isValidAuthenticationMethod (by restriction, it returns True) + # prepareForStep (by restriction, it returns True) + # getExtraParametersForStep (by restriction, it returns None) + print "Casa. simulateFirstStep. Calling authenticate (step 1) for %s module" % acr + if acr in self.authenticators: + module = self.authenticators[acr] + auth = module.authenticate(module.configAttrs, requestParameters, 1) + print "Casa. simulateFirstStep. returned value was %s" % auth + + +# 2FA policy enforcement + + def parsePlatformData(self, requestParameters): + try: + #Find device info passed in HTTP request params (see index.xhtml) + platform = ServerUtil.getFirstValue(requestParameters, "loginForm:platform") + deviceInf = json.loads(platform) + except: + print "Casa. parsePlatformData. Error parsing platform data" + deviceInf = None + + return deviceInf + + + def getSuitableAcr(self, user, deviceInf, preferred): + + onMobile = deviceInf != None and 'isMobile' in deviceInf and deviceInf['isMobile'] + id = user.getUserId() + strongest = -1 + acr = None + user_methods = self.getAvailMethodsUser(user) + + for s in self.scriptsList: + name = s.getName() + level = Integer.MAX_VALUE if name == preferred else s.getLevel() + if user_methods.contains(name) and level > strongest and (not onMobile or name in self.mobile_methods): + acr = name + strongest = level + + print "Casa. getSuitableAcr. On mobile = %s" % onMobile + if acr == None and onMobile: + print "Casa. getSuitableAcr. No mobile-friendly authentication method available for user %s" % id + # user_methods is not empty when this function is called, so just pick any + acr = user_methods.stream().findFirst().get() + + print "Casa. getSuitableAcr. %s was selected for user %s" % (acr, id) + return acr + + + def determineSkip2FA(self, userService, identity, foundUser, deviceInf): + + cmConfigs = self.getSettings() + + if cmConfigs == None: + print "Casa. determineSkip2FA. Failed to read policy_2fa" + return False + + missing = False + if not 'plugins_settings' in cmConfigs: + missing = True + elif not 'strong-authn-settings' in cmConfigs['plugins_settings']: + missing = True + else: + cmConfigs = cmConfigs['plugins_settings']['strong-authn-settings'] + + policy2FA = 'EVERY_LOGIN' + if not missing and 'policy_2fa' in cmConfigs: + policy2FA = ','.join(cmConfigs['policy_2fa']) + + print "Casa. determineSkip2FA with general policy %s" % policy2FA + policy2FA += ',' + skip2FA = False + + if 'CUSTOM,' in policy2FA: + #read setting from user profile + policy = foundUser.getAttribute("oxStrongAuthPolicy") + if policy == None: + policy = 'EVERY_LOGIN,' + else: + policy = policy.upper() + ',' + print "Casa. determineSkip2FA. Using user's enforcement policy %s" % policy + + else: + #If it's not custom, then apply the global setting admin defined + policy = policy2FA + + if not 'EVERY_LOGIN,' in policy: + locationCriterion = 'LOCATION_UNKNOWN,' in policy + deviceCriterion = 'DEVICE_UNKNOWN,' in policy + + if locationCriterion or deviceCriterion: + if deviceInf == None: + print "Casa. determineSkip2FA. No user device data. Forcing 2FA to take place..." + else: + skip2FA = self.process2FAPolicy(identity, foundUser, deviceInf, locationCriterion, deviceCriterion) + + if skip2FA: + print "Casa. determineSkip2FA. Second factor is skipped" + #Update attribute if authentication will not have second step + devInf = identity.getWorkingParameter("trustedDevicesInfo") + if devInf != None: + foundUser.setAttribute("oxTrustedDevicesInfo", devInf) + userService.updateUser(foundUser) + else: + print "Casa. determineSkip2FA. Unknown %s policy: cannot skip 2FA" % policy + + return skip2FA + + + def process2FAPolicy(self, identity, foundUser, deviceInf, locationCriterion, deviceCriterion): + + skip2FA = False + #Retrieve user's devices info + devicesInfo = foundUser.getAttribute("oxTrustedDevicesInfo") + + #do geolocation + geodata = self.getGeolocation(identity) + if geodata == None: + print "Casa. process2FAPolicy: Geolocation data not obtained. 2FA skipping based on location cannot take place" + + try: + encService = CdiUtil.bean(EncryptionService) + + if devicesInfo == None: + print "Casa. process2FAPolicy: There are no trusted devices for user yet" + #Simulate empty list + devicesInfo = "[]" + else: + devicesInfo = encService.decrypt(devicesInfo) + + devicesInfo = json.loads(devicesInfo) + + partialMatch = False + idx = 0 + #Try to find a match for device only + for device in devicesInfo: + partialMatch = device['browser']['name']==deviceInf['name'] and device['os']['version']==deviceInf['os']['version'] and device['os']['family']==deviceInf['os']['family'] + if partialMatch: + break + idx+=1 + + matchFound = False + + #At least one of locationCriterion or deviceCriterion is True + if locationCriterion and not deviceCriterion: + #this check makes sense if there is city data only + if geodata!=None: + for device in devicesInfo: + #Search all registered cities that are found in trusted devices + for origin in device['origins']: + matchFound = matchFound or origin['city']==geodata['city'] + + elif partialMatch: + #In this branch deviceCriterion is True + if not locationCriterion: + matchFound = True + elif geodata!=None: + for origin in devicesInfo[idx]['origins']: + matchFound = matchFound or origin['city']==geodata['city'] + + skip2FA = matchFound + now = Date().getTime() + + #Update attribute oxTrustedDevicesInfo accordingly + if partialMatch: + #Update an existing record (update timestamp in city, or else add it) + if geodata != None: + partialMatch = False + idxCity = 0 + + for origin in devicesInfo[idx]['origins']: + partialMatch = origin['city']==geodata['city'] + if partialMatch: + break; + idxCity+=1 + + if partialMatch: + devicesInfo[idx]['origins'][idxCity]['timestamp'] = now + else: + devicesInfo[idx]['origins'].append({"city": geodata['city'], "country": geodata['country'], "timestamp": now}) + else: + #Create a new entry + browser = {"name": deviceInf['name'], "version": deviceInf['version']} + os = {"family": deviceInf['os']['family'], "version": deviceInf['os']['version']} + + if geodata == None: + origins = [] + else: + origins = [{"city": geodata['city'], "country": geodata['country'], "timestamp": now}] + + obj = {"browser": browser, "os": os, "addedOn": now, "origins": origins} + devicesInfo.append(obj) + + enc = json.dumps(devicesInfo, separators=(',',':')) + enc = encService.encrypt(enc) + identity.setWorkingParameter("trustedDevicesInfo", enc) + + except: + print "Casa. process2FAPolicy. Error!", sys.exc_info()[1] + + return skip2FA + + + def getGeolocation(self, identity): + + session_attributes = identity.getSessionId().getSessionAttributes() + if session_attributes.containsKey("remote_ip"): + remote_ip = session_attributes.get("remote_ip").split(",", 2)[0].strip() + if StringHelper.isNotEmpty(remote_ip): + + httpService = CdiUtil.bean(HttpService) + + http_client = httpService.getHttpsClient() + http_client_params = http_client.getParams() + http_client_params.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 4 * 1000) + + geolocation_service_url = "http://ip-api.com/json/%s?fields=country,city,status,message" % remote_ip + geolocation_service_headers = { "Accept" : "application/json" } + + try: + http_service_response = httpService.executeGet(http_client, geolocation_service_url, geolocation_service_headers) + http_response = http_service_response.getHttpResponse() + except: + print "Casa. Determine remote location. Exception: ", sys.exc_info()[1] + return None + + try: + if not httpService.isResponseStastusCodeOk(http_response): + print "Casa. Determine remote location. Get non 200 OK response from server:", str(http_response.getStatusLine().getStatusCode()) + httpService.consume(http_response) + return None + + response_bytes = httpService.getResponseContent(http_response) + response_string = httpService.convertEntityToString(response_bytes, Charset.forName("UTF-8")) + httpService.consume(http_response) + finally: + http_service_response.closeConnection() + + if response_string == None: + print "Casa. Determine remote location. Get empty response from location server" + return None + + response = json.loads(response_string) + + if not StringHelper.equalsIgnoreCase(response['status'], "success"): + print "Casa. Determine remote location. Get response with status: '%s'" % response['status'] + return None + + return response + + return None + + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + + def authenticateLdapGetPK(self, keyValue, userPassword): + authenticationService = CdiUtil.bean(AuthenticationService) + + metricService = CdiUtil.bean(MetricService) + timerContext = metricService.getTimer(MetricType.OXAUTH_USER_AUTHENTICATION_RATE).time() + try: + if (StringHelper.isNotEmptyString(keyValue) and StringHelper.isNotEmptyString(userPassword)): + for ldapExtendedEntryManager in self.ldapExtendedEntryManagers: + ldapConfiguration = ldapExtendedEntryManager["ldapConfiguration"] + ldapEntryManager = ldapExtendedEntryManager["ldapEntryManager"] + loginAttributes = ldapExtendedEntryManager["loginAttributes"] + localLoginAttributes = ldapExtendedEntryManager["localLoginAttributes"] + + print "Attempting authentication using configuration: " + ldapConfiguration.getConfigId() + + idx = 0 + count = len(loginAttributes) # why multiple login attributes? + while (idx < count): + primaryKey = loginAttributes[idx] + localPrimaryKey = localLoginAttributes[idx] + + loggedIn = authenticationService.authenticate(ldapConfiguration, ldapEntryManager, keyValue, userPassword, primaryKey, localPrimaryKey) + if (loggedIn): + metricService.incCounter(MetricType.OXAUTH_USER_AUTHENTICATION_SUCCESS) + return localPrimaryKey + idx += 1 + finally: + timerContext.stop() + + metricService.incCounter(MetricType.OXAUTH_USER_AUTHENTICATION_FAILURES) + + return None + + + def loadAuthConfiguration(self, authConfigurationFile): + authConfiguration = None + + # Load authentication configuration from file + f = open(authConfigurationFile, 'r') + try: + authConfiguration = json.loads(f.read()) + except: + print "Basic (multi auth conf). Load auth configuration. Failed to load authentication configuration from file:", authConfigurationFile + return None + finally: + f.close() + + return authConfiguration + + def validateAuthConfiguration(self, authConfiguration): + isValid = True + + if (not ("ldap_configuration" in authConfiguration)): + print "Basic (multi auth conf). Validate auth configuration. There is no ldap_configuration section in configuration" + return False + + idx = 1 + for ldapConfiguration in authConfiguration["ldap_configuration"]: + if (not self.containsAttributeString(ldapConfiguration, "configId")): + print "Basic (multi auth conf). Validate auth configuration. There is no 'configId' attribute in ldap_configuration section #" + str(idx) + return False + + configId = ldapConfiguration["configId"] + + if (not self.containsAttributeArray(ldapConfiguration, "servers")): + print "Basic (multi auth conf). Validate auth configuration. Property 'servers' in configuration '" + configId + "' is invalid" + return False + + if (self.containsAttributeString(ldapConfiguration, "bindDN")): + if (not self.containsAttributeString(ldapConfiguration, "bindPassword")): + print "Basic (multi auth conf). Validate auth configuration. Property 'bindPassword' in configuration '" + configId + "' is invalid" + return False + + if (not self.containsAttributeString(ldapConfiguration, "useSSL")): + print "Basic (multi auth conf). Validate auth configuration. Property 'useSSL' in configuration '" + configId + "' is invalid" + return False + + if (not self.containsAttributeString(ldapConfiguration, "maxConnections")): + print "Basic (multi auth conf). Validate auth configuration. Property 'maxConnections' in configuration '" + configId + "' is invalid" + return False + + if (not self.containsAttributeArray(ldapConfiguration, "baseDNs")): + print "Basic (multi auth conf). Validate auth configuration. Property 'baseDNs' in configuration '" + configId + "' is invalid" + return False + + if (not self.containsAttributeArray(ldapConfiguration, "loginAttributes")): + print "Basic (multi auth conf). Validate auth configuration. Property 'loginAttributes' in configuration '" + configId + "' is invalid" + return False + + if (not self.containsAttributeArray(ldapConfiguration, "localLoginAttributes")): + print "Basic (multi auth conf). Validate auth configuration. Property 'localLoginAttributes' in configuration '" + configId + "' is invalid" + return False + + if (len(ldapConfiguration["loginAttributes"]) != len(ldapConfiguration["localLoginAttributes"])): + print "Basic (multi auth conf). Validate auth configuration. The number of attributes in 'loginAttributes' and 'localLoginAttributes' isn't equal in configuration '" + configId + "'" + return False + + idx += 1 + + return True + + def createLdapExtendedEntryManagers(self, authConfiguration): + ldapExtendedConfigurations = self.createLdapExtendedConfigurations(authConfiguration) + + appInitializer = CdiUtil.bean(AppInitializer) + persistanceFactoryService = CdiUtil.bean(PersistanceFactoryService) + ldapEntryManagerFactory = persistanceFactoryService.getPersistenceEntryManagerFactory(LdapEntryManagerFactory) + persistenceType = ldapEntryManagerFactory.getPersistenceType() + + ldapExtendedEntryManagers = [] + for ldapExtendedConfiguration in ldapExtendedConfigurations: + connectionConfiguration = ldapExtendedConfiguration["connectionConfiguration"] + ldapConfiguration = ldapExtendedConfiguration["ldapConfiguration"] + + ldapProperties = Properties() + for key, value in connectionConfiguration.items(): + value_string = value + if isinstance(value_string, list): + value_string = ", ".join(value) + else: + value_string = str(value) + + ldapProperties.setProperty(persistenceType + "#" + key, value_string) + + if StringHelper.isNotEmptyString(ldapConfiguration.getBindPassword()): + ldapProperties.setProperty(persistenceType + "#bindPassword", ldapConfiguration.getBindPassword()) + + ldapEntryManager = ldapEntryManagerFactory.createEntryManager(ldapProperties) + + ldapExtendedEntryManagers.append({ "ldapConfiguration" : ldapConfiguration, "ldapProperties" : ldapProperties, "loginAttributes" : ldapExtendedConfiguration["loginAttributes"], "localLoginAttributes" : ldapExtendedConfiguration["localLoginAttributes"], "ldapEntryManager" : ldapEntryManager }) + + return ldapExtendedEntryManagers + + def createLdapExtendedConfigurations(self, authConfiguration): + ldapExtendedConfigurations = [] + + for connectionConfiguration in authConfiguration["ldap_configuration"]: + configId = connectionConfiguration["configId"] + print "Basic (multi auth conf). Processing configuration of %s" % configId + + servers = connectionConfiguration["servers"] + + bindDN = None + bindPassword = None + useAnonymousBind = True + if (self.containsAttributeString(connectionConfiguration, "bindDN")): + useAnonymousBind = False + bindDN = connectionConfiguration["bindDN"] + bindPassword = CdiUtil.bean(EncryptionService).decrypt(connectionConfiguration["bindPassword"]) + + useSSL = connectionConfiguration["useSSL"] + maxConnections = connectionConfiguration["maxConnections"] + baseDNs = connectionConfiguration["baseDNs"] + loginAttributes = connectionConfiguration["loginAttributes"] + localLoginAttributes = connectionConfiguration["localLoginAttributes"] + + ldapConfiguration = GluuLdapConfiguration() + ldapConfiguration.setConfigId(configId) + ldapConfiguration.setBindDN(bindDN) + ldapConfiguration.setBindPassword(bindPassword) + ldapConfiguration.setServers(Arrays.asList(servers)) + ldapConfiguration.setMaxConnections(maxConnections) + ldapConfiguration.setUseSSL(useSSL) + ldapConfiguration.setBaseDNs(Arrays.asList(baseDNs)) + ldapConfiguration.setPrimaryKey(loginAttributes[0]) + ldapConfiguration.setLocalPrimaryKey(localLoginAttributes[0]) + ldapConfiguration.setUseAnonymousBind(useAnonymousBind) + + ldapExtendedConfigurations.append({ "ldapConfiguration" : ldapConfiguration, "connectionConfiguration" : connectionConfiguration, "loginAttributes" : loginAttributes, "localLoginAttributes" : localLoginAttributes }) + + return ldapExtendedConfigurations + + def containsAttributeString(self, dictionary, attribute): + return ((attribute in dictionary) and StringHelper.isNotEmptyString(dictionary[attribute])) + + def containsAttributeArray(self, dictionary, attribute): + return ((attribute in dictionary) and (len(dictionary[attribute]) > 0)) diff --git a/oxAuth/Server/integrations/casa_external_user/readme.md b/oxAuth/Server/integrations/casa_external_user/readme.md new file mode 100644 index 00000000..c6da24a8 --- /dev/null +++ b/oxAuth/Server/integrations/casa_external_user/readme.md @@ -0,0 +1,25 @@ +# CASA with external user [ Work in progress ] + +This script integrate basic_multi_auth ( https://github.com/GluuFederation/oxAuth/tree/master/Server/integrations/basic.multi_auth_conf ) with CASA which allows organization to allow their external users ( who are being pulled or pushed from remote AD or LDAP ) to use CASA. + +## How to use this script: + + - This feature is only available from 4.5 and above. + - We need new CASA war to use this script for now: + - Download war: https://maven.gluu.org/maven/org/gluu/casa/4.5.4.Final/casa-4.5.4.Final.war + - Update your server with this war. + - Append new java parameter ( "-Dadmin.lock=/opt/gluu/jetty/casa/.administrable") in `/etc/default/casa`: + - `JAVA_OPTIONS="-server -Xms128m -Xmx846m -XX:+DisableExplicitGC -Dgluu.base=/etc/gluu -Dserver.base=/opt/gluu/jetty/casa -Dlog.base=/opt/gluu/jetty/casa -Dadmin.lock=/opt/gluu/jetty/casa/.administrable"` + - Restart oxauth, identity and casa service + - Use attached script in Person Authentication Script. + - in "Custom Property" use this value: + - `auth_configuration_file` == `/etc/certs/multi_auth_conf.json` + + +## If SuperGluu.... + +If you want to use SuperGluu in this whole setup, you have to: + + - Download SG script from here ( https://raw.githubusercontent.com/GluuFederation/community-edition-setup/version_4.5.4/static/casa/scripts/casa-external_super_gluu.py ) and copy it inside `/opt/gluu/python/lib` + - Make sure to change permission to "jetty:gluu" + - Restart oxauth service. diff --git a/oxAuth/Server/integrations/cert/Generate certs guide.md b/oxAuth/Server/integrations/cert/Generate certs guide.md new file mode 100644 index 00000000..4c3045b6 --- /dev/null +++ b/oxAuth/Server/integrations/cert/Generate certs guide.md @@ -0,0 +1,407 @@ +For testing purpouses there is archive with CA/Intermidiate/User certs in [archive](./sample/generated_certs.zip). + +## 1. Create and sign Root CA + +### 1.1. Generate password protected a 8192-bit long SHA-256 RSA key for root CA: + +`openssl genrsa -aes256 -out rootca.key 8192` + +Example output: +``` +Generating RSA private key, 8192 bit long modulus (2 primes) +....................+++ +........................................................................................................................................................................................................................................................................................................................................................................................+++ +e is 65537 (0x010001) +Enter pass phrase for rootca.key: +Verifying - Enter pass phrase for rootca.key: +``` + +### 1.2 Create the self-signed root CA certificate ca.crt; you'll need to provide an identity for your root CA: + +`openssl req -sha256 -new -x509 -days 1826 -key rootca.key -out rootca.crt` + +Example output: +``` +Enter pass phrase for rootca.key: +You are about to be asked to enter information that will be incorporated +into your certificate request. +What you are about to enter is what is called a Distinguished Name or a DN. +There are quite a few fields but you can leave some blank +For some fields there will be a default value, +If you enter '.', the field will be left blank. +----- +Country Name (2 letter code) [AU]:US +State or Province Name (full name) [Some-State]:TX +Locality Name (eg, city) []:Austin +Organization Name (eg, company) [Internet Widgits Pty Ltd]:Gluu, Inc. +Organizational Unit Name (eg, section) []:Gluu CA +Common Name (e.g. server FQDN or YOUR name) []:Gluu Root CA +Email Address []: +``` + +### 1.3. Create `root-ca.conf` file: + +``` +[ ca ] +default_ca = gluuca + +[ crl_ext ] +issuerAltName=issuer:copy +authorityKeyIdentifier=keyid:always + +[ gluuca ] +dir = ./ +new_certs_dir = $dir +unique_subject = no +certificate = $dir/rootca.crt +database = $dir/certindex +private_key = $dir/rootca.key +serial = $dir/certserial +default_days = 730 +default_md = sha1 +policy = gluuca_policy +x509_extensions = gluuca_extensions +crlnumber = $dir/crlnumber +default_crl_days = 730 + +[ gluuca_policy ] +commonName = supplied +stateOrProvinceName = supplied +countryName = optional +emailAddress = optional +organizationName = supplied +organizationalUnitName = optional + +[ gluuca_extensions ] +basicConstraints = critical,CA:TRUE,pathlen:0 +keyUsage = critical,any +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always,issuer +keyUsage = digitalSignature,cRLSign,keyCertSign +extendedKeyUsage = serverAuth +crlDistributionPoints = @crl_section +subjectAltName = @alt_names +authorityInfoAccess = @ocsp_section + +[alt_names] +DNS.0 = Gluu Intermidiate CA 1 +DNS.1 = Gluu CA Intermidiate 1 + +[crl_section] +URI.0 = http://pki.gluu.org/GluuRoot.crl +URI.1 = http://pki.backup.com/GluuRoot.crl + +[ocsp_section] +caIssuers;URI.0 = http://pki.gluu.org/GluuRoot.crt +caIssuers;URI.1 = http://pki.backup.com/GluuRoot.crt +OCSP;URI.0 = http://pki.gluu.org/ocsp/ +OCSP;URI.1 = http://pki.backup.com/ocsp/ + +``` +### 1.4. Create a few files where the CA will store it's serials: + +``` +touch certindex +echo 1000 > certserial +echo 1000 > crlnumber +``` + +### 1.5. If you need to set a specific certificate start / expiry date, add the following to [gluuca] +``` +# format: YYYYMMDDHHMMSS +default_enddate = 20191222035911 +default_startdate = 20181222035911 +``` + +## 2. Create and sign Intermediate 1 CA + +### 2.1. Generate the intermediate CA's private key: + +`openssl genrsa -out intermediate1.key 4096` + +Example output: +``` +Generating RSA private key, 4096 bit long modulus (2 primes) +........................................................++++ +.........++++ +e is 65537 (0x010001) +``` +### 2.2. Generate the intermediate1 CA's CSR: + +`openssl req -new -sha256 -key intermediate1.key -out intermediate1.csr` + +Example output: +``` +You are about to be asked to enter information that will be incorporated +into your certificate request. +What you are about to enter is what is called a Distinguished Name or a DN. +There are quite a few fields but you can leave some blank +For some fields there will be a default value, +If you enter '.', the field will be left blank. +----- +Country Name (2 letter code) [AU]:US +State or Province Name (full name) [Some-State]:TX +Locality Name (eg, city) []:Austin +Organization Name (eg, company) [Internet Widgits Pty Ltd]:Gluu, Inc. +Organizational Unit Name (eg, section) []:Gluu CA +Common Name (e.g. server FQDN or YOUR name) []:Gluu Intermediate CA +Email Address []: + +Please enter the following 'extra' attributes +to be sent with your certificate request +A challenge password []: +An optional company name []: +``` + +### 2.3. Sign the intermediate1 CSR with the Root CA: + +`openssl ca -batch -config root-ca.conf -notext -in intermediate1.csr -out intermediate1.crt` + +Example output: +``` +Using configuration from root-ca.conf +Enter pass phrase for .//rootca.key: +Check that the request matches the signature +Signature ok +The Subject's Distinguished Name is as follows +countryName :PRINTABLE:'US' +stateOrProvinceName :ASN.1 12:'TX' +localityName :ASN.1 12:'Austin' +organizationName :ASN.1 12:'Gluu, Inc.' +organizationalUnitName:ASN.1 12:'Gluu CA' +commonName :ASN.1 12:'Gluu Intermediate CA' +Certificate is to be certified until Dec 22 07:39:24 2021 GMT (730 days) + +Write out database with 1 new entries +Data Base Updated +``` + +### 2.4. Generate the CRL (both in PEM and DER): + +``` +openssl ca -config root-ca.conf -gencrl -keyfile rootca.key -cert rootca.crt -out rootca.crl.pem +openssl crl -inform PEM -in rootca.crl.pem -outform DER -out rootca.crl +``` + +Generate the CRL after every certificate you sign with the CA. + +### 2.5. Configuring the Intermediate CA 1 + +Create a new folder for this intermediate and move in to it: + +``` +mkdir intermediate1 +cd ./intermediate1 +``` + +Copy the Intermediate cert and key from the Root CA: + +`mv ../intermediate1.* ./` + +Create the index files: + +``` +touch certindex +echo 1000 > certserial +echo 1000 > crlnumber +``` + +### 2.6. Create a new intermediate-ca.conf file: + +``` +[ ca ] +default_ca = gluuca + +[ crl_ext ] +issuerAltName=issuer:copy +authorityKeyIdentifier=keyid:always + +[ gluuca ] +dir = ./ +new_certs_dir = $dir +unique_subject = no +certificate = $dir/intermediate1.crt +database = $dir/certindex +private_key = $dir/intermediate1.key +serial = $dir/certserial +default_days = 365 +default_md = sha1 +policy = gluuca_policy +x509_extensions = gluuca_extensions +crlnumber = $dir/crlnumber +default_crl_days = 365 + +[ gluuca_policy ] +commonName = supplied +stateOrProvinceName = supplied +countryName = optional +emailAddress = optional +organizationName = supplied +organizationalUnitName = optional + +[ gluuca_extensions ] +basicConstraints = critical,CA:FALSE +keyUsage = critical,any +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always,issuer +keyUsage = digitalSignature, nonRepudiation, keyEncipherment +extendedKeyUsage = clientAuth +crlDistributionPoints = @crl_section +subjectAltName = @alt_names +authorityInfoAccess = @ocsp_section + +[alt_names] +DNS.0 = example.com +DNS.1 = example.org + +[crl_section] +URI.0 = http://pki.gluu.org/GluuIntermidiate1.crl +URI.1 = http://pki.backup.com/GluuIntermidiate1.crl + +[ocsp_section] +caIssuers;URI.0 = http://pki.gluu.org/GluuIntermediate1.crt +caIssuers;URI.1 = http://pki.backup.com/GluuIntermediate1.crt +OCSP;URI.0 = http://pki.gluu.org/ocsp/ +OCSP;URI.1 = http://pki.backup.com/ocsp/ +``` + +Change the [alt_names] section to whatever you need as Subject Alternative names. Remove it including the subjectAltName = @alt_names line if you don't want a Subject Alternative Name. + +If you need to set a specific certificate start / expiry date, add the following to [gluuca] + +``` +# format: YYYYMMDDHHMMSS +default_enddate = 20191222035911 +default_startdate = 20181222035911 +``` + +Generate an empty CRL (both in PEM and DER): + +``` +openssl ca -config intermediate-ca.conf -gencrl -keyfile intermediate1.key -cert intermediate1.crt -out intermediate1.crl.pem +openssl crl -inform PEM -in intermediate1.crl.pem -outform DER -out intermediate1.crl +``` + +### 2.7. This is sample to show how to revoke cert. Use it only when you need to revoke the intermediate cert: + +`openssl ca -config root-ca.conf -revoke intermediate1.crt -keyfile rootca.key -cert rootca.crt` + + +## 3. Creating end user certificates +We use this new intermediate CA to generate an end user certificate. Repeat these steps for every end user certificate you want to sign with this CA. + +### 3.1. Create folder for end user certs: +`mkdir enduser-certs` + +### 3.2. Generate the end user's private key: + +`openssl genrsa -out enduser-certs/user-gluu.org.key 4096` + +### 3.3. Generate the end user's CSR: + +`openssl req -new -sha256 -key enduser-certs/user-gluu.org.key -out enduser-certs/user-gluu.org.csr` + +Example output: +``` +You are about to be asked to enter information that will be incorporated +into your certificate request. +What you are about to enter is what is called a Distinguished Name or a DN. +There are quite a few fields but you can leave some blank +For some fields there will be a default value, +If you enter '.', the field will be left blank. +----- +Country Name (2 letter code) [AU]:US +State or Province Name (full name) [Some-State]:TX +Locality Name (eg, city) []:Austin +Organization Name (eg, company) [Internet Widgits Pty Ltd]:Gluu, Inc. +Organizational Unit Name (eg, section) []:User +Common Name (e.g. server FQDN or YOUR name) []:Full User Name +Email Address []: + +Please enter the following 'extra' attributes +to be sent with your certificate request +A challenge password []: +An optional company name []: +``` + +### 3.4. Sign the end user's CSR with the Intermediate 1 CA: + +`openssl ca -batch -config intermediate-ca.conf -notext -in enduser-certs/user-gluu.org.csr -out enduser-certs/user-gluu.org.crt` + +Example output: +``` +Using configuration from intermediate-ca.conf +Check that the request matches the signature +Signature ok +The Subject's Distinguished Name is as follows +countryName :PRINTABLE:'US' +stateOrProvinceName :ASN.1 12:'TX' +localityName :ASN.1 12:'Austin' +organizationName :ASN.1 12:'Gluu, Inc.' +organizationalUnitName:ASN.1 12:'User' +commonName :ASN.1 12:'Full User Name' +Certificate is to be certified until Dec 22 08:07:15 2020 GMT (365 days) + +Write out database with 1 new entries +Data Base Updated +``` + +### 3.5. Generate the CRL (both in PEM and DER): + +``` +openssl ca -config intermediate-ca.conf -gencrl -keyfile intermediate1.key -cert intermediate1.crt -out intermediate1.crl.pem +openssl crl -inform PEM -in intermediate1.crl.pem -outform DER -out intermediate1.crl +``` + +Generate the CRL after every certificate you sign with the CA. + +### 3.6. Create the certificate chain file by concatenating the Root and intermediate 1 certificates together. + +`cat ../rootca.crt intermediate1.crt > enduser-certs/user-gluu.org.chain` + +Send the following files to the end user: + +``` +user-gluu.org.crt +user-gluu.org.key +user-gluu.org.chain +``` + +You can also let the end user supply their own CSR and just send them the .crt file. Do not delete that from the server, otherwise you cannot revoke it. + +### 3.7. This is sample to show how to revoke cert. Use it only when you need to revoke the end users cert: + +`openssl ca -config intermediate-ca.conf -revoke enduser-certs/enduser-gluu.org.crt -keyfile intermediate1.key -cert intermediate1.crt` + +## 4. Validating the certificate + +### 4.1. You can validate the end user certificate against the chain using the following command: + +`openssl verify -CAfile enduser-certs/user-gluu.org.chain enduser-certs/user-gluu.org.crt` + +Example output: +``` +enduser-certs/user-gluu.org.crt: OK +``` + +### 4.2. You can also validate it against the CRL. Concatenate the PEM CRL and the chain together first: + +`cat ../rootca.crt intermediate1.crt intermediate1.crl.pem > enduser-certs/user-gluu.org.crl.chain` + +Verify the certificate: + +`openssl verify -crl_check -CAfile enduser-certs/user-gluu.org.crl.chain enduser-certs/user-gluu.org.crt` + +Example output: +``` +enduser-certs/user-gluu.org.crt: OK +``` + +### 5. Export end user certificate to PKCS#12 + +Convert a PEM certificate file and a private key to PKCS#12 (.pfx .p12) + +``` +openssl pkcs12 -export -out enduser-certs/user-gluu.org.pfx -inkey enduser-certs/user-gluu.org.key -in enduser-certs/user-gluu.org.crt -certfile enduser-certs/user-gluu.org.chain +``` + diff --git a/oxAuth/Server/integrations/cert/Generate certs without configs.md b/oxAuth/Server/integrations/cert/Generate certs without configs.md new file mode 100644 index 00000000..69ce2e6f --- /dev/null +++ b/oxAuth/Server/integrations/cert/Generate certs without configs.md @@ -0,0 +1,27 @@ +## 1. Generate a certificate authority (CA) cert. + +### 1. Generate your CA certificate using this command: + +`openssl req -newkey rsa:4096 -keyform PEM -keyout ca.key -x509 -days 3650 -outform PEM -out ca.cer` + +## 2. Generate a client SSL certificate + +### 1. Generate a private key for the SSL client. + +`openssl genrsa -out client.key 4096` + +### 2. Use the client’s private key to generate a cert request. + +`openssl req -new -key client.key -out client.req` + +### 3. Issue the client certificate using the cert request and the CA cert/key. + +`openssl x509 -req -in client.req -CA ca.cer -CAkey ca.key -set_serial 101 -extensions client -days 365 -outform PEM -out client.cer` + +### 4. Convert the client certificate and private key to pkcs#12 format for use by browsers. + +`openssl pkcs12 -export -inkey client.key -in client.cer -out client.p12` + +### 5. Clean up – remove the client private key, client cert and client request files as the pkcs12 has everything needed. + +`rm client.key client.cer client.req` diff --git a/oxAuth/Server/integrations/cert/Quick certs guide for testing.md b/oxAuth/Server/integrations/cert/Quick certs guide for testing.md new file mode 100644 index 00000000..a6303ceb --- /dev/null +++ b/oxAuth/Server/integrations/cert/Quick certs guide for testing.md @@ -0,0 +1,35 @@ +### 1. Create a user_cert.conf file and update `user_dn` section: + +``` +[ req ] +days = 365 +prompt = no +distinguished_name = user_dn +x509_extensions = v3_ca + +[ user_dn ] + countryName = US + stateOrProvinceName = TX + localityName = Austin + organizationName = Gluu, Inc. +organizationalUnitName = Gluu SSL department + commonName = secure.gluu.com + emailAddress = john@gluu.org + +[ v3_ca ] +basicConstraints = CA:FALSE +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always, issuer +extendedKeyUsage = clientAuth, emailProtection +``` + +### 2. Generate user cert + +``` +openssl req -x509 -config user_cert.conf -nodes -newkey rsa:4096 -keyout user_cert.key -out user_cert.crt +``` + +### 3. Export end user certificate to PKCS#12 +``` +openssl pkcs12 -export -inkey user_cert.key -in user_cert.crt -out user_cert.p12 +``` diff --git a/oxAuth/Server/integrations/cert/README.txt b/oxAuth/Server/integrations/cert/README.txt new file mode 100644 index 00000000..b24c3a0e --- /dev/null +++ b/oxAuth/Server/integrations/cert/README.txt @@ -0,0 +1,22 @@ +This is a person authentication module for oxAuth that enables user Certificate Authentication. + +The module has a few properties: + +1) chain_cert_file_path - It's mandatory property. It's path to file with cert chains in pem format. + Example: '/etc/certs/chain_cert.pem' + +2) map_user_cert - Specify if script should map new user to local account. If true, then on the first authentication, the script will prompt for a username/password in step 2, and then store the certificate fingerprint in the `oxExternalUid` attribute. + Allowed values: true/false + Example: true + +3) use_generic_validator, use_path_validator, use_ocsp_validator, use_crl_validator - Enable/Disable specific certificate validation. + Allowed values: true/false + Example: true + +4) crl_max_response_size - Specify maximum allowed size of CRL response + Allowed values: integer value greater that 0 + Example: 10485760 + Default value: 5242880 + +5) credentials_file - Patch to file with reCAPTCHA credentials. + Example: '/etc/certs/cert_credentials.json' diff --git a/oxAuth/Server/integrations/cert/UserCertExternalAuthenticator.py b/oxAuth/Server/integrations/cert/UserCertExternalAuthenticator.py new file mode 100644 index 00000000..9d43d345 --- /dev/null +++ b/oxAuth/Server/integrations/cert/UserCertExternalAuthenticator.py @@ -0,0 +1,482 @@ +# +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2016, Gluu +# +# Author: Yuriy Movchan +# + +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from javax.faces.context import FacesContext +from org.gluu.oxauth.security import Identity +from org.gluu.oxauth.service import AuthenticationService +from org.gluu.oxauth.service.common import UserService +from org.gluu.util import StringHelper +from org.gluu.oxauth.util import ServerUtil +from org.gluu.oxauth.service.common import EncryptionService +from java.util import Arrays +from org.gluu.oxauth.cert.fingerprint import FingerprintHelper +from org.gluu.oxauth.cert.validation import GenericCertificateVerifier, PathCertificateVerifier, OCSPCertificateVerifier, CRLCertificateVerifier +from org.gluu.oxauth.cert.validation.model import ValidationStatus +from org.gluu.oxauth.util import CertUtil +from org.gluu.oxauth.model.util import CertUtils +from org.gluu.oxauth.service.net import HttpService +from org.apache.http.params import CoreConnectionPNames + +import sys +import base64 +import urllib + +import java +import json + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Cert. Initialization" + + if not (configurationAttributes.containsKey("chain_cert_file_path")): + print "Cert. Initialization. Property chain_cert_file_path is mandatory" + return False + + if not (configurationAttributes.containsKey("map_user_cert")): + print "Cert. Initialization. Property map_user_cert is mandatory" + return False + + chain_cert_file_path = configurationAttributes.get("chain_cert_file_path").getValue2() + + self.chain_certs = CertUtil.loadX509CertificateFromFile(chain_cert_file_path) + if self.chain_certs == None: + print "Cert. Initialization. Failed to load chain certificates from '%s'" % chain_cert_file_path + return False + + print "Cert. Initialization. Loaded '%d' chain certificates" % self.chain_certs.size() + + crl_max_response_size = 5 * 1024 * 1024 # 10Mb + if configurationAttributes.containsKey("crl_max_response_size"): + crl_max_response_size = StringHelper.toInteger(configurationAttributes.get("crl_max_response_size").getValue2(), crl_max_response_size) + print "Cert. Initialization. CRL max response size is '%d'" % crl_max_response_size + + # Define array to order methods correctly + self.validator_types = [ 'generic', 'path', 'ocsp', 'crl'] + self.validators = { 'generic' : [GenericCertificateVerifier(), False], + 'path' : [PathCertificateVerifier(False), False], + 'ocsp' : [OCSPCertificateVerifier(), False], + 'crl' : [CRLCertificateVerifier(crl_max_response_size), False] } + + for type in self.validator_types: + validator_param_name = "use_%s_validator" % type + if configurationAttributes.containsKey(validator_param_name): + validator_status = StringHelper.toBoolean(configurationAttributes.get(validator_param_name).getValue2(), False) + self.validators[type][1] = validator_status + + print "Cert. Initialization. Validation method '%s' status: '%s'" % (type, self.validators[type][1]) + + self.map_user_cert = StringHelper.toBoolean(configurationAttributes.get("map_user_cert").getValue2(), False) + print "Cert. Initialization. map_user_cert: '%s'" % self.map_user_cert + + self.enabled_recaptcha = self.initRecaptcha(configurationAttributes) + print "Cert. Initialization. enabled_recaptcha: '%s'" % self.enabled_recaptcha + + print "Cert. Initialized successfully" + + return True + + def destroy(self, configurationAttributes): + print "Cert. Destroy" + + for type in self.validator_types: + self.validators[type][0].destroy() + + print "Cert. Destroyed successfully" + + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + user_name = credentials.getUsername() + + userService = CdiUtil.bean(UserService) + authenticationService = CdiUtil.bean(AuthenticationService) + + if step == 1: + print "Cert. Authenticate for step 1" + login_button = ServerUtil.getFirstValue(requestParameters, "loginForm:loginButton") + if StringHelper.isEmpty(login_button): + print "Cert. Authenticate for step 1. Form were submitted incorrectly" + return False + if self.enabled_recaptcha: + print "Cert. Authenticate for step 1. Validating recaptcha response" + recaptcha_response = ServerUtil.getFirstValue(requestParameters, "g-recaptcha-response") + + recaptcha_result = self.validateRecaptcha(recaptcha_response) + print "Cert. Authenticate for step 1. recaptcha_result: '%s'" % recaptcha_result + + return recaptcha_result + + return True + elif step == 2: + print "Cert. Authenticate for step 2" + + # Validate if user selected certificate + cert_x509 = self.getSessionAttribute("cert_x509") + if cert_x509 == None: + print "Cert. Authenticate for step 2. User not selected any certs" + identity.setWorkingParameter("cert_selected", False) + + # Return True to inform user how to reset workflow + return True + else: + identity.setWorkingParameter("cert_selected", True) + x509Certificate = self.certFromString(cert_x509) + + subjectX500Principal = x509Certificate.getSubjectX500Principal() + print "Cert. Authenticate for step 2. User selected certificate with DN '%s'" % subjectX500Principal + + # Validate certificates which user selected + valid = self.validateCertificate(x509Certificate) + if not valid: + print "Cert. Authenticate for step 2. Certificate DN '%s' is not valid" % subjectX500Principal + identity.setWorkingParameter("cert_valid", False) + + # Return True to inform user how to reset workflow + return True + + identity.setWorkingParameter("cert_valid", True) + + # Calculate certificate fingerprint + x509CertificateFingerprint = self.calculateCertificateFingerprint(x509Certificate) + identity.setWorkingParameter("cert_x509_fingerprint", x509CertificateFingerprint) + print "Cert. Authenticate for step 2. Fingerprint is '%s' of certificate with DN '%s'" % (x509CertificateFingerprint, subjectX500Principal) + + # Attempt to find user by certificate fingerprint + cert_user_external_uid = "cert:%s" % x509CertificateFingerprint + print "Cert. Authenticate for step 2. Attempting to find user by oxExternalUid attribute value %s" % cert_user_external_uid + + find_user_by_external_uid = userService.getUserByAttribute("oxExternalUid", cert_user_external_uid, True) + if find_user_by_external_uid == None: + print "Cert. Authenticate for step 2. Failed to find user" + + if self.map_user_cert: + print "Cert. Authenticate for step 2. Storing cert_user_external_uid for step 3" + identity.setWorkingParameter("cert_user_external_uid", cert_user_external_uid) + return True + else: + print "Cert. Authenticate for step 2. Mapping cert to user account is not allowed" + identity.setWorkingParameter("cert_count_login_steps", 2) + return False + + foundUserName = find_user_by_external_uid.getUserId() + print "Cert. Authenticate for step 2. foundUserName: " + foundUserName + + logged_in = False + userService = CdiUtil.bean(UserService) + logged_in = authenticationService.authenticate(foundUserName) + + print "Cert. Authenticate for step 2. Setting count steps to 2" + identity.setWorkingParameter("cert_count_login_steps", 2) + + return logged_in + elif step == 3: + print "Cert. Authenticate for step 3" + + cert_user_external_uid = self.getSessionAttribute("cert_user_external_uid") + if cert_user_external_uid == None: + print "Cert. Authenticate for step 3. cert_user_external_uid is empty" + return False + + user_password = credentials.getPassword() + + logged_in = False + if (StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password)): + logged_in = authenticationService.authenticate(user_name, user_password) + + if (not logged_in): + return False + + # Double check just to make sure. We did checking in previous step + # Check if there is user which has cert_user_external_uid + # Avoid mapping user cert to more than one IDP account + find_user_by_external_uid = userService.getUserByAttribute("oxExternalUid", cert_user_external_uid, True) + if find_user_by_external_uid == None: + # Add cert_user_external_uid to user's external GUID list + find_user_by_external_uid = userService.addUserAttribute(user_name, "oxExternalUid", cert_user_external_uid, True) + if find_user_by_external_uid == None: + print "Cert. Authenticate for step 3. Failed to update current user" + return False + + return True + + return True + else: + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + print "Cert. Prepare for step %d" % step + identity = CdiUtil.bean(Identity) + + if step == 1: + if self.enabled_recaptcha: + identity.setWorkingParameter("recaptcha_site_key", self.recaptcha_creds['site_key']) + elif step == 2: + # Store certificate in session + facesContext = CdiUtil.bean(FacesContext) + externalContext = facesContext.getExternalContext() + request = externalContext.getRequest() + + # Try to get certificate from header X-ClientCert + clientCertificate = externalContext.getRequestHeaderMap().get("X-ClientCert") + if clientCertificate != None: + x509Certificate = self.certFromPemString(clientCertificate) + identity.setWorkingParameter("cert_x509", self.certToString(x509Certificate)) + print "Cert. Prepare for step 2. Storing user certificate obtained from 'X-ClientCert' header" + return True + + # Try to get certificate from attribute javax.servlet.request.X509Certificate + x509Certificates = request.getAttribute('javax.servlet.request.X509Certificate') + if (x509Certificates != None) and (len(x509Certificates) > 0): + identity.setWorkingParameter("cert_x509", self.certToString(x509Certificates[0])) + print "Cert. Prepare for step 2. Storing user certificate obtained from 'javax.servlet.request.X509Certificate' attribute" + return True + + if step < 4: + return True + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + return Arrays.asList("cert_selected", "cert_valid", "cert_x509", "cert_x509_fingerprint", "cert_count_login_steps", "cert_user_external_uid") + + def getCountAuthenticationSteps(self, configurationAttributes): + cert_count_login_steps = self.getSessionAttribute("cert_count_login_steps") + if cert_count_login_steps != None: + return cert_count_login_steps + else: + return 3 + + def getPageForStep(self, configurationAttributes, step): + if step == 1: + return "/auth/cert/login.xhtml" + if step == 2: + return "/auth/cert/cert-login.xhtml" + elif step == 3: + cert_selected = self.getSessionAttribute("cert_selected") + if True != cert_selected: + return "/auth/cert/cert-not-selected.xhtml" + + cert_valid = self.getSessionAttribute("cert_valid") + if True != cert_valid: + return "/auth/cert/cert-invalid.xhtml" + + return "/login.xhtml" + + return "" + + def logout(self, configurationAttributes, requestParameters): + return True + + def processBasicAuthentication(self, credentials): + userService = CdiUtil.bean(UserService) + authenticationService = CdiUtil.bean(AuthenticationService) + + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + logged_in = False + if (StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password)): + logged_in = authenticationService.authenticate(user_name, user_password) + + if (not logged_in): + return None + + find_user_by_uid = authenticationService.getAuthenticatedUser() + if (find_user_by_uid == None): + print "Cert. Process basic authentication. Failed to find user '%s'" % user_name + return None + + return find_user_by_uid + + def getSessionAttribute(self, attribute_name): + identity = CdiUtil.bean(Identity) + + # Try to get attribute value from Seam event context + if identity.isSetWorkingParameter(attribute_name): + return identity.getWorkingParameter(attribute_name) + + # Try to get attribute from persistent session + session_id = identity.getSessionId() + if session_id == None: + return None + + session_attributes = session_id.getSessionAttributes() + if session_attributes == None: + return None + + if session_attributes.containsKey(attribute_name): + return session_attributes.get(attribute_name) + + return None + + def calculateCertificateFingerprint(self, x509Certificate): + print "Cert. Calculate fingerprint for certificate DN '%s'" % x509Certificate.getSubjectX500Principal() + + publicKey = x509Certificate.getPublicKey() + + # Use oxAuth implementation + fingerprint = FingerprintHelper.getPublicKeySshFingerprint(publicKey) + + return fingerprint + + def validateCertificate(self, x509Certificate): + subjectX500Principal = x509Certificate.getSubjectX500Principal() + + print "Cert. Validating certificate with DN '%s'" % subjectX500Principal + + validation_date = java.util.Date() + + for type in self.validator_types: + if self.validators[type][1]: + result = self.validators[type][0].validate(x509Certificate, self.chain_certs, validation_date) + print "Cert. Validate certificate: '%s'. Validation method '%s' result: '%s'" % (subjectX500Principal, type, result) + + if (result.getValidity() != ValidationStatus.CertificateValidity.VALID): + print "Cert. Certificate: '%s' is invalid" % subjectX500Principal + return False + + return True + + def certToString(self, x509Certificate): + if x509Certificate == None: + return None + return base64.b64encode(x509Certificate.getEncoded()) + + def certFromString(self, x509CertificateEncoded): + x509CertificateDecoded = base64.b64decode(x509CertificateEncoded) + return CertUtils.x509CertificateFromBytes(x509CertificateDecoded) + + def certFromPemString(self, pemCertificate): + x509CertificateEncoded = pemCertificate.replace("-----BEGIN CERTIFICATE-----", "").replace("-----END CERTIFICATE-----", "").strip() + return self.certFromString(x509CertificateEncoded) + + def initRecaptcha(self, configurationAttributes): + print "Cert. Initialize recaptcha" + if not configurationAttributes.containsKey("credentials_file"): + return False + + cert_creds_file = configurationAttributes.get("credentials_file").getValue2() + + # Load credentials from file + f = open(cert_creds_file, 'r') + try: + creds = json.loads(f.read()) + except: + print "Cert. Initialize recaptcha. Failed to load credentials from file: %s" % cert_creds_file + return False + finally: + f.close() + + try: + recaptcha_creds = creds["recaptcha"] + except: + print "Cert. Initialize recaptcha. Invalid credentials file '%s' format:" % cert_creds_file + return False + + self.recaptcha_creds = None + if recaptcha_creds["enabled"]: + print "Cert. Initialize recaptcha. Recaptcha is enabled" + + encryptionService = CdiUtil.bean(EncryptionService) + + site_key = recaptcha_creds["site_key"] + secret_key = recaptcha_creds["secret_key"] + + try: + site_key = encryptionService.decrypt(site_key) + except: + # Ignore exception. Value is not encrypted + print "Cert. Initialize recaptcha. Assuming that 'site_key' in not encrypted" + + try: + secret_key = encryptionService.decrypt(secret_key) + except: + # Ignore exception. Value is not encrypted + print "Cert. Initialize recaptcha. Assuming that 'secret_key' in not encrypted" + + + self.recaptcha_creds = { 'site_key' : site_key, "secret_key" : secret_key } + print "Cert. Initialize recaptcha. Recaptcha is configured correctly" + + return True + else: + print "Cert. Initialize recaptcha. Recaptcha is disabled" + + return False + + def validateRecaptcha(self, recaptcha_response): + print "Cert. Validate recaptcha response" + + facesContext = CdiUtil.bean(FacesContext) + request = facesContext.getExternalContext().getRequest() + + remoteip = ServerUtil.getIpAddress(request) + print "Cert. Validate recaptcha response. remoteip: '%s'" % remoteip + + httpService = CdiUtil.bean(HttpService) + + http_client = httpService.getHttpsClient() + http_client_params = http_client.getParams() + http_client_params.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 15 * 1000) + + recaptcha_validation_url = "https://www.google.com/recaptcha/api/siteverify" + recaptcha_validation_request = urllib.urlencode({ "secret" : self.recaptcha_creds['secret_key'], "response" : recaptcha_response, "remoteip" : remoteip }) + recaptcha_validation_headers = { "Content-type" : "application/x-www-form-urlencoded", "Accept" : "application/json" } + + try: + http_service_response = httpService.executePost(http_client, recaptcha_validation_url, None, recaptcha_validation_headers, recaptcha_validation_request) + http_response = http_service_response.getHttpResponse() + except: + print "Cert. Validate recaptcha response. Exception: ", sys.exc_info()[1] + return False + + try: + if not httpService.isResponseStastusCodeOk(http_response): + print "Cert. Validate recaptcha response. Get invalid response from validation server: ", str(http_response.getStatusLine().getStatusCode()) + httpService.consume(http_response) + return False + + response_bytes = httpService.getResponseContent(http_response) + response_string = httpService.convertEntityToString(response_bytes) + httpService.consume(http_response) + finally: + http_service_response.closeConnection() + + if response_string == None: + print "Cert. Validate recaptcha response. Get empty response from validation server" + return False + + response = json.loads(response_string) + + return response["success"] + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None \ No newline at end of file diff --git a/oxAuth/Server/integrations/cert/docs/Cert design.dia b/oxAuth/Server/integrations/cert/docs/Cert design.dia new file mode 100644 index 0000000000000000000000000000000000000000..3952dea0b3cf9456964a5495c8161efc9d2b9265 GIT binary patch literal 11048 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 literal 0 HcmV?d00001 diff --git a/oxAuth/Server/integrations/cert/docs/Cert design.jpg b/oxAuth/Server/integrations/cert/docs/Cert design.jpg new file mode 100644 index 0000000000000000000000000000000000000000..555079788a9b0bc44b6aa3c8c9806e8c5bfb01a1 GIT binary patch literal 110176 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 UcxeXB_eKU_mn%(-ax(P)0BudkbpQYW literal 0 HcmV?d00001 diff --git a/oxAuth/Server/integrations/cert/sample/cert_creds.json b/oxAuth/Server/integrations/cert/sample/cert_creds.json new file mode 100644 index 00000000..2e7df116 --- /dev/null +++ b/oxAuth/Server/integrations/cert/sample/cert_creds.json @@ -0,0 +1,7 @@ +{ + "recaptcha":{ + "enabled":false, + "site_key":"", + "secret_key":"" + } +} diff --git a/oxAuth/Server/integrations/cert/sample/generated_certs.zip b/oxAuth/Server/integrations/cert/sample/generated_certs.zip new file mode 100644 index 0000000000000000000000000000000000000000..cbd235c2039654754ba7511757872f3c66563901 GIT binary patch literal 53946 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 literal 0 HcmV?d00001 diff --git a/oxAuth/Server/integrations/ciba/FirebaseEndUserNotification.py b/oxAuth/Server/integrations/ciba/FirebaseEndUserNotification.py new file mode 100644 index 00000000..2eda0c4d --- /dev/null +++ b/oxAuth/Server/integrations/ciba/FirebaseEndUserNotification.py @@ -0,0 +1,68 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2018, Gluu +# +# Author: Milton BO +# +# + +from org.gluu.oxauth.client.fcm import FirebaseCloudMessagingResponse +from org.gluu.oxauth.client.fcm import FirebaseCloudMessagingClient +from org.gluu.oxauth.client.fcm import FirebaseCloudMessagingRequest +from org.gluu.oxauth.util import RedirectUri +from org.gluu.model.custom.script.type.ciba import EndUserNotificationType +from java.lang import String +from java.util import UUID + +class EndUserNotification(EndUserNotificationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, configurationAttributes): + print "Firebase EndUserNotification script. Initializing ..." + print "Firebase EndUserNotification script. Initialized successfully" + + return True + + def destroy(self, configurationAttributes): + print "Firebase EndUserNotification script. Destroying ..." + print "Firebase EndUserNotification script. Destroyed successfully" + return True + + def getApiVersion(self): + return 1 + + # Returns boolean true or false depending on the process, if the notification + # is sent successfully or not. + def notifyEndUser(self, context): + print 'Sending push notification using Firebase Cloud Messaging' + appConfiguration = context.getAppConfiguration() + encryptionService = context.getEncryptionService() + clientId = appConfiguration.getBackchannelClientId() + redirectUri = appConfiguration.getBackchannelRedirectUri() + url = appConfiguration.getCibaEndUserNotificationConfig().getNotificationUrl() + key = encryptionService.decrypt(appConfiguration.getCibaEndUserNotificationConfig().getNotificationKey(), True) + to = context.getDeviceRegistrationToken() + title = "oxAuth Authentication Request" + body = "Client Initiated Backchannel Authentication (CIBA)" + + authorizationRequestUri = RedirectUri(appConfiguration.getAuthorizationEndpoint()) + authorizationRequestUri.addResponseParameter("client_id", clientId) + authorizationRequestUri.addResponseParameter("response_type", "id_token") + authorizationRequestUri.addResponseParameter("scope", context.getScope()) + authorizationRequestUri.addResponseParameter("acr_values", context.getAcrValues()) + authorizationRequestUri.addResponseParameter("redirect_uri", redirectUri) + authorizationRequestUri.addResponseParameter("state", UUID.randomUUID().toString()) + authorizationRequestUri.addResponseParameter("nonce", UUID.randomUUID().toString()) + authorizationRequestUri.addResponseParameter("prompt", "consent") + authorizationRequestUri.addResponseParameter("auth_req_id", context.getAuthReqId()) + + clickAction = authorizationRequestUri.toString() + + firebaseCloudMessagingRequest = FirebaseCloudMessagingRequest(key, to, title, body, clickAction) + firebaseCloudMessagingClient = FirebaseCloudMessagingClient(url) + firebaseCloudMessagingClient.setRequest(firebaseCloudMessagingRequest) + firebaseCloudMessagingResponse = firebaseCloudMessagingClient.exec() + + responseStatus = firebaseCloudMessagingResponse.getStatus() + print "CIBA: firebase cloud messaging result status " + str(responseStatus) + return (responseStatus >= 200 and responseStatus < 300 ) diff --git a/oxAuth/Server/integrations/compromised_password/compromised_password.py b/oxAuth/Server/integrations/compromised_password/compromised_password.py new file mode 100644 index 00000000..f52a00c9 --- /dev/null +++ b/oxAuth/Server/integrations/compromised_password/compromised_password.py @@ -0,0 +1,223 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2016, Gluu +# +# Author: Yuriy Movchan +# + +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.service import UserService, AuthenticationService +from org.gluu.util import StringHelper +import javax.crypto.spec.SecretKeySpec as SecretKeySpec +import javax.crypto.spec.IvParameterSpec as IvParameterSpec +import javax.crypto.Cipher +from javax.crypto import * +from org.gluu.util import ArrayHelper +from java.util import Arrays +import urllib, urllib2, json + +import java +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "compromised_password. Initialization" + if not configurationAttributes.containsKey("secret_question"): + print "compromised_password. Initialization. Property secret_question is mandatory" + return False + self.secretquestion = configurationAttributes.get("secret_question").getValue2() + + if not configurationAttributes.containsKey("credentials_file"): + print "credentials_file property not defined" + return False + self.credentialfile = configurationAttributes.get("credentials_file").getValue2() + + if not configurationAttributes.containsKey("secret_answer"): + print "compromised_password. Initialization. Property secret_answer is mandatory" + return False + self.secretanswer = configurationAttributes.get("secret_answer").getValue2() + print "compromised_password. Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "compromised_password. Destroy" + print "compromised_password. Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + identity = CdiUtil.bean(Identity) + userService = CdiUtil.bean(UserService) + authenticationService = CdiUtil.bean(AuthenticationService) + + if step == 1: + credentials = identity.getCredentials() + user_name = credentials.getUsername() + user_password = credentials.getPassword() + logged_in = False + if (StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password)): + userService = CdiUtil.bean(UserService) + logged_in = authenticationService.authenticate(user_name, user_password) + if (not logged_in): + return False + else: + find_user_by_uid = authenticationService.getAuthenticatedUser() + status_attribute_value = userService.getCustomAttribute(find_user_by_uid, "mail") + user_mail = status_attribute_value.getValue() + self.setRequestScopedParameters(identity) + isCompromised = False + isCompromised = self.is_compromised(user_mail,user_password,configurationAttributes) + if(isCompromised): + identity.setWorkingParameter("pwd_compromised", isCompromised) + identity.setWorkingParameter("user_name", user_name) + return True + else: + return True + elif step == 2: + print "compromised_password. Authenticate for step 2" + form_answer_array = requestParameters.get("loginForm:question") + if ArrayHelper.isEmpty(form_answer_array): + return False + form_answer = form_answer_array[0] + if (form_answer == self.secretanswer): + return True + return False + elif step == 3: + authenticationService = CdiUtil.bean(AuthenticationService) + print "compromised_password (with password update). Authenticate for step 3" + userService = CdiUtil.bean(UserService) + update_button = requestParameters.get("loginForm:updateButton") + new_password_array = requestParameters.get("new_password") + if ArrayHelper.isEmpty(new_password_array) or StringHelper.isEmpty(new_password_array[0]): + print "compromised_password (with password update). Authenticate for step 3. New password is empty" + return False + new_password = new_password_array[0] + + user = authenticationService.getAuthenticatedUser() + if user == None: + print "compromised_password (with password update). Authenticate for step 3. Failed to determine user name" + return False + + user_name = user.getUserId() + print "compromised_password (with password update). Authenticate for step 3. Attempting to set new user '" + user_name + "' password" + find_user_by_uid = userService.getUser(user_name) + if (find_user_by_uid == None): + print "compromised_password (with password update). Authenticate for step 3. Failed to find user" + return False + + find_user_by_uid.setAttribute("userPassword", new_password) + userService.updateUser(find_user_by_uid) + print "compromised_password (with password update). Authenticate for step 3. Password updated successfully" + logged_in = authenticationService.authenticate(user_name) + return True + + def prepareForStep(self, configurationAttributes, requestParameters, step): + identity = CdiUtil.bean(Identity) + self.setRequestScopedParameters(identity) + session_attributes = identity.getSessionId().getSessionAttributes() + pwdcompromised = session_attributes.get("pwd_compromised") + if(pwdcompromised != None): + if step == 1: + print "compromised_password. Prepare for step 1" + return True + elif step == 2: + print "compromised_password. Prepare for step 2" + return True + return False + else: + print "compromised_password. Prepare for step 1" + return True + + def getExtraParametersForStep(self, configurationAttributes, step): + return Arrays.asList("pwd_compromised","user_name") + + def getCountAuthenticationSteps(self, configurationAttributes): + identity = CdiUtil.bean(Identity) + self.setRequestScopedParameters(identity) + self.setRequestScopedParameters(identity) + session_attributes = identity.getSessionId().getSessionAttributes() + pwdcompromised = session_attributes.get("pwd_compromised") + if(pwdcompromised != None): + return 3 + return 1 + + def getPageForStep(self, configurationAttributes, step): + identity = CdiUtil.bean(Identity) + session_attributes = identity.getSessionId().getSessionAttributes() + pwdcompromised = session_attributes.get("pwd_compromised") + if(pwdcompromised != None): + if step == 2: + return "/auth/compromised/complogin.xhtml" + elif step == 3: + return "/auth/compromised/newpassword.xhtml" + return "" + else: + return "" + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True + + def setRequestScopedParameters(self, identity): + identity.setWorkingParameter("question_label", self.secretquestion) + + def is_compromised(self, userid, password,configurationAttributes): + print "Vericloud APIs Initialization" + + vericloud_gluu_creds_file = self.credentialfile + # Load credentials from file + f = open(vericloud_gluu_creds_file, 'r') + try: + creds = json.loads(f.read()) + except: + print "Vericloud API. Initialize notification services. Failed to load credentials from file:", vericloud_gluu_creds_file + return False + finally: + f.close() + + try: + url = str(creds["api_url"]) + api_key=str(creds["api_key"]) + api_secret= str(creds["api_secret"]) + except: + print "Vericloud API. Initialize notification services. Invalid credentials file '%s' format:" % super_gluu_creds_file + return False + + + reqdata = {"mode":"search_leaked_password_with_userid", "api_key": api_key, "api_secret": api_secret, "userid": userid} + reqdata = urllib.urlencode(reqdata) + resp = urllib2.urlopen(urllib2.Request(url, reqdata)).read() + resp = json.loads(resp) + if resp['result'] != 'succeeded': + return None + for pass_enc in resp['passwords_encrypted']: + plaintext = self.AESCipherdecrypt(api_secret, pass_enc) + if (len(password), password[0], password[-1]) == (len(plaintext), plaintext[0], plaintext[-1]) : + return True + return False + + def AESCipherdecrypt(self, key, enc ): + enc, iv = enc.split(':') + cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING") + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key.decode("hex"), "AES"),IvParameterSpec(iv.decode("hex"))) + decrypted_password = cipher.doFinal(enc.decode("hex")) + return decrypted_password.tostring() diff --git a/oxAuth/Server/integrations/compromised_password/readme.txt b/oxAuth/Server/integrations/compromised_password/readme.txt new file mode 100644 index 00000000..c40a14ac --- /dev/null +++ b/oxAuth/Server/integrations/compromised_password/readme.txt @@ -0,0 +1,7 @@ +This is a person authentication module for oxAuth to verify if user password has been compromised and allows user to change password immediately after providing answer to secret question set by admin. + +1) credentials_file = /etc/certs/vericloud_gluu_creds.json +2) secret_question = Set this parameter for the question to be disaplayed to user +3) secret_answer = Set this parameter for the answer to be provided by user to reset password + +Update vericloud_gluu_creds.json file with Vericloud API username and secret \ No newline at end of file diff --git a/oxAuth/Server/integrations/consent-gathering-with-redirection/ConsentGatheringSample_redirect.py b/oxAuth/Server/integrations/consent-gathering-with-redirection/ConsentGatheringSample_redirect.py new file mode 100644 index 00000000..b77a6dc3 --- /dev/null +++ b/oxAuth/Server/integrations/consent-gathering-with-redirection/ConsentGatheringSample_redirect.py @@ -0,0 +1,87 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2017, Gluu +# +# Author: Madhumita S +# + +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.authz import ConsentGatheringType +from org.gluu.util import StringHelper +from org.gluu.jsf2.service import FacesService +from org.gluu.jsf2.message import FacesMessages +from org.gluu.oxauth.util import ServerUtil + +import java +import random + +class ConsentGathering(ConsentGatheringType): + + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + + if (not configurationAttributes.containsKey("third_party_URL")): + print "Consent-Gathering. - Thirdparty URL. Initialization. Property third_party_URL is not specified" + return False + else: + self.third_party_URL = configurationAttributes.get("third_party_URL").getValue2() + + print "Consent-Gathering. Initializing ..." + print "Consent-Gathering. Initialized successfully" + + return True + + def destroy(self, configurationAttributes): + print "Consent-Gathering. Destroying ..." + print "Consent-Gathering. Destroyed successfully" + + return True + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def getApiVersion(self): + return 11 + + # Main consent-gather method. Must return True (if gathering performed successfully) or False (if fail). + # All user entered values can be access via Map context.getPageAttributes() + # context is reference of org.gluu.oxauth.service.external.context.ConsentGatheringContext + def authorize(self, step, context): + print "Consent-Gathering. Authorizing... %s " % step + + allow = ServerUtil.getFirstValue(context.getRequestParameters(), "allow") + print "allow : %s " % allow + if (allow is not None) and (allow == "true"): + print "Consent-Gathering. Authorization success for step 1" + return True + else: + print "Consent-Gathering. Authorization declined for step 1" + return False + + + def getNextStep(self, step, context): + return -1 + + def prepareForStep(self, step, context): + print "Consent-Gathering. prepare for step... %s" % step + + if not context.isAuthenticated(): + print "User is not authenticated. Aborting authorization flow ..." + return False + + print "Consent-Gathering. Redirecting to ... %s " % self.third_party_URL + facesService = CdiUtil.bean(FacesService) + facesService.redirectToExternalURL(self.third_party_URL ) + return True + + def getStepsCount(self, context): + return 1 + + def getPageForStep(self, step, context): + print "Consent-Gathering. getPageForStep... %s" % step + if step == 1: + return "/authz/redirect.xhtml" + + return "" diff --git a/oxAuth/Server/integrations/consent-gathering-with-redirection/README b/oxAuth/Server/integrations/consent-gathering-with-redirection/README new file mode 100644 index 00000000..0d3b9339 --- /dev/null +++ b/oxAuth/Server/integrations/consent-gathering-with-redirection/README @@ -0,0 +1,45 @@ + +## Developer notes: Redirecting to a third-party application in a custom script. + +In many cases of user authentication, consent gathering there might be a need to redirect to a third party application to perform some operation and redirect back to the Gluu server. +This can be done inside ```prepareForStep``` method of the custom script. + +### Steps for redirection in a ***Consent Gathering script***. - + +1. Return from def getPageForStep(self, step, context) a page /authz/method_name/redirect.html with content similar to the code snippet below - + +``` + def getPageForStep(self, step, context): + return "/authz/method_name/redirect.html" +``` + +``` +... + + + + +``` + +2. In method prepareForStep prepare data needed for redirect and do redirect to external service. + +``` +def prepareForStep(self, step, context): + facesService = CdiUtil.bean(FacesService) + facesService.redirectToExternalURL(self.third_party_URL ) + + return True + +``` + +3. In order to resume flow after the redirection we can add postauthorize.html. +In this new page we need make a call: +``` + + + +``` + +4. The action in step 3 takes us to the ``` def authorize(self, step, context) ```. Here you can use parameters from request, call external API to validate data if needed etc. And finally, return false/true from this method. + + diff --git a/oxAuth/Server/integrations/consent-gathering-with-redirection/postauthorize.xhtml b/oxAuth/Server/integrations/consent-gathering-with-redirection/postauthorize.xhtml new file mode 100644 index 00000000..d93a3ef2 --- /dev/null +++ b/oxAuth/Server/integrations/consent-gathering-with-redirection/postauthorize.xhtml @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/oxAuth/Server/integrations/consent-gathering-with-redirection/redirect.xhtml b/oxAuth/Server/integrations/consent-gathering-with-redirection/redirect.xhtml new file mode 100644 index 00000000..8a267a3a --- /dev/null +++ b/oxAuth/Server/integrations/consent-gathering-with-redirection/redirect.xhtml @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/oxAuth/Server/integrations/consent-gathering-with-redirection/third-party-mock-app/README.txt.txt b/oxAuth/Server/integrations/consent-gathering-with-redirection/third-party-mock-app/README.txt.txt new file mode 100644 index 00000000..75b88766 --- /dev/null +++ b/oxAuth/Server/integrations/consent-gathering-with-redirection/third-party-mock-app/README.txt.txt @@ -0,0 +1,6 @@ +1. place this html file inside a folder say sample-app inside /root folder + +2. run the following command to run an http server- + python3 -m http.server 8321 + +3. \ No newline at end of file diff --git a/oxAuth/Server/integrations/consent-gathering-with-redirection/third-party-mock-app/consent_page.html b/oxAuth/Server/integrations/consent-gathering-with-redirection/third-party-mock-app/consent_page.html new file mode 100644 index 00000000..b6ccea0c --- /dev/null +++ b/oxAuth/Server/integrations/consent-gathering-with-redirection/third-party-mock-app/consent_page.html @@ -0,0 +1,45 @@ + + +Sample consent + + + + + +
+

Consent form

+ +
+ +
+

Consent

+ +
+ + +
+
+ +
+
+ + + + + + + + + + + + + + + + +
+ + + + + diff --git a/oxAuth/Server/integrations/custom_registration/register.py b/oxAuth/Server/integrations/custom_registration/register.py new file mode 100644 index 00000000..94293860 --- /dev/null +++ b/oxAuth/Server/integrations/custom_registration/register.py @@ -0,0 +1,267 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2018, Gluu +# +# Author: Jose Gonzalez +# Author: Gasmyr Mougang + +from org.gluu.oxauth.model.common import User, WebKeyStorage +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.xdi.oxauth.security import Identity +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.service import UserService, AuthenticationService +from org.gluu.oxauth.util import ServerUtil +from org.gluu.util import StringHelper, ArrayHelper +from java.util import Arrays +from javax.faces.application import FacesMessage +from org.gluu.jsf2.message import FacesMessages +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from org.gluu.service import MailService + + +import org.codehaus.jettison.json.JSONArray as JSONArray + +import json, ast +import java +import random +import jarray +import smtplib + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + self.emailid = None + self.identity = CdiUtil.bean(Identity) + + def init(self, customScript, configurationAttributes): + + print "Register. Initialized successfully" + if not (configurationAttributes.containsKey("attributes_json_file_path")): + #print "Cert. Initialization. Property chain_cert_file_path is mandatory" + return False + self.attributes_json_file_path = configurationAttributes.get("attributes_json_file_path").getValue2() + + return True + + def destroy(self, configurationAttributes): + print "Register. Destroy" + print "Register. Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + + userService = CdiUtil.bean(UserService) + identity = CdiUtil.bean(Identity) + authenticationService = CdiUtil.bean(AuthenticationService) + + facesMessages = CdiUtil.bean(FacesMessages) + facesMessages.setKeepMessages() + + session_attributes = self.identity.getSessionId().getSessionAttributes() + form_passcode = ServerUtil.getFirstValue(requestParameters, "passcode") + + + print "Register. form_response_passcode: %s" % str(form_passcode) + + if step == 1: + print "inside step 1" + ufnm = ServerUtil.getFirstValue(requestParameters, "fnm") + ulnm = ServerUtil.getFirstValue(requestParameters, "lnm") + umnm = ServerUtil.getFirstValue(requestParameters, "mnm") + umail = ServerUtil.getFirstValue(requestParameters, "email") + upass = ServerUtil.getFirstValue(requestParameters, "pass") + + + + #rufnm1 = identity.getWorkingParameter("vufnm") + #print "rufnm" + #print rufnm1 + + #print "Register. Step 1 Password Authentication" + + + + # Generate Random six digit code and store it in array + code = random.randint(100000, 999999) + + + # Get code and save it in LDAP temporarily with special session entry + self.identity.setWorkingParameter("vufnm", ufnm) + self.identity.setWorkingParameter("vulnm", ulnm) + self.identity.setWorkingParameter("vumnm", umnm) + self.identity.setWorkingParameter("vumail", umail) + self.identity.setWorkingParameter("vupass", upass) + self.identity.setWorkingParameter("code", code) + + try: + mailService = CdiUtil.bean(MailService) + subject = "Registration Details" + + + body = "

Welcome


" + + if ufnm is not None: + body = body + "

First Name : "+str(ufnm)+",

" + + else: + body = body + + + if ulnm is not None: + body = body + "

Last Name "+str(ulnm)+",

" + + else: + body = body + + + if umnm is not None: + body = body + "

Middle Name "+str(umnm)+",

" + + else: + body = body + + body = body + "

Email : "+str(umail)+",

Password : "+str(upass)+",

Use "+str(code)+" OTP to finish Registration.

" + + mailService.sendMailSigned(umail, None, subject, body, body) + + return True + except Exception, ex: + facesMessages.add(FacesMessage.SEVERITY_ERROR,"Failed to send message to mobile phone") + print "Register. Error sending message to Twilio" + print "Register. Unexpected error:", ex + + return False + elif step == 2: + # Retrieve the session attribute + print "Register. Step 2 SMS/OTP Authentication" + code = session_attributes.get("code") + rufnm = identity.getWorkingParameter("vufnm") + rulnm = identity.getWorkingParameter("vulnm") + rumnm = identity.getWorkingParameter("vumnm") + rumail = identity.getWorkingParameter("vumail") + rupass = identity.getWorkingParameter("vupass") + + + + + + print "----------------------------------" + print "Register. Code: %s" % str(code) + print "----------------------------------" + + if code is None: + print "Register. Failed to find previously sent code" + return False + + if form_passcode is None: + print "Register. Passcode is empty" + return False + + if len(form_passcode) != 6: + print "Register. Passcode from response is not 6 digits: %s" % form_passcode + return False + + if form_passcode == code: + print "Register, SUCCESS! User entered the same code!" + + newUser = User() + newUser.setAttribute("givenName", rufnm) + newUser.setAttribute("sn", rulnm) + newUser.setAttribute("middleName", rumnm) + newUser.setAttribute("mail", rumail) + newUser.setAttribute("uid", rufnm) + newUser.setAttribute("userPassword", rupass) + userService.addUser(newUser, True) + + logged_in = False + logged_in = authenticationService.authenticate(rufnm, rupass) + + if (not logged_in): + return False + + return True + + #return True + + print "+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++" + print "Register. FAIL! User entered the wrong code! %s != %s" % (form_passcode, code) + print "+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++" + #facesMessages.add(facesMessage.SEVERITY_ERROR, "Incorrect Twilio code, please try again.") + + + + + + + + def prepareForStep(self, configurationAttributes, requestParameters, step): + if step == 1: + print "Register. Prepare for Step 1" + identity = CdiUtil.bean(Identity) + print self.getAttributesFromJson() + print "pass strength" + print self.getPasswordStrength() + identity.setWorkingParameter("CustomAtrributes", self.getAttributesFromJson()) + identity.setWorkingParameter("passStrength", str(self.getPasswordStrength())) + return True + elif step == 2: + print "Register. Prepare for Step 2" + return True + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + if step == 1: + return Arrays.asList("CustomAtrributes","PasswordStrength") + elif step == 2: + return Arrays.asList("code","vufnm","vulnm","vumnm","vumail","vupass") + + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + return 2 + + def getPageForStep(self, configurationAttributes, step): + if step == 1: + return "/auth/reg.xhtml" + elif step == 2: + return "/auth/otp_sms/otp_sms.xhtml" + + return "" + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True + + def getAttributesFromJson(self): + f = open(self.attributes_json_file_path) + data = json.load(f) + data = ast.literal_eval(json.dumps(data)) + attributes = data["en"].keys() + + jsonString = ",".join(attributes) + return jsonString + def getPasswordStrength(self): + f = open(self.attributes_json_file_path) + data = json.load(f) + data = ast.literal_eval(json.dumps(data)) + strength = data["passStrength"] + return strength diff --git a/oxAuth/Server/integrations/deduce/PasswordlessAuthenticationWithDeduceImpossTravel.py b/oxAuth/Server/integrations/deduce/PasswordlessAuthenticationWithDeduceImpossTravel.py new file mode 100644 index 00000000..3ed30499 --- /dev/null +++ b/oxAuth/Server/integrations/deduce/PasswordlessAuthenticationWithDeduceImpossTravel.py @@ -0,0 +1,563 @@ +# Author: Jose Gonzalez +# Author: Madhumita Subramaniam + +from java.lang import System +from java.net import URLDecoder, URLEncoder +from java.util import Arrays, ArrayList, Collections, HashMap +from org.gluu.oxauth.service import ClientService +from org.gluu.service.net import NetworkService +from org.gluu.oxauth.service.net import HttpService +from javax.faces.application import FacesMessage +from javax.servlet.http import Cookie +from javax.faces.context import FacesContext +from org.oxauth.persistence.model.configuration import GluuConfiguration +from org.gluu.oxauth.security import Identity +from org.gluu.oxauth.util import ServerUtil +from org.gluu.service import CacheService +from org.gluu.oxauth.service import AuthenticationService, UserService +from org.gluu.oxauth.service.custom import CustomScriptService +from org.gluu.model.custom.script import CustomScriptType +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.model import SimpleCustomProperty +from org.gluu.persist import PersistenceEntryManager +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.util import StringHelper +from java.time import LocalDateTime, Duration +from org.gluu.jsf2.message import FacesMessages + +try: + import json +except ImportError: + import simplejson as json +import sys + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + self.ACR_SG = "super_gluu" + self.PREV_LOGIN_SETTING = "prevLoginsCookieSettings" + self.modulePrefix = "pwdless-external_" + # expiration time in seconds - adds up to 1 year + self.lockExpirationTime = 31536000 + + + def init(self, customScript, configurationAttributes): + print "Deduce Passwordless. init called" + if not configurationAttributes.containsKey("DEDUCE_ENDPOINT"): + print "Deduce Passwordless. Initialization. Property DEDUCE_ENDPOINT is mandatory" + return False + self.DEDUCE_ENDPOINT = configurationAttributes.get("DEDUCE_ENDPOINT").getValue2() + + if not configurationAttributes.containsKey("DEDUCE_SITE"): + print "Deduce Passwordless. Initialization. Property DEDUCE_SITE is mandatory" + return False + self.DEDUCE_SITE = configurationAttributes.get("DEDUCE_SITE").getValue2() + + + if not configurationAttributes.containsKey("DEDUCE_API_KEY"): + print "Deduce Passwordless. Initialization. Property DEDUCE_API_KEY is mandatory" + return False + self.DEDUCE_API_KEY = configurationAttributes.get("DEDUCE_API_KEY").getValue2() + + self.authenticators = {} + self.uid_attr = self.getLocalPrimaryKey() + + self.prevLoginsSettings = self.computePrevLoginsSettings(configurationAttributes.get(self.PREV_LOGIN_SETTING)) + + custScriptService = CdiUtil.bean(CustomScriptService) + self.scriptsList = custScriptService.findCustomScripts(Collections.singletonList(CustomScriptType.PERSON_AUTHENTICATION), "oxConfigurationProperty", "displayName", "oxEnabled") + dynamicMethods = self.computeMethods(configurationAttributes.get("snd_step_methods"), self.scriptsList) + + if len(dynamicMethods) > 0: + print "Deduce Passwordless. init. Loading scripts for dynamic modules: %s" % dynamicMethods + + for acr in dynamicMethods: + moduleName = self.modulePrefix + acr + try: + external = __import__(moduleName, globals(), locals(), ["PersonAuthentication"], -1) + module = external.PersonAuthentication(self.currentTimeMillis) + + print "Deduce Passwordless. init. Got dynamic module for acr %s" % acr + configAttrs = self.getConfigurationAttributes(acr, self.scriptsList) + + if acr == self.ACR_SG: + application_id = configurationAttributes.get("supergluu_app_id").getValue2() + configAttrs.put("application_id", SimpleCustomProperty("application_id", application_id)) + + if module.init(None, configAttrs): + module.configAttrs = configAttrs + self.authenticators[acr] = module + else: + print "Deduce Passwordless. init. Call to init in module '%s' returned False" % moduleName + except: + print "Deduce Passwordless. init. Failed to load module %s" % moduleName + print "Exception: ", sys.exc_info()[1] + else: + print "Deduce Passwordless. init. Not enough custom scripts enabled. Check config property 'snd_step_methods'" + return False + + print "Deduce Passwordless. init. Initialized successfully" + return True + + def destroy(self, configurationAttributes): + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, configurationAttributes): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + print "Deduce Passwordless. authenticate for step %d" % step + + userService = CdiUtil.bean(UserService) + authenticationService = CdiUtil.bean(AuthenticationService) + identity = CdiUtil.bean(Identity) + + if step == 1: + user_name = identity.getCredentials().getUsername() + if StringHelper.isNotEmptyString(user_name): + + foundUser = userService.getUserByAttribute(self.uid_attr, user_name) + + if foundUser == None: + print "Deduce Passwordless. Unknown username '%s'" % user_name + elif authenticationService.authenticate(user_name): + availMethods = self.getAvailMethodsUser(foundUser) + + # deduce - impossible travel feature + identity = CdiUtil.bean(Identity) + #session_attributes = identity.getSessionId().getSessionAttributes() + ip = str( ServerUtil.getFirstValue(requestParameters, "loginForm:clientIP")).encode('utf-8') + email = str(foundUser.getAttribute("mail")).encode('utf-8') + print "platform %s" %str( ServerUtil.getFirstValue(requestParameters, "loginForm:platform")) + platform = json.loads(str( ServerUtil.getFirstValue(requestParameters, "loginForm:platform")).encode('utf-8')) + action = "auth.success" + impossibleTravel = self.sendInsightsRequest(ip, email, platform["name"], action) + if (impossibleTravel is True): + self.lockUser(user_name) + else: + if availMethods.size() > 0: + acr = availMethods.get(0) + print "Deduce Passwordless. Method to try in 2nd step will be: %s" % acr + + module = self.authenticators[acr] + logged_in = module.authenticate(module.configAttrs, requestParameters, step) + + if logged_in: + identity.setWorkingParameter("ACR", acr) + print "Deduce Passwordless. Authentication passed for step %d" % step + return True + + else: + self.setError("Cannot proceed. You don't have suitable credentials for passwordless login") + else: + self.setError("Wrong username or password") + else: + print "authenticate step 2" + user = authenticationService.getAuthenticatedUser() + if user == None: + print "Deduce Passwordless. authenticate for step 2. Cannot retrieve logged user" + return False + + #see alternative.xhtml + identity = CdiUtil.bean(Identity) + session_attributes = identity.getSessionId().getSessionAttributes() + alter = session_attributes.get("alternativeMethod") + if alter != None: + #bypass the rest of this step if an alternative method was provided. Current step will be retried (see getNextStep) + self.simulateFirstStep(requestParameters, alter) + return True + + session_attributes = identity.getSessionId().getSessionAttributes() + acr = session_attributes.get("ACR") + #this working parameter is used in alternative.xhtml + identity.setWorkingParameter("methods", self.getAvailMethodsUser(user, acr)) + + success = False + if acr in self.authenticators: + module = self.authenticators[acr] + success = module.authenticate(module.configAttrs, requestParameters, step) + + if success: + print "Deduce Passwordless. authenticate. 2FA authentication was successful" + if self.prevLoginsSettings != None: + self.persistCookie(user) + else: + print "Deduce Passwordless. authenticate. 2FA authentication failed" + + return success + + return False + + + def prepareForStep(self, configurationAttributes, requestParameters, step): + print "Deduce Passwordless. prepareForStep %d" % step + + identity = CdiUtil.bean(Identity) + session_attributes = identity.getSessionId().getSessionAttributes() + + if step == 1: + try: + loginHint = session_attributes.get("login_hint") + print "Deduce Passwordless. prepareForStep. Login hint is %s" % loginHint + isLoginHint = loginHint != None + + if self.prevLoginsSettings == None: + if isLoginHint: + identity.setWorkingParameter("loginHint", loginHint) + else: + users = self.getCookieValue() + + if isLoginHint: + + idx = self.findUid(loginHint, users) + if idx >= 0: + u = users.pop(idx) + users.insert(0, u) + else: + identity.setWorkingParameter("loginHint", loginHint) + + if len(users) > 0: + identity.setWorkingParameter("users", json.dumps(users, separators=(',',':'))) + + # In login.xhtml both loginHint and users are used to properly display the login form + except: + print "Deduce Passwordless. prepareForStep. Error!", sys.exc_info()[1] + + return True + + else: + user = CdiUtil.bean(AuthenticationService).getAuthenticatedUser() + + if user == None: + print "Deduce Passwordless. prepareForStep. Cannot retrieve logged user" + return False + + acr = session_attributes.get("ACR") + print "Deduce Passwordless. prepareForStep. ACR = %s" % acr + + identity.setWorkingParameter("methods", ArrayList(self.getAvailMethodsUser(user, acr))) + + if acr in self.authenticators: + module = self.authenticators[acr] + return module.prepareForStep(module.configAttrs, requestParameters, step) + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + + print "Deduce Passwordless. getExtraParametersForStep %d" % step + list = ArrayList() + if step > 1: + acr = CdiUtil.bean(Identity).getWorkingParameter("ACR") + + if acr in self.authenticators: + module = self.authenticators[acr] + params = module.getExtraParametersForStep(module.configAttrs, step) + if params != None: + list.addAll(params) + + list.addAll(Arrays.asList("ACR", "methods")) + print "extras are %s" % list + return list + + def getCountAuthenticationSteps(self, configurationAttributes): + return 2 + + def getPageForStep(self, configurationAttributes, step): + if step > 1: + identity = CdiUtil.bean(Identity) + acr = CdiUtil.bean(Identity).getWorkingParameter("ACR") + if acr in self.authenticators: + module = self.authenticators[acr] + page = module.getPageForStep(module.configAttrs, step) + + print "Deduce Passwordless. getPageForStep %d is %s" % (step, page) + return page + + return "/auth/deduce/loginD.xhtml" + + def getNextStep(self, configurationAttributes, requestParameters, step): + print "Deduce Passwordless. getNextStep called %d" % step + identity = CdiUtil.bean(Identity) + if step > 1: + session_attributes = identity.getSessionId().getSessionAttributes() + acr = session_attributes.get("alternativeMethod") + if acr != None: + print "Deduce Passwordless. getNextStep. Use alternative method %s" % acr + CdiUtil.bean(Identity).setWorkingParameter("ACR", acr) + #retry step with different acr + return 2 + return -1 + + def logout(self, configurationAttributes, requestParameters): + return True + +# Miscelaneous + + def getLocalPrimaryKey(self): + entryManager = CdiUtil.bean(PersistenceEntryManager) + config = GluuConfiguration() + config = entryManager.find(config.getClass(), "ou=configuration,o=gluu") + #Pick (one) attribute where user id is stored (e.g. uid/mail) + uid_attr = config.getOxIDPAuthentication().get(0).getConfig().getPrimaryKey() + print "Deduce Passwordless. init. uid attribute is '%s'" % uid_attr + return uid_attr + + + def setError(self, msg): + facesMessages = CdiUtil.bean(FacesMessages) + facesMessages.setKeepMessages() + facesMessages.clear() + facesMessages.add(FacesMessage.SEVERITY_ERROR, msg) + + + def computeMethods(self, sndStepMethods, scriptsList): + snd_step_methods = [] if sndStepMethods == None else StringHelper.split(sndStepMethods.getValue2(), ",") + methods = [] + + for m in snd_step_methods: + for customScript in scriptsList: + if customScript.getName() == m and customScript.isEnabled(): + methods.append(m) + + print "Deduce Passwordless. computeMethods. %s" % methods + return methods + + + def getConfigurationAttributes(self, acr, scriptsList): + + configMap = HashMap() + for customScript in scriptsList: + if customScript.getName() == acr: + for prop in customScript.getConfigurationProperties(): + configMap.put(prop.getValue1(), SimpleCustomProperty(prop.getValue1(), prop.getValue2())) + + print "Deduce Passwordless. getConfigurationAttributes. %d configuration properties were found for %s" % (configMap.size(), acr) + return configMap + + + def getAvailMethodsUser(self, user, skip=None): + methods = ArrayList() + + for method in self.authenticators: + try: + module = self.authenticators[method] + if module.hasEnrollments(module.configAttrs, user) and (skip == None or skip != method): + methods.add(method) + except: + print "Deduce Passwordless. getAvailMethodsUser. hasEnrollments call could not be issued for %s module" % method + print "Exception: ", sys.exc_info()[1] + + print "Deduce Passwordless. getAvailMethodsUser %s" % methods.toString() + return methods + + + def simulateFirstStep(self, requestParameters, acr): + #To simulate 1st step, there is no need to call: + # getPageforstep (no need as user/pwd won't be shown again) + # isValidAuthenticationMethod (by restriction, it returns True) + # prepareForStep (by restriction, it returns True) + # getExtraParametersForStep (by restriction, it returns None) + print "Deduce Passwordless. simulateFirstStep. Calling authenticate (step 1) for %s module" % acr + if acr in self.authenticators: + module = self.authenticators[acr] + auth = module.authenticate(module.configAttrs, requestParameters, 1) + print "Deduce Passwordless. simulateFirstStep. returned value was %s" % auth + + def computePrevLoginsSettings(self, customProperty): + settings = None + if customProperty == None: + print "Deduce Passwordless. Previous logins feature is not configured. Set config property '%s' if desired" % self.PREV_LOGIN_SETTING + else: + try: + settings = json.loads(customProperty.getValue2()) + if settings['enabled']: + print "Deduce Passwordless. PrevLoginsSettings are %s" % settings + else: + settings = None + print "Deduce Passwordless. Previous logins feature is disabled" + except: + print "Deduce Passwordless. Unparsable config property '%s'" % self.PREV_LOGIN_SETTING + + return settings + + def getCookieValue(self): + ulist = [] + coo = None + httpRequest = ServerUtil.getRequestOrNull() + + if httpRequest != None: + for cookie in httpRequest.getCookies(): + if cookie.getName() == self.prevLoginsSettings['cookieName']: + coo = cookie + + if coo == None: + print "Deduce Passwordless. getCookie. No cookie found" + else: + print "Deduce Passwordless. getCookie. Found cookie" + forgetMs = self.prevLoginsSettings['forgetEntriesAfterMinutes'] * 60 * 1000 + + try: + now = System.currentTimeMillis() + value = URLDecoder.decode(coo.getValue(), "utf-8") + # value is an array of objects with properties: uid, displayName, lastLogon + value = json.loads(value) + + for v in value: + if now - v['lastLogon'] < forgetMs: + ulist.append(v) + # print "==========", ulist + except: + print "Deduce Passwordless. getCookie. Unparsable value, dropping cookie..." + + return ulist + + + def findUid(self, uid, users): + + i = 0 + idx = -1 + for user in users: + if user['uid'] == uid: + idx = i + break + i+=1 + return idx + + + def persistCookie(self, user): + try: + now = System.currentTimeMillis() + uid = user.getUserId() + dname = user.getAttribute("displayName") + + users = self.getCookieValue() + idx = self.findUid(uid, users) + + if idx >= 0: + u = users.pop(idx) + else: + u = { 'uid': uid, 'displayName': '' if dname == None else dname } + u['lastLogon'] = now + + # The most recent goes first :) + users.insert(0, u) + + excess = len(users) - self.prevLoginsSettings['maxListSize'] + if excess > 0: + print "Deduce Passwordless. persistCookie. Shortening list..." + users = users[:self.prevLoginsSettings['maxListSize']] + + value = json.dumps(users, separators=(',',':')) + value = URLEncoder.encode(value, "utf-8") + coo = Cookie(self.prevLoginsSettings['cookieName'], value) + coo.setSecure(True) + coo.setHttpOnly(True) + # One week + coo.setMaxAge(7 * 24 * 60 * 60) + + response = self.getHttpResponse() + if response != None: + print "Deduce Passwordless. persistCookie. Adding cookie to response" + response.addCookie(coo) + except: + print "Deduce Passwordless. persistCookie. Exception: ", sys.exc_info()[1] + + + def getHttpResponse(self): + try: + return FacesContext.getCurrentInstance().getExternalContext().getResponse() + except: + print "Deduce Passwordless. Error accessing HTTP response object: ", sys.exc_info()[1] + return None + + def getSessionAttribute(self, attribute_name): + identity = CdiUtil.bean(Identity) + # Try to get attribute value from Seam event context + if identity.isSetWorkingParameter(attribute_name): + return identity.getWorkingParameter(attribute_name) + # Try to get attribute from persistent session + session_id = identity.getSessionId() + if session_id == None: + return None + session_attributes = session_id.getSessionAttributes() + if session_attributes == None: + return None + if session_attributes.containsKey(attribute_name): + return session_attributes.get(attribute_name) + return None + + def sendInsightsRequest(self, ip, email, platform, action): + httpService = CdiUtil.bean(HttpService) + http_client = httpService.getHttpsClient() + http_client_params = http_client.getParams() + + data = { "site":self.DEDUCE_SITE,"apikey":self.DEDUCE_API_KEY,"ip":ip, "email":email,"user_agent":platform, "action": action } + payload = json.dumps(data) + headers = { "Accept" : "application/json" } + print "Deduce Passwordless. payload %s " % payload + try: + http_service_response = httpService.executePost(http_client, self.DEDUCE_ENDPOINT, None, headers, payload) + http_response = http_service_response.getHttpResponse() + print "http_response %s" % http_response + except: + print "Deduce Passwordless. Exception: ", sys.exc_info()[1] + return False + + try: + if not httpService.isResponseStastusCodeOk(http_response): + print "Deduce Passwordless. Got invalid response from validation server: ", str(http_response.getStatusLine().getStatusCode()) + httpService.consume(http_response) + return False + + response_bytes = httpService.getResponseContent(http_response) + response_string = httpService.convertEntityToString(response_bytes) + print "response_string %s" %response_string + httpService.consume(http_response) + if ("IMPOSSIBLE_TRAVEL" in response_string): + return True + finally: + http_service_response.closeConnection() + + if response_string is None: + print "Deduce Passwordless. Got empty response from validation server" + return False + + return False + + def lockUser(self, user_name): + if StringHelper.isEmpty(user_name): + return None + + userService = CdiUtil.bean(UserService) + cacheService= CdiUtil.bean(CacheService) + + find_user_by_uid = userService.getUser(user_name) + if (find_user_by_uid == None): + return None + + status_attribute_value = userService.getCustomAttribute(find_user_by_uid, "gluuStatus") + if status_attribute_value != None: + user_status = status_attribute_value.getValue() + if StringHelper.equals(user_status, "inactive"): + print "Deduce Passwordless. (lock account). Lock user. User '%s' locked already" % user_name + return + + userService.setCustomAttribute(find_user_by_uid, "gluuStatus", "inactive") + updated_user = userService.updateUser(find_user_by_uid) + + object_to_store = json.dumps({'locked': True, 'created': LocalDateTime.now().toString()}, separators=(',',':')) + + cacheService.put(StringHelper.toString(self.lockExpirationTime), "lock_user_"+user_name, object_to_store); + self.setError( "Impossible travel detected. Your account has been locked. Please contact the administrator") + + print "Deduce Passwordless. (lock account). Lock user. User '%s' locked" % user_name \ No newline at end of file diff --git a/oxAuth/Server/integrations/deduce/README_PASSWORDLESS_AUTHN_WITH_DEDUCE.md b/oxAuth/Server/integrations/deduce/README_PASSWORDLESS_AUTHN_WITH_DEDUCE.md new file mode 100644 index 00000000..4b33482a --- /dev/null +++ b/oxAuth/Server/integrations/deduce/README_PASSWORDLESS_AUTHN_WITH_DEDUCE.md @@ -0,0 +1,111 @@ +# Integrating Impossible travel feature by Deduce Insights in Passwordless Authentication flow. + +## Overview +[Deduce](https://www.deduce.com/) is a cybersecurity company built on the premise of using data for good. We’ve repeatedly seen the impact of bad actors working together to cause damage and compromise customers and organizations alike. Deduce democratizes technologies and shifts the advantage back to the security community. + +Deduce Identity Insights acts as a cyber security radar for validating good users and detecting fraudulent activity. The platform provides actionable data in relation to a user’s digital activity to enable smarter, more accurate security decisions. The Impossible travel alert which is a part of Insights service is raised when a user travels to a new location from a previous location within an unrealistic timeframe. + +This document explains how to use the Gluu Server's included interception script that integrates this feature "Impossible travel " into the passwordless authentication flow. + +This interception script allows administrators to deploy a passwordless authentication flow in Gluu Server. In short the flow works as follows: + +- A form is shown where a username is prompted +- A query is issued in the underlying database for credentials that potentially may be employed as a second factor +- A form is shown where the user must present a certain credential in order to gain access. Depending on the available credentials and configuration, the user may choose to present a different alternative credential +- Once the second factor is presented and validated successfully, the user's browser is redirected to the target application + +Additionally, there are some features worth noting: + +- [login hint](#login-hint) support +- [Account choice](#account-choice) +- Configurable [authentication mechanisms for second factor](#authentication-mechanisms-for-second-factor) + +## Flow setup + +### Requirements + +- Ensure you have a running instance of Gluu Server 4.3 +- While not a requisite, usage of [Gluu Casa](https://casa.gluu.org) is highly recommended as part of your 2FA solution. Among others this app helps users to enroll their authentication credentials which is a key aspect for passwordless authentication to take place. + +### Enable 2FA-related scripts + +1. Log in to oxTrust with admin credentials +2. Visit `Configuration` > `Person Authentication Scripts`, click on `fido2` and ensure the script is flagged as enabled +3. If you want to support [Super Gluu](https://super.gluu.org/home/) as second factor too, enable the `super_gluu` script. Support for biometric authentication is available as well, for this purpose follow [these instructions](https://www.gluu.org/docs/gluu-server/authn-guide/BioID/) + + + +### Add the passwordless script + +1. Log in to oxTrust with admin credentials +2. Visit `Configuration` > `Person Authentication Scripts`. At the bottom click on `Add custom script configuration` and fill values as follows: + - For `name` use a meaningful identifier, like `passwordless` + - In the `script` field use the contents of this [file](https://github.com/GluuFederation/oxAuth/raw/version_4.2.3/Server/integrations/passwordless/PasswordlessAuthenticationWithDeduceImpossTravel.py) + - Tick the `enabled` checkbox + - For the rest of fields, you can accept the defaults +3. Click on `Add new property`. On the left type `snd_step_methods`, on the right use `fido2,super_gluu` or whatever suits your needs best. See [Authentication mechanisms for second factor](#authentication-mechanisms-for-second-factor) for more +4. The script has the following additional properties. Add the properties to the passwordless script. + +| Property | Description | Example | +|-----------------------|-------------------------------|---------------| +|DEDUCE_ENDPOINT |API endpoint provided by Deduce. |`https://api.deducesecurity.com/insights`| +|DEDUCE_API_KEY |API Key provided by Deduce. |`87d55ba11acd1a23ee6c054f3460154e`| +|DEDUCE_SITE |site provided by Deduce. |`site name`| + +5. If `super_gluu` was listed in the previous step, click on `Add new property`. On the left type `supergluu_app_id`, on the right use `https:///casa`. This is the URL (aka application ID) that Super Gluu enrollments are already (or will be) associated to. +6. Scroll down and click on the `Update` button at the bottom of the page + +**Notes:** + +If you want to support Account Choice see the [corresponding section](#account-choice). + +### Transfer script assets to your server + +Extract [this file](https://github.com/GluuFederation/oxAuth/raw/version_4.2.3/Server/integrations/passwordless/bundle.zip) to the root (ie. `/`) of your Gluu server. In a standard CE installation this means extraction should take place under `/opt/gluu-server`. + +The zip file contains UI pages (forms), associated javascript and CSS files, as well as miscellaneous python code required for the flow to run properly. When extracting use the `root` user. + +## Login Hint + +When the authentication request that triggers the authentication contains the `login_hint` parameter (see http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest), this value is used to automatically populate the username input field in the initial form. + +## Account Choice + +This feature shows a list of selectable usernames in order to save users some typing. The list is populated with usernames that were employed in recent successful login events in the given browser. In addition to the list, a "Use another account" link allows users to enter a different username if needed. + +In order to configure this feature follow the steps below: + +1. Log into oxTrust with admin credentials +2. Visit `Configuration` > `Person Authentication Scripts` and select the recently created entry for the passwordless script +3. Scroll down and click on `Add new property` +4. In the empty text field appearing on the left type `prevLoginsCookieSettings` +5. In the field on the right hand side paste the following: + +``` +{"enabled": true, "maxListSize": 4, "forgetEntriesAfterMinutes": 10080, "cookieName": "pwdlesscookie"} +``` +6. Scroll down and click on the `Update` button + +Note how the JSON content helps drive the behavior of account choice: + +- `enabled` turns on and off this feature +- `maxListSize` helps keeping the list small. Only the most recent usernames will be shown up to the limit set by this property +- `forgetEntriesAfterMinutes` is used to make individual list entries expire: if the last successful login attempt for a user took place long ago, it will not be part of the list anymore. The value provided in the example above corresponds to one week +- `cookieName`. Account choice is implemented by means of a browser cookie. This property is used to control its name. + +Account choice also works in conjunction with [Login hint](#login-hint). If the given hint matches any of the remembered usernames, such username will appear first in the list, otherwise the hint will be shown once the "Use another account" link is clicked. + +## Authentication mechanisms for second factor + +In a passwordless scenario you may want to offer a trusted/restricted set of authentication methods for use in the second step. A popular choice for this is FIDO. The passwordless flow offered by Gluu also supports [Super Gluu](https://super.gluu.org/home/) as well as Biometric authentication by [BioID](https://www.bioid.com/). + +Please note the `snd_step_methods` custom property of the passwordless interception script in oxTrust. It contains a comma-separated list of identifiers of authentication methods that will be part of the second step of the flow. Note order is relevant: a method appearing first is preferred (prompted) over one appearing further in the list. + +## Test + +Create one or more users for testing. These users should have already enrolled credentials belonging to one or more of the methods listed in `snd_step_methods` property of the script. For this purpose [Casa](https://casa.gluu.org) is a natural choice. + +In a testing RP (eg. web application) issue authentication requests such that the `acr_values` parameter is set to the name of the passwordless script. Parameter `login_hint` can optionally be set. + +!!! Note + Users without credentials belonging to any of the methods in `snd_step_methods` won't get past the first step of the flow. diff --git a/oxAuth/Server/integrations/duo-universal-prompt/DuoUniversalPromptExternalAuthenticator.py b/oxAuth/Server/integrations/duo-universal-prompt/DuoUniversalPromptExternalAuthenticator.py new file mode 100644 index 00000000..52754158 --- /dev/null +++ b/oxAuth/Server/integrations/duo-universal-prompt/DuoUniversalPromptExternalAuthenticator.py @@ -0,0 +1,177 @@ +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.service import AuthenticationService +from org.gluu.oxauth.service.common import UserService +from org.gluu.util import ArrayHelper +from org.gluu.util import StringHelper +from java.util import Arrays +from javax.faces.context import FacesContext +from org.gluu.oxauth.service.net import HttpService +import os +import java +import sys +from com.duosecurity import Client +from com.duosecurity.exception import DuoException +from com.duosecurity.model import Token +from org.gluu.jsf2.service import FacesService +from org.gluu.jsf2.message import FacesMessages +from org.gluu.oxauth.util import ServerUtil + + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Duo-Universal. Initialization" + + if (not configurationAttributes.containsKey("client_id")): + print "Duo Universal. Initialization. Property client_id is not specified" + return False + else: + self.client_id = configurationAttributes.get("client_id").getValue2() + + if (not configurationAttributes.containsKey("client_secret")): + print "Duo Universal. Initialization. Property client_secret is not specified" + return False + else: + self.client_secret = configurationAttributes.get("client_secret").getValue2() + + if (not configurationAttributes.containsKey("api_hostname")): + print "Duo Universal. Initialization. Property api_hostname is not specified" + return False + else: + self.api_hostname = configurationAttributes.get("api_hostname").getValue2() + + print "Duo-Universal. Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "Duo-Universal. Destroy" + print "Duo-Universal. Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + print "Duo-Universal. Authenticate for step %s" % step + + identity = CdiUtil.bean(Identity) + if (step == 1): + authenticationService = CdiUtil.bean(AuthenticationService) + + # Check if user authenticated already in another custom script + user = authenticationService.getAuthenticatedUser() + + if user == None: + print "user is none" + credentials = identity.getCredentials() + + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + logged_in = False + if (StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password)): + userService = CdiUtil.bean(UserService) + logged_in = authenticationService.authenticate(user_name, user_password) + + if (not logged_in): + print "return false" + return False + identity.setWorkingParameter('username',user_name) + return True + + elif (step == 2): + + identity = CdiUtil.bean(Identity) + + state = ServerUtil.getFirstValue(requestParameters, "state") + # Get state to verify consistency and originality + if identity.getWorkingParameter('state_duo') == state : + + # Get authorization token to trade for 2FA + duoCode = ServerUtil.getFirstValue(requestParameters, "duo_code") + try: + token = self.duo_client.exchangeAuthorizationCodeFor2FAResult(duoCode, identity.getWorkingParameter('username')) + print "token status %s " % token.getAuth_result().getStatus() + except: + # Handle authentication failure. + print "authentication failure", sys.exc_info()[1] + return False + + # User successfully passed Duo authentication. + + if "allow" == token.getAuth_result().getStatus(): + return True + + return False + + else: + print "Neither step 1 or 2" + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + print "Duo-Universal. Prepare for step %s" % step + + if (step == 1): + return True + elif (step == 2): + identity = CdiUtil.bean(Identity) + user_name = identity.getWorkingParameter('username') + facesContext = CdiUtil.bean(FacesContext) + request = facesContext.getExternalContext().getRequest() + httpService = CdiUtil.bean(HttpService) + url = httpService.constructServerUrl(request) + "/postlogin.htm" + + try: + print "before health check" + self.duo_client = Client(self.client_id,self.client_secret,self.api_hostname,url) + self.duo_client.healthCheck() + print "after health check" + except: + print "Duo-Universal. Duo config error. Verify the values in Duo-Universal.conf are correct ", sys.exc_info()[1] + return False + + state = self.duo_client.generateState() + identity.setWorkingParameter("state_duo",state) + prompt_uri = self.duo_client.createAuthUrl(user_name, state) + + facesService = CdiUtil.bean(FacesService) + facesService.redirectToExternalURL(prompt_uri ) + + return True + + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + return Arrays.asList("state_duo", "username") + + def getCountAuthenticationSteps(self, configurationAttributes): + return 2 + + def getPageForStep(self, configurationAttributes, step): + print "Duo-Universal. getPageForStep - %s " % step + return "" + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True + diff --git a/oxAuth/Server/integrations/duo-universal-prompt/README.txt b/oxAuth/Server/integrations/duo-universal-prompt/README.txt new file mode 100644 index 00000000..dbf34955 --- /dev/null +++ b/oxAuth/Server/integrations/duo-universal-prompt/README.txt @@ -0,0 +1,98 @@ +# Duo Security using Universal Prompt +## Overview +[Duo Security](https://duosecurity.com) is a SaaS authentication provider. This document will explain how to use Gluu's [Duo interception script](https://github.com/GluuFederation/oxAuth/blob/master/Server/integrations/duo-universal-prompt/DuoUniversalPromptExternalAuthenticator.py) to configure the Gluu Server for a two-step authentication process with username and password as the first step, and Duo as the second step. The script invokes the Universal Prompt which is a redesign of Duo’s traditional authentication prompt. + +In order to use this authentication mechanism your organization will need a Duo account and users will need to download the Duo mobile app. + +## Prerequisites +- A Gluu Server ([installation instructions](../installation-guide/index.md)); +- [Duo interception script](https://github.com/GluuFederation/oxAuth/blob/master/Server/integrations/duo-universal-prompt/DuoUniversalPromptExternalAuthenticator.py) (included in the default Gluu Server distribution); +- An account with [Duo Security](https://duo.com/). + + +## Configure Duo AccountS + +1. [Sign up](https://duo.com/) for a Duo account. + +2. Log in to the Duo Admin Panel and navigate to Applications. + +3. Click Protect an Application and locate Web SDK in the applications list. Click Protect this Application to get your client ID, secret key, and API hostname. + +For additional info for the steps refer to Duo's Web SDK 4, check [this article](https://duo.com/docs/duoweb-v4). + +## Add the duo-universal Dependency to your oxAuth + +Note: The dependencies have to be added seperately as mentioned in the steps below. Using a fat jar (duo-universal-sdk-1.0.3-with-dependencies.jar leads to conflicts.) + 1. Copy these jar files to the following oxAuth folder inside the Gluu Server chroot: /opt/gluu/jetty/oxauth/custom/libs + Dependency jars : + [duo-universal-sdk-1.0.3.jar](https://repo1.maven.org/maven2/com/duosecurity/duo-universal-sdk/1.0.3/duo-universal-sdk-1.0.3.jar) , + [converter-jackson-2.1.0.jar](https://repo1.maven.org/maven2/com/squareup/retrofit2/converter-jackson/2.1.0/converter-jackson-2.1.0.jar) , + [java-jwt-3.3.0.jar] (https://repo1.maven.org/maven2/com/auth0/java-jwt/3.3.0/java-jwt-3.3.0.jar), + [logging-interceptor-3.3.1.jar](https://repo1.maven.org/maven2/com/squareup/okhttp3/logging-interceptor/3.3.1/logging-interceptor-3.3.1.jar), + [lombok-1.18.16.jar](https://repo1.maven.org/maven2/org/projectlombok/lombok/1.18.16/lombok-1.18.16.jar), + [retrofit-2.5.0.jar](https://repo1.maven.org/maven2/com/squareup/retrofit2/retrofit/2.5.0/retrofit-2.5.0.jar), + [okio-2.9.0.jar](https://repo1.maven.org/maven2/com/squareup/okio/okio/2.9.0/okio-2.9.0.jar), + [okhttp-3.12.0.jar](https://repo1.maven.org/maven2/com/squareup/okhttp3/okhttp/3.12.0/okhttp-3.12.0.jar), + [kotlin-stdlib-1.4.21.jar](https://repo1.maven.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.4.21/kotlin-stdlib-1.4.21.jar) + + 1. Edit /opt/gluu/jetty/oxauth/webapps/oxauth.xml and add the following line: +./custom/libs/duo-universal-sdk-1.0.3.jar,./custom/libs/converter-jackson-2.1.0.jar,./custom/libs/java-jwt-3.3.0.jar,./custom/libs/logging-interceptor-3.3.1.jar,./custom/libs/lombok-1.18.16.jar,./custom/libs/retrofit-2.5.0.jar,./custom/libs/okio-2.9.0.jar,./custom/libs/okhttp-3.12.0.jar,./custom/libs/kotlin-stdlib-1.4.21.jar + + + 1. Restart the oxauth service + + +## Configure oxTrust + +Follow the steps below to configure the Duo module in the oxTrust Admin GUI. + +1. Navigate to `Configuration` > `Person Authentication Scripts`. + Add a custom script for the 2 factor authentication using DUO credentials and name it duo2 (specifically because this is version 2 ). + + +1. Add the following Custom Property ( key/value pairs ) + + +| Property |Status | Description | Example | +|-----------------------|---------------|-----------------------|-----------------------| +|api_hostname |Mandatory |URL of the Duo API Server|api-random.duosecurity.com| +|client_id |Mandatory |Value from the Duo application using Web SDK 4 that was registered using DUO Admin console|DI3ICTTJKLL8PPPNGH7YI| +|client_secret |Mandatory|Value from the Duo application using Web SDK 4 that was registered using DUO Admin console|eEbJdi3hg42zxyFYbHArU5RuioPP| + +1. Enable the script by ticking the check box +![enable](../img/admin-guide/enable.png) + +Now Duo is an available authentication mechanism for your Gluu Server. This means that, using OpenID Connect `acr_values`, applications can now request Duo authentication for users. + +!!! Note + To make sure Duo has been enabled successfully, you can check your Gluu Server's OpenID Connect configuration by navigating to the following URL: `https:///.well-known/openid-configuration`. Find `"acr_values_supported":` and you should see `"duo2"`. + +## Make Duo the Default Authentication Mechanism + +Now applications can request Duo authentication, but what if you want to make Duo your default authentication mechanism? You can follow these instructions: + +1. Navigate to `Configuration` > `Manage Authentication`. +2. Select the `Default Authentication Method` tab. +3. In the Default Authentication Method window you will see two options: `Default acr` and `oxTrust acr`. + + - The `oxTrust acr` field controls the authentication mechanism that is presented to access the oxTrust dashboard GUI (the application you are in). + - The `Default acr` field controls the default authentication mechanism that is presented to users from all applications that leverage your Gluu Server for authentication. + +You can change one or both fields to Duo authentication as you see fit. If you want Duo to be the default authentication mechanism for access to oxTrust and all other applications that leverage your Gluu Server, change both fields to Duo. + +!!! Note + Currently, the DUO Universal Prompt has not yet been released. However, DUO has enabled customers to be application ready so that the switch to the newer User Interface can be seamless. + +## Upgrading to the DUO Universal Prompt from the older user interface for enrolling DUO credentials + +### In DUO Admin Console: +1. Register a new Duo's Web SDK 4 application, check [this article](https://duo.com/docs/duoweb-v4). +1. Save the client id and secret + +### In oxTrust : +1. Update the original duo script to reflect the latest contents. +1. Add script properties as mentioned in the above steps. The client ID and client secret can be obtained from the Web SDK 4 application of the DUO admin console. + +### Add the duo-universal Dependency to your oxAuth +Follow the exact steps mentioned previously in the document + diff --git a/oxAuth/Server/integrations/duo.passport.combine/Passport_and_Duo-Universal.py b/oxAuth/Server/integrations/duo.passport.combine/Passport_and_Duo-Universal.py new file mode 100644 index 00000000..f954c0b2 --- /dev/null +++ b/oxAuth/Server/integrations/duo.passport.combine/Passport_and_Duo-Universal.py @@ -0,0 +1,802 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2019, Gluu +# Author: Jose Gonzalez +# Author: Yuriy Movchan +# Co-Author: Md Mostafejur Rahman + +from org.gluu.jsf2.service import FacesService +from org.gluu.jsf2.message import FacesMessages + +from org.gluu.oxauth.model.common import User, WebKeyStorage +from org.gluu.oxauth.model.configuration import AppConfiguration +from org.gluu.oxauth.model.crypto import CryptoProviderFactory +from org.gluu.oxauth.model.jwt import Jwt, JwtClaimName +from org.gluu.oxauth.model.util import Base64Util +from org.gluu.oxauth.service import AppInitializer, AuthenticationService +from org.gluu.oxauth.service.common import UserService, EncryptionService +from org.gluu.oxauth.service.net import HttpService +from org.gluu.oxauth.security import Identity +from org.gluu.oxauth.util import ServerUtil +from org.gluu.config.oxtrust import LdapOxPassportConfiguration +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.persist import PersistenceEntryManager +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.util import StringHelper +from java.util import ArrayList, Arrays, Collections + +from javax.faces.application import FacesMessage +from javax.faces.context import FacesContext + +import duo_web +import json +import sys +import datetime +from com.duosecurity import Client +from com.duosecurity.exception import DuoException +from com.duosecurity.model import Token +from org.gluu.jsf2.service import FacesService +from org.gluu.jsf2.message import FacesMessages +from org.gluu.oxauth.util import ServerUtil + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Passport. and Duo-Universal init called" + self.duo_flow = False + self.passport_flow = False + self.countStep = 0 + self.extensionModule = self.loadExternalModule(configurationAttributes.get("extension_module")) + extensionResult = self.extensionInit(configurationAttributes) + if extensionResult != None: + return extensionResult + + print "Passport. init. Behaviour is social" + success = self.processKeyStoreProperties(configurationAttributes) + + if success: + self.providerKey = "provider" + self.customAuthzParameter = self.getCustomAuthzParameter(configurationAttributes.get("authz_req_param_provider")) + self.passportDN = self.getPassportConfigDN() + print "Passport. init. Initialization success" + else: + print "Passport. init. Initialization failed" + + + print "Duo-Universal. Initialization" + + if (not configurationAttributes.containsKey("client_id")): + print "Duo Universal. Initialization. Property client_id is not specified" + return False + else: + self.client_id = configurationAttributes.get("client_id").getValue2() + + if (not configurationAttributes.containsKey("client_secret")): + print "Duo Universal. Initialization. Property client_secret is not specified" + return False + else: + self.client_secret = configurationAttributes.get("client_secret").getValue2() + + if (not configurationAttributes.containsKey("api_hostname")): + print "Duo Universal. Initialization. Property api_hostname is not specified" + return False + else: + self.api_hostname = configurationAttributes.get("api_hostname").getValue2() + + print "Duo-Universal. Initialized successfully" + print "Passport. and Duo-Universal Initialized successfully" + + return success + + def destroy(self, configurationAttributes): + print "Passport. and Duo-Universal destroy called" + return True + + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + + def authenticate(self, configurationAttributes, requestParameters, step): + + extensionResult = self.extensionAuthenticate(configurationAttributes, requestParameters, step) + if extensionResult != None: + return extensionResult + + print "Passport. and Duo-Universal. authenticate for step %s called" % str(step) + identity = CdiUtil.bean(Identity) + authenticationService = CdiUtil.bean(AuthenticationService) + # Loading self.registeredProviders in case passport destroyed + if not hasattr(self,'registeredProviders'): + print "Passport. Fetching registered providers." + self.parseProviderConfigs() + + if step == 1: + # Get JWT token + jwt_param = ServerUtil.getFirstValue(requestParameters, "user") + + if jwt_param != None: + print "Passport. authenticate for step 1. JWT user profile token found" + self.setDuoOrPassportFlow('passportFlow') + print "Is PassportFlow : %s"%identity.getWorkingParameter('passportFlow') + # Parse JWT and validate + jwt = Jwt.parse(jwt_param) + if not self.validSignature(jwt): + return False + + if self.jwtHasExpired(jwt): + return False + + (user_profile, jsonp) = self.getUserProfile(jwt) + if user_profile == None: + return False + + sessionAttributes = identity.getSessionId().getSessionAttributes() + self.skipProfileUpdate = StringHelper.equalsIgnoreCase(sessionAttributes.get("skipPassportProfileUpdate"), "true") + + return self.attemptAuthentication(identity, user_profile, jsonp) + + #See passportlogin.xhtml + provider = ServerUtil.getFirstValue(requestParameters, "loginForm:provider") + # Check if user authenticated already in another custom script + user = authenticationService.getAuthenticatedUser() + if StringHelper.isEmpty(provider): + + #it's username + passw auth + print "Passport. and Duo-Universal authenticate for step 1. Basic authentication detected" + logged_in = False + + credentials = identity.getCredentials() + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + if StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password): + authenticationService = CdiUtil.bean(AuthenticationService) + logged_in = authenticationService.authenticate(user_name, user_password) + + print "Passport. and Duo-Universal authenticate for step 1. Basic authentication returned: %s" % logged_in + identity.setWorkingParameter('username',user_name) + # print"Is Duoflow : %s"%identity.getWorkingParameter('duoFlow') + self.setDuoOrPassportFlow("duoFlow") + return logged_in + + elif provider in self.registeredProviders: + #it's a recognized external IDP + identity.setWorkingParameter("selectedProvider", provider) + print "Passport. and Duo-Universal authenticate for step 1. Retrying step 1" + #see prepareForStep (step = 1) + # identity.setWorkingParameter('passportFlow', True) + self.setDuoOrPassportFlow('passportFlow') + # self.passport_flow = True + return True + + if step == 2: + print("is DuoFlow for authStep-2 : %s"%self.isDuoOrPassportFlow('duoFlow')) + print("is PassportFlow for authrStep-2 : %s"%self.isDuoOrPassportFlow('passportFlow')) + if self.isDuoOrPassportFlow('duoFlow'): + state = ServerUtil.getFirstValue(requestParameters, "state") + # Get state to verify consistency and originality + if identity.getWorkingParameter('state_duo') == state : + + # Get authorization token to trade for 2FA + duoCode = ServerUtil.getFirstValue(requestParameters, "duo_code") + try: + token = self.duo_client.exchangeAuthorizationCodeFor2FAResult(duoCode, identity.getWorkingParameter('username')) + print "token status %s "%token.getAuth_result().getStatus() + except: + # Handle authentication failure. + print "authentication failure", sys.exc_info()[1] + return False + + # User successfully passed Duo authentication. + + if "allow" == token.getAuth_result().getStatus(): + return True + + return False + + elif(self.isDuoOrPassportFlow('passportFlow')): + # else: + mail = ServerUtil.getFirstValue(requestParameters, "loginForm:email") + jsonp = identity.getWorkingParameter("passport_user_profile") + + if mail == None: + self.setMessageError(FacesMessage.SEVERITY_ERROR, "Email was missing in user profile") + elif jsonp != None: + # Completion of profile takes place + user_profile = json.loads(jsonp) + user_profile["mail"] = [ mail ] + + return self.attemptAuthentication(identity, user_profile, jsonp) + + print "Passport. authenticate for step 2. Failed: expected mail value in HTTP request and json profile in session" + return False + else: + print "Neither step 1 or 2" + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + + extensionResult = self.extensionPrepareForStep(configurationAttributes, requestParameters, step) + if extensionResult != None: + return extensionResult + + print "Passport. and Duo-Universal prepareForStep called %s" % str(step) + if step == 1: + identity = CdiUtil.bean(Identity) + #re-read the strategies config (for instance to know which strategies have enabled the email account linking) + self.parseProviderConfigs() + identity.setWorkingParameter("externalProviders", json.dumps(self.registeredProviders)) + + providerParam = self.customAuthzParameter + url = None + + sessionAttributes = identity.getSessionId().getSessionAttributes() + self.skipProfileUpdate = StringHelper.equalsIgnoreCase(sessionAttributes.get("skipPassportProfileUpdate"), "true") + + #this param could have been set previously in authenticate step if current step is being retried + provider = identity.getWorkingParameter("selectedProvider") + if provider != None: + url = self.getPassportRedirectUrl(provider) + identity.setWorkingParameter("selectedProvider", None) + + elif providerParam != None: + paramValue = sessionAttributes.get(providerParam) + + if paramValue != None: + print "Passport. prepareForStep. Found value in custom param of authorization request: %s" % paramValue + provider = self.getProviderFromJson(paramValue) + + if provider == None: + print "Passport. prepareForStep. A provider value could not be extracted from custom authorization request parameter" + elif not provider in self.registeredProviders: + print "Passport. prepareForStep. Provider '%s' not part of known configured IDPs/OPs" % provider + else: + url = self.getPassportRedirectUrl(provider) + + if url == None: + print "Passport. prepareForStep. A page to manually select an identity provider will be shown" + else: + facesService = CdiUtil.bean(FacesService) + facesService.redirectToExternalURL(url) + + return True + + elif (step == 2): + identity = CdiUtil.bean(Identity) + print("is DuoFlow prepareforStep-2 : %s"%self.isDuoOrPassportFlow('duoFlow')) + print("is Passport prepareforStep-2 : %s"%self.isDuoOrPassportFlow('passportFlow')) + if self.isDuoOrPassportFlow('duoFlow'): + print("Prepare for Duo Flow for steps %s"%step) + user_name = identity.getWorkingParameter('username') + facesContext = CdiUtil.bean(FacesContext) + request = facesContext.getExternalContext().getRequest() + httpService = CdiUtil.bean(HttpService) + url = httpService.constructServerUrl(request) + "/postlogin.htm" + + try: + print "before health check" + self.duo_client = Client(self.client_id,self.client_secret,self.api_hostname,url) + self.duo_client.healthCheck() + print "after health check" + except: + print "Duo-Universal. Duo config error. Verify the values in Duo-Universal.conf are correct ", sys.exc_info()[1] + return False + + state = self.duo_client.generateState() + identity.setWorkingParameter("state_duo",state) + prompt_uri = self.duo_client.createAuthUrl(user_name, state) + + facesService = CdiUtil.bean(FacesService) + facesService.redirectToExternalURL(prompt_uri ) + # self.duo_flow = False + return True + else: + return True + + else: + identity = CdiUtil.bean(Identity) + if (self.isDuoOrPassportFlow('duoFlow')): + print("Prepare for Duo Flow for steps %s"%step) + return False + elif (self.isDuoOrPassportFlow('passportFlow')): + print("Prepare for Passport Flow for steps %s"%step) + return True + else: + return True + + + def getExtraParametersForStep(self, configurationAttributes, step): + print "Passport. and Duo-Universal getExtraParametersForStep called %s"%step + identity = CdiUtil.bean(Identity) + if step == 1: + return Arrays.asList("selectedProvider", "externalProviders", "state_duo", "username") + if step == 2: + if self.isDuoOrPassportFlow('duoFlow'): + return Arrays.asList("state_duo", "username") + else: + return Arrays.asList("passport_user_profile") + + if step == 3: + print("You are in steps Three %s"%step) + return Arrays.asList("state_duo", "username") + + return Arrays.asList("state_duo", "username") + + + def getCountAuthenticationSteps(self, configurationAttributes): + self.countStep+=1 + print ("Passport. and Duo-Universal getCountAuthenticationSteps called %s"%self.countStep) + identity = CdiUtil.bean(Identity) + if identity.getWorkingParameter("passport_user_profile") != None: + print("GetCount authentication steps for PassportFlow ") + if self.countStep>=2: + self.resetFlow('passportFlow', False) + self.countStep =0 + return 2 + + if self.isDuoOrPassportFlow('duoFlow'): + print("GetCount authentication steps for Duoflow 2") + if self.countStep>=2: + self.resetFlow('duoFlow', False) + self.countStep =0 + return 2 + else: + if self.countStep>=2: + self.resetFlow('passportFlow', False) + self.countStep =0 + return 1 + + + def getPageForStep(self, configurationAttributes, step): + print "Passport. and Duo-Universal. getPageForStep called %s"%step + identity = CdiUtil.bean(Identity) + extensionResult = self.extensionGetPageForStep(configurationAttributes, step) + if extensionResult != None: + return extensionResult + + if step == 1: + return "/auth/passport/passportlogin.xhtml" + + elif (self.isDuoOrPassportFlow('duoFlow')): + print("Getpage for Duo Flow for steps %s"%step) + return "" + + elif(self.isDuoOrPassportFlow('passportFlow')): + print("Getpage for passport Flow for steps %s"%step) + return "/auth/passport/passportpostlogin.xhtml" + + return "" + + + def getNextStep(self, configurationAttributes, requestParameters, step): + if step == 1: + identity = CdiUtil.bean(Identity) + provider = identity.getWorkingParameter("selectedProvider") + if provider != None: + return 1 + + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True + + def setDuoOrPassportFlow(self, flow): + identity = CdiUtil.bean(Identity) + identity.setWorkingParameter(flow, True) + if flow=="duoFlow": + self.duo_flow = True + elif flow=="passportFlow": + self.passport_flow = True + + + def isDuoOrPassportFlow(self, flow): + if flow=="duoFlow": + return self.duo_flow + elif flow=="passportFlow": + return self.passport_flow + + def resetFlow(self, flow, value): + identity = CdiUtil.bean(Identity) + if flow=="duoFlow": + self.duo_flow = value + identity.setWorkingParameter(flow, value) + elif flow=="passportFlow": + self.passport_flow = value + identity.setWorkingParameter(flow, value) + + print("Successfully reset Parameters DuoFlow: %s and PassportFlow: %s"%(self.duo_flow, self.passport_flow)) + +# Extension module related functions + + def extensionInit(self, configurationAttributes): + + if self.extensionModule == None: + return None + return self.extensionModule.init(configurationAttributes) + + + def extensionAuthenticate(self, configurationAttributes, requestParameters, step): + + if self.extensionModule == None: + return None + return self.extensionModule.authenticate(configurationAttributes, requestParameters, step) + + + def extensionPrepareForStep(self, configurationAttributes, requestParameters, step): + + if self.extensionModule == None: + return None + return self.extensionModule.prepareForStep(configurationAttributes, requestParameters, step) + + + def extensionGetPageForStep(self, configurationAttributes, step): + + if self.extensionModule == None: + return None + return self.extensionModule.getPageForStep(configurationAttributes, step) + +# Initalization routines + + def loadExternalModule(self, simpleCustProperty): + + if simpleCustProperty != None: + print "Passport. loadExternalModule. Loading passport extension module..." + moduleName = simpleCustProperty.getValue2() + try: + module = __import__(moduleName) + return module + except: + print "Passport. loadExternalModule. Failed to load module %s" % moduleName + print "Exception: ", sys.exc_info()[1] + print "Passport. loadExternalModule. Flow will be driven entirely by routines of main passport script" + return None + + + def processKeyStoreProperties(self, attrs): + file = attrs.get("key_store_file") + password = attrs.get("key_store_password") + + if file != None and password != None: + file = file.getValue2() + password = password.getValue2() + + if StringHelper.isNotEmpty(file) and StringHelper.isNotEmpty(password): + self.keyStoreFile = file + self.keyStorePassword = password + return True + + print "Passport. readKeyStoreProperties. Properties key_store_file or key_store_password not found or empty" + return False + + + def getCustomAuthzParameter(self, simpleCustProperty): + + customAuthzParameter = None + if simpleCustProperty != None: + prop = simpleCustProperty.getValue2() + if StringHelper.isNotEmpty(prop): + customAuthzParameter = prop + + if customAuthzParameter == None: + print "Passport. getCustomAuthzParameter. No custom param for OIDC authz request in script properties" + print "Passport. getCustomAuthzParameter. Passport flow cannot be initiated by doing an OpenID connect authorization request" + else: + print "Passport. getCustomAuthzParameter. Custom param for OIDC authz request in script properties: %s" % customAuthzParameter + + return customAuthzParameter + +# Configuration parsing + + def getPassportConfigDN(self): + + f = open('/etc/gluu/conf/gluu.properties', 'r') + for line in f: + prop = line.split("=") + if prop[0] == "oxpassport_ConfigurationEntryDN": + prop.pop(0) + break + + f.close() + return "=".join(prop).strip() + + + def parseAllProviders(self): + + registeredProviders = {} + print "Passport. parseAllProviders. Adding providers" + entryManager = CdiUtil.bean(PersistenceEntryManager) + + config = LdapOxPassportConfiguration() + config = entryManager.find(config.getClass(), self.passportDN).getPassportConfiguration() + config = config.getProviders() if config != None else config + + if config != None and len(config) > 0: + for prvdetails in config: + if prvdetails.isEnabled(): + registeredProviders[prvdetails.getId()] = { + "emailLinkingSafe": prvdetails.isEmailLinkingSafe(), + "requestForEmail" : prvdetails.isRequestForEmail(), + "logo_img": prvdetails.getLogoImg(), + "displayName": prvdetails.getDisplayName(), + "type": prvdetails.getType() + } + + return registeredProviders + + + def parseProviderConfigs(self): + + registeredProviders = {} + try: + registeredProviders = self.parseAllProviders() + toRemove = [] + + for provider in registeredProviders: + if registeredProviders[provider]["type"] == "saml": + toRemove.append(provider) + else: + registeredProviders[provider]["saml"] = False + + for provider in toRemove: + registeredProviders.pop(provider) + + if len(registeredProviders.keys()) > 0: + print "Passport. parseProviderConfigs. Configured providers:", registeredProviders + else: + print "Passport. parseProviderConfigs. No providers registered yet" + except: + print "Passport. parseProviderConfigs. An error occurred while building the list of supported authentication providers", sys.exc_info()[1] + + self.registeredProviders = registeredProviders + +# Auxiliary routines + + def getProviderFromJson(self, providerJson): + + provider = None + try: + obj = json.loads(Base64Util.base64urldecodeToString(providerJson)) + provider = obj[self.providerKey] + except: + print "Passport. getProviderFromJson. Could not parse provided Json string. Returning None" + + return provider + + + def getPassportRedirectUrl(self, provider): + + # provider is assumed to exist in self.registeredProviders + url = None + try: + facesContext = CdiUtil.bean(FacesContext) + tokenEndpoint = "https://%s/passport/token" % facesContext.getExternalContext().getRequest().getServerName() + + httpService = CdiUtil.bean(HttpService) + httpclient = httpService.getHttpsClient() + + print "Passport. getPassportRedirectUrl. Obtaining token from passport at %s" % tokenEndpoint + resultResponse = httpService.executeGet(httpclient, tokenEndpoint, Collections.singletonMap("Accept", "text/json")) + httpResponse = resultResponse.getHttpResponse() + bytes = httpService.getResponseContent(httpResponse) + + response = httpService.convertEntityToString(bytes) + print "Passport. getPassportRedirectUrl. Response was %s" % httpResponse.getStatusLine().getStatusCode() + + tokenObj = json.loads(response) + url = "/passport/auth/%s/%s" % (provider, tokenObj["token_"]) + except: + print "Passport. getPassportRedirectUrl. Error building redirect URL: ", sys.exc_info()[1] + + return url + + + def validSignature(self, jwt): + + print "Passport. validSignature. Checking JWT token signature" + valid = False + + try: + appConfiguration = AppConfiguration() + appConfiguration.setWebKeysStorage(WebKeyStorage.KEYSTORE) + appConfiguration.setKeyStoreFile(self.keyStoreFile) + appConfiguration.setKeyStoreSecret(self.keyStorePassword) + appConfiguration.setKeyRegenerationEnabled(False) + + cryptoProvider = CryptoProviderFactory.getCryptoProvider(appConfiguration) + valid = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), jwt.getHeader().getKeyId(), + None, None, jwt.getHeader().getSignatureAlgorithm()) + except: + print "Exception: ", sys.exc_info()[1] + + print "Passport. validSignature. Validation result was %s" % valid + return valid + + + def jwtHasExpired(self, jwt): + # Check if jwt has expired + jwt_claims = jwt.getClaims() + try: + exp_date_timestamp = float(jwt_claims.getClaimAsString(JwtClaimName.EXPIRATION_TIME)) + exp_date = datetime.datetime.fromtimestamp(exp_date_timestamp) + hasExpired = exp_date < datetime.datetime.now() + except: + print "Exception: The JWT does not have '%s' attribute" % JwtClaimName.EXPIRATION_TIME + return False + + return hasExpired + + + def getUserProfile(self, jwt): + jwt_claims = jwt.getClaims() + user_profile_json = None + + try: + user_profile_json = CdiUtil.bean(EncryptionService).decrypt(jwt_claims.getClaimAsString("data")) + user_profile = json.loads(user_profile_json) + except: + print "Passport. getUserProfile. Problem obtaining user profile json representation" + + return (user_profile, user_profile_json) + + + def attemptAuthentication(self, identity, user_profile, user_profile_json): + + uidKey = "uid" + if not self.checkRequiredAttributes(user_profile, [uidKey, self.providerKey]): + return False + + provider = user_profile[self.providerKey] + if not provider in self.registeredProviders: + print "Passport. attemptAuthentication. Identity Provider %s not recognized" % provider + return False + + uid = user_profile[uidKey][0] + externalUid = "passport-%s:%s" % (provider, uid) + + userService = CdiUtil.bean(UserService) + userByUid = userService.getUserByAttribute("oxExternalUid", externalUid, True) + + email = None + if "mail" in user_profile: + email = user_profile["mail"] + if len(email) == 0: + email = None + else: + email = email[0] + user_profile["mail"] = [ email ] + + if email == None and self.registeredProviders[provider]["requestForEmail"]: + print "Passport. attemptAuthentication. Email was not received" + + if userByUid != None: + # This avoids asking for the email over every login attempt + email = userByUid.getAttribute("mail") + if email != None: + print "Passport. attemptAuthentication. Filling missing email value with %s" % email + user_profile["mail"] = [ email ] + + if email == None: + # Store user profile in session and abort this routine + identity.setWorkingParameter("passport_user_profile", user_profile_json) + return True + + userByMail = None if email == None else userService.getUserByAttribute("mail", email) + + # Determine if we should add entry, update existing, or deny access + doUpdate = False + doAdd = False + if userByUid != None: + print "User with externalUid '%s' already exists" % externalUid + if userByMail == None: + doUpdate = True + else: + if userByMail.getUserId() == userByUid.getUserId(): + doUpdate = True + else: + print "Users with externalUid '%s' and mail '%s' are different. Access will be denied. Impersonation attempt?" % (externalUid, email) + self.setMessageError(FacesMessage.SEVERITY_ERROR, "Email value corresponds to an already existing provisioned account") + else: + if userByMail == None: + doAdd = True + elif self.registeredProviders[provider]["emailLinkingSafe"]: + + tmpList = userByMail.getAttributeValues("oxExternalUid") + tmpList = ArrayList() if tmpList == None else ArrayList(tmpList) + tmpList.add(externalUid) + userByMail.setAttribute("oxExternalUid", tmpList, True) + + userByUid = userByMail + print "External user supplying mail %s will be linked to existing account '%s'" % (email, userByMail.getUserId()) + doUpdate = True + else: + print "An attempt to supply an email of an existing user was made. Turn on 'emailLinkingSafe' if you want to enable linking" + self.setMessageError(FacesMessage.SEVERITY_ERROR, "Email value corresponds to an already existing account. If you already have a username and password use those instead of an external authentication site to get access.") + + username = None + try: + if doUpdate: + username = userByUid.getUserId() + print "Passport. attemptAuthentication. Updating user %s" % username + self.updateUser(userByUid, user_profile, userService) + elif doAdd: + print "Passport. attemptAuthentication. Creating user %s" % externalUid + newUser = self.addUser(externalUid, user_profile, userService) + username = newUser.getUserId() + except: + print "Exception: ", sys.exc_info()[1] + print "Passport. attemptAuthentication. Authentication failed" + return False + + if username == None: + print "Passport. attemptAuthentication. Authentication attempt was rejected" + return False + else: + logged_in = CdiUtil.bean(AuthenticationService).authenticate(username) + print "Passport. attemptAuthentication. Authentication for %s returned %s" % (username, logged_in) + return logged_in + + + def setMessageError(self, severity, msg): + facesMessages = CdiUtil.bean(FacesMessages) + facesMessages.setKeepMessages() + facesMessages.clear() + facesMessages.add(severity, msg) + + + def checkRequiredAttributes(self, profile, attrs): + + for attr in attrs: + if (not attr in profile) or len(profile[attr]) == 0: + print "Passport. checkRequiredAttributes. Attribute '%s' is missing in profile" % attr + return False + return True + + + def addUser(self, externalUid, profile, userService): + + newUser = User() + #Fill user attrs + newUser.setAttribute("oxExternalUid", externalUid, True) + self.fillUser(newUser, profile) + newUser = userService.addUser(newUser, True) + return newUser + + + def updateUser(self, foundUser, profile, userService): + + # when this is false, there might still some updates taking place (e.g. not related to profile attrs released by external provider) + if (not self.skipProfileUpdate): + self.fillUser(foundUser, profile) + userService.updateUser(foundUser) + + + def fillUser(self, foundUser, profile): + + for attr in profile: + # "provider" is disregarded if part of mapping + if attr != self.providerKey: + values = profile[attr] + print "%s = %s" % (attr, values) + foundUser.setAttribute(attr, values) + + if attr == "mail": + oxtrustMails = [] + for mail in values: + oxtrustMails.append('{"value":"%s","primary":false}' % mail) + foundUser.setAttribute("oxTrustEmail", oxtrustMails) diff --git a/oxAuth/Server/integrations/duo.passport.combine/Passport_and_Duo-Universal_ignore_empty.py b/oxAuth/Server/integrations/duo.passport.combine/Passport_and_Duo-Universal_ignore_empty.py new file mode 100644 index 00000000..6351351e --- /dev/null +++ b/oxAuth/Server/integrations/duo.passport.combine/Passport_and_Duo-Universal_ignore_empty.py @@ -0,0 +1,641 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2019, Gluu +# +# Author: Jose Gonzalez +# Author: Yuriy Movchan +# + +# Patched by Alex: this version will ignore attributes with empty values send by upstream IDP, preventing removal of values previously assigned to users in the local database +# See the additional "if" clause in fillUser() function checking for an empty values in response + +from org.gluu.jsf2.service import FacesService +from org.gluu.jsf2.message import FacesMessages + +from org.gluu.oxauth.model.common import User, WebKeyStorage +from org.gluu.oxauth.model.configuration import AppConfiguration +from org.gluu.oxauth.model.crypto import CryptoProviderFactory +from org.gluu.oxauth.model.jwt import Jwt, JwtClaimName +from org.gluu.oxauth.model.util import Base64Util +from org.gluu.oxauth.service import AppInitializer, AuthenticationService +from org.gluu.oxauth.service.common import UserService, EncryptionService +from org.gluu.oxauth.service.net import HttpService +from org.gluu.oxauth.security import Identity +from org.gluu.oxauth.util import ServerUtil +from org.gluu.config.oxtrust import LdapOxPassportConfiguration +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.persist import PersistenceEntryManager +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.util import StringHelper +from java.util import ArrayList, Arrays, Collections + +from javax.faces.application import FacesMessage +from javax.faces.context import FacesContext + +import json +import sys +import datetime + +from org.gluu.util import ArrayHelper + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Passport. init called" + + self.extensionModule = self.loadExternalModule(configurationAttributes.get("extension_module")) + extensionResult = self.extensionInit(configurationAttributes) + if extensionResult != None: + return extensionResult + + print "Passport. init. Behaviour is social" + success = self.processKeyStoreProperties(configurationAttributes) + + if success: + self.providerKey = "provider" + self.customAuthzParameter = self.getCustomAuthzParameter(configurationAttributes.get("authz_req_param_provider")) + self.passportDN = self.getPassportConfigDN() + print "Passport. init. Initialization success" + else: + print "Passport. init. Initialization failed" + return success + + + def destroy(self, configurationAttributes): + print "Passport. destroy called" + return True + + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + + def authenticate(self, configurationAttributes, requestParameters, step): + + extensionResult = self.extensionAuthenticate(configurationAttributes, requestParameters, step) + if extensionResult != None: + return extensionResult + + print "Passport. authenticate for step %s called" % str(step) + identity = CdiUtil.bean(Identity) + + # Loading self.registeredProviders in case passport destroyed + if not hasattr(self,'registeredProviders'): + print "Passport. Fetching registered providers." + self.parseProviderConfigs() + + if step == 1: + # Get JWT token + jwt_param = ServerUtil.getFirstValue(requestParameters, "user") + + if jwt_param != None: + print "Passport. authenticate for step 1. JWT user profile token found" + + # Parse JWT and validate + jwt = Jwt.parse(jwt_param) + if not self.validSignature(jwt): + return False + + if self.jwtHasExpired(jwt): + return False + + (user_profile, jsonp) = self.getUserProfile(jwt) + if user_profile == None: + return False + + sessionAttributes = identity.getSessionId().getSessionAttributes() + self.skipProfileUpdate = StringHelper.equalsIgnoreCase(sessionAttributes.get("skipPassportProfileUpdate"), "true") + + return self.attemptAuthentication(identity, user_profile, jsonp) + + #See passportlogin.xhtml + provider = ServerUtil.getFirstValue(requestParameters, "loginForm:provider") + if StringHelper.isEmpty(provider): + + #it's username + passw auth + print "Passport. authenticate for step 1. Basic authentication detected" + logged_in = False + + credentials = identity.getCredentials() + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + if StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password): + authenticationService = CdiUtil.bean(AuthenticationService) + logged_in = authenticationService.authenticate(user_name, user_password) + + print "Passport. authenticate for step 1. Basic authentication returned: %s" % logged_in + return logged_in + + elif provider in self.registeredProviders: + #it's a recognized external IDP + identity.setWorkingParameter("selectedProvider", provider) + print "Passport. authenticate for step 1. Retrying step 1" + #see prepareForStep (step = 1) + return True + + if step == 2: + mail = ServerUtil.getFirstValue(requestParameters, "loginForm:email") + jsonp = identity.getWorkingParameter("passport_user_profile") + + if mail == None: + self.setMessageError(FacesMessage.SEVERITY_ERROR, "Email was missing in user profile") + elif jsonp != None: + # Completion of profile takes place + user_profile = json.loads(jsonp) + user_profile["mail"] = [ mail ] + + return self.attemptAuthentication(identity, user_profile, jsonp) + + print "Passport. authenticate for step 2. Failed: expected mail value in HTTP request and json profile in session" + return False + + + def prepareForStep(self, configurationAttributes, requestParameters, step): + + extensionResult = self.extensionPrepareForStep(configurationAttributes, requestParameters, step) + if extensionResult != None: + return extensionResult + + print "Passport. prepareForStep called %s" % str(step) + identity = CdiUtil.bean(Identity) + + if step == 1: + #re-read the strategies config (for instance to know which strategies have enabled the email account linking) + self.parseProviderConfigs() + identity.setWorkingParameter("externalProviders", json.dumps(self.registeredProviders)) + + providerParam = self.customAuthzParameter + url = None + + sessionAttributes = identity.getSessionId().getSessionAttributes() + self.skipProfileUpdate = StringHelper.equalsIgnoreCase(sessionAttributes.get("skipPassportProfileUpdate"), "true") + + #this param could have been set previously in authenticate step if current step is being retried + provider = identity.getWorkingParameter("selectedProvider") + if provider != None: + url = self.getPassportRedirectUrl(provider) + identity.setWorkingParameter("selectedProvider", None) + + elif providerParam != None: + paramValue = sessionAttributes.get(providerParam) + + if paramValue != None: + print "Passport. prepareForStep. Found value in custom param of authorization request: %s" % paramValue + provider = self.getProviderFromJson(paramValue) + + if provider == None: + print "Passport. prepareForStep. A provider value could not be extracted from custom authorization request parameter" + elif not provider in self.registeredProviders: + print "Passport. prepareForStep. Provider '%s' not part of known configured IDPs/OPs" % provider + else: + url = self.getPassportRedirectUrl(provider) + + if url == None: + print "Passport. prepareForStep. A page to manually select an identity provider will be shown" + else: + facesService = CdiUtil.bean(FacesService) + facesService.redirectToExternalURL(url) + + return True + + + def getExtraParametersForStep(self, configurationAttributes, step): + print "Passport. getExtraParametersForStep called" + if step == 1: + return Arrays.asList("selectedProvider", "externalProviders") + elif step == 2: + return Arrays.asList("passport_user_profile") + return None + + + def getCountAuthenticationSteps(self, configurationAttributes): + print "Passport. getCountAuthenticationSteps called" + identity = CdiUtil.bean(Identity) + if identity.getWorkingParameter("passport_user_profile") != None: + return 2 + return 1 + + + def getPageForStep(self, configurationAttributes, step): + print "Passport. getPageForStep called" + + extensionResult = self.extensionGetPageForStep(configurationAttributes, step) + if extensionResult != None: + return extensionResult + + if step == 1: + return "/auth/passport/passportlogin.xhtml" + return "/auth/passport/passportpostlogin.xhtml" + + + def getNextStep(self, configurationAttributes, requestParameters, step): + + if step == 1: + identity = CdiUtil.bean(Identity) + provider = identity.getWorkingParameter("selectedProvider") + if provider != None: + return 1 + + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True + +# Extension module related functions + + def extensionInit(self, configurationAttributes): + + if self.extensionModule == None: + return None + return self.extensionModule.init(configurationAttributes) + + + def extensionAuthenticate(self, configurationAttributes, requestParameters, step): + + if self.extensionModule == None: + return None + return self.extensionModule.authenticate(configurationAttributes, requestParameters, step) + + + def extensionPrepareForStep(self, configurationAttributes, requestParameters, step): + + if self.extensionModule == None: + return None + return self.extensionModule.prepareForStep(configurationAttributes, requestParameters, step) + + + def extensionGetPageForStep(self, configurationAttributes, step): + + if self.extensionModule == None: + return None + return self.extensionModule.getPageForStep(configurationAttributes, step) + +# Initalization routines + + def loadExternalModule(self, simpleCustProperty): + + if simpleCustProperty != None: + print "Passport. loadExternalModule. Loading passport extension module..." + moduleName = simpleCustProperty.getValue2() + try: + module = __import__(moduleName) + return module + except: + print "Passport. loadExternalModule. Failed to load module %s" % moduleName + print "Exception: ", sys.exc_info()[1] + print "Passport. loadExternalModule. Flow will be driven entirely by routines of main passport script" + return None + + + def processKeyStoreProperties(self, attrs): + file = attrs.get("key_store_file") + password = attrs.get("key_store_password") + + if file != None and password != None: + file = file.getValue2() + password = password.getValue2() + + if StringHelper.isNotEmpty(file) and StringHelper.isNotEmpty(password): + self.keyStoreFile = file + self.keyStorePassword = password + return True + + print "Passport. readKeyStoreProperties. Properties key_store_file or key_store_password not found or empty" + return False + + + def getCustomAuthzParameter(self, simpleCustProperty): + + customAuthzParameter = None + if simpleCustProperty != None: + prop = simpleCustProperty.getValue2() + if StringHelper.isNotEmpty(prop): + customAuthzParameter = prop + + if customAuthzParameter == None: + print "Passport. getCustomAuthzParameter. No custom param for OIDC authz request in script properties" + print "Passport. getCustomAuthzParameter. Passport flow cannot be initiated by doing an OpenID connect authorization request" + else: + print "Passport. getCustomAuthzParameter. Custom param for OIDC authz request in script properties: %s" % customAuthzParameter + + return customAuthzParameter + +# Configuration parsing + + def getPassportConfigDN(self): + + f = open('/etc/gluu/conf/gluu.properties', 'r') + for line in f: + prop = line.split("=") + if prop[0] == "oxpassport_ConfigurationEntryDN": + prop.pop(0) + break + + f.close() + return "=".join(prop).strip() + + + def parseAllProviders(self): + + registeredProviders = {} + print "Passport. parseAllProviders. Adding providers" + entryManager = CdiUtil.bean(PersistenceEntryManager) + + config = LdapOxPassportConfiguration() + config = entryManager.find(config.getClass(), self.passportDN).getPassportConfiguration() + config = config.getProviders() if config != None else config + + if config != None and len(config) > 0: + for prvdetails in config: + if prvdetails.isEnabled(): + registeredProviders[prvdetails.getId()] = { + "emailLinkingSafe": prvdetails.isEmailLinkingSafe(), + "requestForEmail" : prvdetails.isRequestForEmail(), + "logo_img": prvdetails.getLogoImg(), + "displayName": prvdetails.getDisplayName(), + "type": prvdetails.getType() + } + + return registeredProviders + + + def parseProviderConfigs(self): + + registeredProviders = {} + try: + registeredProviders = self.parseAllProviders() + toRemove = [] + + for provider in registeredProviders: + if registeredProviders[provider]["type"] == "saml": + toRemove.append(provider) + else: + registeredProviders[provider]["saml"] = False + + for provider in toRemove: + registeredProviders.pop(provider) + + if len(registeredProviders.keys()) > 0: + print "Passport. parseProviderConfigs. Configured providers:", registeredProviders + else: + print "Passport. parseProviderConfigs. No providers registered yet" + except: + print "Passport. parseProviderConfigs. An error occurred while building the list of supported authentication providers", sys.exc_info()[1] + + self.registeredProviders = registeredProviders + +# Auxiliary routines + + def getProviderFromJson(self, providerJson): + + provider = None + try: + obj = json.loads(Base64Util.base64urldecodeToString(providerJson)) + provider = obj[self.providerKey] + except: + print "Passport. getProviderFromJson. Could not parse provided Json string. Returning None" + + return provider + + + def getPassportRedirectUrl(self, provider): + + # provider is assumed to exist in self.registeredProviders + url = None + try: + facesContext = CdiUtil.bean(FacesContext) + tokenEndpoint = "https://%s/passport/token" % facesContext.getExternalContext().getRequest().getServerName() + + httpService = CdiUtil.bean(HttpService) + httpclient = httpService.getHttpsClient() + + print "Passport. getPassportRedirectUrl. Obtaining token from passport at %s" % tokenEndpoint + resultResponse = httpService.executeGet(httpclient, tokenEndpoint, Collections.singletonMap("Accept", "text/json")) + httpResponse = resultResponse.getHttpResponse() + bytes = httpService.getResponseContent(httpResponse) + + response = httpService.convertEntityToString(bytes) + print "Passport. getPassportRedirectUrl. Response was %s" % httpResponse.getStatusLine().getStatusCode() + + tokenObj = json.loads(response) + url = "/passport/auth/%s/%s" % (provider, tokenObj["token_"]) + except: + print "Passport. getPassportRedirectUrl. Error building redirect URL: ", sys.exc_info()[1] + + return url + + + def validSignature(self, jwt): + + print "Passport. validSignature. Checking JWT token signature" + valid = False + + try: + appConfiguration = AppConfiguration() + appConfiguration.setWebKeysStorage(WebKeyStorage.KEYSTORE) + appConfiguration.setKeyStoreFile(self.keyStoreFile) + appConfiguration.setKeyStoreSecret(self.keyStorePassword) + appConfiguration.setKeyRegenerationEnabled(False) + + cryptoProvider = CryptoProviderFactory.getCryptoProvider(appConfiguration) + valid = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), jwt.getHeader().getKeyId(), + None, None, jwt.getHeader().getSignatureAlgorithm()) + except: + print "Exception: ", sys.exc_info()[1] + + print "Passport. validSignature. Validation result was %s" % valid + return valid + + + def jwtHasExpired(self, jwt): + # Check if jwt has expired + jwt_claims = jwt.getClaims() + try: + exp_date_timestamp = float(jwt_claims.getClaimAsString(JwtClaimName.EXPIRATION_TIME)) + exp_date = datetime.datetime.fromtimestamp(exp_date_timestamp) + hasExpired = exp_date < datetime.datetime.now() + except: + print "Exception: The JWT does not have '%s' attribute" % JwtClaimName.EXPIRATION_TIME + return False + + return hasExpired + + + def getUserProfile(self, jwt): + jwt_claims = jwt.getClaims() + user_profile_json = None + + try: + user_profile_json = CdiUtil.bean(EncryptionService).decrypt(jwt_claims.getClaimAsString("data")) + user_profile = json.loads(user_profile_json) + except: + print "Passport. getUserProfile. Problem obtaining user profile json representation" + + return (user_profile, user_profile_json) + + + def attemptAuthentication(self, identity, user_profile, user_profile_json): + + uidKey = "uid" + if not self.checkRequiredAttributes(user_profile, [uidKey, self.providerKey]): + return False + + provider = user_profile[self.providerKey] + if not provider in self.registeredProviders: + print "Passport. attemptAuthentication. Identity Provider %s not recognized" % provider + return False + + uid = user_profile[uidKey][0] + externalUid = "passport-%s:%s" % (provider, uid) + + userService = CdiUtil.bean(UserService) + userByUid = userService.getUserByAttribute("oxExternalUid", externalUid, True) + + email = None + if "mail" in user_profile: + email = user_profile["mail"] + if len(email) == 0: + email = None + else: + email = email[0] + user_profile["mail"] = [ email ] + + if email == None and self.registeredProviders[provider]["requestForEmail"]: + print "Passport. attemptAuthentication. Email was not received" + + if userByUid != None: + # This avoids asking for the email over every login attempt + email = userByUid.getAttribute("mail") + if email != None: + print "Passport. attemptAuthentication. Filling missing email value with %s" % email + user_profile["mail"] = [ email ] + + if email == None: + # Store user profile in session and abort this routine + identity.setWorkingParameter("passport_user_profile", user_profile_json) + return True + + userByMail = None if email == None else userService.getUserByAttribute("nidGSuite", email) + + # Determine if we should add entry, update existing, or deny access + doUpdate = False + doAdd = False + if userByUid != None: + print "User with externalUid '%s' already exists" % externalUid + if userByMail == None: + doUpdate = True + else: + if userByMail.getUserId() == userByUid.getUserId(): + doUpdate = True + else: + print "Users with externalUid '%s' and mail '%s' are different. Access will be denied. Impersonation attempt?" % (externalUid, email) + self.setMessageError(FacesMessage.SEVERITY_ERROR, "Email value corresponds to an already existing provisioned account") + else: + if userByMail == None: + doAdd = True + elif self.registeredProviders[provider]["emailLinkingSafe"]: + + tmpList = userByMail.getAttributeValues("oxExternalUid") + tmpList = ArrayList() if tmpList == None else ArrayList(tmpList) + tmpList.add(externalUid) + userByMail.setAttribute("oxExternalUid", tmpList, True) + + userByUid = userByMail + print "External user supplying mail %s will be linked to existing account '%s'" % (email, userByMail.getUserId()) + doUpdate = True + else: + print "An attempt to supply an email of an existing user was made. Turn on 'emailLinkingSafe' if you want to enable linking" + self.setMessageError(FacesMessage.SEVERITY_ERROR, "Email value corresponds to an already existing account. If you already have a username and password use those instead of an external authentication site to get access.") + + username = None + try: + if doUpdate: + username = userByUid.getUserId() + print "Passport. attemptAuthentication. Updating user %s" % username + self.updateUser(userByUid, user_profile, userService) + elif doAdd: + print "Passport. attemptAuthentication. Creating user %s" % externalUid + newUser = self.addUser(externalUid, user_profile, userService) + username = newUser.getUserId() + except: + print "Exception: ", sys.exc_info()[1] + print "Passport. attemptAuthentication. Authentication failed" + return False + + if username == None: + print "Passport. attemptAuthentication. Authentication attempt was rejected" + return False + else: + logged_in = CdiUtil.bean(AuthenticationService).authenticate(username) + print "Passport. attemptAuthentication. Authentication for %s returned %s" % (username, logged_in) + return logged_in + + + def setMessageError(self, severity, msg): + facesMessages = CdiUtil.bean(FacesMessages) + facesMessages.setKeepMessages() + facesMessages.clear() + facesMessages.add(severity, msg) + + + def checkRequiredAttributes(self, profile, attrs): + + for attr in attrs: + if (not attr in profile) or len(profile[attr]) == 0: + print "Passport. checkRequiredAttributes. Attribute '%s' is missing in profile" % attr + return False + return True + + + def addUser(self, externalUid, profile, userService): + + newUser = User() + #Fill user attrs + newUser.setAttribute("oxExternalUid", externalUid, True) + self.fillUser(newUser, profile) + newUser = userService.addUser(newUser, True) + return newUser + + + def updateUser(self, foundUser, profile, userService): + + # when this is false, there might still some updates taking place (e.g. not related to profile attrs released by external provider) + if (not self.skipProfileUpdate): + self.fillUser(foundUser, profile) + userService.updateUser(foundUser) + + + def fillUser(self, foundUser, profile): + + for attr in profile: + # "provider" is disregarded if part of mapping + if attr != self.providerKey: + values = profile[attr] + print "%s = %s" % (attr, values) + # Patch to prevent clearing of attribute values on the local users if IDP send an empty value in its response + if (values != None and not (ArrayHelper.isEmpty(values))): + foundUser.setAttribute(attr, values) + else: + print "Ommiting attribute named '%s' as upstream IDP sent an empty value for it" % attr + + if attr == "mail": + oxtrustMails = [] + for mail in values: + oxtrustMails.append('{"value":"%s","primary":false}' % mail) + foundUser.setAttribute("oxTrustEmail", oxtrustMails) diff --git a/oxAuth/Server/integrations/duo.passport.combine/readme.md b/oxAuth/Server/integrations/duo.passport.combine/readme.md new file mode 100644 index 00000000..5c9892ec --- /dev/null +++ b/oxAuth/Server/integrations/duo.passport.combine/readme.md @@ -0,0 +1,76 @@ +## DUO-Passport multiAuth set up [ Work in progress ... ] + +This script allow users to use Duo as two factor in a Passport enabled Gluu Server. +There are two options in a Passport enabled Gluu Server: + - oxAuth login ( left side ) + - Passport login ( right side ) + +This script will ask for Duo when user will use oxAuth login. + +### Implementation Note + + - This implemented in one customer's 4.4.0.sp1 + - This script should work in 4.5 without downloading oxauth and Jetty modification ( below step number 1 and 2 ) + +### Configuration in Gluu CE 4.4.0 + +#### Download oxAuth + +Get `oxauth.war` from https://maven.gluu.org/maven/org/gluu/oxauth-server/4.4.0.sp1/oxauth-server-4.4.0.sp1.war + +#### Jetty Compatible + +By default old war files are for version 9. As result it apply small changes in war file to run it correctly under jetty 10. + + - Run `/opt/gluu/bin/jetty10CompatibleWar.py` to update it to conform jetty 10. +``` +$ ./jetty10CompatibleWar.py -in-file[Downloaded server] -out-file[Downloaded server] +example +$ ./jetty10CompatibleWar.py -in-file /opt/gluu/jetty/oxauth/webapps/4.4.0.sp1/oxauth-server-4.4.0.sp1.war -out-file /opt/gluu/jetty/oxauth/webapps/4.4.0.sp1/oxauth.war +``` + - Stop your **oxauth** service `systemctl stop oxauth` + + - Replace JettyCompatible war file at `/opt/gluu/jetty/oxauth/webapps/oxauth.war` + +#### Add External Dependency + +Follow [this](https://github.com/GluuFederation/oxAuth/tree/master/Server/integrations/duo-universal-prompt) doc to: + - Add the duo-universal Dependency to your oxAuth at`/opt/gluu/jetty/oxauth/custom/libs/*.jar` + - Register custom libs in oxauth.xml `/opt/gluu/jetty/oxauth/webapps/oxauth.xml` + +Start the **oxauth** service `systemctl start oxauth` + +### Add Custom Script +- Navigate to `Configuration` > `Person Authentication Scripts`. + Add new custom script for the 2 factor authentication using DUO and Passport credentials. + +- Add the following Custom Property ( key/value pairs ): + - For DUO security + - `client_id` + - `client_secret` + - `api_hostname` + - For Passport social + - `key_store_file` + - `key_store_password` + +- Enable and save. +- *NOTE*: you have to make sure that your `passport_social` and/or `passport_saml` + `Duo Universal` scripts are enabled. This is a combine operation so three scripts must have to runn successfully. + +- A successful configuration should throw snippet like below in `/opt/gluu/jetty/oxauth/oxauth_script.log` + + ``` + 2024-06-12 17:56:26,937 INFO [oxAuthScheduler_Worker-4] [org.gluu.service.PythonService$PythonLoggerOutputStream] (PythonService.java:243) - Passport. init. Initialization success + 2024-06-12 17:56:26,937 INFO [oxAuthScheduler_Worker-4] [org.gluu.service.PythonService$PythonLoggerOutputStream] (PythonService.java:243) - Duo-Universal. Initialization + 2024-06-12 17:56:26,937 INFO [oxAuthScheduler_Worker-4] [org.gluu.service.PythonService$PythonLoggerOutputStream] (PythonService.java:243) - Duo-Universal. Initialized successfully + 2024-06-12 17:56:26,937 INFO [oxAuthScheduler_Worker-4] [org.gluu.service.PythonService$PythonLoggerOutputStream] (PythonService.java:243) - Passport. and Duo-Universal Initialized successfully + 2024-06-12 17:56:26,941 TRACE [oxAuthScheduler_Worker-4] [org.gluu.service.custom.script.CustomScriptManager] (CustomScriptManager.java:134) - Last finished time '2024-06-12T17:56:26.941+0000' + ``` + +### Test + +To test your setup always use incognito or new browser. + + - Go to `Manage Authentication` > `Default Authentication Method` + - Change `oxTrust ACR` to "DuoPassportCombine" ( or whichever name you supplied ) + - `Update` + - Test diff --git a/oxAuth/Server/integrations/duo/DuoExternalAuthenticator.py b/oxAuth/Server/integrations/duo/DuoExternalAuthenticator.py new file mode 100644 index 00000000..2d87beba --- /dev/null +++ b/oxAuth/Server/integrations/duo/DuoExternalAuthenticator.py @@ -0,0 +1,239 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2016, Gluu +# +# Author: Yuriy Movchan +# + +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.service import AuthenticationService +from org.gluu.oxauth.service.common import UserService +from org.gluu.service import MailService +from org.gluu.util import ArrayHelper +from org.gluu.util import StringHelper +from java.util import Arrays + +import duo_web +import json + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Duo. Initialization" + + duo_creds_file = configurationAttributes.get("duo_creds_file").getValue2() + # Load credentials from file + f = open(duo_creds_file, 'r') + try: + creds = json.loads(f.read()) + except: + print "Duo. Initialization. Failed to load creds from file:", duo_creds_file + return False + finally: + f.close() + + self.ikey = str(creds["ikey"]) + self.skey = str(creds["skey"]) + self.akey = str(creds["akey"]) + + self.use_duo_group = False + if (configurationAttributes.containsKey("duo_group")): + self.duo_group = configurationAttributes.get("duo_group").getValue2() + self.use_duo_group = True + print "Duo. Initialization. Using Duo only if user belong to group:", self.duo_group + + self.use_audit_group = False + if (configurationAttributes.containsKey("audit_group")): + self.audit_group = configurationAttributes.get("audit_group").getValue2() + + if (not configurationAttributes.containsKey("audit_group_email")): + print "Duo. Initialization. Property audit_group_email is not specified" + return False + + self.audit_email = configurationAttributes.get("audit_group_email").getValue2() + self.use_audit_group = True + + print "Duo. Initialization. Using audito group:", self.audit_group + + if (self.use_duo_group or self.use_audit_group): + if (not configurationAttributes.containsKey("audit_attribute")): + print "Duo. Initialization. Property audit_attribute is not specified" + return False + else: + self.audit_attribute = configurationAttributes.get("audit_attribute").getValue2() + + print "Duo. Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "Duo. Destroy" + print "Duo. Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + duo_host = configurationAttributes.get("duo_host").getValue2() + + authenticationService = CdiUtil.bean(AuthenticationService) + + identity = CdiUtil.bean(Identity) + + if (step == 1): + print "Duo. Authenticate for step 1" + + # Check if user authenticated already in another custom script + user = authenticationService.getAuthenticatedUser() + if user == None: + credentials = identity.getCredentials() + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + logged_in = False + if (StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password)): + userService = CdiUtil.bean(UserService) + logged_in = authenticationService.authenticate(user_name, user_password) + + if (not logged_in): + return False + + user = authenticationService.getAuthenticatedUser() + + if (self.use_duo_group): + print "Duo. Authenticate for step 1. Checking if user belong to Duo group" + is_member_duo_group = self.isUserMemberOfGroup(user, self.audit_attribute, self.duo_group) + if (is_member_duo_group): + print "Duo. Authenticate for step 1. User '" + user.getUserId() + "' member of Duo group" + duo_count_login_steps = 2 + else: + self.processAuditGroup(user) + duo_count_login_steps = 1 + + identity.setWorkingParameter("duo_count_login_steps", duo_count_login_steps) + + return True + elif (step == 2): + print "Duo. Authenticate for step 2" + user = authenticationService.getAuthenticatedUser() + if user == None: + print "Duo. Authenticate for step 2. Failed to determine user name" + return False + + user_name = user.getUserId() + + sig_response_array = requestParameters.get("sig_response") + if ArrayHelper.isEmpty(sig_response_array): + print "Duo. Authenticate for step 2. sig_response is empty" + return False + + duo_sig_response = sig_response_array[0] + + print "Duo. Authenticate for step 2. duo_sig_response: " + duo_sig_response + + authenticated_username = duo_web.verify_response(self.ikey, self.skey, self.akey, duo_sig_response) + + print "Duo. Authenticate for step 2. authenticated_username: " + authenticated_username + ", expected user_name: " + user_name + + if (not StringHelper.equals(user_name, authenticated_username)): + return False + + self.processAuditGroup(user) + + return True + else: + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + identity = CdiUtil.bean(Identity) + authenticationService = CdiUtil.bean(AuthenticationService) + + duo_host = configurationAttributes.get("duo_host").getValue2() + + if (step == 1): + print "Duo. Prepare for step 1" + + return True + elif (step == 2): + print "Duo. Prepare for step 2" + + user = authenticationService.getAuthenticatedUser() + if (user == None): + print "Duo. Prepare for step 2. Failed to determine user name" + return False + user_name = user.getUserId() + + duo_sig_request = duo_web.sign_request(self.ikey, self.skey, self.akey, user_name) + print "Duo. Prepare for step 2. duo_sig_request: " + duo_sig_request + + identity.setWorkingParameter("duo_host", duo_host) + identity.setWorkingParameter("duo_sig_request", duo_sig_request) + + return True + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + if step == 2: + return Arrays.asList("duo_count_login_steps", "cas2_user_uid") + + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + identity = CdiUtil.bean(Identity) + if (identity.isSetWorkingParameter("duo_count_login_steps")): + return int(identity.getWorkingParameter("duo_count_login_steps")) + + return 2 + + def getPageForStep(self, configurationAttributes, step): + if (step == 2): + return "/auth/duo/duologin.xhtml" + return "" + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True + + def isUserMemberOfGroup(self, user, attribute, group): + is_member = False + member_of_list = user.getAttributeValues(attribute) + if (member_of_list != None): + for member_of in member_of_list: + if StringHelper.equalsIgnoreCase(group, member_of) or member_of.endswith(group): + is_member = True + break + + return is_member + + def processAuditGroup(self, user): + if (self.use_audit_group): + is_member = self.isUserMemberOfGroup(user, self.audit_attribute, self.audit_group) + if (is_member): + print "Duo. Authenticate for processAuditGroup. User '" + user.getUserId() + "' member of audit group" + print "Duo. Authenticate for processAuditGroup. Sending e-mail about user '" + user.getUserId() + "' login to", self.audit_email + + # Send e-mail to administrator + user_id = user.getUserId() + mailService = CdiUtil.bean(MailService) + subject = "User log in: " + user_id + body = "User log in: " + user_id + mailService.sendMail(self.audit_email, subject, body) diff --git a/oxAuth/Server/integrations/duo/INSTALLATION.txt b/oxAuth/Server/integrations/duo/INSTALLATION.txt new file mode 100644 index 00000000..051f6ae6 --- /dev/null +++ b/oxAuth/Server/integrations/duo/INSTALLATION.txt @@ -0,0 +1,44 @@ +This list of steps needed to do to enable DUO person authentication module. + +1. This module depends on python libraries. In order to use it we need to install Jython. Please use next articles to proper Jython installation: + - Installation notest: http://ox.gluu.org/doku.php?id=oxtauth:customauthscript#jython_installation_optional + - Jython integration: http://ox.gluu.org/doku.php?id=oxtauth:customauthscript#jython_python_integration + +2. Copy required python libraries from ./lib folder to $CATALINA_HOME/conf/python folder. + +3. Prepare DUO creds file /etc/certs/duo_creds.json with ikey, akey, skey + +4. Confire new custom module in oxTrust: + - Log into oxTrust with administrative permissions. + - Open "Configuration→Manage Custom Scripts" page. + - Select "Person Authentication" tab. + - Click on "Add custom script configuration" link. + - Enter name = duo + - Enter level = 0-100 (priority of this method). + - Select usage type "Interactive". + - Add custom required and optional properties which specified in README.txt. + - Copy/paste script from DuoPersonAuthentication.py. + - Activate it via "Enabled" checkbox. + - Click "Update" button at the bottom of this page. + +5. Configure oxAuth to use DUO authentication by default: + - Log into oxTrust with administrative permissions. + - Open "Configuration→Manage Authentication" page. + - Scroll to "Default Authentication Method" panel. Select "duo" authentication mode. + - Click "Update" button at the bottom of this page. + +6. Try to log in using DUO authentication method: + - Wait 30 seconds and try to log in again. During this time oxAuth reload list of available person authentication modules. + - Open second browser or second browsing session and try to log in again. It's better to try to do that from another browser session because we can return back to previous authentication method if something will go wrong. + +7. This step is an optional. We need to define SMTP configuration if we are plaaning to use audit_group property. In order to set SMTP configuration + we need to do: + - Open "Configuration"->"Organization configuration" page. + - Scroll down to "SMTP Server Configuration" section. + - Fill SMTP configuration parameters. + - Click "Test Configuration" in order to verify SMTP configuration. + - Click "Update" button at the bottom of this page. + - Restart tomcat in order to instruct oxAuth to use new SMTP configuration. + +There are log messages in this custom authentication script. In order to debug this module we can use command like this: +tail -f /opt/tomcat/logs/wrapper.log | grep "Duo" diff --git a/oxAuth/Server/integrations/duo/README.txt b/oxAuth/Server/integrations/duo/README.txt new file mode 100644 index 00000000..0ece1453 --- /dev/null +++ b/oxAuth/Server/integrations/duo/README.txt @@ -0,0 +1,21 @@ +This is a person authentication module for oxAuth that enables [Duo Authentication](https://www.duosecurity.com) for user authentication. + +The module has a few properties: + +1) duo_creds_file - It's mandatory property. It's path to file which contains contains ikey, skey, akey. These keys are required for DUO authentication. +Example: `/etc/certs/duo_creds.json` +Example content of this file: +`{"ikey": "ikey_value", "skey": "skey_value", "akey": "akey_value"}` + +2) duo_host - It's mandatory property. The URL of the DUO API server. + Example: `api-random.duosecurity.com` + +3) audit_attribute - It's optional property. It allows to define an attribute which the module should check for to determine whether the user belongs to duo_group or audit_group. Person DUO authentication module uses it if there is `duo_group` or `audit_group` property. + Example: `memberOf` + +4) duo_group - It's optional property. It's an optional attribute that alows to specify if DUO should be used for specific users. i.e. use DUO only for users who have audit_attribute (`memberOf`) attribute value equal to `duo_group`. If there is none DUO will be enforced for all users. + +5) audit_group - It's optional property. Specify if module should send an e-mail to administrator upon login of a user who has audit_attribute `memberOf` attribute value equal to `audit_group`. + +6) audit_group_email - It's optional property. It's the administrator's e-mail. Person DUO authentication module uses it if there is `audit_group` property. + diff --git a/oxAuth/Server/integrations/duo/lib/duo_web.py b/oxAuth/Server/integrations/duo/lib/duo_web.py new file mode 100644 index 00000000..7bdfb6dd --- /dev/null +++ b/oxAuth/Server/integrations/duo/lib/duo_web.py @@ -0,0 +1,175 @@ +# +# duo_web.py +# +# Copyright (c) 2011 Duo Security +# All rights reserved, all wrongs reversed. +# + +import base64 +import hashlib +import hmac +import time + +DUO_PREFIX = 'TX' +APP_PREFIX = 'APP' +AUTH_PREFIX = 'AUTH' +ENROLL_PREFIX = 'ENROLL' +ENROLL_REQUEST_PREFIX = 'ENROLL_REQUEST' + +DUO_EXPIRE = 300 +APP_EXPIRE = 3600 + +IKEY_LEN = 20 +SKEY_LEN = 40 +AKEY_LEN = 40 + +ERR_USER = 'ERR|The username passed to sign_request() is invalid.' +ERR_IKEY = 'ERR|The Duo integration key passed to sign_request() is invalid.' +ERR_SKEY = 'ERR|The Duo secret key passed to sign_request() is invalid.' +ERR_AKEY = 'ERR|The application secret key passed to sign_request() must be at least %s characters.' % AKEY_LEN +ERR_UNKNOWN = 'ERR|An unknown error has occurred.' + +def _hmac_sha1(key, msg): + ctx = hmac.new(key, msg, hashlib.sha1) + return ctx.hexdigest() + +def _sign_vals(key, vals, prefix, expire): + exp = str(int(time.time()) + expire) + + val = '|'.join(vals + [ exp ]) + b64 = base64.b64encode(val.encode('utf-8')).decode('utf-8') + cookie = '%s|%s' % (prefix, b64) + + sig = _hmac_sha1(key.encode('utf-8'), cookie.encode('utf-8')) + return '%s|%s' % (cookie, sig) + +def _parse_vals(key, val, prefix, ikey): + ts = int(time.time()) + u_prefix, u_b64, u_sig = val.split('|') + cookie = '%s|%s' % (u_prefix, u_b64) + e_key = key.encode('utf-8') + e_cookie = cookie.encode('utf-8') + + sig = _hmac_sha1(e_key, e_cookie) + if _hmac_sha1(e_key, sig.encode('utf-8')) != _hmac_sha1(e_key, u_sig.encode('utf-8')): + return None + + if u_prefix != prefix: + return None + + decoded = base64.b64decode(u_b64).decode('utf-8') + user, u_ikey, exp = decoded.split('|') + + if u_ikey != ikey: + return None + + if ts >= int(exp): + return None + + return user + +def _sign_request(ikey, skey, akey, username, prefix): + """Generate a signed request for Duo authentication. + The returned value should be passed into the Duo.init() call + in the rendered web page used for Duo authentication. + Arguments: + ikey -- Duo integration key + skey -- Duo secret key + akey -- Application secret key + username -- Primary-authenticated username + prefix -- DUO_PREFIX or ENROLL_REQUEST_PREFIX + """ + if not username: + return ERR_USER + if '|' in username: + return ERR_USER + if not ikey or len(ikey) != IKEY_LEN: + return ERR_IKEY + if not skey or len(skey) != SKEY_LEN: + return ERR_SKEY + if not akey or len(akey) < AKEY_LEN: + return ERR_AKEY + + vals = [ username, ikey ] + + try: + duo_sig = _sign_vals(skey, vals, prefix, DUO_EXPIRE) + app_sig = _sign_vals(akey, vals, APP_PREFIX, APP_EXPIRE) + except Exception: + return ERR_UNKNOWN + + return '%s:%s' % (duo_sig, app_sig) + + +def sign_request(ikey, skey, akey, username): + """Generate a signed request for Duo authentication. + The returned value should be passed into the Duo.init() call + in the rendered web page used for Duo authentication. + Arguments: + ikey -- Duo integration key + skey -- Duo secret key + akey -- Application secret key + username -- Primary-authenticated username + """ + return _sign_request(ikey, skey, akey, username, DUO_PREFIX) + + +def sign_enroll_request(ikey, skey, akey, username): + """Generate a signed request for Duo authentication. + The returned value should be passed into the Duo.init() call + in the rendered web page used for Duo authentication. + Arguments: + ikey -- Duo integration key + skey -- Duo secret key + akey -- Application secret key + username -- Primary-authenticated username + """ + return _sign_request(ikey, skey, akey, username, ENROLL_REQUEST_PREFIX) + + +def _verify_response(ikey, skey, akey, prefix, sig_response): + """Validate the signed response returned from Duo. + Returns the username of the authenticated user, or None. + Arguments: + ikey -- Duo integration key + skey -- Duo secret key + akey -- Application secret key + prefix -- AUTH_PREFIX or ENROLL_PREFIX that sig_response + must match + sig_response -- The signed response POST'ed to the server + """ + try: + auth_sig, app_sig = sig_response.split(':') + auth_user = _parse_vals(skey, auth_sig, AUTH_PREFIX, ikey) + app_user = _parse_vals(akey, app_sig, APP_PREFIX, ikey) + except Exception: + return None + + if auth_user != app_user: + return None + + return auth_user + + +def verify_response(ikey, skey, akey, sig_response): + """Validate the signed response returned from Duo. + Returns the username of the authenticated user, or None. + Arguments: + ikey -- Duo integration key + skey -- Duo secret key + akey -- Application secret key + sig_response -- The signed response POST'ed to the server + """ + return _verify_response(ikey, skey, akey, AUTH_PREFIX, sig_response) + + +def verify_enroll_response(ikey, skey, akey, sig_response): + """Validate the signed response returned from Duo. + Returns the username of the enrolled user, or None. + Arguments: + ikey -- Duo integration key + skey -- Duo secret key + akey -- Application secret key + sig_response -- The signed response POST'ed to the server + """ + return _verify_response(ikey, skey, akey, ENROLL_PREFIX, sig_response) diff --git a/oxAuth/Server/integrations/duo2_Gluu3/DUO_Universal_3.1.7.py b/oxAuth/Server/integrations/duo2_Gluu3/DUO_Universal_3.1.7.py new file mode 100644 index 00000000..6b57736f --- /dev/null +++ b/oxAuth/Server/integrations/duo2_Gluu3/DUO_Universal_3.1.7.py @@ -0,0 +1,177 @@ +from org.xdi.service.cdi.util import CdiUtil +from org.xdi.oxauth.security import Identity +from org.xdi.model.custom.script.type.auth import PersonAuthenticationType +from org.xdi.oxauth.service import AuthenticationService +from org.xdi.util import StringHelper +from java.util import Arrays +from javax.faces.context import FacesContext +from org.gluu.jsf2.service import FacesService +from org.xdi.oxauth.service.net import HttpService +import os +import java +import sys +from com.duosecurity import Client +from com.duosecurity.exception import DuoException +from com.duosecurity.model import Token +from org.xdi.oxauth.util import ServerUtil + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, configurationAttributes): + print "Duo-Universal. Initialization" + + if (not configurationAttributes.containsKey("client_id")): + print "Duo Universal. Initialization. Property client_id is not specified" + return False + else: + self.client_id = configurationAttributes.get("client_id").getValue2() + + if (not configurationAttributes.containsKey("client_secret")): + print "Duo Universal. Initialization. Property client_secret is not specified" + return False + else: + self.client_secret = configurationAttributes.get("client_secret").getValue2() + + if (not configurationAttributes.containsKey("api_hostname")): + print "Duo Universal. Initialization. Property api_hostname is not specified" + return False + else: + self.api_hostname = configurationAttributes.get("api_hostname").getValue2() + + print "Duo-Universal. Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "Duo-Universal. Destroy" + print "Duo-Universal. Destroyed successfully" + return True + + def getApiVersion(self): + return 1 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + print "Duo-Universal. Authenticate for step %s" % step + + identity = CdiUtil.bean(Identity) + if (step == 1): + authenticationService = CdiUtil.bean(AuthenticationService) + + # Check if user authenticated already in another custom script + user = authenticationService.getAuthenticatedUser() + + if user == None: + print "user is none" + credentials = identity.getCredentials() + + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + logged_in = False + if (StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password)): + print "before login" + logged_in = authenticationService.authenticate(user_name, user_password) + print "after login" + print logged_in + if (not logged_in): + print "return false" + return False + identity.setWorkingParameter('username',user_name) + return True + + elif (step == 2): + + identity = CdiUtil.bean(Identity) + + state = ServerUtil.getFirstValue(requestParameters, "state") + # Get state to verify consistency and originality + if identity.getWorkingParameter('state_duo') == state : + + # Get authorization token to trade for 2FA + duoCode = ServerUtil.getFirstValue(requestParameters, "duo_code") + try: + token = self.duo_client.exchangeAuthorizationCodeFor2FAResult(duoCode, identity.getWorkingParameter('username')) + print "token status %s " % token.getAuth_result().getStatus() + except: + # Handle authentication failure. + print "authentication failure", sys.exc_info()[1] + return False + + # User successfully passed Duo authentication. + + if "allow" == token.getAuth_result().getStatus(): + return True + + return False + + else: + print "Neither step 1 or 2" + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + print "Duo-Universal. Prepare for step %s" % step + + if (step == 1): + return True + elif (step == 2): + identity = CdiUtil.bean(Identity) + user_name = identity.getWorkingParameter('username') + facesContext = CdiUtil.bean(FacesContext) + request = facesContext.getExternalContext().getRequest() + httpService = CdiUtil.bean(HttpService) + url = httpService.constructServerUrl(request) + "/postlogin.htm" + print url + print user_name + try: + print "before health check" + self.duo_client = Client(self.client_id,self.client_secret,self.api_hostname,url) + self.duo_client.healthCheck() + print "after health check" + except: + print "Duo-Universal. Duo config error. Verify the values in Duo-Universal.conf are correct ", sys.exc_info()[1] + return False + + state = self.duo_client.generateState() + identity.setWorkingParameter("state_duo",state) + prompt_uri = self.duo_client.createAuthUrl(user_name, state) + + facesService = CdiUtil.bean(FacesService) + facesService.redirectToExternalURL(prompt_uri ) + + return True + + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + return Arrays.asList("state_duo", "username") + + def getCountAuthenticationSteps(self, configurationAttributes): + return 2 + + def getPageForStep(self, configurationAttributes, step): + print "Duo-Universal. getPageForStep - %s " % step + if (step == 2): + return "/auth/duo/duologin.xhtml" + return "" + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True + \ No newline at end of file diff --git a/oxAuth/Server/integrations/duo2_Gluu3/readme.md b/oxAuth/Server/integrations/duo2_Gluu3/readme.md new file mode 100644 index 00000000..363f029a --- /dev/null +++ b/oxAuth/Server/integrations/duo2_Gluu3/readme.md @@ -0,0 +1,83 @@ +# Duo Universal Prompt configuration with Gluu 3.1.7 + +## Overview +[Duo Security](https://duosecurity.com) is a SaaS authentication provider. This document will explain how to configure Duo University Prompt with Gluu Server 3.1.7 for a two-step authentication process with username and password as the first step, and Duo as the second step. The script invokes the Universal Prompt which is a redesign of Duo’s traditional authentication prompt. + +In order to use this authentication mechanism your organization will need a Duo account and users will need to download the Duo mobile app. + +## Prerequisites +- A Gluu Server 3.1.7; +- Speical Duo2 script specific for Gluu Server 3.1.7 which is included above. +- An account with [Duo Security](https://duo.com/). + + +## Configure Duo AccountS + +1. [Sign up](https://duo.com/) for a Duo account. + +2. Log in to the Duo Admin Panel and navigate to Applications. + +3. Click Protect an Application and locate Web SDK in the applications list. Click Protect this Application to get your client ID, secret key, and API hostname. + +For additional info for the steps refer to Duo's Web SDK 4, check [this article](https://duo.com/docs/duoweb-v4). + +## Add the duo-universal Dependency to your oxAuth + +1. Download Duo-Universal-SDK 1.0.3 (wget https://repo1.maven.org/maven2/com/duosecurity/duo-universal-sdk/1.0.3/duo-universal-sdk-1.0.3-jar-with-dependencies.jar) and put it inside `/opt/gluu/jetty/oxauth/custom/libs` + +1. Restart the oxauth service + + +## Configure oxTrust + +Follow the steps below to configure the Duo module in the oxTrust Admin GUI. + +1. Navigate to `Configuration` > `Person Authentication Scripts`. + Add a custom script for the 2 factor authentication using DUO credentials and name it duo2 (specifically because this is version 2 ). + + +1. Add the following Custom Property ( key/value pairs ) + + +| Property |Status | Description | Example | +|-----------------------|---------------|-----------------------|-----------------------| +|api_hostname |Mandatory |URL of the Duo API Server|api-random.duosecurity.com| +|client_id |Mandatory |Value from the Duo application using Web SDK 4 that was registered using DUO Admin console|DIxxxxxxxxxxxXxxxI| +|client_secret |Mandatory|Value from the Duo application using Web SDK 4 that was registered using DUO Admin console|eXXXXXXXXXXXXXXXP| + +1. Enable the script by ticking the check box + +Now Duo is an available authentication mechanism for your Gluu Server. This means that, using OpenID Connect `acr_values`, applications can now request Duo authentication for users. + +!!! Note + To make sure Duo has been enabled successfully, you can check your Gluu Server's OpenID Connect configuration by navigating to the following URL: `https:///.well-known/openid-configuration`. Find `"acr_values_supported":` and you should see `"duo2"`. + +## Make Duo the Default Authentication Mechanism + +Now applications can request Duo authentication, but what if you want to make Duo your default authentication mechanism? You can follow these instructions: + +1. Navigate to `Configuration` > `Manage Authentication`. +2. Select the `Default Authentication Method` tab. +3. In the Default Authentication Method window you will see two options: `Default acr` and `oxTrust acr`. + + - The `oxTrust acr` field controls the authentication mechanism that is presented to access the oxTrust dashboard GUI (the application you are in). + - The `Default acr` field controls the default authentication mechanism that is presented to users from all applications that leverage your Gluu Server for authentication. + +You can change one or both fields to Duo authentication as you see fit. If you want Duo to be the default authentication mechanism for access to oxTrust and all other applications that leverage your Gluu Server, change both fields to Duo. + +!!! Note + Currently, the DUO Universal Prompt has not yet been released. However, DUO has enabled customers to be application ready so that the switch to the newer User Interface can be seamless. + +## Upgrading to the DUO Universal Prompt from the older user interface for enrolling DUO credentials + +### In DUO Admin Console: +1. Register a new Duo's Web SDK 4 application, check [this article](https://duo.com/docs/duoweb-v4). +1. Save the client id and secret + +### In oxTrust : +1. Update the original duo script to reflect the latest contents. +1. Add script properties as mentioned in the above steps. The client ID and client secret can be obtained from the Web SDK 4 application of the DUO admin console. + +### Add the duo-universal Dependency to your oxAuth +Follow the exact steps mentioned previously in the document + diff --git a/oxAuth/Server/integrations/email2FA/README.md b/oxAuth/Server/integrations/email2FA/README.md new file mode 100644 index 00000000..74ab40eb --- /dev/null +++ b/oxAuth/Server/integrations/email2FA/README.md @@ -0,0 +1,55 @@ +# One-Time Password (OTP) Authentication over Email + +## Overview +This document explains how to use the Gluu Server's included +[Email_2fa interception script](https://raw.githubusercontent.com/GluuFederation/oxAuth/master/Server/integrations/email2FA/email2FAExternalAuthenticator.py) +to implement a two-step, two-factor authentication (2FA) process with username / password as the first step, and an OTP recieved on email as the second step. + + +## Prerequisites +- A Gluu Server ([installation instructions](../installation-guide/index.md)) +- [Email_2fa interception script](https://raw.githubusercontent.com/GluuFederation/oxAuth/master/Server/integrations/email2FA/email2FAExternalAuthenticator.py) +- SMTP configuration as per https://gluu.org/docs/gluu-server/4.3/admin-guide/oxtrust-ui/#smtp-server-configuration + + + +## Properties +The OTP authentication script has the following properties: + +| Property | Description | Example | +|-----------------------|-------------------------------|---------------| +|token_length |length (number of characters) of the OTP token| eg : 5 +|token_lifetime |In minutes| 2 + + +## Enable Email_2fa +Follow the steps below to enable Super Gluu authentication: + +1. In oxTrust, navigate to `Configuration` > `Person Authentication Scripts` +1. Create a new script and give it a relevant name say email_2fa and In the script field use the contents of this [file](https://raw.githubusercontent.com/GluuFederation/oxAuth/master/Server/integrations/email2FA/email2FAExternalAuthenticator.py) +1. Enable the script by checking the box +1. Scroll to the bottom of the page and click `Update` + +Now email_2fa is an available authentication mechanism for your Gluu Server. This means that, using OpenID Connect `acr_values`, applications can now request OTP authentication for users. + +## Make email_2fa the Default + +If OTP should be the default authentication mechanism, follow these instructions: + +1. Navigate to `Configuration` > `Manage Authentication` + +1. Select the `Default Authentication Method` tab + +1. In the Default Authentication Method window you will see two options: `Default acr` and `oxTrust acr` + + - `oxTrust acr` sets the authentication mechanism for accessing the oxTrust dashboard GUI (only managers should have acccess to oxTrust) + + - `Default acr` sets the default authentication mechanism for accessing all applications that leverage your Gluu Server for authentication (unless otherwise specified) + +If email_2fa should be the default authentication mechanism for all access, change both fields to email_2fa. + +# Add 2FA login pages to oxauth + +1. ` mkdir -p /opt/gluu/jetty/oxauth/custom/pages/auth/email_auth ` + +1. ` cp entertoken.xhtml /opt/gluu/jetty/oxauth/custom/pages/auth/email_auth ` diff --git a/oxAuth/Server/integrations/email2FA/email2FAExternalAuthenticator.py b/oxAuth/Server/integrations/email2FA/email2FAExternalAuthenticator.py new file mode 100644 index 00000000..577ece78 --- /dev/null +++ b/oxAuth/Server/integrations/email2FA/email2FAExternalAuthenticator.py @@ -0,0 +1,366 @@ + + +from org.gluu.oxauth.service import AuthenticationService +from org.gluu.oxauth.service import UserService +from org.gluu.oxauth.auth import Authenticator +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.util import StringHelper +from org.gluu.oxauth.util import ServerUtil +from org.gluu.oxauth.service.common import ConfigurationService +from org.gluu.oxauth.service.common import EncryptionService +from org.gluu.jsf2.message import FacesMessages +from javax.faces.application import FacesMessage +from org.gluu.persist.exception import AuthenticationException +from datetime import datetime, timedelta +from java.util import GregorianCalendar, TimeZone +from java.io import File +from java.io import FileInputStream +from java.util import Enumeration, Properties + +#dealing with smtp server +from java.security import Security +from javax.mail.internet import MimeMessage, InternetAddress +from javax.mail import Session, Message, Transport + +from java.util import Arrays +import random +import string +import re +import urllib +import java +try: + import json +except ImportError: + import simplejson as json + +class EmailValidator(): + regex = '^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$' + + def check(self, email): + + if(re.search(self.regex,email)): + print "EmailOTP. - %s is a valid email format" % email + return True + else: + print "EmailOTP. - %s is an invalid email format" % email + return False + +class Token: + #class that deals with string token + + def generateToken(self,lent): + rand1="1234567890123456789123456789" + rand2="9876543210123456789123456789" + first = int(rand1[:int(lent)]) + first1 = int(rand2[:int(lent)]) + token = random.randint(first, first1) + return token + + +class PersonAuthentication(PersonAuthenticationType): + + + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + + print "EmailOTP. - Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "EmailOTP. - Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + def getAuthenticationMethodClaims(self, configurationAttributes): + return None + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + + print "Email 2FA - Authenticate for step %s" % ( step) + authenticationService = CdiUtil.bean(AuthenticationService) + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + user_name = credentials.getUsername() + user_password = credentials.getPassword() + facesMessages = CdiUtil.bean(FacesMessages) + facesMessages.setKeepMessages() + subject = "Gluu Authentication Token" + session_attributes = identity.getSessionId().getSessionAttributes() + multipleEmails = session_attributes.get("emailIds") + + if step == 1: + try: + # Check if user authenticated already in another custom script + user2 = authenticationService.getAuthenticatedUser() + if user2 == None: + credentials = identity.getCredentials() + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + logged_in = False + if (StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password)): + userService = CdiUtil.bean(UserService) + logged_in = authenticationService.authenticate(user_name, user_password) + if logged_in is True: + user2 = authenticationService.getAuthenticatedUser() + emailIds = user2.getAttribute("oxEmailAlternate") + if StringHelper.isNotEmptyString(emailIds): + data = json.loads(emailIds) + if len(data['email-ids']) > 1: + commaSeperatedEmailString = [] + for email in data['email-ids']: + reciever_id = email['email'] + commaSeperatedEmailString.append(reciever_id) + # setting this in session is used to determine if this is a 2 or 3 step flow + identity.setWorkingParameter("emailIds", ",".join(commaSeperatedEmailString)) + + return logged_in + except AuthenticationException as err: + print err + return False + else: + #Means the selection email page was used + user2 = authenticationService.getAuthenticatedUser() + emailIds = user2.getAttribute("oxEmailAlternate") + if emailIds != None: + multipleEmails = [] + token = identity.getWorkingParameter("token") + + if StringHelper.isNotEmptyString(emailIds): + data = json.loads(emailIds) + + # step2 and multiple email ids present, then user has been presented a choice of email which is fetched in OtpEmailLoginForm:indexOfEmail, send email + if step == 2 and len(data['email-ids']) > 1 : + + for email in data['email-ids']: + reciever_id = email['email'] + multipleEmails.append(reciever_id) + + + idx = ServerUtil.getFirstValue(requestParameters, "OtpEmailLoginForm:indexOfEmail") + if idx != None and token != None: + sendToEmail = multipleEmails[int(idx)] + print "EmailOtp. Sending email to : %s " % sendToEmail + + body = "Here is your token: %s" % token + sender = EmailSender() + sender.sendEmail( sendToEmail, subject, body) + return True + else: + print "EmailOTP. Something wrong with index or token" + return False + # token verificaation - step 3 incase of email selection , else step 2 + else: + input_token = ServerUtil.getFirstValue(requestParameters, "OtpEmailLoginForm:passcode") + print "input token %s" % input_token + print "EmailOTP. - Token input by user is %s" % input_token + + token = str(identity.getWorkingParameter("token")) + min11 = int(identity.getWorkingParameter("sentmin")) + nww = datetime.now() + te = str(nww) + listew = te.split(':') + curtime = int(listew[1]) + + token_lifetime = int(configurationAttributes.get("token_lifetime").getValue2()) + if ((min11<= 60) and (min11>= 50)): + if ((curtime>=50) and (curtime<=60)): + timediff1 = curtime - min11 + if timediff1>token_lifetime: + #print "OTP Expired" + facesMessages.add(FacesMessage.SEVERITY_ERROR, "OTP Expired") + return False + elif ((curtime>=0) or (curtime<=10)): + timediff1 = 60 - min11 + timediff1 = timediff1 + curtime + if timediff1>token_lifetime: + #print "OTP Expired" + facesMessages.add(FacesMessage.SEVERITY_ERROR, "OTP Expired") + return False + + if ((min11>=0) and (min11<=60) and (curtime>=0) and (curtime<=60)): + timediff2 = curtime - min11 + if timediff2>token_lifetime: + #print "OTP Expired" + facesMessages.add(FacesMessage.SEVERITY_ERROR, "OTP Expired") + return False + # compares token sent and token entered by user + print "Token from session: %s " % token + if input_token == token: + print "Email 2FA - token entered correctly" + identity.setWorkingParameter("token_valid", True) + + return True + + else: + facesMessages = CdiUtil.bean(FacesMessages) + facesMessages.setKeepMessages() + facesMessages.clear() + facesMessages.add(FacesMessage.SEVERITY_ERROR, "Wrong code entered") + print "EmailOTP. Wrong code entered" + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + print "EmailOTP. - Preparing for step %s" % step + authenticationService = CdiUtil.bean(AuthenticationService) + + user2 = authenticationService.getAuthenticatedUser() + + + if step == 2 and user2 is not None: + uid = user2.getAttribute("uid") + identity = CdiUtil.bean(Identity) + lent = configurationAttributes.get("token_length").getValue2() + new_token = Token() + token = new_token.generateToken(lent) + subject = "Gluu Authentication Token" + body = "Here is your token: %s" % token + + sender = EmailSender() + emailIds = user2.getAttribute("oxEmailAlternate") + + print "emailIds : %s" % emailIds + data = json.loads(emailIds) + + #Attempt to send message now if user has only one email id + if len(data['email-ids']) == 1: + email = data['email-ids'][0] + print "EmailOTP. email to - %s" % email['email'] + sender.sendEmail( email['email'], subject, body) + + else: + commaSeperatedEmailString = [] + for email in data['email-ids']: + reciever_id = email['email'] + print "EmailOTP. Email to - %s" % reciever_id + #sender.sendEmail( reciever_id, subject, body) + commaSeperatedEmailString.append(self.getMaskedEmail(reciever_id)) + identity.setWorkingParameter("emailIds", ",".join(commaSeperatedEmailString)) + + otptime1 = datetime.now() + tess = str(otptime1) + listee = tess.split(':') + + identity.setWorkingParameter("sentmin", listee[1]) + identity.setWorkingParameter("token", token) + + return True + + return True + + def getExtraParametersForStep(self, configurationAttributes, step): + return Arrays.asList("token","emailIds","token_valid","sentmin") + + + def getCountAuthenticationSteps(self, configurationAttributes): + + print "EmailOTP. getCountAuthenticationSteps called" + + if CdiUtil.bean(Identity).getWorkingParameter("emailIds") == None: + print "EmailOTP. getCountAuthenticationSteps called - 2 steps" + return 2 + else: + print "EmailOTP. getCountAuthenticationSteps called 3 steps" + return 3 + + + def getPageForStep(self, configurationAttributes, step): + print "EmailOTP. getPageForStep called %s" % step + + defPage = "/casa/otp_email.xhtml" + if step == 2: + if CdiUtil.bean(Identity).getWorkingParameter("emailIds") == None: + print "emailIds not set, returning otp_email page" + return defPage + else: + return "/casa/otp_email_prompt.xhtml" + elif step == 3: + return defPage + return "" + + + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def logout(self, configurationAttributes, requestParameters): + return True + + def hasEnrollments(self, configurationAttributes, user): + values = user.getAttributeValues("oxEmailAlternate") + if values != None: + return True + else: + return False + + + def getMaskedEmail (self, emailid): + regex = r"(?<=.)[^@\n](?=[^@\n]*?@)|(?:(?<=@.)|(?!^)\G(?=[^@\n]*$)).(?=.*\.)" + subst = "*" + result = re.sub(regex, subst, emailid, 0, re.MULTILINE) + if result: + print (result) + return result + +class EmailSender(): + #class that sends e-mail through smtp + + def getSmtpConfig(self): + + smtp_config = None + smtpconfig = CdiUtil.bean(ConfigurationService).getConfiguration().getSmtpConfiguration() + + if smtpconfig is None: + print "Sign Email - SMTP CONFIG DOESN'T EXIST - Please configure" + + else: + encryptionService = CdiUtil.bean(EncryptionService) + smtp_config = { + 'host' : smtpconfig.getHost(), + 'port' : smtpconfig.getPort(), + 'user' : smtpconfig.getUserName(), + 'from' : smtpconfig.getFromEmailAddress(), + 'pwd_decrypted' : encryptionService.decrypt(smtpconfig.getPassword()), + 'req_ssl' : smtpconfig.isRequiresSsl(), + 'requires_authentication' : smtpconfig.isRequiresAuthentication(), + 'server_trust' : smtpconfig.isServerTrust() + } + + return smtp_config + + + + def sendEmail(self, useremail, subject, messageText): + # server connection + smtpconfig = self.getSmtpConfig() + + properties = Properties() + properties.setProperty("mail.smtp.host", smtpconfig['host']) + properties.setProperty("mail.smtp.port", str(smtpconfig['port'])) + properties.setProperty("mail.smtp.starttls.enable", "true") + session = Session.getDefaultInstance(properties) + + message = MimeMessage(session) + message.setFrom(InternetAddress(smtpconfig['from'])) + message.addRecipient(Message.RecipientType.TO,InternetAddress(useremail)) + message.setSubject(subject) + #message.setText(messageText) + message.setContent(messageText, "text/html") + + transport = session.getTransport("smtp") + transport.connect(properties.get("mail.smtp.host"),int(properties.get("mail.smtp.port")), smtpconfig['user'], smtpconfig['pwd_decrypted']) + transport.sendMessage(message,message.getRecipients(Message.RecipientType.TO)) + + + transport.close() diff --git a/oxAuth/Server/integrations/email2FA/entertoken.xhtml b/oxAuth/Server/integrations/email2FA/entertoken.xhtml new file mode 100644 index 00000000..ffba4cd3 --- /dev/null +++ b/oxAuth/Server/integrations/email2FA/entertoken.xhtml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + +
+
+
+

Authentication Token

+

Enter the token received in your email

+ ver_code + + + + + + +
+
+
+
+ + +
+ +
\ No newline at end of file diff --git a/oxAuth/Server/integrations/fido2/Fido2ExternalAuthenticator.py b/oxAuth/Server/integrations/fido2/Fido2ExternalAuthenticator.py new file mode 100644 index 00000000..d97a0325 --- /dev/null +++ b/oxAuth/Server/integrations/fido2/Fido2ExternalAuthenticator.py @@ -0,0 +1,277 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2018, Gluu +# +# Author: Yuriy Movchan +# + +from javax.ws.rs import ClientErrorException +from javax.ws.rs.core import Response +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.fido2.client import Fido2ClientFactory +from org.gluu.oxauth.security import Identity +from org.gluu.oxauth.service import AuthenticationService, SessionIdService +from org.gluu.oxauth.service.common import UserService +from org.gluu.oxauth.util import ServerUtil +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.util import StringHelper +from java.util import Arrays +from java.util.concurrent.locks import ReentrantLock +from javax.ws.rs import ClientErrorException +from javax.ws.rs.core import Response +from javax.faces.context import FacesContext + +import java +import sys +import json + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Fido2. Initialization" + + if not configurationAttributes.containsKey("fido2_server_uri"): + print "fido2_server_uri. Initialization. Property fido2_server_uri is not specified" + return False + + self.fido2_server_uri = configurationAttributes.get("fido2_server_uri").getValue2() + + self.fido2_domain = None + if configurationAttributes.containsKey("fido2_domain"): + self.fido2_domain = configurationAttributes.get("fido2_domain").getValue2() + + self.metaDataLoaderLock = ReentrantLock() + self.metaDataConfiguration = None + + print "Fido2. Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "Fido2. Destroy" + print "Fido2. Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + authenticationService = CdiUtil.bean(AuthenticationService) + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + user_name = credentials.getUsername() + + if step == 1: + print "Fido2. Authenticate for step 1" + identity.setWorkingParameter("platformAuthenticatorAvailable",ServerUtil.getFirstValue(requestParameters, "loginForm:platformAuthenticator")) + + user_password = credentials.getPassword() + logged_in = False + if StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password): + userService = CdiUtil.bean(UserService) + logged_in = authenticationService.authenticate(user_name, user_password) + + if not logged_in: + return False + + return True + elif step == 2: + print "Fido2. Authenticate for step 2" + + token_response = ServerUtil.getFirstValue(requestParameters, "tokenResponse") + if token_response == None: + print "Fido2. Authenticate for step 2. tokenResponse is empty" + return False + + auth_method = ServerUtil.getFirstValue(requestParameters, "authMethod") + if auth_method == None: + print "Fido2. Authenticate for step 2. authMethod is empty" + return False + + authenticationService = CdiUtil.bean(AuthenticationService) + user = authenticationService.getAuthenticatedUser() + if user == None: + print "Fido2. Prepare for step 2. Failed to determine user name" + return False + + if auth_method == 'authenticate': + print "Fido2. Prepare for step 2. Call Fido2 in order to finish authentication flow" + assertionService = Fido2ClientFactory.instance().createAssertionService(self.metaDataConfiguration) + assertionStatus = assertionService.verify(token_response) + authenticationStatusEntity = assertionStatus.readEntity(java.lang.String) + + if assertionStatus.getStatus() != Response.Status.OK.getStatusCode(): + print "Fido2. Authenticate for step 2. Get invalid authentication status from Fido2 server" + return False + + return True + elif auth_method == 'enroll': + print "Fido2. Prepare for step 2. Call Fido2 in order to finish registration flow" + attestationService = Fido2ClientFactory.instance().createAttestationService(self.metaDataConfiguration) + attestationStatus = attestationService.verify(token_response) + + if attestationStatus.getStatus() != Response.Status.OK.getStatusCode(): + print "Fido2. Authenticate for step 2. Get invalid registration status from Fido2 server" + return False + + return True + else: + print "Fido2. Prepare for step 2. Authentication method is invalid" + return False + + return False + else: + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + identity = CdiUtil.bean(Identity) + + if step == 1: + return True + elif step == 2: + print "Fido2. Prepare for step 2" + + session = CdiUtil.bean(SessionIdService).getSessionId() + if session == None: + print "Fido2. Prepare for step 2. Failed to determine session_id" + return False + + authenticationService = CdiUtil.bean(AuthenticationService) + user = authenticationService.getAuthenticatedUser() + if user == None: + print "Fido2. Prepare for step 2. Failed to determine user name" + return False + + userName = user.getUserId() + + metaDataConfiguration = self.getMetaDataConfiguration() + + assertionResponse = None + attestationResponse = None + + facesContext = CdiUtil.bean(FacesContext) + domain = facesContext.getExternalContext().getRequest().getServerName() + + + # Check if user have registered devices + count = CdiUtil.bean(UserService).countFido2RegisteredDevices(userName, domain) + + if count > 0: + print "Fido2. Prepare for step 2. Call Fido2 endpoint in order to start assertion flow" + + try: + assertionService = Fido2ClientFactory.instance().createAssertionService(metaDataConfiguration) + assertionRequest = json.dumps({'username': userName}, separators=(',', ':')) + assertionResponse = assertionService.authenticate(assertionRequest).readEntity(java.lang.String) + # if device has only platform authenticator and assertion is expecting a security key + if "internal" in assertionResponse: + identity.setWorkingParameter("platformAuthenticatorAvailable", "true") + else: + identity.setWorkingParameter("platformAuthenticatorAvailable", "false") + + except ClientErrorException, ex: + print "Fido2. Prepare for step 2. Failed to start assertion flow. Exception:", sys.exc_info()[1] + return False + else: + print "Fido2. Prepare for step 2. Call Fido2 endpoint in order to start attestation flow" + + try: + attestationService = Fido2ClientFactory.instance().createAttestationService(metaDataConfiguration) + platformAuthenticatorAvailable = identity.getWorkingParameter("platformAuthenticatorAvailable") == "true" + basic_json = {'username': userName, 'displayName': userName, 'attestation' : 'direct'} + print "% s" % identity.getWorkingParameter("platformAuthenticatorAvailable") + if platformAuthenticatorAvailable is True: + # the reason behind userVerification = discouraged --> https://chromium.googlesource.com/chromium/src/+/master/content/browser/webauth/uv_preferred.md + platform_json = {"authenticatorSelection":{"authenticatorAttachment":"platform","requireResidentKey" : "false", "userVerification" : "discouraged" } } + basic_json.update(platform_json) + + # also need to add this --> excludeCredentials : [//registered ids] + print " basic_json %s" % basic_json + + attestationRequest = json.dumps(basic_json) + #, separators=(',', ':')) + + attestationResponse = attestationService.register(attestationRequest).readEntity(java.lang.String) + except ClientErrorException, ex: + print "Fido2. Prepare for step 2. Failed to start attestation flow. Exception:", sys.exc_info()[1] + return False + + identity.setWorkingParameter("fido2_assertion_request", ServerUtil.asJson(assertionResponse)) + identity.setWorkingParameter("fido2_attestation_request", ServerUtil.asJson(attestationResponse)) + print "Fido2. Prepare for step 2. Successfully start flow with next requests.\nfido2_assertion_request: '%s'\nfido2_attestation_request: '%s'" % ( assertionResponse, attestationResponse ) + + return True + elif step == 3: + print "Fido2. Prepare for step 3" + + return True + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + return Arrays.asList( "platformAuthenticatorAvailable") + + def getCountAuthenticationSteps(self, configurationAttributes): + return 2 + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getPageForStep(self, configurationAttributes, step): + if step == 1: + return "/auth/fido2/step1.xhtml" + elif step == 2: + identity = CdiUtil.bean(Identity) + if identity.getWorkingParameter("platformAuthenticatorAvailable") == "true": + return "/auth/fido2/platform.xhtml" + else: + return "/auth/fido2/secKeys.xhtml" + return "" + + def logout(self, configurationAttributes, requestParameters): + return True + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def getMetaDataConfiguration(self): + if self.metaDataConfiguration != None: + return self.metaDataConfiguration + + self.metaDataLoaderLock.lock() + # Make sure that another thread not loaded configuration already + if self.metaDataConfiguration != None: + return self.metaDataConfiguration + + try: + print "Fido2. Initialization. Downloading Fido2 metadata" + self.fido2_server_metadata_uri = self.fido2_server_uri + "/.well-known/fido2-configuration" + + metaDataConfigurationService = Fido2ClientFactory.instance().createMetaDataConfigurationService(self.fido2_server_metadata_uri) + + max_attempts = 10 + for attempt in range(1, max_attempts + 1): + try: + self.metaDataConfiguration = metaDataConfigurationService.getMetadataConfiguration().readEntity(java.lang.String) + return self.metaDataConfiguration + except ClientErrorException, ex: + # Detect if last try or we still get Service Unavailable HTTP error + if (attempt == max_attempts) or (ex.getResponse().getResponseStatus() != Response.Status.SERVICE_UNAVAILABLE): + raise ex + + java.lang.Thread.sleep(3000) + print "Attempting to load metadata: %d" % attempt + finally: + self.metaDataLoaderLock.unlock() diff --git a/oxAuth/Server/integrations/forgot_password/README.md b/oxAuth/Server/integrations/forgot_password/README.md new file mode 100644 index 00000000..82fef8be --- /dev/null +++ b/oxAuth/Server/integrations/forgot_password/README.md @@ -0,0 +1,26 @@ +# Forgot Password 2FA Token Interception Script + +## Description + +This script is a 2 in 1. It can be used to enable user to reset its password **or** to enable 2FA sending a token to user's email : + +How they work: + +### Forgot Password +* Step 1: User enters e-mail and if e-mail exists, user receive a token via email +* Step 2: User enters token received by e-mail +* Step 3: User enters new password + +### Emai 2FA +* Step 1: User enters username and password +* Step 2: User enters token received by e-mail + +## Instalation + +* Make sure you have your **SMTP settings correctly Gluu Server** - Navigate to Configuration > Organization Configuration > SMTP Server Configuration +* Set up the script function (Forgot Password or Email 2FA) - Navigate to Configuration > Manage Custom Scripts, choose the script and set a **custom attribute** with key `SCRIPT_FUNCTION` and value `forgot_password` for Forgot Password mode, or `email_2FA` for Email 2FA mode. +* Enable the custom script. + +Please notice the xhtml files for this script are currently located at `oxAuth/Server/src/main/webapp/auth/forgot_password/` + +Note: If you want to use both scripts, you'll need to copy it and change the **custom attribute** from the copied script. diff --git a/oxAuth/Server/integrations/forgot_password/forgot_password.py b/oxAuth/Server/integrations/forgot_password/forgot_password.py new file mode 100644 index 00000000..4ade0ec4 --- /dev/null +++ b/oxAuth/Server/integrations/forgot_password/forgot_password.py @@ -0,0 +1,451 @@ +# coding: utf-8 +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2020, Gluu +# +# Author: Christian Eland + + +from org.xdi.oxauth.service import AuthenticationService +from org.gluu.oxauth.service import UserService +from org.gluu.oxauth.auth import Authenticator +from org.xdi.oxauth.security import Identity +from org.xdi.model.custom.script.type.auth import PersonAuthenticationType +from org.xdi.service.cdi.util import CdiUtil +from org.xdi.util import StringHelper +from org.xdi.oxauth.util import ServerUtil +from org.gluu.oxauth.service.common import ConfigurationService, EncryptionService +from org.gluu.jsf2.message import FacesMessages +from javax.faces.application import FacesMessage +from org.gluu.persist.exception import AuthenticationException + +#dealing with smtp server +import smtplib + +#dealing with emails +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText + +# This one is from core Java +from java.util import Arrays + +# to generate string token +import random +import string + +# regex +import re + +import urllib + +import java + +class EmailValidator(): + ''' + Class to check e-mail format + ''' + regex = '^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$' + + def check(self, email): + ''' + Check if email format is valid + returns: boolean + ''' + + if(re.search(self.regex,email)): + print "Forgot Password - %s is a valid email format" % email + return True + else: + print "Forgot Password - %s is an invalid email format" % email + return False + +class Token: + #class that deals with string token + + def generateToken(self): + ''' method to generate token string + returns: String + ''' + letters = string.ascii_lowercase + + #token lenght + lenght = 20 + + #generate token + token = ''.join(random.choice(letters) for i in range(lenght)) + print "Forgot Password - Generating token" + + return token + + +class EmailSender(): + #class that sends e-mail through smtp + + def getSmtpConfig(self): + ''' + get SMTP config from Gluu Server + return dict + ''' + + smtpconfig = CdiUtil.bean(ConfigurationService).getConfiguration().getSmtpConfiguration() + + if smtpconfig is None: + print "Forgot Password - SMTP CONFIG DOESN'T EXIST - Please configure" + + else: + print "Forgot Password - SMTP CONFIG FOUND" + encryptionService = CdiUtil.bean(EncryptionService) + smtp_config = { + 'host' : smtpconfig.getHost(), + 'port' : smtpconfig.getPort(), + 'user' : smtpconfig.getUserName(), + 'from' : smtpconfig.getFromEmailAddress(), + 'pwd_decrypted' : encryptionService.decrypt(smtpconfig.getPassword()), + 'req_ssl' : smtpconfig.getConnectProtection(), + 'requires_authentication' : smtpconfig.isRequiresAuthentication(), + 'server_trust' : smtpconfig.isServerTrust() + } + + return smtp_config + + + + def sendEmail(self,useremail,token): + ''' + send token by e-mail to useremail + ''' + + # server connection + smtpconfig = self.getSmtpConfig() + host = str(smtpconfig.get('host')) + port = smtpconfig.get('port') + user = str(smtpconfig.get('user')) + user_pass = str(smtpconfig.get('pwd_decrypted')) + sender = str(smtpconfig.get('from')) + receiver = str(useremail) + + try: + s = smtplib.SMTP(host, port) + + + if smtpconfig['requires_authentication']: + + if smtpconfig['req_ssl'] is not None: + s.starttls() + + s.login(user, user_pass) + + + #message setup + msg = MIMEMultipart() #create message + + message = "Here is your token: %s" % token + + msg['From'] = sender + msg['To'] = receiver + msg['Subject'] = "Password Reset Request" #subject + + #attach message body + msg.attach(MIMEText(message, 'plain')) + + #send message via smtp server + # send_message method is for python3 only s.send_message(msg) + + #send email (python2) + s.sendmail(sender,receiver,msg.as_string()) + + #after sent, delete + del msg + + #terminating session + s.quit() + + except smtplib.SMTPAuthenticationError as err: + print "Forgot Password - SMTPAuthenticationError - %s - %s" % (user,user_pass) + print err + + except smtplib.SMTPSenderRefused as err: + print "Forgot Password - SMTPSenderRefused - " + err + except smtplib.SMTPRecipientsRefused as err: + print "Forgot Password - SMTPRecipientsRefused - " + err + except smtplib.SMTPDataError as err: + print "Forgot Password - SMTPDataError - " + err + except smtplib.SMTPHeloError as err: + print "Forgot Password - SMTPHeloError - " + err + except: + print "Forgot Password - Not Found - Failed to send your message. Error not found" + + +class PersonAuthentication(PersonAuthenticationType): + + + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + + print "Forgot Password - Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "Forgot Password - Destroyed successfully" + return True + + def getApiVersion(self): + # I'm not sure why is 11 and not 2 + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + ''' + Authenticates user + Step 1 will be defined according to SCRIPT_FUNCTION custom attribute + returns: boolean + ''' + + #gets custom attribute + sf = configurationAttributes.get("SCRIPT_FUNCTION").getValue2() + + print "Forgot Password - %s - Authenticate for step %s" % (sf, step) + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + + if step == 1: + + if sf == "forgot_password": + + + authenticationService = CdiUtil.bean(AuthenticationService) + + logged_in = authenticationService.authenticate(user_name, user_password) + + + if not logged_in: + + + email = ServerUtil.getFirstValue(requestParameters, "ForgotPasswordForm:useremail") + validator = EmailValidator() + if not validator.check(email): + print "Forgot Password - Email format invalid" + return False + + else: + print "Forgot Password -Email format valid" + + print "Forgot Password - Entered email is %s" % email + identity.setWorkingParameter("useremail",email) + + # Just trying to get the user by the email + user_service = CdiUtil.bean(UserService) + user2 = user_service.getUserByAttribute("mail", email) + + if user2 is not None: + + print user2 + print "Forgot Password - User with e-mail %s found." % user2.getAttribute("mail") + + # send email + new_token = Token() + token = new_token.generateToken() + sender = EmailSender() + print "Email: " + email + print "Token: " + token + sender.sendEmail(email,token) + + + identity.setWorkingParameter("token", token) + print identity.getWorkingParameter("token") + + + + else: + print "Forgot Password - User with e-mail %s not found" % email + + return True + + + else: + # if user is already authenticated, returns true. + + user = authenticationService.getAuthenticatedUser() + print "Forgot Password - User %s is authenticated" % user.getUserId() + + return True + + if sf == "email_2FA": + + try: + # Just trying to get the user by the uid + authenticationService = CdiUtil.bean(AuthenticationService) + logged_in = authenticationService.authenticate(user_name, user_password) + + print 'email_2FA user_name: ' + str(user_name) + + user_service = CdiUtil.bean(UserService) + user2 = user_service.getUserByAttribute("uid", user_name) + + if user2 is not None: + print "user:" + print user2 + print "Forgot Password - User with e-mail %s found." % user2.getAttribute("mail") + email = user2.getAttribute("mail") + uid = user2.getAttribute("uid") + + # send token + # send email + new_token = Token() + token = new_token.generateToken() + sender = EmailSender() + print "Email: " + email + print "Token: " + token + sender.sendEmail(email,token) + + identity.setWorkingParameter("token", token) + + return True + + except AuthenticationException as err: + print err + return False + + + + + if step == 2: + # step 2 user enters token + credentials = identity.getCredentials() + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + authenticationService = CdiUtil.bean(AuthenticationService) + logged_in = authenticationService.authenticate(user_name, user_password) + + # retrieves token typed by user + input_token = ServerUtil.getFirstValue(requestParameters, "ResetTokenForm:inputToken") + + print "Forgot Password - Token inputed by user is %s" % input_token + + token = identity.getWorkingParameter("token") + print "Forgot Password - Retrieved token" + email = identity.getWorkingParameter("useremail") + print "Forgot Password - Retrieved email" + + # compares token sent and token entered by user + if input_token == token: + print "Forgot Password - token entered correctly" + identity.setWorkingParameter("token_valid", True) + + return True + + else: + print "Forgot Password - wrong token" + return False + + + if step == 3: + # step 3 enters new password (only runs if custom attibute is forgot_password + + user_service = CdiUtil.bean(UserService) + + email = identity.getWorkingParameter("useremail") + user2 = user_service.getUserByAttribute("mail", email) + + + user_name = user2.getUserId() + + new_password = ServerUtil.getFirstValue(requestParameters, "UpdatePasswordForm:newPassword") + + print "Forgot Password - New password submited" + + # update user info with new password + user2.setAttribute("userPassword",new_password) + print "Forgot Password - user uid is %s" % user_name + print "Forgot Password - Updating user with new password..." + user_service.updateUser(user2) + print "Forgot Password - User updated with new password" + # authenticates and login user + print "Forgot Password - Loading authentication service..." + authenticationService2 = CdiUtil.bean(AuthenticationService) + + print "Forgot Password - Trying to authenticate user..." + login = authenticationService2.authenticate(user_name, new_password) + + return True + + def prepareForStep(self, configurationAttributes, requestParameters, step): + + print "Forgot Password - Preparing for step %s" % step + + return True + + + # Return value is a java.util.List + def getExtraParametersForStep(self, configurationAttributes, step): + return Arrays.asList("token","useremail","token_valid") + + + # This method determines how many steps the authentication flow may have + # It doesn't have to be a constant value + def getCountAuthenticationSteps(self, configurationAttributes): + + sf = configurationAttributes.get("SCRIPT_FUNCTION").getValue2() + + + # if option is forgot_token + if sf == "forgot_password": + print "Entered sf == forgot_password" + return 3 + + # if ption is email_2FA + if sf == "email_2FA": + print "Entered if sf=email_2FA" + return 2 + + else: + print "Forgot Password - Custom Script Custom Property Incorrect, please check" + + + # The xhtml page to render upon each step of the flow + # returns a string relative to oxAuth webapp root + def getPageForStep(self, configurationAttributes, step): + + sf = configurationAttributes.get("SCRIPT_FUNCTION").getValue2() + + if step == 1: + + if sf == "forgot_password": + return "/auth/forgot_password/forgot.xhtml" + + if sf == 'email_2FA': + return "" + + if step == 2: + return "/auth/forgot_password/entertoken.xhtml" + + if step == 3: + if sf == "forgot_password": + return "/auth/forgot_password/newpassword.xhtml" + + + def getNextStep(self, configurationAttributes, requestParameters, step): + # Method used on version 2 (11?) + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True diff --git a/oxAuth/Server/integrations/fortinet/FortinetExternalAuthenticator.py b/oxAuth/Server/integrations/fortinet/FortinetExternalAuthenticator.py new file mode 100644 index 00000000..44f95a74 --- /dev/null +++ b/oxAuth/Server/integrations/fortinet/FortinetExternalAuthenticator.py @@ -0,0 +1,127 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2016, Gluu +# +# Author: Madhumita Subramaniam +# + +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.service import AuthenticationService +from org.gluu.util import StringHelper + +from net.sourceforge.jradiusclient import RadiusClient +from net.sourceforge.jradiusclient import RadiusAttribute +from net.sourceforge.jradiusclient import RadiusAttributeValues +from net.sourceforge.jradiusclient import RadiusPacket + +import java + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Radius. Initialization" + if not configurationAttributes.containsKey("RADIUS_SERVER_IP"): + print "Fortinet. Initialization. Property RADIUS_SERVER_IP is mandatory" + return False + self.RADIUS_SERVER_IP = configurationAttributes.get("RADIUS_SERVER_IP").getValue2() + + if not configurationAttributes.containsKey("RADIUS_SERVER_SECRET"): + print "Fortinet. Initialization. Property RADIUS_SERVER_SECRET is mandatory" + return False + self.RADIUS_SERVER_SECRET = configurationAttributes.get("RADIUS_SERVER_SECRET").getValue2() + + if not configurationAttributes.containsKey("RADIUS_SERVER_AUTH_PORT"): + print "Fortinet. Initialization. Property RADIUS_SERVER_AUTH_PORT is mandatory" + return False + self.RADIUS_SERVER_AUTH_PORT = configurationAttributes.get("RADIUS_SERVER_AUTH_PORT").getValue2() + + if not configurationAttributes.containsKey("RADIUS_SERVER_ACCT_PORT"): + print "Fortinet. Initialization. Property RADIUS_SERVER_ACCT_PORT is mandatory" + return False + self.RADIUS_SERVER_ACCT_PORT = configurationAttributes.get("RADIUS_SERVER_ACCT_PORT").getValue2() + + print "Radius. Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "Radius. Destroy" + print "Radius. Destroyed successfully" + return True + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def getApiVersion(self): + return 11 + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + authenticationService = CdiUtil.bean(AuthenticationService) + + if (step == 1): + print "Radius. Authenticate for step 1" + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + if StringHelper.isNotEmptyString(user_name ) and StringHelper.isNotEmptyString(user_password ): + user_exists_in_gluu = authenticationService.authenticate(user_name ) + if user_exists_in_gluu : + client = RadiusClient(self.RADIUS_SERVER_IP,int (self.RADIUS_SERVER_AUTH_PORT), int(self.RADIUS_SERVER_ACCT_PORT), self.RADIUS_SERVER_SECRET) + accessRequest = RadiusPacket(RadiusPacket.ACCESS_REQUEST) + userNameAttribute = RadiusAttribute(RadiusAttributeValues.USER_NAME,user_name ) + userPasswordAttribute = RadiusAttribute(RadiusAttributeValues.USER_PASSWORD,user_password ) + accessRequest.setAttribute(userNameAttribute) + accessRequest.setAttribute(userPasswordAttribute) + accessResponse = client.authenticate(accessRequest) + print "Packet type - %s " % accessResponse.getPacketType() + if accessResponse.getPacketType() == RadiusPacket.ACCESS_ACCEPT: + return True + #elif accessResponse.getPacketType() == RadiusPacket.ACCESS_CHALLENGE: + # return False + + + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + if (step == 1): + print "Radius. Prepare for Step 1" + return True + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + return 1 + + def getPageForStep(self, configurationAttributes, step): + return "" + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True + + + def printRadiusPacket(self, radiusPacket): + attributes = radiusPacket.getAttributes() + for attribute in attributes: + print("%s : %s" %attribute.getType() % attribute.getValue) \ No newline at end of file diff --git a/oxAuth/Server/integrations/fortinet/README.txt b/oxAuth/Server/integrations/fortinet/README.txt new file mode 100644 index 00000000..35e713b6 --- /dev/null +++ b/oxAuth/Server/integrations/fortinet/README.txt @@ -0,0 +1,89 @@ +# Fortinet - RADIUS server Authentication + +## Overview +This document explains how to configure the Gluu Server so that when a user logs in, an authentication request is made to Fortinet's remote RADIUS (Remote Authentication Dial-In User Service) server which then validates the user name and password. + +## Prerequisites + +- A Gluu Server (installation instructions [here](../installation-guide/index.md)) which will play the role of RADIUS client +- The [Fortinet script](https://github.com/GluuFederation/oxAuth/blob/master/Server/integrations/fortinet/FortinetExternalAuthenticator.py) (included in the default Gluu Server distribution); +- A Fortinet server which is the RADIUS server. +- The jradius-client [jar library](https://sourceforge.net/projects/jradius-client/files/) added to oxAuth + + + +## Fortinet Configuration + +In `Authentication` -> `Radius Service` -> `Clients`, create a new client (which is Gluu server). Enter the "secret" which will be used by the interception script to exchange RADIUS packets. + +## Gluu Server Configuration +### Add JRadius library to oxAuth + +- Copy the jradius-client jar file to the following oxAuth folder inside the Gluu Server chroot: `/opt/gluu/jetty/oxauth/custom/libs` + +- Edit `/opt/gluu/jetty/oxauth/webapps/oxauth.xml` and add the following line: + + ``` + /opt/gluu/jetty/oxauth/custom/libs/jradius-client.jar + ``` + +- [Restart](../operation/services.md#restart) the `oxauth` service + + + +### Enable Interception Script + +Follow the steps below to enable Fortinet's RADIUS authentication: + +1. Navigate to `Configuration` > `Person Authentication Scripts` + +1. Find the `fortinet` script. + + ![fortinet](../img/admin-guide/multi-factor/fortinet-custom-script.png) + +1. Populate the properties table with the details from your Fortinet account: + + +| Property | Description | Input value | +|-----------------------|-------------------------------|---------------| +|RADIUS_SERVER_IP |IP address of Fortinet's RADIUS Server | 10.10.10.1 | +|RADIUS_SERVER_SECRET |Configured when the RADIUS client is registered. | spam | +|RADIUS_SERVER_AUTH_PORT |Authentication port | 1812 | +|RADIUS_SERVER_ACCT_PORT |Accounting port | 1813 | + +1. Enable the script by checking the box + +1. Scroll to the bottom of the page and click `Update` + +Now authenticating a user against Fortinet's RADIUS server is possible from your Gluu Server. This means that, using OpenID Connect `acr_values`, applications can now request Fortinet authentication for users. + +!!! Note + To make sure this method has been enabled successfully, you can check your Gluu Server's OpenID Connect configuration by navigating to the following URL: `https:///.well-known/openid-configuration`. Find `"acr_values_supported":` and you should see `"fortinet"`. + +### Make Fortinet's user-password authentication the Default mechanism. +If fortinet should be the default authentication mechanism, follow these instructions: + +1. Navigate to `Configuration` > `Manage Authentication`. + +1. Select the `Default Authentication Method` tab. + +1. In the Default Authentication Method window you will see two options: `Default acr` and `oxTrust acr`. + +![fortinet](../img/admin-guide/multi-factor/fortinet.png) + + - `oxTrust acr` sets the authentication mechanism for accessing the oxTrust dashboard GUI (only managers should have acccess to oxTrust). + + - `Default acr` sets the default authentication mechanism for accessing all applications that leverage your Gluu Server for authentication (unless otherwise specified). + +If Fortinet should be the default authentication mechanism for all access, change both fields to fortinet. + +## Troubleshooting +If problems are encountered, take a look at the logs, specifically `/opt/gluu/jetty/oxauth/logs/oxauth_script.log`. Inspect all messages related to Fortinet. For instance, the following messages show an example of correct script initialization: + +``` +Fortinet. Initialization +Fortinet. Initialized successfully +``` + +Also make sure you are using the latest version of the script that can be found [here](https://github.com/GluuFederation/oxAuth/blob/master/Server/integrations/fortinet/FortinetExternalAuthenticator.py). + diff --git a/oxAuth/Server/integrations/gplus/GooglePlusExternalAuthenticator.py b/oxAuth/Server/integrations/gplus/GooglePlusExternalAuthenticator.py new file mode 100644 index 00000000..52a17ecd --- /dev/null +++ b/oxAuth/Server/integrations/gplus/GooglePlusExternalAuthenticator.py @@ -0,0 +1,557 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2016, Gluu +# +# Author: Yuriy Movchan +# + +import java +import json +from java.util import Arrays, HashMap, IdentityHashMap +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.client import TokenClient, TokenRequest, UserInfoClient +from org.gluu.oxauth.model.common import GrantType, AuthenticationMethod +from org.gluu.oxauth.model.common import User +from org.gluu.oxauth.model.jwt import Jwt, JwtClaimName +from org.gluu.oxauth.security import Identity +from org.gluu.oxauth.service import UserService, ClientService, AuthenticationService +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.util import StringHelper, ArrayHelper + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Google+ Initialization" + + if (not configurationAttributes.containsKey("gplus_client_secrets_file")): + print "Google+ Initialization. The property gplus_client_secrets_file is empty" + return False + + clientSecretsFile = configurationAttributes.get("gplus_client_secrets_file").getValue2() + self.clientSecrets = self.loadClientSecrets(clientSecretsFile) + if (self.clientSecrets == None): + print "Google+ Initialization. File with Google+ client secrets should be not empty" + return False + + self.attributesMapping = None + if (configurationAttributes.containsKey("gplus_remote_attributes_list") and + configurationAttributes.containsKey("gplus_local_attributes_list")): + + remoteAttributesList = configurationAttributes.get("gplus_remote_attributes_list").getValue2() + if (StringHelper.isEmpty(remoteAttributesList)): + print "Google+ Initialization. The property gplus_remote_attributes_list is empty" + return False + + localAttributesList = configurationAttributes.get("gplus_local_attributes_list").getValue2() + if (StringHelper.isEmpty(localAttributesList)): + print "Google+ Initialization. The property gplus_local_attributes_list is empty" + return False + + self.attributesMapping = self.prepareAttributesMapping(remoteAttributesList, localAttributesList) + if (self.attributesMapping == None): + print "Google+ Initialization. The attributes mapping isn't valid" + return False + + self.extensionModule = None + if (configurationAttributes.containsKey("extension_module")): + extensionModuleName = configurationAttributes.get("extension_module").getValue2() + try: + self.extensionModule = __import__(extensionModuleName) + extensionModuleInitResult = self.extensionModule.init(configurationAttributes) + if (not extensionModuleInitResult): + return False + except ImportError, ex: + print "Google+ Initialization. Failed to load gplus_extension_module: '%s'" % extensionModuleName + print "Google+ Initialization. Unexpected error:", ex + return False + + print "Google+ Initialized successfully" + return True + + def destroy(self, authConfiguration): + print "Google+ Destroy" + print "Google+ Destroyed successfully" + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + identity = CdiUtil.bean(Identity) + userService = CdiUtil.bean(UserService) + authenticationService = CdiUtil.bean(AuthenticationService) + + mapUserDeployment = False + enrollUserDeployment = False + if (configurationAttributes.containsKey("gplus_deployment_type")): + deploymentType = StringHelper.toLowerCase(configurationAttributes.get("gplus_deployment_type").getValue2()) + + if (StringHelper.equalsIgnoreCase(deploymentType, "map")): + mapUserDeployment = True + if (StringHelper.equalsIgnoreCase(deploymentType, "enroll")): + enrollUserDeployment = True + + if (step == 1): + print "Google+ Authenticate for step 1" + + gplusAuthCodeArray = requestParameters.get("gplus_auth_code") + gplusAuthCode = gplusAuthCodeArray[0] + + # Check if user uses basic method to log in + useBasicAuth = False + if (StringHelper.isEmptyString(gplusAuthCode)): + useBasicAuth = True + + # Use basic method to log in + if (useBasicAuth): + print "Google+ Authenticate for step 1. Basic authentication" + + identity.setWorkingParameter("gplus_count_login_steps", 1) + + credentials = identity.getCredentials() + + userName = credentials.getUsername() + userPassword = credentials.getPassword() + + loggedIn = False + if (StringHelper.isNotEmptyString(userName) and StringHelper.isNotEmptyString(userPassword)): + userService = CdiUtil.bean(UserService) + loggedIn = authenticationService.authenticate(userName, userPassword) + + if (not loggedIn): + return False + + return True + + # Use Google+ method to log in + print "Google+ Authenticate for step 1. gplusAuthCode:", gplusAuthCode + + currentClientSecrets = self.getCurrentClientSecrets(self.clientSecrets, configurationAttributes, requestParameters) + if (currentClientSecrets == None): + print "Google+ Authenticate for step 1. Client secrets configuration is invalid" + return False + + print "Google+ Authenticate for step 1. Attempting to gets tokens" + tokenResponse = self.getTokensByCode(self.clientSecrets, configurationAttributes, gplusAuthCode) + if ((tokenResponse == None) or (tokenResponse.getIdToken() == None) or (tokenResponse.getAccessToken() == None)): + print "Google+ Authenticate for step 1. Failed to get tokens" + return False + else: + print "Google+ Authenticate for step 1. Successfully gets tokens" + + jwt = Jwt.parse(tokenResponse.getIdToken()) + # TODO: Validate ID Token Signature + + gplusUserUid = jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER) + print "Google+ Authenticate for step 1. Found Google user ID in the ID token: '%s'" % gplusUserUid + + if (mapUserDeployment): + # Use mapping to local IDP user + print "Google+ Authenticate for step 1. Attempting to find user by oxExternalUid: 'gplus:%s'" % gplusUserUid + + # Check if there is user with specified gplusUserUid + foundUser = userService.getUserByAttribute("oxExternalUid", "gplus:" + gplusUserUid) + + if (foundUser == None): + print "Google+ Authenticate for step 1. Failed to find user" + print "Google+ Authenticate for step 1. Setting count steps to 2" + identity.setWorkingParameter("gplus_count_login_steps", 2) + identity.setWorkingParameter("gplus_user_uid", gplusUserUid) + return True + + foundUserName = foundUser.getUserId() + print "Google+ Authenticate for step 1. foundUserName: '%s'" % foundUserName + + userAuthenticated = authenticationService.authenticate(foundUserName) + if (userAuthenticated == False): + print "Google+ Authenticate for step 1. Failed to authenticate user" + return False + + print "Google+ Authenticate for step 1. Setting count steps to 1" + identity.setWorkingParameter("gplus_count_login_steps", 1) + + postLoginResult = self.extensionPostLogin(configurationAttributes, foundUser) + print "Google+ Authenticate for step 1. postLoginResult: '%s'" % postLoginResult + + return postLoginResult + elif (enrollUserDeployment): + # Use auto enrollment to local IDP + print "Google+ Authenticate for step 1. Attempting to find user by oxExternalUid: 'gplus:%s'" % gplusUserUid + + # Check if there is user with specified gplusUserUid + foundUser = userService.getUserByAttribute("oxExternalUid", "gplus:" + gplusUserUid) + + if (foundUser == None): + # Auto user enrollemnt + print "Google+ Authenticate for step 1. There is no user in LDAP. Adding user to local LDAP" + + print "Google+ Authenticate for step 1. Attempting to gets user info" + userInfoResponse = self.getUserInfo(currentClientSecrets, configurationAttributes, tokenResponse.getAccessToken()) + if ((userInfoResponse == None) or (userInfoResponse.getClaims().size() == 0)): + print "Google+ Authenticate for step 1. Failed to get user info" + return False + else: + print "Google+ Authenticate for step 1. Successfully gets user info" + + gplusResponseAttributes = userInfoResponse.getClaims() + + # Convert Google+ user claims to lover case + gplusResponseNormalizedAttributes = HashMap() + for gplusResponseAttributeEntry in gplusResponseAttributes.entrySet(): + gplusResponseNormalizedAttributes.put( + StringHelper.toLowerCase(gplusResponseAttributeEntry.getKey()), gplusResponseAttributeEntry.getValue()) + + currentAttributesMapping = self.getCurrentAttributesMapping(self.attributesMapping, configurationAttributes, requestParameters) + print "Google+ Authenticate for step 1. Using next attributes mapping '%s'" % currentAttributesMapping + + newUser = User() + for attributesMappingEntry in currentAttributesMapping.entrySet(): + remoteAttribute = attributesMappingEntry.getKey() + localAttribute = attributesMappingEntry.getValue() + + localAttributeValue = gplusResponseNormalizedAttributes.get(remoteAttribute) + if (localAttribute != None): + newUser.setAttribute(localAttribute, localAttributeValue) + + if (newUser.getAttribute("sn") == None): + newUser.setAttribute("sn", gplusUserUid) + + if (newUser.getAttribute("cn") == None): + newUser.setAttribute("cn", gplusUserUid) + + # Add mail to oxTrustEmail so that the user's + # email is available through the SCIM interface + # too. + if (newUser.getAttribute("oxTrustEmail") is None and + newUser.getAttribute("mail") is not None): + oxTrustEmail = { + "value": newUser.getAttribute("mail"), + "display": newUser.getAttribute("mail"), + "primary": True, + "operation": None, + "reference": None, + "type": "other" + } + newUser.setAttribute("oxTrustEmail", json.dumps(oxTrustEmail)) + + newUser.setAttribute("oxExternalUid", "gplus:" + gplusUserUid) + print "Google+ Authenticate for step 1. Attempting to add user '%s' with next attributes '%s'" % (gplusUserUid, newUser.getCustomAttributes()) + + foundUser = userService.addUser(newUser, True) + print "Google+ Authenticate for step 1. Added new user with UID: '%s'" % foundUser.getUserId() + + foundUserName = foundUser.getUserId() + print "Google+ Authenticate for step 1. foundUserName: '%s'" % foundUserName + + userAuthenticated = authenticationService.authenticate(foundUserName) + if (userAuthenticated == False): + print "Google+ Authenticate for step 1. Failed to authenticate user" + return False + + print "Google+ Authenticate for step 1. Setting count steps to 1" + identity.setWorkingParameter("gplus_count_login_steps", 1) + + print "Google+ Authenticate for step 1. Attempting to run extension postLogin" + postLoginResult = self.extensionPostLogin(configurationAttributes, foundUser) + print "Google+ Authenticate for step 1. postLoginResult: '%s'" % postLoginResult + + return postLoginResult + else: + # Check if there is user with specified gplusUserUid + print "Google+ Authenticate for step 1. Attempting to find user by uid: '%s'" % gplusUserUid + + foundUser = userService.getUser(gplusUserUid) + if (foundUser == None): + print "Google+ Authenticate for step 1. Failed to find user" + return False + + foundUserName = foundUser.getUserId() + print "Google+ Authenticate for step 1. foundUserName: '%s'" % foundUserName + + userAuthenticated = authenticationService.authenticate(foundUserName) + if (userAuthenticated == False): + print "Google+ Authenticate for step 1. Failed to authenticate user" + return False + + print "Google+ Authenticate for step 1. Setting count steps to 1" + identity.setWorkingParameter("gplus_count_login_steps", 1) + + postLoginResult = self.extensionPostLogin(configurationAttributes, foundUser) + print "Google+ Authenticate for step 1. postLoginResult: '%s'" % postLoginResult + + return postLoginResult + elif (step == 2): + print "Google+ Authenticate for step 2" + + sessionAttributes = identity.getSessionId().getSessionAttributes() + if (sessionAttributes == None) or not sessionAttributes.containsKey("gplus_user_uid"): + print "Google+ Authenticate for step 2. gplus_user_uid is empty" + return False + + gplusUserUid = sessionAttributes.get("gplus_user_uid") + passed_step1 = StringHelper.isNotEmptyString(gplusUserUid) + if (not passed_step1): + return False + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + userName = credentials.getUsername() + userPassword = credentials.getPassword() + + loggedIn = False + if (StringHelper.isNotEmptyString(userName) and StringHelper.isNotEmptyString(userPassword)): + loggedIn = authenticationService.authenticate(userName, userPassword) + + if (not loggedIn): + return False + + # Check if there is user which has gplusUserUid + # Avoid mapping Google account to more than one IDP account + foundUser = userService.getUserByAttribute("oxExternalUid", "gplus:" + gplusUserUid) + + if (foundUser == None): + # Add gplusUserUid to user one id UIDs + foundUser = userService.addUserAttribute(userName, "oxExternalUid", "gplus:" + gplusUserUid) + if (foundUser == None): + print "Google+ Authenticate for step 2. Failed to update current user" + return False + + postLoginResult = self.extensionPostLogin(configurationAttributes, foundUser) + print "Google+ Authenticate for step 2. postLoginResult: '%s'" % postLoginResult + + return postLoginResult + else: + foundUserName = foundUser.getUserId() + print "Google+ Authenticate for step 2. foundUserName: '%s'" % foundUserName + + if StringHelper.equals(userName, foundUserName): + postLoginResult = self.extensionPostLogin(configurationAttributes, foundUser) + print "Google+ Authenticate for step 2. postLoginResult: '%s'" % postLoginResult + + return postLoginResult + + return False + else: + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + identity = CdiUtil.bean(Identity) + authenticationService = CdiUtil.bean(AuthenticationService) + + if (step == 1): + print "Google+ Prepare for step 1" + + currentClientSecrets = self.getCurrentClientSecrets(self.clientSecrets, configurationAttributes, requestParameters) + if (currentClientSecrets == None): + print "Google+ Prepare for step 1. Google+ client configuration is invalid" + return False + + identity.setWorkingParameter("gplus_client_id", currentClientSecrets["web"]["client_id"]) + identity.setWorkingParameter("gplus_client_secret", currentClientSecrets["web"]["client_secret"]) + + return True + elif (step == 2): + print "Google+ Prepare for step 2" + + return True + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + if (step == 2): + return Arrays.asList("gplus_user_uid") + + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + identity = CdiUtil.bean(Identity) + if (identity.isSetWorkingParameter("gplus_count_login_steps")): + return identity.getWorkingParameter("gplus_count_login_steps") + + return 2 + + def getPageForStep(self, configurationAttributes, step): + if (step == 1): + return "/auth/gplus/gpluslogin.xhtml" + + return "/auth/gplus/gpluspostlogin.xhtml" + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + # TODO Revoke token + return True + + def loadClientSecrets(self, clientSecretsFile): + clientSecrets = None + + # Load certificate from file + f = open(clientSecretsFile, 'r') + try: + clientSecrets = json.loads(f.read()) + except: + print "Failed to load Google+ client secrets from file: '%s'" % clientSecrets + return None + finally: + f.close() + + return clientSecrets + + def getClientConfiguration(self, configurationAttributes, requestParameters): + # Get client configuration + if (configurationAttributes.containsKey("gplus_client_configuration_attribute")): + clientConfigurationAttribute = configurationAttributes.get("gplus_client_configuration_attribute").getValue2() + print "Google+ GetClientConfiguration. Using client attribute: '%s'" % clientConfigurationAttribute + + if (requestParameters == None): + return None + + clientId = None + + # Attempt to determine client_id from request + clientIdArray = requestParameters.get("client_id") + if (ArrayHelper.isNotEmpty(clientIdArray) and StringHelper.isNotEmptyString(clientIdArray[0])): + clientId = clientIdArray[0] + + # Attempt to determine client_id from event context + if (clientId == None): + identity = CdiUtil.bean(Identity) + if (identity.isSetWorkingParameter("sessionAttributes")): + clientId = identity.getSessionId().getSessionAttributes().get("client_id") + + if (clientId == None): + print "Google+ GetClientConfiguration. client_id is empty" + return None + + clientService = CdiUtil.bean(ClientService) + client = clientService.getClient(clientId) + if (client == None): + print "Google+ GetClientConfiguration. Failed to find client '%s' in local LDAP" % clientId + return None + + clientConfiguration = clientService.getCustomAttribute(client, clientConfigurationAttribute) + if ((clientConfiguration == None) or StringHelper.isEmpty(clientConfiguration.getValue())): + print "Google+ GetClientConfiguration. Client '%s' attribute '%s' is empty" % (clientId, clientConfigurationAttribute) + else: + print "Google+ GetClientConfiguration. Client '%s' attribute '%s' is '%s'" % (clientId, clientConfigurationAttribute, clientConfiguration) + return clientConfiguration + + return None + + def getCurrentClientSecrets(self, currentClientSecrets, configurationAttributes, requestParameters): + clientConfiguration = self.getClientConfiguration(configurationAttributes, requestParameters) + if (clientConfiguration == None): + return currentClientSecrets + + clientConfigurationValue = json.loads(clientConfiguration.getValue()) + + return clientConfigurationValue["gplus"] + + def getCurrentAttributesMapping(self, currentAttributesMapping, configurationAttributes, requestParameters): + clientConfiguration = self.getClientConfiguration(configurationAttributes, requestParameters) + if (clientConfiguration == None): + return currentAttributesMapping + + clientConfigurationValue = json.loads(clientConfiguration.getValue()) + + clientAttributesMapping = self.prepareAttributesMapping(clientConfigurationValue["gplus_remote_attributes_list"], clientConfigurationValue["gplus_local_attributes_list"]) + if (clientAttributesMapping == None): + print "Google+ GetCurrentAttributesMapping. Client attributes mapping is invalid. Using default one" + return currentAttributesMapping + + return clientAttributesMapping + + def prepareAttributesMapping(self, remoteAttributesList, localAttributesList): + remoteAttributesListArray = StringHelper.split(remoteAttributesList, ",") + if (ArrayHelper.isEmpty(remoteAttributesListArray)): + print "Google+ PrepareAttributesMapping. There is no attributes specified in remoteAttributesList property" + return None + + localAttributesListArray = StringHelper.split(localAttributesList, ",") + if (ArrayHelper.isEmpty(localAttributesListArray)): + print "Google+ PrepareAttributesMapping. There is no attributes specified in localAttributesList property" + return None + + if (len(remoteAttributesListArray) != len(localAttributesListArray)): + print "Google+ PrepareAttributesMapping. The number of attributes in remoteAttributesList and localAttributesList isn't equal" + return None + + attributeMapping = IdentityHashMap() + containsUid = False + i = 0 + count = len(remoteAttributesListArray) + while (i < count): + remoteAttribute = StringHelper.toLowerCase(remoteAttributesListArray[i]) + localAttribute = StringHelper.toLowerCase(localAttributesListArray[i]) + attributeMapping.put(remoteAttribute, localAttribute) + + if (StringHelper.equalsIgnoreCase(localAttribute, "uid")): + containsUid = True + + i = i + 1 + + if (not containsUid): + print "Google+ PrepareAttributesMapping. There is no mapping to mandatory 'uid' attribute" + return None + + return attributeMapping + + def getTokensByCode(self, currentClientSecrets, configurationAttributes, code): + tokenRequest = TokenRequest(GrantType.CLIENT_CREDENTIALS) + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_POST) + tokenRequest.setCode(code) + tokenRequest.setAuthUsername(currentClientSecrets["web"]["client_id"]) + tokenRequest.setAuthPassword(currentClientSecrets["web"]["client_secret"]) + tokenRequest.setRedirectUri("postmessage") + tokenRequest.setGrantType(GrantType.AUTHORIZATION_CODE) + + tokenClient = TokenClient(currentClientSecrets["web"]["token_uri"]) + tokenClient.setRequest(tokenRequest) + + tokenResponse = tokenClient.exec() + if ((tokenResponse == None) or (tokenResponse.getStatus() != 200)): + return None + + return tokenResponse + + def getUserInfo(self, currentClientSecrets, configurationAttributes, accessToken): + userInfoClient = UserInfoClient("https://www.googleapis.com/plus/v1/people/me/openIdConnect") + + userInfoResponse = userInfoClient.execUserInfo(accessToken) + if ((userInfoResponse == None) or (userInfoResponse.getStatus() != 200)): + return None + + return userInfoResponse + + def extensionPostLogin(self, configurationAttributes, user): + if (self.extensionModule != None): + try: + postLoginResult = self.extensionModule.postLogin(configurationAttributes, user) + print "Google+ PostLogin result: '%s'" % postLoginResult + + return postLoginResult + except Exception, ex: + print "Google+ PostLogin. Failed to execute postLogin method" + print "Google+ PostLogin. Unexpected error:", ex + return False + except java.lang.Throwable, ex: + print "Google+ PostLogin. Failed to execute postLogin method" + ex.printStackTrace() + return False + + return True diff --git a/oxAuth/Server/integrations/gplus/README.txt b/oxAuth/Server/integrations/gplus/README.txt new file mode 100644 index 00000000..e362a635 --- /dev/null +++ b/oxAuth/Server/integrations/gplus/README.txt @@ -0,0 +1,54 @@ +This is a person authentication module for oxAuth that enables [Google+ Authentication](https://www.google.com) for user authentication. + +The module has a few properties: + +1) gplus_client_secrets_file - It's mandatory property. It's path to application configuration file downloaded from Google console for application. +Example: `/etc/certs/gplus_client_secrets.json` +These are steps needed to get it: + a) Log into: https://console.developers.google.com/project + b) Click "Create project" and enter project name + c) Open new project "API & auth -> Credentials" menu item in configuration navigation tree + d) Click "Add credential" with type "OAuth 2.0 client ID" + e) Select "Web application" application type + f) Enter "Authorized JavaScript origins". It should be CE server DNS name. Example: https://gluu.info + g) Click "Create" and Click "OK" in next dialog + h) Click "Download JSON" in order to download gplus_client_secrets.json file +Also it's mandatory to enable Google+ API: + a) Log into: https://console.developers.google.com/project + b) Select project and enter project name + c) Open new project "API & auth -> API" menu item in configuration navigation tree + d) Click "Google+ API" + e) Click "Enable API" button + +2) gplus_deployment_type - Specify deployment mode. It's optional property. If this property isn't specified script + tries to find user in local LDAP by 'subject_identifier' claim specified in id_token. If this property has 'map' value script + allow to map 'subject_identifier' to local user account. If this property has 'enroll' value script should add new user to local LDAP + with status 'acrtive'. In order to map IDP attributes to local attributes it uses properties gplus_remote_attributes_list and + gplus_local_attributes_list. + Allowed values: map/enroll + Example: enroll + +3) gplus_remote_attributes_list - Comma separated list of attribute names. Specify list of Google+ claims(attributes) which script should use to map to local attributes. + It's optional property. It's mandatory only if gplus_deployment_type has value 'enroll'. + The count of attributes in this property should be equal to count attributes in gplus_local_attributes_list property. + Example: email, email, name, family_name, given_name, locale + +4) gplus_local_attributes_list - Comma separated list of attribute names. Specify list of local attributes mapped from Google+ userInfo OpenId response. + It's optional property. It's mandatory only if gplus_deployment_type has value 'enroll'. + The count of attributes in this property should be equal to count attributes in gplus_remote_attributes_list property. + Local attributes list should contains next mandatory attributes: uid, mail, givenName, sn, cn. + Example: uid, mail, givenName, sn, cn, preferredLanguage + +5) extension_module - Specify external module name. It's optional property. External module should implements 2 methods: + def init(conf_attr): + ... + return True/False + + def postLogin(conf_attr, user): + ... + return True/False + + Scripts calls init method at initialization. And calls postLogin after user log in order to execute additional custom workflow. + + 6) gplus_client_configuration_attribute - Specify client entry attribute name which can override gplus_client_secrets_file file content. It's optional property. + It can be used in cases when all clients should use separate gplus_client_secrets.json configuration. diff --git a/oxAuth/Server/integrations/gplus/sample/custom_script_entry.ldif b/oxAuth/Server/integrations/gplus/sample/custom_script_entry.ldif new file mode 100644 index 00000000..e1f3815e --- /dev/null +++ b/oxAuth/Server/integrations/gplus/sample/custom_script_entry.ldif @@ -0,0 +1,22 @@ +dn: inum=@!1111!E521.2AE6,ou=scripts,o=gluu +objectClass: oxCustomScript +objectClass: top +displayName: gplus +gluuStatus: true +inum: @!1111!E521.2AE6 +oxConfigurationProperty: {"value1":"gplus_client_secrets_file","value2":"/et + c/certs/gplus_client_secrets.json","description":""} +oxConfigurationProperty: {"value1":"gplus_deployment_type","value2":"enroll" + ,"description":""} +oxConfigurationProperty: {"value1":"gplus_local_attributes_list","value2":"u + id, mail, givenName, sn, cn, preferredLanguage","description":""} +oxConfigurationProperty: {"value1":"gplus_remote_attributes_list","value2":" + email, email, given_name, family_name, given_name, locale","description":""} +oxLevel: 22 +oxModuleProperty: {"value1":"usage_type","value2":"interactive","description + ":""} +oxRevision: 1 +oxScript: +oxScriptType: person_authentication +programmingLanguage: python + diff --git a/oxAuth/Server/integrations/gplus/sample/gplus_client_secrets.json b/oxAuth/Server/integrations/gplus/sample/gplus_client_secrets.json new file mode 100644 index 00000000..50c880ae --- /dev/null +++ b/oxAuth/Server/integrations/gplus/sample/gplus_client_secrets.json @@ -0,0 +1 @@ +{"web":{"auth_uri":"https://accounts.google.com/o/oauth2/auth","client_secret":"","token_uri":"https://accounts.google.com/o/oauth2/token","client_email":"","redirect_uris":["https://myproductionurl.example.com/oauth2callback"],"client_x509_cert_url":"https://www.googleapis.com/robot/v1/metadata/x509/","client_id":"","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","javascript_origins":["https://myproductionurl.example.com"]}} \ No newline at end of file diff --git a/oxAuth/Server/integrations/gplus/workflow.txt b/oxAuth/Server/integrations/gplus/workflow.txt new file mode 100644 index 00000000..bf84de75 --- /dev/null +++ b/oxAuth/Server/integrations/gplus/workflow.txt @@ -0,0 +1,39 @@ +title Google+ oxAuth external authenticator plugin + +User Agent -> oxTrust: Access site protected by mod_ox/shibboleth + +oxTrust -> oxAuth: Redirect for user authentication + +oxAuth -> User Agent: Show login form with Google+ widget + +User Agent -> Google Server: Log into Google account + +Google Server -> User Agent: Returns OAuth2 code, id_token, etc.. + +User Agent -> oxAuth: Send OAuth2 code to Google+ oxAuth external authenticator + +oxAuth -> Google Server: Get OAuth2 id_token by code + +Google Server -> oxAuth: Returns id_token + +oxAuth -> oxAuth: Get Subject Identifier (Google persistent Id) from id_token + +oxAuth -> oxAuth: Check if there is IDP user with oxExternalUid: "gplus:" + google_persistent_id (Google persistent Id) + +oxAuth -> oxTrust: Allow access if user with gplus:google_persistent_id exists + +oxTrust -> User Agent: Allow access to resource + +oxAuth -> oxAuth : Start enrollment if user with gplus:google_persistent_id not exists + +oxAuth -> Google Server: Get OAuth2 user profile + +Google Server -> oxAuth: Returns OAuth2 claims + +oxAuth -> oxAuth: Add new user with gplus:google_persistent_id. Map user claims to user attributes. Set status of new user to "register". + +oxAuth -> oxTrust: Allow access to resource + +oxTrust -> oxTrust: Show user registration form + +oxTrust -> User Agent: Allow access to resource diff --git a/oxAuth/Server/integrations/idfirst/README_idfirst.md b/oxAuth/Server/integrations/idfirst/README_idfirst.md new file mode 100644 index 00000000..818d7909 --- /dev/null +++ b/oxAuth/Server/integrations/idfirst/README_idfirst.md @@ -0,0 +1,37 @@ +# Identifier-first authentication + +This custom script allows administrators to implement the following workflow: + +1. User is presented with a form asking for username only +2. User is shown a form asking for password only (username field is not editable) +3. User is taken to a page where he is challenged to present a second factor (strong credential) for completion of authentication + +After form submission in step 1, the suitable user entry is looked up and an attribute is expected to contain an acr value corresponding to an already existing and enabled custom script that determines the second factor. + +If the attribute is not existing, the "basic" acr will be used. Thus, the basic script needs to be enabled as well in Gluu server. + + +For numerals 2 and 3 to work properly, all custom scripts to be used (basic, u2f, super_gluu, ect.) should be edited so that the getPageForStep uses the custom page named "alter_login.xhtml". In other words, comment out the line showing + + `return ""` + +by prefixing it with a `#` character and add one with + + `return "/auth/idfirst/alter_login.xhtml"` + +in the subroutine "getPageForStep" of every script. + +## Requirements: + +* To configure the user attribute to lookup for an acr (and thus a second factor), add "*acr_attribute*" as a property of the Identifier-first custom script via oxTrust. + +Example: + +name: acr_attribute +value: oxPreferredMethod + +where `oxPreferredMethod` is an LDAP attribute part of GluuPerson object class. + +* Copy the accompanying [custom pages](https://github.com/GluuFederation/oxAuth/tree/master/Server/src/main/webapp/auth/idfirst) to `/opt/gluu/jetty/oxauth/custom/pages/idfirst`, namely `alter_login.xhtml`, `alter_login.page.xml` (3.0.2 only), and `idfirst_login.xhtml`. This is only required if your Gluu Server wasn't originally bundled with this script + +* Ensure this script has a low level set in oxTrust. All other scripts to which this scripts forwards to must be greater in level than this. \ No newline at end of file diff --git a/oxAuth/Server/integrations/idfirst/idfirst.py b/oxAuth/Server/integrations/idfirst/idfirst.py new file mode 100644 index 00000000..b10bc460 --- /dev/null +++ b/oxAuth/Server/integrations/idfirst/idfirst.py @@ -0,0 +1,98 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2016, Gluu +# +# Author: Jose Gonzalez (based on acr_routerauthenticator.py) +# +# NOTE: before using this script, see the accompanying readme file + +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.service import UserService +from org.gluu.util import StringHelper +from org.gluu.service.cdi.util import CdiUtil + +import java + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Identifier First. Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "Identifier First. Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + print "Identifier First. isValidAuthenticationMethod called" + return False + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + print "Identifier First. getAlternativeAuthenticationMethod" + + identity = CdiUtil.bean(Identity) + user_name = identity.getCredentials().getUsername() + print "Identifier First. Inspecting user %s" % user_name + + attributes=identity.getSessionId().getSessionAttributes() + attributes.put("roUserName", user_name) + + acr = None + try: + userService = CdiUtil.bean(UserService) + foundUser = userService.getUserByAttribute("uid", user_name) + + if foundUser == None: + print "Identifier First. User does not exist" + return "" + + attr = configurationAttributes.get("acr_attribute").getValue2() + acr=foundUser.getAttribute(attr) + #acr="u2f" or "otp" or "twilio_sms", etc... + if acr == None: + acr = "basic" + except: + print "Identifier First. Error looking up user or his preferred method" + + print "Identifier First. new acr value %s" % acr + return acr + + def authenticate(self, configurationAttributes, requestParameters, step): + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + print "Identifier First. prepareForStep %s" % str(step) + return True + + def getExtraParametersForStep(self, configurationAttributes, step): + print "Identifier First. getExtraParametersForStep %s" % str(step) + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + print "Identifier First. getCountAuthenticationSteps called" + return 2 + + def getPageForStep(self, configurationAttributes, step): + print "Identifier First. getPageForStep called %s" % str(step) + if step == 1: + return "/auth/idfirst/idfirst_login.xhtml" + return "" + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + print "Identifier First. logout called" + return True diff --git a/oxAuth/Server/integrations/inwebo/inwebo.py b/oxAuth/Server/integrations/inwebo/inwebo.py new file mode 100644 index 00000000..dfc02767 --- /dev/null +++ b/oxAuth/Server/integrations/inwebo/inwebo.py @@ -0,0 +1,338 @@ +from org.gluu.oxauth.service import AuthenticationService +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.util import StringHelper,ArrayHelper +from org.gluu.oxauth.util import ServerUtil +from org.gluu.oxauth.service import UserService, AuthenticationService,SessionIdService +from org.gluu.oxauth.service.net import HttpService +from org.gluu.oxauth.service.common import EncryptionService +from javax.faces.application import FacesMessage +from javax.faces.context import FacesContext +from org.gluu.jsf2.service import FacesService +from org.gluu.jsf2.message import FacesMessages +from java.util import Arrays +from java.lang import String +from java.lang import System +import java +import sys +import json + + + +class PersonAuthentication(PersonAuthenticationType): + + + def __init__(self, currentTimeMillis): + self.otp = "false" + self.client = None + + def init(self, customScript, configurationAttributes): + + print "inWebo. Initialization" + iw_cert_store_type = configurationAttributes.get("iw_cert_store_type").getValue2() + iw_cert_path = configurationAttributes.get("iw_cert_path").getValue2() + iw_creds_file = configurationAttributes.get("iw_creds_file").getValue2() + + self.push_withoutpin = "false" + self.push_fail = "false" + + #permissible values = true , false + self.push_withoutpin = 1 + if StringHelper.equalsIgnoreCase("false" ,configurationAttributes.get("iw_push_withoutpin").getValue2()): + self.push_withoutpin = 0 + self.api_uri = configurationAttributes.get("iw_api_uri").getValue2() + self.service_id = configurationAttributes.get("iw_service_id").getValue2() + + + # Load credentials from file + f = open(iw_creds_file, 'r') + try: + creds = json.loads(f.read()) + except: + print "unexpected error - "+sys.exc_info()[0] + return False + finally: + f.close() + iw_cert_password = creds["CERT_PASSWORD"] + + #TODO: the password should not be in plaintext + #try: + # encryptionService = CdiUtil.bean(EncryptionService) + # iw_cert_password = encryptionService.decrypt(iw_cert_password) + #except: + # print("oops!",sys.exc_info()[0],"occured.") + # return False + + httpService = CdiUtil.bean(HttpService) + self.client = httpService.getHttpsClient(None, None, None, iw_cert_store_type, iw_cert_path, iw_cert_password) + print "inWebo. Initialized successfully" + return True + + + def destroy(self, configurationAttributes): + print "inWebo. Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + + userService = CdiUtil.bean(UserService) + authenticationService = CdiUtil.bean(AuthenticationService) + identity = CdiUtil.bean(Identity) + + credentials = identity.getCredentials() + user_name = credentials.getUsername() + + iw_otp = requestParameters.get("loginForm:otp") + + if ArrayHelper.isNotEmpty(iw_otp) and StringHelper.equalsIgnoreCase("true", iw_otp[0]) and step == 2: + identity.setWorkingParameter("iw_count_login_steps", 3) + return True + + elif StringHelper.isEmptyString(user_name) and step == 1: + print "empty user_name in step1 indicates browser token notfound" + identity.setWorkingParameter("iw_count_login_steps", 2) + return True + else: + + response_check = False + user_exists_in_gluu = authenticationService.authenticate(user_name) + identity.setWorkingParameter("iw_count_login_steps", step) + + if (step == 1 or step == 3): + print "if (step == 1 or step == 3):" + password = credentials.getPassword() + if StringHelper.isEmpty(password): + print "InWebo. Authenticate for step 2. otp token is empty" + return False + #password is the otp token + response_check = self.validateInweboToken(self.api_uri, self.service_id, user_name, password, step) + elif (step == 2): + print "elif (step == 2):" + session = CdiUtil.bean(SessionIdService).getSessionId() + if session == None: + print "InWebo. Authenticate for step 2. session_id is not exists" + return False + + response_check = self.checkStatus(self.api_uri, self.service_id, user_name, session.getId(), self.push_withoutpin) + + if self.push_fail is not None: + self.setErrorMessage(self.push_fail) + identity.setWorkingParameter("iw_count_login_steps", 3) + + return response_check and user_exists_in_gluu + + def prepareForStep(self, configurationAttributes, requestParameters, step): + + if (step == 1): + print "InWebo. Prepare for step 1" + return True + elif (step == 2): + print "InWebo. Prepare for step 2" + return True + elif (step == 3): + print "inWebo. Prepare for step 3" + return True + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + print "inside getCountAuthenticationSteps" + identity = CdiUtil.bean(Identity) + if (identity.isSetWorkingParameter("iw_count_login_steps")): + print "identity.getWorkingParameter(iw_count_login_steps) - ", identity.getWorkingParameter("iw_count_login_steps") + return identity.getWorkingParameter("iw_count_login_steps") + + return 3 + + def getPageForStep(self, configurationAttributes, step): + + identity = CdiUtil.bean(Identity) + if (step == 1): + return "/auth/inwebo/iw_va.xhtml" + elif (step == 2): + return "/auth/inwebo/iwpushnotification.xhtml" + elif (step == 3): + return "/auth/inwebo/iwauthenticate.xhtml" + else: + return "" + + def isPassedDefaultAuthentication(self): + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + user_name = credentials.getUsername() + passed_step1 = StringHelper.isNotEmptyString(user_name) + return passed_step1 + + def validateInweboToken(self, iw_api_uri, iw_service_id, user_name, iw_token, step): + httpService = CdiUtil.bean(HttpService) + + request_uri = iw_api_uri + "action=authenticateExtended" + "&serviceId=" + str(iw_service_id) + "&userId=" + httpService.encodeUrl(user_name) + "&token=" + str(iw_token)+"&format=json" + print "InWebo. Token verification. Attempting to send authentication request:", request_uri + + try: + http_service_response = httpService.executeGet(self.client, request_uri) + http_response = http_service_response.getHttpResponse() + print "status - ", http_response.getStatusLine().getStatusCode() + except: + print "inWebo validate method. Exception: ", sys.exc_info()[1] + return False + + try: + if (http_response.getStatusLine().getStatusCode() != 200): + print "inWebo. Invalid response from validation server: ", str(http_response.getStatusLine().getStatusCode()) + httpService.consume(http_response) + return None + + response_bytes = httpService.getResponseContent(http_response) + response_string = httpService.convertEntityToString(response_bytes) + httpService.consume(http_response) + + finally: + http_service_response.closeConnection() + + if response_string is None: + print "inWebo. Get empty response from inWebo server" + return None + + print "response string:",response_string + json_response = json.loads(response_string) + + if not StringHelper.equalsIgnoreCase(json_response['err'], "OK"): + print "inWebo. Get response with status: ", json_response['err'] + return False + else: + return True # response_validation + + def checkStatus(self, iw_api_uri, iw_service_id, user_name, session_id,without_pin): + print "inside check status ", user_name+session_id + # step 1: call action=pushAthenticate + httpService = CdiUtil.bean(HttpService) + + request_uri = iw_api_uri + "action=pushAuthenticate" + "&serviceId=" + str(iw_service_id) + "&userId=" + httpService.encodeUrl(user_name) + "&format=json&withoutpin="+str(without_pin) + #curTime = java.lang.System.currentTimeMillis() + #endTime = curTime + (timeout * 1000) + + try: + response_status = None + http_service_response = httpService.executeGet(self.client, request_uri) + http_response = http_service_response.getHttpResponse() + + if (http_response.getStatusLine().getStatusCode() != 200): + print "inWebo. Invalid response from inwebo server: checkStatus ", str(http_response.getStatusLine().getStatusCode()) + httpService.consume(http_response) + return None + + response_bytes = httpService.getResponseContent(http_response) + response_string = httpService.convertEntityToString(response_bytes) + httpService.consume(http_response) + + except: + print "inWebo validate method. Exception: ", sys.exc_info()[1] + return False + + finally: + http_service_response.closeConnection() + + print "response string:", response_string + json_response = json.loads(response_string) + + if StringHelper.equalsIgnoreCase(json_response['err'], "OK"): + + session_id = json_response['sessionId'] + checkResult_uri = iw_api_uri + "action=checkPushResult" + "&serviceId=" + str(iw_service_id) + "&userId=" + httpService.encodeUrl(user_name) + "&sessionId="+ httpService.encodeUrl(session_id) + "&format=json&withoutpin=1" + print "checkPushResult_uri:",checkResult_uri + + startTime = System.currentTimeMillis(); + currentTime = startTime; + endTime = startTime + long(25000) + print "start time ----> ",startTime + print "end time ",endTime + while (endTime > currentTime ): + try: + # step 2: call action=checkPushResult; using session id from step 1 + http_check_push_response = httpService.executeGet(self.client, checkResult_uri) + check_push_response = http_check_push_response.getHttpResponse() + check_push_response_bytes = httpService.getResponseContent(check_push_response) + check_push_response_string = httpService.convertEntityToString(check_push_response_bytes) + httpService.consume(check_push_response) + + check_push_json_response = json.loads(check_push_response_string) + print "check_push_json_response :",check_push_json_response + if StringHelper.equalsIgnoreCase(check_push_json_response['err'], "OK"): + self.push_fail = None + return True + elif StringHelper.equalsIgnoreCase(check_push_json_response['err'], "NOK:REFUSED"): + print "Push request notification for session", session_id + self.push_fail = "inwebo.push.notification.rejected" + return False + elif StringHelper.equalsIgnoreCase(check_push_json_response['err'], "NOK:TIMEOUT"): + print "Push request timed out for session", session_id + self.push_fail = "inwebo.push.notification.timed.out.for.session" + return False + elif StringHelper.equalsIgnoreCase(check_push_json_response['err'], "NOK:WAITING"): + self.push_fail = "inwebo.push.notification.timed.out.for.session" + currentTime = System.currentTimeMillis(); + print " NOw ######## ", currentTime + java.lang.Thread.sleep(5000) + continue + else: + self.push_fail = "inwebo.push.notification.failed" + return False + + + + finally: + http_check_push_response.closeConnection() + + + + elif StringHelper.equalsIgnoreCase(json_response['err'], "NOK:SN"): + self.push_fail ="inwebo.no.username" + return False + elif StringHelper.equalsIgnoreCase(json_response['err'], "NOK:account unknown"): + self.push_fail ="inwebo.no.username" + return False + else: + print "No response from server." + self.push_fail ="inwebo.push.notification.timed.out.for.session" + return False + + print "inWebo. CheckStatus. The process has not received a response from the phone yet" + + return False + + def setErrorMessage(self, errorMessage): + facesMessages = CdiUtil.bean(FacesMessages) + facesMessages.setKeepMessages() + facesMessages.clear() + facesMessages.add(FacesMessage.SEVERITY_ERROR, String.format("#{msgs['%s']}", errorMessage)) + + def hasEnrollments(self, configurationAttributes,user): + return True; + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True \ No newline at end of file diff --git a/oxAuth/Server/integrations/inwebo/iw_creds.json b/oxAuth/Server/integrations/inwebo/iw_creds.json new file mode 100644 index 00000000..3fe86815 --- /dev/null +++ b/oxAuth/Server/integrations/inwebo/iw_creds.json @@ -0,0 +1 @@ +{"CERT_PASSWORD": "example"} diff --git a/oxAuth/Server/integrations/inwebo/iw_va.xhtml b/oxAuth/Server/integrations/inwebo/iw_va.xhtml new file mode 100644 index 00000000..8a69c6f5 --- /dev/null +++ b/oxAuth/Server/integrations/inwebo/iw_va.xhtml @@ -0,0 +1,93 @@ + + + + + + + + + + + + #{msgs['inwebo.title.va']} + + + + +
+ + + + + + + + + +
+
\ No newline at end of file diff --git a/oxAuth/Server/integrations/inwebo/iwauthenticate.xhtml b/oxAuth/Server/integrations/inwebo/iwauthenticate.xhtml new file mode 100644 index 00000000..71d619d0 --- /dev/null +++ b/oxAuth/Server/integrations/inwebo/iwauthenticate.xhtml @@ -0,0 +1,99 @@ + + + + + + + + + + + #{msgs['inwebo.pageTitle']} + + + + + + + + + + + + + + + + + diff --git a/oxAuth/Server/integrations/inwebo/iwlogin.xhtml b/oxAuth/Server/integrations/inwebo/iwlogin.xhtml new file mode 100644 index 00000000..4d8ebd4f --- /dev/null +++ b/oxAuth/Server/integrations/inwebo/iwlogin.xhtml @@ -0,0 +1,94 @@ + + + + + + + + + + + + #{msgs['inwebo.title']} + + + + + + + + + + + + + diff --git a/oxAuth/Server/integrations/inwebo/iwlogin_without_password.xhtml b/oxAuth/Server/integrations/inwebo/iwlogin_without_password.xhtml new file mode 100644 index 00000000..847383d9 --- /dev/null +++ b/oxAuth/Server/integrations/inwebo/iwlogin_without_password.xhtml @@ -0,0 +1,91 @@ + + + + + + + + + + + + #{msgs['inwebo.title']} + + + + + + + + + + + + + diff --git a/oxAuth/Server/integrations/inwebo/iwpushnotification.xhtml b/oxAuth/Server/integrations/inwebo/iwpushnotification.xhtml new file mode 100644 index 00000000..002873b0 --- /dev/null +++ b/oxAuth/Server/integrations/inwebo/iwpushnotification.xhtml @@ -0,0 +1,90 @@ + + + + + + + + + + + #{msgs['inwebo.push.notification.message']} + + + + + + + + + + \ No newline at end of file diff --git a/oxAuth/Server/integrations/inwebo/oxauth.properties b/oxAuth/Server/integrations/inwebo/oxauth.properties new file mode 100644 index 00000000..1a7e33cc --- /dev/null +++ b/oxAuth/Server/integrations/inwebo/oxauth.properties @@ -0,0 +1,19 @@ + +#inwebo +inwebo.title=inWebo Login +inwebo.loginLabel=Login +inwebo.pleaseLoginHere=Please login here +inwebo.username=Username +inwebo.password=Password +inwebo.pageTitle=inWebo 2FA +inwebo.2fa.mobile.app=inWebo Authenticator +inwebo.authenticationToken=inWebo OTP +inwebo.push.notification.message=Click here to push notification to your phone +inwebo.inwebo.authenticator=inWebo Authenticator +inwebo.otp.login.info=Enter OTP from your mobile or use Web authenticator +inwebo.otp=OTP token +inwebo.push.notification.rejected=Push notification has been rejected. +inwebo.push.notification.failed=Push notification has failed +inwebo.no.username=Please enter a registered username +inwebo.authenticate.using.otp=Authenticate using OTP instead? +inwebo.push.notification.timed.out.for.session=Push notification has timed out. diff --git a/oxAuth/Server/integrations/new_acr_link/README.md b/oxAuth/Server/integrations/new_acr_link/README.md new file mode 100644 index 00000000..8b117c77 --- /dev/null +++ b/oxAuth/Server/integrations/new_acr_link/README.md @@ -0,0 +1,34 @@ +# New `acr_values` link script +Generates new authz request link replacing the original request's `acr_values` value to be sent to and handled by the login page, preserving other params. + +## Use Case +User wants to present an alternatives acr_values in login page, i.e. e-mail token, certificate login, etc. + +## Configuration Attributes +- `new_acr_values_x` (replace X by a number or a string) : the new acr_values value to be in the new authz request, i.e.: `forgot_password` +- `link_text_x` (replace X by a number or a string): the text to be displayed (injected in ``), i.e.: `Click here if you preffer to receive a token instead` + +Example: +- `new_acr_values_1` : `forgot_password` +- `link_text_1`: `Forgot Password` +- `new_acr_values_2` : `certificate_login` +- `link_text_2`: `certificate_login` + +## Custom login.xhtml +Custom `login.xhtml` should be placed on `/opt/gluu/jetty/oxauth/custom/pages/auth/new_acr_link/login.xhtml` + +**Please notice:**: login.xhtml should have the following tag inside ``: ``, so `prepareForStep` method is called. + +Example: +```xhtml + + + + +``` + +To handle the configuration attributes in xhtml, use: +- `new_acr_values_x` use: `#{identity.getWorkingParameter('new_acr_values_x')}` +- `link_text_x` use: `#{identity.getWorkingParameter('link_text_x')}` + +Example: `#{identity.getWorkingParameter('link_text_1')}` diff --git a/oxAuth/Server/integrations/new_acr_link/new_acr_link.py b/oxAuth/Server/integrations/new_acr_link/new_acr_link.py new file mode 100644 index 00000000..32742bd0 --- /dev/null +++ b/oxAuth/Server/integrations/new_acr_link/new_acr_link.py @@ -0,0 +1,148 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2016, Gluu +# +# Author: Christian Eland +# + +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.service import AuthenticationService +from org.gluu.util import StringHelper +from urlparse import urlparse, parse_qsl, urlunparse +from urllib import urlencode +from javax.faces.context import FacesContext + +import java + +class Utils(): + + def getNewAcrValuesUrl(self, new_acr_values): + ''' Generates authz request url link with new acr_values + Args: + new_acr_values (str): the desired new acr_values + Returns: + str: authz url with new `acr_values` + ''' + + facesContext = CdiUtil.bean(FacesContext) + authenticationService = CdiUtil.bean(AuthenticationService) + + parameters_as_string = authenticationService.parametersAsString() + scheme = facesContext.getExternalContext().getRequest().getScheme() + server_name = facesContext.getExternalContext().getRequest().getServerName() + + url_object = urlparse( + '%s://%s/oxauth/authorize.htm?%s' % ( + scheme, server_name, parameters_as_string) + ) + query_dict = dict(parse_qsl(url_object.query)) + query_dict["acr_values"] = new_acr_values + new_query_string = urlencode(query_dict) + new_url_link= urlunparse([ + url_object.scheme, url_object.netloc, url_object.path, + url_object.params, new_query_string, url_object.fragment + ]) + return new_url_link + + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + self.utils = Utils() + + def init(self, customScript, configurationAttributes): + print "New Acr Link. Initialization" + print "New Acr Link. Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "New Acr Link. Destroy" + print "New Acr Link. Destroyed successfully" + return True + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def getApiVersion(self): + return 11 + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + authenticationService = CdiUtil.bean(AuthenticationService) + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + if (step == 1): + print "New Acr Link. Authenticate for step 1" + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + logged_in = False + if (StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password)): + logged_in = authenticationService.authenticate(user_name, user_password) + + if (not logged_in): + return False + + return True + else: + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + print "New Acr Link. Prepare for step %s" % step + if (step == 1): + + identity = CdiUtil.bean(Identity) + + # Fetch NEW_ACR_VALUES from custom script attribute and setWorkingParameters + for item in configurationAttributes: + if item.startswith('new_acr_values'): + acr_values = configurationAttributes.get(item).getValue2() + new_authz_request_link = self.utils.getNewAcrValuesUrl(acr_values) + print "New Acr Link. Setting working parameter for item %s with value %s" % ( + item, new_authz_request_link + ) + identity.setWorkingParameter(item, new_authz_request_link) + if not item.startswith("new_acr_values"): + text = configurationAttributes.get(item).getValue2() + print "New Acr Link. Setting working parameter for item %s with text %s" % ( + item, text + ) + identity.setWorkingParameter(item, text) + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + return 1 + + def getPageForStep(self, configurationAttributes, step): + print "New Acr Link. entered getPageForStep - step %s" % step + if step == 1: + print "New Acr Link. returning custom login.xhtml" + return "/auth/new_acr_link/login.xhtml" + + return "" + + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True diff --git a/oxAuth/Server/integrations/otp/Installation.md b/oxAuth/Server/integrations/otp/Installation.md new file mode 100644 index 00000000..69d455a8 --- /dev/null +++ b/oxAuth/Server/integrations/otp/Installation.md @@ -0,0 +1,27 @@ +This list of steps needed to do to enable SAML person authentication module. + +1. Confire new custom module in oxTrust: + - Log into oxTrust with administrative permissions. + - Open "Configuration→Manage Custom Scripts" page. + - Select "Person Authentication" tab. + - Click on "Add custom script configuration" link. + - Enter name = otp + - Enter level = 0-100 (priority of this method). + - Select usage type "Interactive". + - Add custom required and optional properties which specified in "Properties description.md". + - Copy/paste script from TotpExternalAuthenticator.py. + - Activate it via "Enabled" checkbox. + - Click "Update" button at the bottom of this page. + +2. Configure oxAuth to use OTP authentication by default: + - Log into oxTrust with administrative permissions. + - Open "Configuration→Manage Authentication" page. + - Scroll to "Default Authentication Method" panel. Select "otp" authentication mode. + - Click "Update" button at the bottom of this page. + +3. Try to log in using OTP authentication method: + - Wait 30 seconds and try to log in again. During this time oxAuth reload list of available person authentication modules. + - Open second browser or second browsing session and try to log in again. It's better to try to do that from another browser session because we can return back to previous authentication method if something will go wrong. + +There are log messages in this custom authentication script. In order to debug this module we can use command like this: +tail -f /opt/tomcat/logs/wrapper.log | grep "OTP" diff --git a/oxAuth/Server/integrations/otp/OtpExternalAuthenticator.py b/oxAuth/Server/integrations/otp/OtpExternalAuthenticator.py new file mode 100644 index 00000000..b3cc4752 --- /dev/null +++ b/oxAuth/Server/integrations/otp/OtpExternalAuthenticator.py @@ -0,0 +1,617 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2016, Gluu +# +# Author: Yuriy Movchan +# + +# Requires the following custom properties and values: +# otp_type: totp/hotp +# issuer: Gluu Inc +# otp_conf_file: /etc/certs/otp_configuration.json +# +# These are non mandatory custom properties and values: +# label: Gluu OTP +# qr_options: { width: 400, height: 400 } +# registration_uri: https://ce-dev.gluu.org/identity/register + +import jarray +import json +import sys +from com.google.common.io import BaseEncoding +from com.lochbridge.oath.otp import HOTP +from com.lochbridge.oath.otp import HOTPValidator +from com.lochbridge.oath.otp import HmacShaAlgorithm +from com.lochbridge.oath.otp import TOTP +from com.lochbridge.oath.otp.keyprovisioning import OTPAuthURIBuilder +from com.lochbridge.oath.otp.keyprovisioning import OTPKey +from com.lochbridge.oath.otp.keyprovisioning.OTPKey import OTPType +from java.security import SecureRandom +from java.util import Arrays +from java.util.concurrent import TimeUnit +from javax.faces.application import FacesMessage +from org.gluu.jsf2.message import FacesMessages +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.security import Identity +from org.gluu.oxauth.service import AuthenticationService, SessionIdService +from org.gluu.oxauth.service.common import UserService +from org.gluu.oxauth.util import ServerUtil +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.util import StringHelper + + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "OTP. Initialization" + + if not configurationAttributes.containsKey("otp_type"): + print "OTP. Initialization. Property otp_type is mandatory" + return False + self.otpType = configurationAttributes.get("otp_type").getValue2() + + if not self.otpType in ["hotp", "totp"]: + print "OTP. Initialization. Property value otp_type is invalid" + return False + + if not configurationAttributes.containsKey("issuer"): + print "OTP. Initialization. Property issuer is mandatory" + return False + self.otpIssuer = configurationAttributes.get("issuer").getValue2() + + self.customLabel = None + if configurationAttributes.containsKey("label"): + self.customLabel = configurationAttributes.get("label").getValue2() + + self.customQrOptions = {} + if configurationAttributes.containsKey("qr_options"): + self.customQrOptions = configurationAttributes.get("qr_options").getValue2() + + self.registrationUri = None + if configurationAttributes.containsKey("registration_uri"): + self.registrationUri = configurationAttributes.get("registration_uri").getValue2() + + validOtpConfiguration = self.loadOtpConfiguration(configurationAttributes) + if not validOtpConfiguration: + return False + + print "OTP. Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "OTP. Destroy" + print "OTP. Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def getNextStep(self, configurationAttributes, requestParameters, step): + print "getNextStep Invoked" + # If user not pass current step change step to previous + identity = CdiUtil.bean(Identity) + retry_current_step = identity.getWorkingParameter("retry_current_step") + if retry_current_step: + print "OTP. Get next step. Retrying current step %s" % step + # Remove old QR code + #identity.setWorkingParameter("super_gluu_request", "timeout") + resultStep = step + return resultStep + return -1 + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + authenticationService = CdiUtil.bean(AuthenticationService) + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + self.setRequestScopedParameters(identity) + + if step == 1: + print "OTP. Authenticate for step 1" + authenticated_user = self.processBasicAuthentication(credentials) + if authenticated_user == None: + return False + + otp_auth_method = "authenticate" + # Uncomment this block if you need to allow user second OTP registration + #enrollment_mode = ServerUtil.getFirstValue(requestParameters, "loginForm:registerButton") + #if StringHelper.isNotEmpty(enrollment_mode): + # otp_auth_method = "enroll" + + if otp_auth_method == "authenticate": + user_enrollments = self.findEnrollments(authenticated_user.getUserId()) + if len(user_enrollments) == 0: + otp_auth_method = "enroll" + print "OTP. Authenticate for step 1. There is no OTP enrollment for user '%s'. Changing otp_auth_method to '%s'" % (authenticated_user.getUserId(), otp_auth_method) + + if otp_auth_method == "enroll": + print "OTP. Authenticate for step 1. Setting count steps: '%s'" % 3 + identity.setWorkingParameter("otp_count_login_steps", 3) + + print "OTP. Authenticate for step 1. otp_auth_method: '%s'" % otp_auth_method + identity.setWorkingParameter("otp_auth_method", otp_auth_method) + + return True + elif step == 2: + print "OTP. Authenticate for step 2" + + authenticationService = CdiUtil.bean(AuthenticationService) + user = authenticationService.getAuthenticatedUser() + if user == None: + print "OTP. Authenticate for step 2. Failed to determine user name" + return False + + session_id_validation = self.validateSessionId(identity) + if not session_id_validation: + return False + + # Restore state from session + identity.setWorkingParameter("retry_current_step", False) + otp_auth_method = identity.getWorkingParameter("otp_auth_method") + if otp_auth_method == 'enroll': + auth_result = ServerUtil.getFirstValue(requestParameters, "auth_result") + if not StringHelper.isEmpty(auth_result): + # defect fix #1225 - Retry the step, show QR code again + if auth_result == 'timeout': + print "OTP. QR-code timeout. Authenticate for step %s. Reinitializing current step" % step + identity.setWorkingParameter("retry_current_step", True) + return True + + print "OTP. Authenticate for step 2. User not enrolled OTP" + return False + + print "OTP. Authenticate for step 2. Skipping this step during enrollment" + return True + + otp_auth_result = self.processOtpAuthentication(requestParameters, user.getUserId(), identity, otp_auth_method) + print "OTP. Authenticate for step 2. OTP authentication result: '%s'" % otp_auth_result + + return otp_auth_result + elif step == 3: + print "OTP. Authenticate for step 3" + + authenticationService = CdiUtil.bean(AuthenticationService) + user = authenticationService.getAuthenticatedUser() + if user == None: + print "OTP. Authenticate for step 2. Failed to determine user name" + return False + + session_id_validation = self.validateSessionId(identity) + if not session_id_validation: + return False + + # Restore state from session + otp_auth_method = identity.getWorkingParameter("otp_auth_method") + if otp_auth_method != 'enroll': + return False + + otp_auth_result = self.processOtpAuthentication(requestParameters, user.getUserId(), identity, otp_auth_method) + print "OTP. Authenticate for step 3. OTP authentication result: '%s'" % otp_auth_result + + return otp_auth_result + else: + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + self.setRequestScopedParameters(identity) + + if step == 1: + print "OTP. Prepare for step 1" + + return True + elif step == 2: + print "OTP. Prepare for step 2" + + session_id_validation = self.validateSessionId(identity) + if not session_id_validation: + return False + + otp_auth_method = identity.getWorkingParameter("otp_auth_method") + print "OTP. Prepare for step 2. otp_auth_method: '%s'" % otp_auth_method + + if otp_auth_method == 'enroll': + authenticationService = CdiUtil.bean(AuthenticationService) + user = authenticationService.getAuthenticatedUser() + if user == None: + print "OTP. Prepare for step 2. Failed to load user enty" + return False + + if self.otpType == "hotp": + otp_secret_key = self.generateSecretHotpKey() + otp_enrollment_request = self.generateHotpSecretKeyUri(otp_secret_key, self.otpIssuer, user.getAttribute("displayName")) + elif self.otpType == "totp": + otp_secret_key = self.generateSecretTotpKey() + otp_enrollment_request = self.generateTotpSecretKeyUri(otp_secret_key, self.otpIssuer, user.getAttribute("displayName")) + else: + print "OTP. Prepare for step 2. Unknown OTP type: '%s'" % self.otpType + return False + + print "OTP. Prepare for step 2. Prepared enrollment request for user: '%s'" % user.getUserId() + identity.setWorkingParameter("otp_secret_key", self.toBase64Url(otp_secret_key)) + identity.setWorkingParameter("otp_enrollment_request", otp_enrollment_request) + + return True + elif step == 3: + print "OTP. Prepare for step 3" + + session_id_validation = self.validateSessionId(identity) + if not session_id_validation: + return False + + otp_auth_method = identity.getWorkingParameter("otp_auth_method") + print "OTP. Prepare for step 3. otp_auth_method: '%s'" % otp_auth_method + + if otp_auth_method == 'enroll': + return True + + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + return Arrays.asList("otp_auth_method", "otp_count_login_steps", "otp_secret_key", "otp_enrollment_request","retry_current_step") + + def getCountAuthenticationSteps(self, configurationAttributes): + identity = CdiUtil.bean(Identity) + + if identity.isSetWorkingParameter("otp_count_login_steps"): + return StringHelper.toInteger("%s" % identity.getWorkingParameter("otp_count_login_steps")) + else: + return 2 + + def getPageForStep(self, configurationAttributes, step): + if step == 2: + identity = CdiUtil.bean(Identity) + + otp_auth_method = identity.getWorkingParameter("otp_auth_method") + print "OTP. Gep page for step 2. otp_auth_method: '%s'" % otp_auth_method + + if otp_auth_method == 'enroll': + return "/auth/otp/enroll.xhtml" + else: + return "/auth/otp/otplogin.xhtml" + elif step == 3: + return "/auth/otp/otplogin.xhtml" + + return "" + + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True + + def setRequestScopedParameters(self, identity): + if self.registrationUri != None: + identity.setWorkingParameter("external_registration_uri", self.registrationUri) + + if self.customLabel != None: + identity.setWorkingParameter("qr_label", self.customLabel) + + identity.setWorkingParameter("qr_options", self.customQrOptions) + + def loadOtpConfiguration(self, configurationAttributes): + print "OTP. Load OTP configuration" + if not configurationAttributes.containsKey("otp_conf_file"): + return False + + otp_conf_file = configurationAttributes.get("otp_conf_file").getValue2() + + # Load configuration from file + f = open(otp_conf_file, 'r') + try: + otpConfiguration = json.loads(f.read()) + except: + print "OTP. Load OTP configuration. Failed to load configuration from file:", otp_conf_file + return False + finally: + f.close() + + # Check configuration file settings + try: + self.hotpConfiguration = otpConfiguration["hotp"] + self.totpConfiguration = otpConfiguration["totp"] + + hmacShaAlgorithm = self.totpConfiguration["hmacShaAlgorithm"] + hmacShaAlgorithmType = None + + if StringHelper.equalsIgnoreCase(hmacShaAlgorithm, "sha1"): + hmacShaAlgorithmType = HmacShaAlgorithm.HMAC_SHA_1 + elif StringHelper.equalsIgnoreCase(hmacShaAlgorithm, "sha256"): + hmacShaAlgorithmType = HmacShaAlgorithm.HMAC_SHA_256 + elif StringHelper.equalsIgnoreCase(hmacShaAlgorithm, "sha512"): + hmacShaAlgorithmType = HmacShaAlgorithm.HMAC_SHA_512 + else: + print "OTP. Load OTP configuration. Invalid TOTP HMAC SHA algorithm: '%s'" % hmacShaAlgorithm + + self.totpConfiguration["hmacShaAlgorithmType"] = hmacShaAlgorithmType + except: + print "OTP. Load OTP configuration. Invalid configuration file '%s' format. Exception: '%s'" % (otp_conf_file, sys.exc_info()[1]) + return False + + + return True + + def processBasicAuthentication(self, credentials): + userService = CdiUtil.bean(UserService) + authenticationService = CdiUtil.bean(AuthenticationService) + + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + logged_in = False + if StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password): + logged_in = authenticationService.authenticate(user_name, user_password) + + if not logged_in: + return None + + find_user_by_uid = authenticationService.getAuthenticatedUser() + if find_user_by_uid == None: + print "OTP. Process basic authentication. Failed to find user '%s'" % user_name + return None + + return find_user_by_uid + + def findEnrollments(self, user_name, skipPrefix = True): + result = [] + + userService = CdiUtil.bean(UserService) + user = userService.getUser(user_name, "oxExternalUid") + if user == None: + print "OTP. Find enrollments. Failed to find user" + return result + + user_custom_ext_attribute = userService.getCustomAttribute(user, "oxExternalUid") + if user_custom_ext_attribute == None: + return result + + otp_prefix = "%s:" % self.otpType + + otp_prefix_length = len(otp_prefix) + for user_external_uid in user_custom_ext_attribute.getValues(): + index = user_external_uid.find(otp_prefix) + if index != -1: + if skipPrefix: + enrollment_uid = user_external_uid[otp_prefix_length:] + else: + enrollment_uid = user_external_uid + + result.append(enrollment_uid) + + return result + + def validateSessionId(self, identity): + session = CdiUtil.bean(SessionIdService).getSessionId() + if session == None: + print "OTP. Validate session id. Failed to determine session_id" + return False + + otp_auth_method = identity.getWorkingParameter("otp_auth_method") + if not otp_auth_method in ['enroll', 'authenticate']: + print "OTP. Validate session id. Failed to authenticate user. otp_auth_method: '%s'" % otp_auth_method + return False + + return True + + def processOtpAuthentication(self, requestParameters, user_name, identity, otp_auth_method): + facesMessages = CdiUtil.bean(FacesMessages) + facesMessages.setKeepMessages() + + userService = CdiUtil.bean(UserService) + + otpCode = ServerUtil.getFirstValue(requestParameters, "loginForm:otpCode") + if StringHelper.isEmpty(otpCode): + facesMessages.add(FacesMessage.SEVERITY_ERROR, "Failed to authenticate. OTP code is empty") + print "OTP. Process OTP authentication. otpCode is empty" + + return False + + if otp_auth_method == "enroll": + # Get key from session + otp_secret_key_encoded = identity.getWorkingParameter("otp_secret_key") + if otp_secret_key_encoded == None: + print "OTP. Process OTP authentication. OTP secret key is invalid" + return False + + otp_secret_key = self.fromBase64Url(otp_secret_key_encoded) + + if self.otpType == "hotp": + validation_result = self.validateHotpKey(otp_secret_key, 1, otpCode) + + if (validation_result != None) and validation_result["result"]: + print "OTP. Process HOTP authentication during enrollment. otpCode is valid" + # Store HOTP Secret Key and moving factor in user entry + otp_user_external_uid = "hotp:%s;%s" % ( otp_secret_key_encoded, validation_result["movingFactor"] ) + + # Add otp_user_external_uid to user's external GUID list + find_user_by_external_uid = userService.addUserAttribute(user_name, "oxExternalUid", otp_user_external_uid, True) + if find_user_by_external_uid != None: + return True + + print "OTP. Process HOTP authentication during enrollment. Failed to update user entry" + elif self.otpType == "totp": + validation_result = self.validateTotpKey(otp_secret_key, otpCode,user_name) + if (validation_result != None) and validation_result["result"]: + print "OTP. Process TOTP authentication during enrollment. otpCode is valid" + # Store TOTP Secret Key and moving factor in user entry + otp_user_external_uid = "totp:%s" % otp_secret_key_encoded + + # Add otp_user_external_uid to user's external GUID list + find_user_by_external_uid = userService.addUserAttribute(user_name, "oxExternalUid", otp_user_external_uid, True) + if find_user_by_external_uid != None: + return True + + print "OTP. Process TOTP authentication during enrollment. Failed to update user entry" + elif otp_auth_method == "authenticate": + user_enrollments = self.findEnrollments(user_name) + + if len(user_enrollments) == 0: + print "OTP. Process OTP authentication. There is no OTP enrollment for user '%s'" % user_name + facesMessages.add(FacesMessage.SEVERITY_ERROR, "There is no valid OTP user enrollments") + return False + + if self.otpType == "hotp": + for user_enrollment in user_enrollments: + user_enrollment_data = user_enrollment.split(";") + otp_secret_key_encoded = user_enrollment_data[0] + + # Get current moving factor from user entry + moving_factor = StringHelper.toInteger(user_enrollment_data[1]) + otp_secret_key = self.fromBase64Url(otp_secret_key_encoded) + + # Validate TOTP + validation_result = self.validateHotpKey(otp_secret_key, moving_factor, otpCode) + if (validation_result != None) and validation_result["result"]: + print "OTP. Process HOTP authentication during authentication. otpCode is valid" + otp_user_external_uid = "hotp:%s;%s" % ( otp_secret_key_encoded, moving_factor ) + new_otp_user_external_uid = "hotp:%s;%s" % ( otp_secret_key_encoded, validation_result["movingFactor"] ) + + # Update moving factor in user entry + find_user_by_external_uid = userService.replaceUserAttribute(user_name, "oxExternalUid", otp_user_external_uid, new_otp_user_external_uid, True) + if find_user_by_external_uid != None: + return True + + print "OTP. Process HOTP authentication during authentication. Failed to update user entry" + elif self.otpType == "totp": + for user_enrollment in user_enrollments: + otp_secret_key = self.fromBase64Url(user_enrollment) + + # Validate TOTP + validation_result = self.validateTotpKey(otp_secret_key, otpCode, user_name) + if (validation_result != None) and validation_result["result"]: + print "OTP. Process TOTP authentication during authentication. otpCode is valid" + return True + + facesMessages.add(FacesMessage.SEVERITY_ERROR, "Failed to authenticate. OTP code is invalid") + print "OTP. Process OTP authentication. OTP code is invalid" + + return False + + # Shared HOTP/TOTP methods + def generateSecretKey(self, keyLength): + bytes = jarray.zeros(keyLength, "b") + secureRandom = SecureRandom() + secureRandom.nextBytes(bytes) + + return bytes + + # HOTP methods + def generateSecretHotpKey(self): + keyLength = self.hotpConfiguration["keyLength"] + + return self.generateSecretKey(keyLength) + + def generateHotpKey(self, secretKey, movingFactor): + digits = self.hotpConfiguration["digits"] + + hotp = HOTP.key(secretKey).digits(digits).movingFactor(movingFactor).build() + + return hotp.value() + + def validateHotpKey(self, secretKey, movingFactor, totpKey): + lookAheadWindow = self.hotpConfiguration["lookAheadWindow"] + digits = self.hotpConfiguration["digits"] + + htopValidationResult = HOTPValidator.lookAheadWindow(lookAheadWindow).validate(secretKey, movingFactor, digits, totpKey) + if htopValidationResult.isValid(): + return { "result": True, "movingFactor": htopValidationResult.getNewMovingFactor() } + + return { "result": False, "movingFactor": None } + + def generateHotpSecretKeyUri(self, secretKey, issuer, userDisplayName): + digits = self.hotpConfiguration["digits"] + + secretKeyBase32 = self.toBase32(secretKey) + otpKey = OTPKey(secretKeyBase32, OTPType.HOTP) + label = issuer + " %s" % userDisplayName + + otpAuthURI = OTPAuthURIBuilder.fromKey(otpKey).label(label).issuer(issuer).digits(digits).build() + + return otpAuthURI.toUriString() + + # TOTP methods + def generateSecretTotpKey(self): + keyLength = self.totpConfiguration["keyLength"] + + return self.generateSecretKey(keyLength) + + def generateTotpKey(self, secretKey): + digits = self.totpConfiguration["digits"] + timeStep = self.totpConfiguration["timeStep"] + hmacShaAlgorithmType = self.totpConfiguration["hmacShaAlgorithmType"] + + totp = TOTP.key(secretKey).digits(digits).timeStep(TimeUnit.SECONDS.toMillis(timeStep)).hmacSha(hmacShaAlgorithmType).build() + + return totp.value() + + def validateTotpKey(self, secretKey, totpKey, user_name): + localTotpKey = self.generateTotpKey(secretKey) + cachedOTP = self.getCachedOTP(user_name) + + if StringHelper.equals(localTotpKey, totpKey) and not StringHelper.equals(localTotpKey, cachedOTP): + userService = CdiUtil.bean(UserService) + if cachedOTP is None: + userService.addUserAttribute(user_name, "oxOTPCache",localTotpKey) + else : + userService.replaceUserAttribute(user_name, "oxOTPCache", cachedOTP, localTotpKey) + print "OTP. Caching OTP: '%s'" % localTotpKey + return { "result": True } + return { "result": False } + + def getCachedOTP(self, user_name): + userService = CdiUtil.bean(UserService) + user = userService.getUser(user_name, "oxOTPCache") + if user is None: + print "OTP. Get Cached OTP. Failed to find OTP" + return None + customAttribute = userService.getCustomAttribute(user, "oxOTPCache") + + if customAttribute is None: + print "OTP. Custom attribute is null" + return None + user_cached_OTP = customAttribute.getValue() + if user_cached_OTP is None: + print "OTP. no OTP is present in LDAP" + return None + + print "OTP.Cached OTP: '%s'" % user_cached_OTP + return user_cached_OTP + + def generateTotpSecretKeyUri(self, secretKey, issuer, userDisplayName): + digits = self.totpConfiguration["digits"] + timeStep = self.totpConfiguration["timeStep"] + + secretKeyBase32 = self.toBase32(secretKey) + otpKey = OTPKey(secretKeyBase32, OTPType.TOTP) + label = issuer + " %s" % userDisplayName + + otpAuthURI = OTPAuthURIBuilder.fromKey(otpKey).label(label).issuer(issuer).digits(digits).timeStep(TimeUnit.SECONDS.toMillis(timeStep)).build() + + return otpAuthURI.toUriString() + + # Utility methods + def toBase32(self, bytes): + return BaseEncoding.base32().omitPadding().encode(bytes) + + def toBase64Url(self, bytes): + return BaseEncoding.base64Url().encode(bytes) + + def fromBase64Url(self, chars): + return BaseEncoding.base64Url().decode(chars) + + diff --git a/oxAuth/Server/integrations/otp/Properties description.md b/oxAuth/Server/integrations/otp/Properties description.md new file mode 100644 index 00000000..0b59d260 --- /dev/null +++ b/oxAuth/Server/integrations/otp/Properties description.md @@ -0,0 +1,23 @@ +This is a person authentication module for oxAuth that enables one-time password for user authentication. + +The module has a few properties: + +1) otp_type - It's mandatory property. It's specify OTP mode: HOTP/ TOTP. + Allowed values: hotp/totp + Example: hotp + +2) issuer - It's mandatory property. It's company name. + Example: Gluu Inc + +3) otp_conf_file - It's mandatory property. It's specify path to OTP configuration JSON file. + Example: /etc/certs/otp_configuration.json + +4) label - It's label inside QR code. It's optional property. + Example: Gluu OTP + +5) qr_options - Specify width and height of QR image. It's optional property. + Example: qr_options: { width: 400, height: 400 } + +6) registration_uri - It's URL to page where user can register new account. It's optional property. + Example: https://ce-dev.gluu.org/identity/register + \ No newline at end of file diff --git a/oxAuth/Server/integrations/otp/Readme.md b/oxAuth/Server/integrations/otp/Readme.md new file mode 100644 index 00000000..4928b363 --- /dev/null +++ b/oxAuth/Server/integrations/otp/Readme.md @@ -0,0 +1,5 @@ +# One-Time Password (OTP) Authentication + +Gluu's OTP interception script uses the two-factor event/counter-based HOTP algorithm [RFC4226](https://tools.ietf.org/html/rfc4226) and the time-based TOTP algorithm [RFC6238](https://tools.ietf.org/html/rfc6238). + +In order to use this authentication mechanism users will need to install a mobile authenticator, like [Google Authenticator 2](https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2), that supports HOTP/TOTP. diff --git a/oxAuth/Server/integrations/otp/img/gluu_otp_integration_authentication_workflow.png b/oxAuth/Server/integrations/otp/img/gluu_otp_integration_authentication_workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..4397d9d7b0805c341510b0168c6f6aaa738462fd GIT binary patch literal 64598 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 H!wdflq>)Ik literal 0 HcmV?d00001 diff --git a/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp-keyprovisioning/0.0.1-SNAPSHOT/_remote.repositories b/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp-keyprovisioning/0.0.1-SNAPSHOT/_remote.repositories new file mode 100644 index 00000000..09593990 --- /dev/null +++ b/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp-keyprovisioning/0.0.1-SNAPSHOT/_remote.repositories @@ -0,0 +1,6 @@ +#NOTE: This is an Aether internal implementation file, its format can be changed without prior notice. +#Mon Sep 26 13:13:34 MSK 2016 +oath-otp-keyprovisioning-0.0.1-SNAPSHOT.pom>= +oath-otp-keyprovisioning-0.0.1-SNAPSHOT-javadoc.jar>= +oath-otp-keyprovisioning-0.0.1-SNAPSHOT.jar>= +oath-otp-keyprovisioning-0.0.1-SNAPSHOT-sources.jar>= diff --git a/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp-keyprovisioning/0.0.1-SNAPSHOT/maven-metadata-local.xml b/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp-keyprovisioning/0.0.1-SNAPSHOT/maven-metadata-local.xml new file mode 100644 index 00000000..db499fc9 --- /dev/null +++ b/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp-keyprovisioning/0.0.1-SNAPSHOT/maven-metadata-local.xml @@ -0,0 +1,36 @@ + + + com.lochbridge.oath + oath-otp-keyprovisioning + 0.0.1-SNAPSHOT + + + true + + 20160926101334 + + + javadoc + jar + 0.0.1-SNAPSHOT + 20160926101334 + + + sources + jar + 0.0.1-SNAPSHOT + 20160926101334 + + + jar + 0.0.1-SNAPSHOT + 20160926101334 + + + pom + 0.0.1-SNAPSHOT + 20160926101334 + + + + diff --git a/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp-keyprovisioning/0.0.1-SNAPSHOT/oath-otp-keyprovisioning-0.0.1-SNAPSHOT-javadoc.jar b/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp-keyprovisioning/0.0.1-SNAPSHOT/oath-otp-keyprovisioning-0.0.1-SNAPSHOT-javadoc.jar new file mode 100644 index 0000000000000000000000000000000000000000..65b92f445fefe553438c05c799a39fc983798329 GIT binary patch literal 71430 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 + 4.0.0 + + com.lochbridge.oath + oath-parent + 0.0.1-SNAPSHOT + + oath-otp-keyprovisioning + OATH OTP Key Provisioning + A module for providing OTP key provisioning support. + + + 3.1.0 + + + + + com.google.guava + guava + + + + + com.google.zxing + core + ${com.google.zxing.version} + + + com.google.zxing + javase + ${com.google.zxing.version} + + + + com.lochbridge.oath + oath-otp + 0.0.1-SNAPSHOT + + + \ No newline at end of file diff --git a/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp-keyprovisioning/maven-metadata-local.xml b/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp-keyprovisioning/maven-metadata-local.xml new file mode 100644 index 00000000..c7f4b52a --- /dev/null +++ b/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp-keyprovisioning/maven-metadata-local.xml @@ -0,0 +1,11 @@ + + + com.lochbridge.oath + oath-otp-keyprovisioning + + + 0.0.1-SNAPSHOT + + 20160926101334 + + diff --git a/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp/0.0.1-SNAPSHOT/_remote.repositories b/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp/0.0.1-SNAPSHOT/_remote.repositories new file mode 100644 index 00000000..865f16cf --- /dev/null +++ b/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp/0.0.1-SNAPSHOT/_remote.repositories @@ -0,0 +1,6 @@ +#NOTE: This is an Aether internal implementation file, its format can be changed without prior notice. +#Mon Sep 26 13:13:32 MSK 2016 +oath-otp-0.0.1-SNAPSHOT.jar>= +oath-otp-0.0.1-SNAPSHOT-sources.jar>= +oath-otp-0.0.1-SNAPSHOT.pom>= +oath-otp-0.0.1-SNAPSHOT-javadoc.jar>= diff --git a/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp/0.0.1-SNAPSHOT/maven-metadata-local.xml b/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp/0.0.1-SNAPSHOT/maven-metadata-local.xml new file mode 100644 index 00000000..cfd6fa0c --- /dev/null +++ b/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp/0.0.1-SNAPSHOT/maven-metadata-local.xml @@ -0,0 +1,36 @@ + + + com.lochbridge.oath + oath-otp + 0.0.1-SNAPSHOT + + + true + + 20160926101332 + + + javadoc + jar + 0.0.1-SNAPSHOT + 20160926101332 + + + sources + jar + 0.0.1-SNAPSHOT + 20160926101332 + + + jar + 0.0.1-SNAPSHOT + 20160926101332 + + + pom + 0.0.1-SNAPSHOT + 20160926101332 + + + + diff --git a/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp/0.0.1-SNAPSHOT/oath-otp-0.0.1-SNAPSHOT-javadoc.jar b/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp/0.0.1-SNAPSHOT/oath-otp-0.0.1-SNAPSHOT-javadoc.jar new file mode 100644 index 0000000000000000000000000000000000000000..9eca484ff18c15ef82507ff7d9cdc990d9badc37 GIT binary patch literal 68069 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%0Dk>H?Rm^Wu=}o9I}P+gErXA*gMU` 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

S5k2M`W1MQu$bh`~O0);rt` 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�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%0D+36BDjLp)M^h1x(Y=pi6V0qQ@K8~DG^JZMf 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%0A;--9|BL(k$~cF? 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 H-+ui+i2vk9 literal 0 HcmV?d00001 diff --git a/oxAuth/Server/integrations/passwurd/PasswurdAuthentication.py b/oxAuth/Server/integrations/passwurd/PasswurdAuthentication.py new file mode 100644 index 00000000..d622ae7d --- /dev/null +++ b/oxAuth/Server/integrations/passwurd/PasswurdAuthentication.py @@ -0,0 +1,894 @@ +# Author: Madhumita Subramaniam + +from org.gluu.oxauth.client import RegisterClient +from org.gluu.oxauth.client import RegisterRequest +from org.gluu.oxauth.client import RegisterResponse +from org.gluu.oxauth.model.common import GrantType; +from org.gluu.oxauth.model.register import ApplicationType; +from org.gluu.oxauth.model.util import StringUtils; +from org.gluu.oxauth.model.common import User, WebKeyStorage +from org.gluu.oxauth.model.configuration import AppConfiguration +from org.gluu.oxauth.model.crypto import OxAuthCryptoProvider +from org.gluu.oxauth.model.crypto.signature import SignatureAlgorithm +from org.oxauth.persistence.model.configuration import GluuConfiguration +from org.gluu.oxauth.security import Identity +from org.gluu.oxauth.util import ServerUtil +from org.gluu.oxauth.service import AuthenticationService, UserService +from org.gluu.oxauth.service.custom import CustomScriptService +from org.gluu.model.custom.script import CustomScriptType +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.model import SimpleCustomProperty +from org.gluu.persist import PersistenceEntryManager +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.jsf2.message import FacesMessages +from org.gluu.oxauth.service.net import HttpService, HttpService2 +from org.json import JSONObject +from org.json import JSONArray +from org.gluu.util import StringHelper +from java.lang import System +from java.lang import String +from java.util import UUID +from java.net import URLDecoder, URLEncoder +from java.util import Arrays, ArrayList, Collections, HashMap +from javax.faces.application import FacesMessage +from javax.servlet.http import Cookie +from javax.faces.context import FacesContext +from org.apache.http.entity import ContentType +import random + +import base64 +import ssl +import json +import urllib2 + +try: + import json +except ImportError: + import simplejson as json +import sys + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + self.ACR_SG = "super_gluu" + self.PREV_LOGIN_SETTING = "prevLoginsCookieSettings" + + self.modulePrefix = "casa-external_" + + def init(self, customScript, configurationAttributes): + print "Passwurd. init called" + + if not configurationAttributes.containsKey("AS_ENDPOINT"): + print "Scan. Initialization. Property AS_ENDPOINT is mandatory" + return False + self.AS_ENDPOINT = configurationAttributes.get("AS_ENDPOINT").getValue2() + + if not configurationAttributes.containsKey("AS_SSA"): + print "Scan. Initialization. Property AS_SSA is mandatory" + return False + self.AS_SSA = configurationAttributes.get("AS_SSA").getValue2() + + if not configurationAttributes.containsKey("AS_CLIENT_ID"): + print "Scan. Initialization. Property AS_CLIENT_ID is mandatory" + return False + self.AS_CLIENT_ID = configurationAttributes.get("AS_CLIENT_ID").getValue2() + + + if not configurationAttributes.containsKey("AS_CLIENT_SECRET"): + print "Scan. Initialization. Property AS_CLIENT_SECRET is mandatory" + return False + self.AS_CLIENT_SECRET = configurationAttributes.get("AS_CLIENT_SECRET").getValue2() + + if not configurationAttributes.containsKey("AS_REDIRECT_URI"): + print "Scan. Initialization. Property AS_REDIRECT_URI is mandatory" + return False + self.AS_REDIRECT_URI = configurationAttributes.get("AS_REDIRECT_URI").getValue2() + + + # JWKS used to sign the SSA + if not configurationAttributes.containsKey("PORTAL_JWKS"): + print "Scan. Initialization. Property PORTAL_JWKS is mandatory" + return False + self.PORTAL_JWKS = configurationAttributes.get("PORTAL_JWKS").getValue2() + + + # KEY A + if not configurationAttributes.containsKey("PASSWURD_KEY_A_KEYSTORE"): + print "Scan. Initialization. Property PASSWURD_KEY_A_KEYSTORE is mandatory" + return False + self.PASSWURD_KEY_A_KEYSTORE = configurationAttributes.get("PASSWURD_KEY_A_KEYSTORE").getValue2() + + # KEY A + if not configurationAttributes.containsKey("PASSWURD_KEY_A_PASSWORD"): + print "Scan. Initialization. Property PASSWURD_KEY_A_PASSWORD is mandatory" + return False + self.PASSWURD_KEY_A_PASSWORD = configurationAttributes.get("PASSWURD_KEY_A_PASSWORD").getValue2() + + # KEY A + if not configurationAttributes.containsKey("PASSWURD_API_URL"): + print "Passwurd. Initialization. Property PASSWURD_API_URL is mandatory" + return False + self.PASSWURD_API_URL = configurationAttributes.get("PASSWURD_API_URL").getValue2() + + self.authenticators = {} + self.uid_attr = self.getLocalPrimaryKey() + + self.prevLoginsSettings = self.computePrevLoginsSettings(configurationAttributes.get(self.PREV_LOGIN_SETTING)) + custScriptService = CdiUtil.bean(CustomScriptService) + self.scriptsList = custScriptService.findCustomScripts(Collections.singletonList(CustomScriptType.PERSON_AUTHENTICATION), "oxConfigurationProperty", "displayName", "oxEnabled") + dynamicMethods = self.computeMethods(configurationAttributes.get("snd_step_methods"), self.scriptsList) + + if len(dynamicMethods) > 0: + + for acr in dynamicMethods: + moduleName = self.modulePrefix + acr + try: + external = __import__(moduleName, globals(), locals(), ["PersonAuthentication"], -1) + module = external.PersonAuthentication(self.currentTimeMillis) + + print "Passwurd. init. Got dynamic module for acr %s" % acr + configAttrs = self.getConfigurationAttributes(acr, self.scriptsList) + + if acr == self.ACR_SG: + application_id = configurationAttributes.get("supergluu_app_id").getValue2() + configAttrs.put("application_id", SimpleCustomProperty("application_id", application_id)) + + if module.init(None, configAttrs): + module.configAttrs = configAttrs + self.authenticators[acr] = module + else: + print "Passwurd. init. Call to init in module '%s' returned False" % moduleName + except: + print "Passwurd. init. Failed to load module %s" % moduleName + print "Exception: ", sys.exc_info()[1] + else: + print "Passwurd. init. Not enough custom scripts enabled. Check config property 'snd_step_methods'" + return False + + self.cryptoProvider = OxAuthCryptoProvider(self.PASSWURD_KEY_A_KEYSTORE, self.PASSWURD_KEY_A_PASSWORD, None); + # upon client creation, this value is populated, after that this call will not go through in subsequent script restart + + + if StringHelper.isEmptyString(self.AS_CLIENT_ID): + clientRegistrationResponse = self.registerScanClient(self.AS_ENDPOINT, self.AS_REDIRECT_URI, self.AS_SSA, customScript) + if clientRegistrationResponse == None: + return False + + self.AS_CLIENT_ID = clientRegistrationResponse['client_id'] + self.AS_CLIENT_SECRET = clientRegistrationResponse['client_secret'] + print self.AS_CLIENT_ID + print self.AS_CLIENT_SECRET + + print "Passwurd. init. Initialized successfully" + return True + + def destroy(self, configurationAttributes): + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, configurationAttributes): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + print "Passwurd. AUTHENTICATE for step %d" % step + + userService = CdiUtil.bean(UserService) + authenticationService = CdiUtil.bean(AuthenticationService) + identity = CdiUtil.bean(Identity) + + if step == 1: + user_name = ServerUtil.getFirstValue(requestParameters, "username") + k_username = ServerUtil.getFirstValue(requestParameters, "k_username") + print "username -%s" % user_name + print "- k_username %s" % k_username + if StringHelper.isNotEmptyString(user_name): + foundUser = userService.getUserByAttribute(self.uid_attr, user_name) + if foundUser == None: + print "Passwurd. Unknown username '%s'" % user_name + return False + else: + logged_in = authenticationService.authenticate(user_name) + if (logged_in is True): + identity.setWorkingParameter("k_username", k_username) + if foundUser.getAttribute("userPassword") is None: + identity.setWorkingParameter("passwordNotSaved", "true"); + else: + identity.setWorkingParameter("passwordNotSaved", "false"); + + availMethods = self.getAvailMethodsUser(foundUser) + if availMethods.size() > 0: + acr = availMethods.get(0) + print "Passwurd. Method to try in incase of 2fa step will be: %s" % acr + identity.setWorkingParameter("ACR", acr) + module = self.authenticators[acr] + logged_in = module.authenticate(module.configAttrs, requestParameters, step) + print "Logged In : %s" % logged_in + return logged_in + elif(step == 2): + user = authenticationService.getAuthenticatedUser() + username = user.getUserId() + if user == None: + print "Passwurd. authenticate for step 2. Cannot retrieve logged user" + return False + + elif(CdiUtil.bean(Identity).getWorkingParameter("passwordScanFailed") == "true"): + result_2fa = self.authenticate2FAStep(requestParameters, user, step) + if result_2fa == True: + CdiUtil.bean(Identity).setWorkingParameter("passwordScanFailedAnd2FAPassed","true") + # call notify API and pass the track_id + track_id = CdiUtil.bean(Identity).getWorkingParameter("track_id") + self.notifyProfilePy( username, track_id) + #self.setError("Password validation failed. You have to authenticate yourself before proceeding") + return result_2fa + + elif(CdiUtil.bean(Identity).getWorkingParameter("passwordNotSaved") == "true" and CdiUtil.bean(Identity).getWorkingParameter("passwordNotSavedAnd2FAPassed") != "true"): + print "passwordNotSaved and 2FA not passed" + result_2fa = self.authenticate2FAStep(requestParameters, user, step) + if result_2fa == True: + CdiUtil.bean(Identity).setWorkingParameter("passwordNotSavedAnd2FAPassed","true") + track_id = CdiUtil.bean(Identity).getWorkingParameter("track_id") + self.notifyProfilePy( username, track_id) + return result_2fa + + + elif (CdiUtil.bean(Identity).getWorkingParameter("passwordNotSaved") == 'true' and CdiUtil.bean(Identity).getWorkingParameter("passwordNotSavedAnd2FAPassed") == 'true'): + # TODO: we are never going to send the password here in plain text, this check should be removed + #password2 = ServerUtil.getFirstValue(requestParameters, "login_form:password2") + #if StringHelper.isEmpty(password): + # self.setError("Password cannot be empty.") + # print "Password cannot be empty." + # CdiUtil.bean(Identity).setWorkingParameter("passwordSaved","false") + # return True + + k_pwd = ServerUtil.getFirstValue(requestParameters, "k_pwd") + print "k_pwd %s" % k_pwd + + result = self.validateKeystrokes(username, identity.getWorkingParameter("k_username"), k_pwd ) + print "result %s" % result + if(result == -1): + # Gluu Authentication complete + print "Passwurd. Authentication successful." + + # TODO: check that - only if MFA was invoked in a prev step, then this needs to be added + #result = self.notifyProfilePy(username, True) + user.setAttribute("userPassword", "true"); + userService.updateUser(user) + return True; + # this means that password scan failed, go for 2fa + elif(result > 0): + CdiUtil.bean(Identity).setWorkingParameter("passwordScanFailed","true") + CdiUtil.bean(Identity).setWorkingParameter("track_id",result) + elif (result == -2): + print "Passwurd. this means that the typed password text didnt match" + return False; + elif (result == 0): + print "Passwurd. There was an exception" + return False; + else: + print result + return False; + + + else: + # (Password has been saved in the past and user is authenticating using his password) + # invoke the GLuu Scan API and validate the pwd + # if it fails, save this session variable passwordScanFailed, and proceed to step 3 is 2FA by casa script + #password = ServerUtil.getFirstValue(requestParameters, "pwd") + k_pwd = ServerUtil.getFirstValue(requestParameters, "k_pwd") + print "k_pwd %s" % k_pwd + + result = self.validateKeystrokes(username, identity.getWorkingParameter("k_username"), k_pwd) + print "result %s" % result + if(result == -1): + # Gluu Authentication complete + print "Passwurd. Authentication successful." + return True; + elif (result == -2): + # the typed password did not tally + print "Passwurd. The typed password did not tally." + self.setError("Incorrect password.") + return False; + elif (result > 0): + # Gluu authentication not complete, go for 2FA + CdiUtil.bean(Identity).setWorkingParameter("passwordScanFailed","true") + CdiUtil.bean(Identity).setWorkingParameter("track_id",result) + print "track id that is being notified is: %s" % result + #self.notifyProfilePy(username, result) + return True + else: + print result + return False; + return False + + + def prepareForStep(self, configurationAttributes, requestParameters, step): + print "Passwurd. prepareForStep %d" % step + + identity = CdiUtil.bean(Identity) + session_attributes = identity.getSessionId().getSessionAttributes() + + if step == 1: + try: + loginHint = session_attributes.get("login_hint") + print "Passwurd. prepareForStep. Login hint is %s" % loginHint + isLoginHint = loginHint != None + if self.prevLoginsSettings == None: + if isLoginHint: + identity.setWorkingParameter("loginHint", loginHint) + else: + users = self.getCookieValue() + if isLoginHint: + idx = self.findUid(loginHint, users) + if idx >= 0: + u = users.pop(idx) + users.insert(0, u) + else: + identity.setWorkingParameter("loginHint", loginHint) + + if len(users) > 0: + identity.setWorkingParameter("users", json.dumps(users, separators=(',',':'))) + + # In login.xhtml both loginHint and users are used to properly display the login form + except: + print "Passwurd. prepareForStep. Error!", sys.exc_info()[1] + + return True + + elif step == 2: + user = CdiUtil.bean(AuthenticationService).getAuthenticatedUser() + if user == None: + print "Passwurd. prepareForStep. Cannot retrieve logged user" + return False + + # password does not exist, step 2 is 2FA authentication + if user.getAttribute("userPassword") == None and CdiUtil.bean(Identity).getWorkingParameter("passwordNotSavedAnd2FAPassed") != "true": + CdiUtil.bean(Identity).setWorkingParameter("passwordNotSaved","true") + self.setError("Password not saved. You have to authenticate yourself before proceeding") + twoFA_result = self.get2FAPrepareForStep ( user, identity.getWorkingParameter("ACR"), requestParameters, step) + return twoFA_result + + + if CdiUtil.bean(Identity).getWorkingParameter("passwordNotSaved") == "true": + # case 1 : saving the password + print "Passwurd. user password does not exists and user will be presented with the save password page which goes to the /enroll endpoint of Gluu passwurd API" + return True + elif CdiUtil.bean(Identity).getWorkingParameter("passwordScanFailed") == "true" : + # case 2: 2FA authentication + self.setError("Password validation failed. You have to authenticate yourself before proceeding") + twoFA_result = self.get2FAPrepareForStep ( user, identity.getWorkingParameter("ACR"), requestParameters, step) + return twoFA_result + else: + # presenting the enterPwd page + # do nothing + return True + + else: + print "Passwurd. Something went wrong" + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + + print "Passwurd. getExtraParametersForStep %d" % step + list = ArrayList() + if step > 1: + acr = CdiUtil.bean(Identity).getWorkingParameter("ACR") + + if acr in self.authenticators: + module = self.authenticators[acr] + params = module.getExtraParametersForStep(module.configAttrs, step) + if params != None: + list.addAll(params) + list.addAll(Arrays.asList("ACR", "methods", "passwordNotSaved", "passwordNotSavedAnd2FAPassed", "passwordScanFailed","passwordScanFailedAnd2FAPassed", "passwordSaved", "k_username", "k_pwd","trial_id")) + print "extras are %s" % list + return list + + def getCountAuthenticationSteps(self, configurationAttributes): + return 2 + + def getPageForStep(self, configurationAttributes, step): + print "getPageForStep %s" % step + if step > 1: + page = None + acr = CdiUtil.bean(Identity).getWorkingParameter("ACR") + if acr in self.authenticators and (( CdiUtil.bean(Identity).getWorkingParameter("passwordNotSaved") == "true" and CdiUtil.bean(Identity).getWorkingParameter("passwordNotSavedAnd2FAPassed") != "true") or (CdiUtil.bean(Identity).getWorkingParameter("passwordScanFailed") == "true" and CdiUtil.bean(Identity).getWorkingParameter("passwordScanFailedAnd2FAPassed") != "true")): + module = self.authenticators[acr] + print module + page = module.getPageForStep(module.configAttrs, 2) + + print "Passwurd. getPageForStep %d is %s" % (2, page) + return page + + if CdiUtil.bean(Identity).getWorkingParameter("passwordNotSaved") == "true" and CdiUtil.bean(Identity).getWorkingParameter("passwordNotSavedAnd2FAPassed") == "true": + return "/passwurd/savePwd.xhtml" + + elif CdiUtil.bean(Identity).getWorkingParameter("passwordNotSaved") is "true" or CdiUtil.bean(Identity).getWorkingParameter("passwordScanFailed") == "true": + return page + + else: + return "/passwurd/enterPwd.xhtml" + + return "/passwurd/login.xhtml" + + + + def getNextStep(self, configurationAttributes, requestParameters, step): + print "Passwurd. getNextStep called %d" % step + + if(step > 1): + print "Passwurd. Step > 1" + acr = ServerUtil.getFirstValue(requestParameters, "alternativeMethod") + if acr != None: + CdiUtil.bean(Identity).setWorkingParameter("ACR", acr) + return 2 + + user = CdiUtil.bean(AuthenticationService).getAuthenticatedUser() + + if(CdiUtil.bean(Identity).getWorkingParameter("passwordSaved") == "false"): + print "passwordSaved" + return 2 + + if user.getAttribute("userPassword") is not None and CdiUtil.bean(Identity).getWorkingParameter("passwordNotSavedAnd2FAPassed") == "true" : + return -1 + + if CdiUtil.bean(Identity).getWorkingParameter("passwordScanFailedAnd2FAPassed") == "true": + return -1 + + if CdiUtil.bean(Identity).getWorkingParameter("passwordNotSavedAnd2FAPassed") == "true" : + return 2 + # reset step to the previous step count, when alternative 2fa method is tried + if CdiUtil.bean(Identity).getWorkingParameter("passwordScanFailed") == "true" and CdiUtil.bean(Identity).getWorkingParameter("passwordNotSavedAnd2FAPassed") is None: + return step + return -1 + + def logout(self, configurationAttributes, requestParameters): + return True + + # Miscelaneous + + def getLocalPrimaryKey(self): + entryManager = CdiUtil.bean(PersistenceEntryManager) + config = GluuConfiguration() + config = entryManager.find(config.getClass(), "ou=configuration,o=gluu") + #Pick (one) attribute where user id is stored (e.g. uid/mail) + uid_attr = config.getOxIDPAuthentication().get(0).getConfig().getPrimaryKey() + print "Passwurd. init. uid attribute is '%s'" % uid_attr + return uid_attr + + + def setError(self, msg): + facesMessages = CdiUtil.bean(FacesMessages) + facesMessages.setKeepMessages() + facesMessages.clear() + facesMessages.add(FacesMessage.SEVERITY_ERROR, msg) + + + def computeMethods(self, sndStepMethods, scriptsList): + snd_step_methods = [] if sndStepMethods == None else StringHelper.split(sndStepMethods.getValue2(), ",") + methods = [] + + for m in snd_step_methods: + for customScript in scriptsList: + if customScript.getName() == m and customScript.isEnabled(): + methods.append(m) + + print "Passwurd. computeMethods. %s" % methods + return methods + + + def getConfigurationAttributes(self, acr, scriptsList): + + configMap = HashMap() + for customScript in scriptsList: + if customScript.getName() == acr: + for prop in customScript.getConfigurationProperties(): + configMap.put(prop.getValue1(), SimpleCustomProperty(prop.getValue1(), prop.getValue2())) + + print "Passwurd. getConfigurationAttributes. %d configuration properties were found for %s" % (configMap.size(), acr) + return configMap + + + def getAvailMethodsUser(self, user, skip=None): + methods = ArrayList() + for method in self.authenticators: + try: + module = self.authenticators[method] + if module.hasEnrollments(module.configAttrs, user) and (skip == None or skip != method): + methods.add(method) + except: + print "Passwurd. getAvailMethodsUser. hasEnrollments call could not be issued for %s module" % method + print "Exception: ", sys.exc_info()[1] + + print "Passwurd. getAvailMethodsUser %s" % methods.toString() + return methods + + + def simulateFirstStep(self, requestParameters, acr): + #To simulate 1st step, there is no need to call: + # getPageforstep (no need as user/pwd won't be shown again) + # isValidAuthenticationMethod (by restriction, it returns True) + # prepareForStep (by restriction, it returns True) + # getExtraParametersForStep (by restriction, it returns None) + print "Passwurd. simulateFirstStep. Calling authenticate (step 1) for %s module" % acr + if acr in self.authenticators: + module = self.authenticators[acr] + auth = module.authenticate(module.configAttrs, requestParameters, 1) + print "Passwurd. simulateFirstStep. returned value was %s" % auth + + def computePrevLoginsSettings(self, customProperty): + settings = None + if customProperty == None: + print "Passwurd. Previous logins feature is not configured. Set config property '%s' if desired" % self.PREV_LOGIN_SETTING + else: + try: + settings = json.loads(customProperty.getValue2()) + if settings['enabled']: + print "Passwurd. PrevLoginsSettings are %s" % settings + else: + settings = None + print "Passwurd. Previous logins feature is disabled" + except: + print "Passwurd. Unparsable config property '%s'" % self.PREV_LOGIN_SETTING + + return settings + + def getCookieValue(self): + ulist = [] + coo = None + httpRequest = ServerUtil.getRequestOrNull() + + if httpRequest != None: + for cookie in httpRequest.getCookies(): + if cookie.getName() == self.prevLoginsSettings['cookieName']: + coo = cookie + + if coo == None: + print "Passwurd. getCookie. No cookie found" + else: + print "Passwurd. getCookie. Found cookie" + forgetMs = self.prevLoginsSettings['forgetEntriesAfterMinutes'] * 60 * 1000 + + try: + now = System.currentTimeMillis() + value = URLDecoder.decode(coo.getValue(), "utf-8") + # value is an array of objects with properties: uid, displayName, lastLogon + value = json.loads(value) + + for v in value: + if now - v['lastLogon'] < forgetMs: + ulist.append(v) + # print "==========", ulist + except: + print "Passwurd. getCookie. Unparsable value, dropping cookie..." + + return ulist + + + def findUid(self, uid, users): + + i = 0 + idx = -1 + for user in users: + if user['uid'] == uid: + idx = i + break + i+=1 + return idx + + + def persistCookie(self, user): + try: + now = System.currentTimeMillis() + uid = user.getUserId() + dname = user.getAttribute("displayName") + + users = self.getCookieValue() + idx = self.findUid(uid, users) + + if idx >= 0: + u = users.pop(idx) + else: + u = { 'uid': uid, 'displayName': '' if dname == None else dname } + u['lastLogon'] = now + + # The most recent goes first :) + users.insert(0, u) + + excess = len(users) - self.prevLoginsSettings['maxListSize'] + if excess > 0: + print "Passwurd. persistCookie. Shortening list..." + users = users[:self.prevLoginsSettings['maxListSize']] + + value = json.dumps(users, separators=(',',':')) + value = URLEncoder.encode(value, "utf-8") + coo = Cookie(self.prevLoginsSettings['cookieName'], value) + coo.setSecure(True) + coo.setHttpOnly(True) + # One week + coo.setMaxAge(7 * 24 * 60 * 60) + + response = self.getHttpResponse() + if response != None: + print "Passwurd. persistCookie. Adding cookie to response" + response.addCookie(coo) + except: + print "Passwurd. persistCookie. Exception: ", sys.exc_info()[1] + + + def getHttpResponse(self): + try: + return FacesContext.getCurrentInstance().getExternalContext().getResponse() + except: + print "Passwurd. Error accessing HTTP response object: ", sys.exc_info()[1] + return None + + # invoking /authenticate endpoint of the GLUU PASSWURD API + def authenticatePassword (self, password): + print "Authenticating password: %s" % password + return random.choice([True, False]) + + # invoking /enroll endpoint of the GLUU PASSWURD API + def enrollPassword (self, password): + print "Enroll password: %s" % password + return True #random.choice([True, False]) + + def get2FAPrepareForStep (self, user, acr, requestParameters, step): + methods = ArrayList(self.getAvailMethodsUser(user, acr)) + print "methods - %s" % methods + CdiUtil.bean(Identity).setWorkingParameter("methods", methods) + print "acr %s " % acr + if acr in self.authenticators: + module = self.authenticators[acr] + return module.prepareForStep(module.configAttrs, requestParameters, step) + else: + return False + + def authenticate2FAStep(self, requestParameters, user, step): + alter = ServerUtil.getFirstValue(requestParameters, "alternativeMethod") + print "alter: %s" % alter + if alter != None: + #bypass the rest of this step if an alternative method was provided. Current step will be retried (see getNextStep) + self.simulateFirstStep(requestParameters, alter) + return True + + session_attributes = CdiUtil.bean(Identity).getSessionId().getSessionAttributes() + acr = session_attributes.get("ACR") + #this working parameter is used in alternative.xhtml + CdiUtil.bean(Identity).setWorkingParameter("methods", self.getAvailMethodsUser(user, acr)) + + success = False + if acr in self.authenticators: + module = self.authenticators[acr] + success = module.authenticate(module.configAttrs, requestParameters, step) + + if success: + print "Passwurd. authenticate. 2FA authentication was successful" + if self.prevLoginsSettings != None: + self.persistCookie(user) + else: + print "Passwurd. authenticate. 2FA authentication failed" + + return success + + def signUid(self, uid): + facesContext = CdiUtil.bean(FacesContext) + alias = "passwurd" # facesContext.getExternalContext().getRequest().getServerName() + print facesContext.getExternalContext().getRequest().getServerName() + print uid + signedUID = self.cryptoProvider.sign(uid, alias, None, SignatureAlgorithm.RS256) + print "SignedUID - %s" % signedUID + return signedUID + + def getAccessTokenJansServer(self ): + + httpService = CdiUtil.bean(HttpService) + http_client = httpService.getHttpsClient() + http_client_params = http_client.getParams() + + url = self.AS_ENDPOINT +"/jans-auth/restv1/token" + data = "grant_type=client_credentials&scope=https://api.gluu.org/auth/scopes/scan.passwurd&redirect_uri="+self.AS_REDIRECT_URI + print url + encodedString = base64.b64encode((self.AS_CLIENT_ID+":"+self.AS_CLIENT_SECRET).encode('utf-8')) + headers = {"Content-type" : "application/x-www-form-urlencoded", "Accept" : "application/json","Authorization": "Basic "+encodedString} + + try: + http_service_response = httpService.executePost(http_client, url, None, headers , data) + + http_response = http_service_response.getHttpResponse() + except: + print "Jans Auth Server - getAccessToken", sys.exc_info()[1] + return None + + try: + if not httpService.isResponseStastusCodeOk(http_response): + print "Passwurd. Jans-Auth getAccessToken. Get invalid response from server: ", str(http_response.getStatusLine().getStatusCode()) + httpService.consume(http_response) + return None + + response_bytes = httpService.getResponseContent(http_response) + response_string = httpService.convertEntityToString(response_bytes) + httpService.consume(http_response) + finally: + http_service_response.closeConnection() + + if response_string == None: + print "Passwurd. getAccessToken. Got empty response from validation server" + return None + + response = json.loads(response_string) + print "Passwurd. response access token: "+ response["access_token"] + return response["access_token"] + + + def validateKeystrokes(self, username, k_username,k_pwd): + print "Passwurd. Attempting to validate keystrokes" + + try: + customer_sig = self.signUid(username) + print customer_sig + access_token = self.getAccessTokenJansServer() + print access_token + + data_org = { "k_username": k_username, "k_pwd": k_pwd, "customer_sig": customer_sig } + + for key in ('k_username', 'k_pwd'): + data_org[key] = json.loads(data_org[key]) + + + data_org['uid'] = username + data_org['customer_sig'] = customer_sig + # TODO: this has to be extracted from the SSA + data_org['org_id']="github:maduvena" + body = json.dumps(data_org) + print body + headers = {"Accept" : "application/json", "Authorization": "Bearer "+access_token} + endpointUrl = self.PASSWURD_API_URL +"/validate" + print endpointUrl + httpService = CdiUtil.bean(HttpService2) + httpClient = httpService.getHttpsClient() + resultResponse = httpService.executePost(httpClient, endpointUrl, None, headers, body, ContentType.APPLICATION_JSON) + httpResponse = resultResponse.getHttpResponse() + httpResponseStatusCode = httpResponse.getStatusLine().getStatusCode() + print "Passwurd. validate keystrokes response status code: %s" % httpResponseStatusCode + + if not httpService.isResponseStastusCodeOk(httpResponse): + print "Passwurd. Failed to validate" + httpService.consume(httpResponse) + return None + + bytes = httpService.getResponseContent(httpResponse) + response = httpService.convertEntityToString(bytes) + response_data = json.loads(response) + print "Response data: %s" % response_data + + if(httpResponseStatusCode == 200): + if(response_data['status'] == 'Enrollment'): + print "Enrollment" + return -1 + elif(response_data['status'] == 'Approved'): + print "Approved" + return -1 + elif(response_data['status'] == 'Denied'): + print "Denied" + track_id = response_data['track_id'] + return track_id + print "Keystrokes validated successfully" + #wrong input + elif(httpResponseStatusCode == 422): + # in this case the password text mismatched, hence we do not offer the 2FA option + return -2 + elif(httpResponseStatusCode == 400): + print "Passwurd. in this case the password text mismatched, hence we do not offer the 2FA option" + return -2 + else: + print "Failed to validate keystrokes, API returned error %s" % httpResponseStatusCode + return 0 + except: + print "Passwurd. Failed to execute /validate.", sys.exc_info()[1] + return 0 + + + def notifyProfilePy(self, username, track_id): + + access_token = self.getAccessTokenJansServer() + print access_token + try: + + data_org = { "uid": username, "track_id": track_id } + + body = json.dumps(data_org) + headers = {"Accept" : "application/json", "Authorization": "Bearer "+access_token} + print body + endpointUrl = self.PASSWURD_API_URL +"/notify" + print endpointUrl + + + httpService = CdiUtil.bean(HttpService2) + httpClient = httpService.getHttpsClient() + resultResponse = httpService.executePost(httpClient, endpointUrl, None, headers, body, ContentType.APPLICATION_JSON) + httpResponse = resultResponse.getHttpResponse() + httpResponseStatusCode = httpResponse.getStatusLine().getStatusCode() + print "Passwurd. Notify response status code: %s" % httpResponseStatusCode + + if not httpService.isResponseStastusCodeOk(httpResponse): + print "Passwurd. Notify response invalid " + httpService.consume(httpResponse) + return None + + bytes = httpService.getResponseContent(httpResponse) + + response = httpService.convertEntityToString(bytes) + response_data = json.loads(response) + print response_data + #status = response_data['status'] + #print status + except: + print "Passwurd. Failed to execute /notify.", sys.exc_info()[1] + # return true irrespective of the result + return True + + def registerScanClient(self, asBaseUrl, asRedirectUri, asSSA, customScript): + print "Passwurd. Attempting to register client" + + redirect_str = "[\"%s\"]" % asRedirectUri + data_org = {'redirect_uris': json.loads(redirect_str), + 'software_statement': asSSA} + body = json.dumps(data_org) + print body + endpointUrl = asBaseUrl + "/jans-auth/restv1/register" + print endpointUrl + headers = {"Accept" : "application/json"} + + try: + httpService = CdiUtil.bean(HttpService2) + httpClient = httpService.getHttpsClient() + resultResponse = httpService.executePost(httpClient, endpointUrl, None, headers, body, ContentType.APPLICATION_JSON) + httpResponse = resultResponse.getHttpResponse() + httpResponseStatusCode = httpResponse.getStatusLine().getStatusCode() + print "Passwurd. Get client registration response status code: %s" % httpResponseStatusCode + + if not httpService.isResponseStastusCodeOk(httpResponse): + print "Passwurd. Scan. Get invalid registration" + httpService.consume(httpResponse) + return None + + bytes = httpService.getResponseContent(httpResponse) + + response = httpService.convertEntityToString(bytes) + except: + print "Passwurd. Failed to send client registration request: ", sys.exc_info()[1] + return None + + response_data = json.loads(response) + client_id = response_data["client_id"] + client_secret = response_data["client_secret"] + + print "Passwurd. Registered client: %s" % client_id + + print "Passwurd. Attempting to store client credentials in script parameters" + try: + custScriptService = CdiUtil.bean(CustomScriptService) + customScript = custScriptService.getScriptByDisplayName(customScript.getName()) + for conf in customScript.getConfigurationProperties(): + print conf.getValue1() + if (StringHelper.equalsIgnoreCase(conf.getValue1(), "AS_CLIENT_ID")): + conf.setValue2(client_id) + elif (StringHelper.equalsIgnoreCase(conf.getValue1(), "AS_CLIENT_SECRET")): + conf.setValue2(client_secret) + custScriptService.update(customScript) + + print "Passwurd. Stored client credentials in script parameters" + except: + print "Passwurd. Failed to store client credentials.", sys.exc_info()[1] + return None + + return {'client_id' : client_id, 'client_secret' : client_secret} + + diff --git a/oxAuth/Server/integrations/passwurd/README.md b/oxAuth/Server/integrations/passwurd/README.md new file mode 100644 index 00000000..78bff52f --- /dev/null +++ b/oxAuth/Server/integrations/passwurd/README.md @@ -0,0 +1,88 @@ +# Passwurd keystroke API Authentication + +## Overview + +This interception script allows administrators to deploy a keystroke authentication flow in Gluu Server. + +In short the flow works as follows: + +### Usecase - Password not yet enrolled: +- A form is shown where a username is prompted +- The server recognizes that the user has not yet enrolled his password and therefore presents the user with the 2FA authentication page. Depending on the available credentials and configuration, the user may choose to present a different alternative credential. +- Once the second factor is presented and validated successfully, the user's browser is redirected to the target application + + +### Usecase - After password enrollment: +- A form is shown where a username is prompted +- Incase, a user has configured / enrolled a password, his password page is presented and the keystrokes of the entered password are validated against the Keystroke API. +- Incase, the Keystroke API fails to recognize the user's keystrokes, a query is issued in the underlying database for credentials that potentially may be employed as a second factor +- A form is shown where the user must present a certain credential in order to gain access. Depending on the available credentials and configuration, the user may choose to present a different alternative credential +- Once the second factor is presented and validated successfully, the user's browser is redirected to the target application + + +### Additionally, there are some features worth noting: + +- Configurable [authentication mechanisms for second factor](#authentication-mechanisms-for-second-factor) + +## Flow setup + +### Requirements + +- Ensure you have a running instance of Gluu Server 4.4 +- While not a requisite, usage of [Gluu Casa](https://casa.gluu.org) is highly recommended as part of your 2FA solution. Among others this app helps users to enroll their authentication credentials which is a key aspect for 2FA authentication to take place. + +### Enable 2FA-related scripts + +1. Log in to oxTrust with admin credentials +2. Visit `Configuration` > `Person Authentication Scripts`, click on `fido2` and ensure the script is flagged as enabled +3. If you want to support [Super Gluu](supergluu.md) as second factor too, enable the `super_gluu` script. Support for biometric authentication is available as well, for this purpose follow [these instructions](https://www.gluu.org/docs/gluu-server/authn-guide/BioID/) + +### Add the Passwurd script + +1. Log in to oxTrust with admin credentials +2. Visit `Configuration` > `Person Authentication Scripts`. At the bottom click on `Add custom script configuration` and fill values as follows: + - For `name` use a meaningful identifier, like `passwurd` + - In the `script` field use the contents of this [file](https://github.com/GluuFederation/oxAuth/raw/version_4.4.0/Server/integrations/passwurd/PasswurdAuthentication.py) + - Tick the `enabled` checkbox + - For the rest of fields, you can accept the defaults +3. Click on `Add new property`. On the left type `snd_step_methods`, on the right use `fido2,super_gluu` or whatever suits your needs best. See [Authentication mechanisms for second factor](#authentication-mechanisms-for-second-factor) for more +4. Configure the following properties + +|Name|Description|Sample value| +|-|-|-| +|`snd_step_methods`|Optional. It contains a comma-separated list of identifiers of authentication methods that will be part of the second step of the flow. Note order is relevant: a method appearing first is preferred (prompted) over one appearing further in the list|otp, twilio_sms, super_gluu| +|`AS_ENDPOINT`|Jans server URL|`https://`| +|`PORTAL_JWKS`|JWKS used for signing SSA|`https:///jwks`| +|`PASSWURD_KEY_A_PASSWORD`|PASSWURD_KEY_A_PASSWORD|`zxcvb`| +|`PASSWURD_KEY_A_KEYSTORE`|PASSWURD_KEY_A_KEYSTORE|`/etc/certs/passwurdAKeystore.pcks12`| +|`AS_CLIENT_ID`|THis should be populated after client creation|`abcdefghij`| +|`AS_CLIENT_SECRET`|This should be populated after client creation in the initialization|`abcdefgh`| +|`AS_REDIRECT_URI`|Jans server's redirect URI|`https://sample-hello-gateway-15pnmz0m.uc.gateway.dev`| +|`AS_SSA`|SSA for client creation on Jans server. JWT signed using the JWKS URI|`eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImdsdXUtc2Nhbi1hcGkifQ.eyJpc3MiOiJodHRwczovL3BvcnRhbC5nbHV1Lm9yZyIsImlhdCI6IjE2NTUzMTkyNjgiLCJqdGkiOiJmN2I1OTkxYy00YzE4LTRjODEtYTY2NC1lNmY4NjcwZjVkNTEiLCJzb2Z0d2FyZV9pZCI6ImdsdXUtc2Nhbi1hcGkiLCJvcmdfaWQiOjEsInNvZnR3YXJlX3JvbGVzIjpbInBhc3N3dXJkIl0sImp3a3NfdXJpIjoiaHR0cHM6Ly9jbG91ZC1kZXYuZ2x1dS5jbG91ZC9wb3J0YWwvandrcyIsIm9yZ19zdGF0dXMiOiJhY3RpdmUiLCJhcHBsaWNhdGlvbl90eXBlIjoid2ViIiwiZ3JhbnRfdHlwZXMiOiJjbGllbnRfY3JlZGVudGlhbHMiLCJyZXNwb25zZV90eXBlcyI6InRva2VuIn0.CfFL4uI-K6jHkN7DB7YofjDgjH_9a5nWTVrC9eILBH72JTuLmbX_JfNYDXkTJlzCGdJGtRilCJuCPa1WCmTNSKu16d8UlpMoRfgFlND01pPOrFDtXitSktDUTMV7jNdhIt7lmRtMF0XjPFi13pf2ur1ZgDVodkokvmV4kebRfjv6RXQ3wCbP57L8eFL9C95WtGUJLefpi0i-88RFxv36XALMYhyq7OYLtCjv62Fh9j8jpcEmWCmQV8FKVNhvqrVyf3GGqoBCyRkQDJOGRCbL-5BAAylzlglvXkAZCM8lP5GovnmCPc_WQY2TK8AsWTMYIs_wWJJ9LAoXPk2CwtC6JKo9gxWsyDJCXnc4a_IkC_rOiWutVqQ_LmaAbjqHdL1KX6eVfmVDLXrIoS6ic4f3PbqlPPk7CIM2c9ydEV4lVi5rFGxlO_yBwS3ptJzMFW0i6rpxZMpHVe9I2F7leZqZhzf0D6ayLJBwpifQwgHps8CX5fFawWVESZgU2kgEq4MR_24ghqk24VC1scolWZdegYZClvZtFOkqcX9T_-9lpKswrGfr6lMEtzuNwfhteccZG6tihC6M-7fXnqMDjpA_ct43FjKFqV79OelLrEtjiZfx8-etfak7K2u-ebm8S_aO3g17dO2BUaQsulV_4uxeH1t3COGaJsyMKNagKkiJg6g`| +|`PASSWURD_API_URL`|This is used to query keystroke endpoint|`https://cloud-dev.gluu.cloud/scan`| + +5. If `super_gluu` was listed in the previous step, click on `Add new property`. On the left type `supergluu_app_id`, on the right use `https:///casa`. This is the URL (aka application ID) that Super Gluu enrollments are already (or will be) associated to. +6. Scroll down and click on the `Update` button at the bottom of the page + + +### Transfer script assets to your server + +Extract [this file](https://github.com/GluuFederation/oxAuth/raw/version_4.4.0/Server/integrations/passwurd/bundle.zip) to the root (ie. `/`) of your Gluu server. In a standard CE installation this means extraction should take place under `/opt/gluu-server`. + +The zip file contains UI pages (forms), associated javascript and CSS files, as well as miscellaneous python code required for the flow to run properly. When extracting use the `root` user. + + +## Authentication mechanisms for second factor + +In a 2fa authentication scenario you may want to offer a trusted/restricted set of authentication methods for use in the second step. A popular choice for this is FIDO. The passwordless flow offered by Gluu also supports [Super Gluu](supergluu.md) as well as Biometric authentication by [BioID](https://www.bioid.com/). + +Please note the `snd_step_methods` custom property of the passwurd interception script in oxTrust. It contains a comma-separated list of identifiers of authentication methods that will be part of the second step of the flow. Note order is relevant: a method appearing first is preferred (prompted) over one appearing further in the list. + +## Test + +Create one or more users for testing. These users should have already enrolled credentials belonging to one or more of the methods listed in `snd_step_methods` property of the script. For this purpose [Casa](https://casa.gluu.org) is a natural choice. + +In a testing RP (eg. web application) issue authentication requests such that the `acr_values` parameter is set to the name of the passwordless script. + +!!! Note + Users without credentials belonging to any of the methods in `snd_step_methods` won't get past the first step of the flow. diff --git a/oxAuth/Server/integrations/passwurd/bundle-bak.zip b/oxAuth/Server/integrations/passwurd/bundle-bak.zip new file mode 100644 index 0000000000000000000000000000000000000000..dfa343323f7f9a2cc98e2e4964b4231a80b9721c GIT binary patch literal 474337 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�`|JYyq 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

icf&zz1uvx7(Py^p9JSG zefsZw&Wi{><(&xrS|`0Xv-4&a$0-sxou~x~mf7rm?~i?7MxRh9P5#3gX*6xi$>+RC z;8Wg7uv&g;zgn*JTf~|A)_b4WFU!OyViyAR!XOt3g2H%p8NS8RM^| z0o742$K_4xj~W);_!^t^of;w+ojbhCM7xk22}@r)Z&|NKYOH#1+cBik;o!#g_X_NQ6Z{7|@!)KZgU8kWFNjs<;oupa{xs7xi z3-JAgg=v?~yWXd-I;83A9a1ma3+P4fv(RU+I^>gAW8JNu-=<;b!pz(Izy9*>`QaSv zJx^{Pn`Pj;A{9NKI8OVuC-y|9l5Y&4Ow2>b9<{O$0wdDb`lCtE`c=@P&pokaK;bG6 zuWQ%&zmGqRcqoaJka&oD+bcDOX>rfAqIXf{@`$o=Oi&nGV#vU$e*sVnHy@K89a9Y>FTLm40q4JLi=y+0@Q=9)LOHbQ4 zYT{)_teBOZs876(sU1@|79Wl-v+ZPV{5i<=w%pR`7VD`l-|8}xVgNJjG?9xjpcqn)H;szm{3lj-#D+`HqCc%57*i@`? zVU|jW(`Kg)l2D8w^ek5{bZDH4lqh0MFz_V7qQs5N%Xu`Fr!O9wHqJ6~0u=S}2~#Wp z5r`670$#GrGb#a#>Vsy%9bsGuUzM1FGATLNeo8_FrI4ROYFLptCCgV)NbTjkvm`z;K&*t?gC99%0tM!wySxjkj4&EErJ{+G6gk1MeiC? z$V4bq!a_~>2MHON%(xK^h{pm|gKZcI0takxlmW?*)hR3Bth^^hGAa6Nwxt8+Q}if~ zAge3xIYPe~>7B}RVAmcn?oOehf-xC<-t<0im}x*i3456+!$F|7))-K|d+MFoq#+q^ z?{ggvJGi7G09;nVPNo#%mS&{#2}qx@fymj6NRt+~FESr#yZbgE^4U&df2JyU+(a!n zfyQazj_bFaW2cli3KPu4av;bVWk!gA?mo-xL+nPw`p>zgUrS-YmAOTi2<<+UIaCbx zC7%#rU-^pNWDEq%JAyFxU>>L#7+<^5aq0roI%)bxuMx^^Xjw8c5Q(c!#t9sNmq@0Y zFeg|~iXV`bWER}-piH!EnmN#fCPdi+s?t?!Q!kNnRb6?`?U+US(&O8i6b_v+BNZ{y z>`JK$KaT+R2eR!b`KODeHhAi~XBxIgDuuT-p`!^Ca(Gd}+v?3^O6TQ}7T_+r@qs10Rhx`bD^$UwujW~|)&u?EI&PRBRidCW37VBi&VtZ}hZp*s{YozWe3M!XE!E!{`WD-^dts=-~6)|L?Mc<=@i5Z#_@H zA%pm4kNVa#{+l)I+db+qwLUz)f4H)GrhnVy`gVl>mfO!aeAbcle}8vL`1)T!_;0#? zoLql>`{91yf4JX0zWeL@`(H1x??*?$EJ}L#RL<%ra%;!wwan92K}09lJ+Xfuf1nbM z*KIk&1-`}eegO;o(kqbg41*b4+a(BdHDw~1&5e4gX1ytH`YkueKk;(Y|!|ub|cbC=R z9$HqkyjFt!jf%i7isAx2JvNON(DaD%?Z>9Vf$y}H_0w-`7kf}=JJv0otpBO(QAjD3 zq_5rT>UNzUw@0_D`}NfxzuK*u(+%Dfc~FH>%}K)iG8+0csV-8pa*&mpWUumYvy2*E z>s_Q$xUp1?_r?53udLu<;Rf9)F9VtIg>d^RNl6BU*l{Y0yt3`B`*jP|sw7Bajs#Lp zAo%A0XYXB_+&GeL-M^By$4pqpGb7JT2D585D2>{z2K$UkU`fTZK~k4RRd@5(U;maz z;#DY0?54WU+4q=4APY!jM#jU#AIq1|-RJ-O?(HSkyhNuEiY|J_F7ZbYAYHVZ(0@AL z8!q;P%T1uG=GQlmr;D>M?sTyW`4*7d*95UbB3I=w>g9Dg+(RQ*<*>Ty%a^}j(tuZ~ zG6!1e;UA{~C*CwfQs?#_@#1AiJH%Mj$Xz04BhRPj@d5qqWlaw+JMw#!QakgDI-H+9 z9A5lvKUpWv4qx+M+=e+Tdng`%27PE8gjNM3muAOVu0JTs!rD}4*(Q(mNWJ6j^RbSn zEJ1yA=i>yL4`0|gXVptw$|imN=J4(6=KovtT@s_{S>I^_eYn4*PoCuhB)+<29P6qK zsZ72eCJxRn3y)&l*fQP`jGcHWytj)sfuyn?s$08homyz4xPG`vCQL{Gu3kz(K?Y=a zFY&x##;V?Y*0wbBK01fwdsLrNCB0`#odG0)z!2;~oq zRIM&3lsIz{msOCd!NbWsOuQjUW!{NgRZYu^FDnH>!RWlra+HUK$al`i62W=)E}}#p zl^O6?+9Pw8t@us2C4Zw1+F4bHhn~{I_ydo^l!ER5vLSiOpAv@-q_)kZxD`&?KrUl?&jkUc6RW+XP!N3<4zC zePdB{BoT!rBF`QaYot-o!fBCe=34f}SA_jaPr z8|8wtypv67WvJK{9*f72@x9x1@r9Q+ zHr=efwr55zP?y_mcUyo9|8VlSZ+grAlVaKSEQhYJ3wZ~foISnCJh`1Mi%Io>#;13` zd@xhVEQk|avEyk;*@{x%vVtxETk2LNikx(r`m_ynF9&n-#9fKK1lAQH$cv61LH#MY zm3>vkrZ*~Y9t8(c=7ZLmtps!(BIEYUeUj+UyIHvofX~F{ms#o~lz3a{0B7YwPb3*s zU#-;ty_52P36pT0?Fs|~D72}E+svw}*-6WYqe;}-811wKg=I(VHGy?hi=lcJDiS~$ zyhTqJ!vQnIQEZ!z+Dwk%IdqPW9tDitDu3BD>?SM>Ss!p;DWREXSK%+Dg_583+x@77 za_c0l4Ymo-A8txBQJkdsOHi>h0er|{*4U-Vsit0FPpp@&K~F%T?J{{AfS)GMLau|? zVTa7s$s=lTSRer zKKMwuDLoXYDgQhx!@yxtx~7LCH*t>EiyE=$MLYmxUWkLlJAyo> zS;h41=E`In4ig{60p8w(eUb)&zUt&F8A>CR4^7Fyun{{nQ{& zB#1N>Wq5L4YgCVOmg zeL)B4hA&e?p3eGmN9nPu)EkxdUG)4VeCI3Hcs+IIb)ehF5BHxhxjFJr2i{!Nn$x0z zUZ~X$RM-=6joWvFNF?zV66o0DE}$99*yW_G%@h#8Od5@aMOiLIi%1?k*A zP%qkyGBQkGHjIG;f-Rby%Y8FY>mf$+Z5GSoP-Gqkk8Ud znl)K!fLuMvr_cjj&a z9xL7+p*GY*V~u{sEL9#z!;Fv?K}7Z=`Uo>4vlz5(6bd87>IR$2Q$*|1DdX)|%3jN)!;T zqDyJ(f&I8bMfVINUC8-c9~P!LV35{kCkrwqZHCiSePH@mrrsu505k7wM)4mTiImJ1 zq^$f-1zgAtqj)qfiX6mR4n@Z%kOEZK82bsFQ(Ydjx?eC(>r?#+degffXKC#4%|u1P zW2b)}cke#!_AjbD*dwo7&YwcnPExG&b0?Nibyu-SIJd-;z4gz=b|5c2VV)Q+m?wPi ze15reYPG4f4H+BHNQTgJ=1C0!6#Jugaoi=v56pYEDlFh16e;4aHQiq zqn;*2S(4h<<}r0oLC}%BLo5Aqx=h`mdRl_;=1^ekJn>_#V3CH)a)_iTT)XBY^_S*7 zsSe97cB?N?;;Q)3%s5Vaavqi&$h5EZli1X$SH(TdzJ$0<|3BHkfj^ltKr#>ikNjm{ zcx>|@_?<-Ar~98jzx(jZ*lT^hq}&$3AQy5f$->#>C{1V5)0qsl2Zcg%rpi(i96D1B zms&3r3D4SSk`e>qTQ!APDVV{@A%*UC_A@b+v^_K?xR+x=w7@-8l#-TJ_tUOd@VrnK zN1-4kqER*))U#vA7ljNaWr~)W8HJ^>2}<6kWb!5}841?W%^bIRN<18yqImyFWQ6ICp1iQ3#O+QiT#3%jGIHTXWV z5Aqd7^~fD34GHvaLdZ`-Z+rBk3eTUF;S5t%5^HW2S(v($8gETyMB?q_C5WqL%%B;w z3mcLC5E8{EkfE_GKE345-fJo<|2#{^j{xm!+TyyTO42-YiD+}PBGt1`f@bqpC7ve% z&22|z_}J34-63~!r%XJJMV{($PZvOy3nlO(Ff6hA>He3GmsGU%t-P_G;t59~nGhle zOGqNQ-&|m^wcJY%iob6XaHy_GmW~ZM+J!1PKn5QEcDrfqfPrz4*S^F4yD? z7T4%R)-S{{;RoVE=#>8Eyq#hY!{b_>(M;;{STjMoNuyrV&W1#r1Hq}}F}Fd0v*9lbn~Y;5D}? zaT8%!6)8^;T?6tTN;2ktRla9X>4lnGF^;ZdrS2CHAl_vZ?@h$f|0co;AxO3(qAg zxrud5FbulN(PL(vv>jYgi!2xH*?{D#X71&UGW-g3=_(S;t_RTx4^%Szkb~R z-4@#meBHyt0bd7h!ci0UOGOK($P|TknM|fc9<~aelQ${W352;HlKj)2aMNdn-iW}i zAMa1DU^lQ{(+vi{!SKfb7Fvr{n^f+!=FAnLSrh@ZX2mVjV^aEu4IE2Y!l|lYPP_&k z^lXgUIUxqWcJi=6d?qF|m#u%7W@Um*;(SkCk_=KVH2L6U~}4)ZBXWW-TH}+mMD12ux07)OuWx zp&c~&CpVwlL6z6x;p?%V?5>Ha4>6ZHKlSPS-1B=3XZQH>qdl7!19eyD$#+j*Yry$M zS?fMy{<)`mpS11o{3_G=w@+Vrj`OSf2K?Lm7y7Vlo}5tdB+sa{nUq&&dgeLlb=Iaz zGT5Hh2yT#GP=hPKE)!H`qqY{Af=9G2VY-ojMVJJ=PO1oRLDk;1*4Q7tl$}l0P|VyS z-{-zwzZr#~l}T?FZmDkiXy33_Oo>X=lyi|J zqi!Wf5e0~pTK1!Yf%rxJ0UZI`@WkZ63uKMMO)qnl&6|8vw+E;`D%j}Y-*fm>9D=Hy zULOmHx=k=k;t;Z(YFae0$guPqs6JLdARvAbfkoa1RPL;t=ZQ} zR_H9V&H5{=bIT~Zf13%z6W1j!U6drhC@(Yiky7zaxdbv&2lEasaAtlTXYAAGcf0?( zS|qo=75Ea*SW|qTrRJ4K{H+U3(Xvctv(2hREB}|&K5vK)m-yVA(ksiB<9P{yDIA0( zPz1>IwNyn+b6>Ga27mSY*v`E)JOn9u9Y^c zkZ;lr2xYdxt_}j@8LW9_)dvO zgj}i8J7o|YkK+HmrA`%w(3c;zi%*Y)74{t(+iuu9n{WQj9*%G8g7}ThL;tak z_4xC6e#lA4n>7DOep^T7>k+5 zRJX7TYe=6RSTym_$|IwlOhs-O`4=2>9fVb}j7b%d$ zPpY&B*w^vsqO~C1vPa+qG!$uL90jw^!DmU4v&VsQ#f*o5wk=gL;y5PCfZq)8fsPhL z@`N))Si3qnpiW)K(UXE~!aFO-W*_aa_eF<21GKgKHa!X75y)u@+=*p82(CCe9t2Rk z%FFYff#QI;lM~U8+*pzC2Qu6u^CRQaUEHC#QS3b65)8*997y)Wu)09{-UcrLrnCp; z0ni{+3R=>HRP{^?vE{pD{_x8KzUB*cOvbKM1ExJ5sy)MFIsx$rO`r)SlB!NBKB71d zUOw1#a`_<^5B2z)Yb>JZz~|MrqvA-A2ZAmts#C(VR~<3=L)y*mRtln?sUl)6V))EL z1fV}H@~o@Ooai4ZK8Oa7%bS48tZ+>6f&!y+8(If75DNt6hqeWp-f&8`jyFZY5+v*i z*LVL4XA-CH+|36^j{+WH@R;PJ95>M2dyoh&-KO*daSuGP{?_fnq?mHHy_7h$IG-j3 zmdX8y(jO<$7?tK%8~tj#ga@;Uxwrcnr{wo`WVuO?pghpmaTSMr#S2`;QeVM*sI+E# ziTR*(NBo!EXrA38Z_^hXu4t+V#(;$@#Lv)pvn#2Zi{co?(7cB-Dq)ZHch~P(?tze7 zQz7R`h7SpSP1!W~qeQ<+cki1fb%7&hCz3nyXxja%*ucdWE(9Y65niUUQR#X{Y0(wM z9vsA`Q>9R2EG~*mDn2EwnJo(&z67ODJ6QwE#tn9Kbh&Nq&;y!tyw~c&$J*ZyuBBXv z7)BANFEE5wZL!YXnfpS8o2T?;J@jQ=S&JD5iR$^`PSL6n4j*lOA`_;nVwFk;)TlK# zXwA`4Gize*%s~upQ8a*K^dPz8G4qzMjm_A{rg?Y4Hq;0;$${^&5fKb)E(|^wB7JRr z78Qn-14uB(MRBoVtwl)t7ax9kO!%ZUi+MJ+5T6x?-*R<4 zueKPiZ)kVy>s52n1G;{3+fX0gzWaP(=1kd@UJ|I;gX12%npqon4(9cLK|u&Rx{b47 zno|L2x(FmciHy=qhRC;!a0w2I=Y_%rN$1`sloH*j<6!x@zwu%!Vep!g%icAy>sTIMr_~n<2((Cij?k*vqy>0F?j)9h% zwf2NJ)5$T`f1`f{xbiR2%fazeUHpGN>|T_(D*_~{z?HzA81vv0lA}qFMVF7c*O(U_ zoUj&PJ}sp#w8m)>=3@eO8V;z3Lq+BvZCXuA+n=msab?=dvK20x*virhxR$Dl0&kM- z;SGj3i1Ub5C&K-uFfoZIXR+323F5f~|4Z8aOqiO|%ypJ_S!&rz--AgL?Z0Xxh}b@i z>)I$;s}KAAyI-H)J&lhq5S>{Gy%dx3ybbBvNt~1vzO>gTd($VQf6@;R-w*xlfijr^ zx3yPdK-oV#{pon3|9^6ylN+eq=19K|S03+y z$I8IBfnfDZt2kD~v#pdbYg58x_8(?txJ|NXb%qSXLDu%BD9ZypoF6o*jRvZdzO3u= zszl~|Hf_((AZviu1Yv8ZjrimjCPU9aQ7hbYJ-FkVE-Bp`pt6cmA)It~9q<({e>Uv# zC-DJ+WjSHEKIO1J9ocXg+Lg)r4m#2~=6l~nv>__Bripl9Hl-T)bCAYRq5?*xLlwZZ zR8{GwQ$pC15ubxp6>Lm~V6mx}%$#J%Fd-SUur$yJ8n`03ke%*>7m}VkIew1iPCUC|#3(r@%JKuV_ffVP^%qFbs5E%5&_k&Bo4ND0GqnPgxz zkN-75Wmw;e#LTj5Nt2F5rKedsG#Ctq&;egD@C>tU52sHg5I6TQsn2u654=#sv7a^i zSqc_r83r?^%}g~?yCz^0zwg-Z3lIgrMFl3$KoM?>p+Co<=P>Qd&W&g>_VoAQ&qT7Y ztl$i+0y*xMI`4Ss<}-3p_3d0F!icciz3KiL>!K+?bkUSRf&aXYf=#SvfD=4Qu@5|LQ zet!7d=NG}dBB(IQ*3qmBT>00{ixuN>VP4#X`85k>^*LTxFkT;OEu?j6-hAH%h|-O9 zg8pwqD~=z;t=wM~_67>rm_5AQP9i{V^=oALAYEP~%h!)@(ZNw~3eqHLx)EnhS9|Y~N zHqAQdnar1tBqfqd7LU+`LG9DCi4?rUMa$6Nrv9JHv}c^`WxqHnf3be>=t#>DWiMU@ zpgaqnEFuGHWr`nLK$2O0M$cxM&)3J(|NGnhrxy%~Ceq#bLqYEQp99)dI~8ZJP2>}k z2aeI9@F7 z#mYyf`?dC4bmUR)-w!=R3Y$3TkT#~XHo=8(J7iVz9|a~Njv!!?_=?0fg3qRw0;`0o zzgLyQ0dq>B1e2sfCYK56-+bHDAVf1n{51Lfm*NH_V3K(*cd7QcXDm(#pN~}hH;0_C zH7RU;Oa(yw3Owhc>Rh%7ix42Goc1P?2;ZeSn^kt_mX>IOaNnk#631-17G*UFek0U) zWrHwDLN(*fl(bJySv8|viJJf=QopBpAl?i}>zAn9+C1sT(Vp0DY!ge`tj)Up(+k$G zorl>dwU&WQ zg(j$}Y@wo`yYcLFKd<^<)lW{iM}$2S;^FMF{jc@vL%+I`Fn+)gom7~bx4so;KEPbA zJJYoyKME>ziSdn|_&lnhioW#EM}2Pp-@pgdsG^nuCz2S>`+~`c>&bpl^|lXG#^s9s zejBUbA+r@0Kr?OM(+EL*crjkRtG7QtK79J{@cE*@=cRgo%S{0y;mCA&7A1Z>NUqCo zZa_(03r*&>(kWi~=9O?}jQLu;vQ2k#NJI}a=Rf+F<6KvjqQ^PGCcYL2fB5aw$CXwG zMxf)wEKVhlU?EomlMFl%xf$#Ade?GnrRU!K~=#|EFi>4IQ(V3EAN{%uLw>TMFiIr z=#d29M7gH%BcXhtNZDT76g`-tf-9=?XgA`1@NZyAL=`S1FqEreg*Bk*B=Qrm2JMP|cdMNI{vR$QLF6{xe`y_U&1~>!4jU zI|A|HZ@b6)55GK&_xl$nOsevCvAS2HgjX~m(<{2#MV`v+D$1g*JIlgdFrU0)xiXb^ zq#Q*h9(no^!%M#p0vp?&wD*)|)n8kLVz#-|dJ>vFdG}!|jpq&Z1`yglGYHwXEJ;IPEavs})0VE&d+n}QI0`1mg>$h^ZY_}X?Cb;iz&^nHUz)Oc7+m?kx6PRh z0Jrj<)5=Zo#4mJ(cHlK2;U+`3p^V`?$s_8NSxez4*C+pD&>#N)c=%{M{4x3;6tEGp zM{zhq^}CX;-u&SHj6 zCu(cY_CQ-`wqB=@wxzjok;(F#fVO9wQ%b-a3WkX!_!ZlEGlHu5UqNPtq@z`Zu z4lp&kI&w}X?6rj=)2LOI@JrOPBYruFKX@^{Kaoec5KyNLoxYs#!RhAS|9AwixRv!V zOXuS8m%cNbfbTq)d{jd%mIJffdCRhPnkQmjF$dK zjIJ;OWMv`xo6cVbKgEyrE764L7hm4%o%Muz4W}xen7kkT*++l&Ev`qLG{qHm6zW>V z>w<%FBKR$;^=-z~o#bpFB+d?tWzHtGa)S7?kN)g(*hpZ~)BV|#yp3_+AA9IeKOP^1 z6hj`r|8aQy(I33O`7e4O_2abz`ftuA$fr*q|N3-oo}}<_2=d!vh6GpHK7+r?pz1EK z#KaXw(B0RhlsOHwZ3p|Lg2ndY<>eNKLs!%gU@K@RWWs2&A#uSk=8e?(k?kp%QhN2) zT7&D<%C0SQ`{2Li#sSV8yE87>?75D(BC)TC@2QAk5j;haP_wauvz~DQlRYo*y9!Wy zi^Y_*w{&a0FfdlZ6LoZFT)#AkPJ6uC-Q5>@m{P8H ziy|oaPjzdvKmFcSOab=Le)Bf?%`+K00D`r=1;(Mr2)AZ|gjc2S^TVeP_xo}G?!$jw zHEfD|!Ng!ZK@D_5lZ?mY4eXW&xDC{mrzgB|H%Ght70#O6ez3V{|4-|=wiV<`B=q>H zj&4sWcx-}p3(`G?aVtA+ph zas)gEaCNEHXWgTIW~USgawu2HQj{xjaL1q@6TIyKxApgSuXTsY3PhirS=K-Nrqf=$6gWow z-ES;@^<@cW0in-YQ!zJ9Bsn`vlJTTt&YPEGTGwk9<=-DJ)$A$(T4t3QiA1Nih2R42 z5K(17RS%{;E1k3Ezj#tL-&VmDn*S`o6(X0-G1YHU=xfPoVSR>a>Q#oNdp?gk}LGIhmm~R-#@)5qBYHvW8=_A$xJGmCL;j_VpYm!R%PH3 z(Hc-2h36^MnpFzZ;z?FfC0w=Cl=~{4kFtDaF^LK5u$Y#4T}*0>3C}3*gb6$qVQX<8 z$ZwvjxV#gIRST;|*~0$T)fEBy!V^a+~-ZwSSykvCCk zwy!PNZ2{VHlq)%QxgtN3U0JFBLx1xhs%T1NgyvhOJnXM&{=tJ8y;I0}38o-30U%2$ zXJ7N3a_07hkgrMY0=3g6O#v48riFb_CNaLHbFKqrz$mXkX->*lw(%7QP6CyX+7C-+ zmH4Y)nE0bAkrcDSvIx1XWC2DFHH3TgQBHhC;4>7`~t{P=PTM8`2q%;Ab zHa{@aV7KzOehE^%C<=i9j6;9(@46gx1+Dfn%UL{YIFH3`99RKA-(O}%wd%nelDXvT z+>OpTHNl*PwS@3?7OVh_gB(9p2w+=b8=E)@f-n;;$k{%?f|!!C7cENhIOMbgpcl~1 zndIU3@Y`{l!SATsXmwNSM^Y4$gtqk?q0j0rXrM$Q2dYO`N+?6wKJ&{$p8$K(ZPNNj ze=@(%s9Wkbqd(cGM;rXn6h)A`T&1ak$NzaGak426af*Z#?>qC+|(&%RAKX z2-*uL-Z>k1Ajk=|7G@jn5|It}PT04+|1x=lCjY;=e=YVEJ z;?MU_pU0o>cmMU*`%iCQI9T)IZk?R!omeBgqhj6T&KxKwJ{?l@m)of1RLWLwBjf?} zdvdiyiG>YZ>@$eY#iYTqo}1gWo{Jk$=W=i;|45inEyRRLkWj$4ZWIezCnDR?^X8)kwF zoxfeGP4^DxARb?*JD3W~DSaKgzdIYuaFmNds=yB?r^t*)FyGh}&VK8t3~TKfAznvh z#OdPGks6mOnsa&ua2T$Src)xM;GuX3&M!V4J%{}CQ;(=WzW8uZrW2~$pK|5kv=D#F zwg0?UPq_0$cv~}g$LD-{pUF>q1PA8Z$4$B%vO!Rs%i;Awzc%JCW=+T5{QPha9^{fO ztAlR{`igZ<-dr(>!hMgM46Z1+mjKq4DDF*6`Egcyp}pD;P*tpv*NFZFH7ZbKK8H9> zd9SlFZ-K3}glisI6>P_w_dwvVhGsE@Rsf8iL@Y9Xj#x6bCZ5)h)9Qgtx3lQ-Ai_%0 z+S@^EZ|7S3aQITP(vSFtw5Lkpf(jSoRvUPf{zVa(*=0|F<}Zmw52rWTpD<7Y>J{23 z?=qnqj%_mq)!gJhf;kEeN+3}C9g*=pTSIXom>ue3CI9!zHwj z;5F}2sXwg(dfH}S)R9y<@JsByB4n7I4;4ooLFkK-wL%1_eU2 zSwyAP4YxmeP)q?i8x{IJ!EQCpyDV2Iv$TmRxYrIXkl?`Xt0y3*7h_Bm#ZiR?#2_NB zq>^t$$7;`(mFY`AH%JVz3P`$@Yk3yuV-WFa5skX-Z7>hxcC4ag0^+zWQ#TW&))jO? z^=R~6eey;3{(k@PKmMnw2`9Tmi#=(7@XF2^X|U1~b|S*g9(gR;xEt3~$CXgA6HESO zQ4kAh!X@7aWzvF3&!Egz^~NO74<731at9B2&X0<%u_N22gHnd7mYBpvV0l2DrogL8 zQJd~ht|07iliJwtfg!L3Es6umhH>hXA`M>rS_=m!{z?^eL~R_SFe$!@L%8$bt0s#K z$2dC#-qz{tB=0~>7jgr9y}bM75q^{Z_3-(HUEKW}*z6JG8=pf}M^%W2ZlS{s1+UK| zsXh}C40sFDkln$AS}H#XX?stQDI+E2voqZlcpkB)MWDlcS=XV z(;i{+p)61&GO}mT+cO40o%bByIa$Cn6^ox`h@@M;jye_&X1gSjCo{^pj_%732(0K^ zrg{}>vp16nMp^%;FQ4LZ^oLiiPrwt_SL&m|J8ZYC=MIbb*hM)#P1>v+6f+O^( zEs|$g`=M=`9$~K+-3i7EswvQ|VcvsI9TpSU8_{A9V(O@al1P?5-S(?P`QqVr|CWbC zpuA!QQ1G?H_xSGl;l&i# z338ieW%=CVQ!vd*jCA_PwMO>Zai$%e>8d@CG=~RbsfvUma~ii~`@o=4Dm{%;n1Dyy z1V3_6ve+F!N8##1ogwveQmEF1L#ccoZqw-F~EO1g5#(IN!+2#a6D`%ak{P63(>Wa?JS`Kh#DXEV}q}pv*gCsYF=lQ&0~FaV5~j za*Suu?+TE|%vODsh?<;Ss}!dbqjA;%AReCsJ_TwGQdRS+muGbZoXKz~EY4zMtsX zeSCXG?#Rz|xLXb$JMQyT2%r8zjkrm<1hB( z>6M|yG&2PXc50|CD%Oz;2?)!~H70Fr)4>nAsg1&Wb&30`Og>EQ8tcn*2c6}=*z2ma zz^{7JGQRgyRed$P@$Oeli?HNN7g)Nleh{Q$nW+^!bhWHqKV0ipAED8QNIXX4wSHAE zjoz;~3zPTj=p8#=>)7%5ll@mNd~*M{nFsfZN4mGxw0pN zV)B?Ce3Cl+bdoA8FC8{I*%8oWv2WN)QIUrX6D7uSA~La-<{%Md0@FqCej?+QLoRCa zTEPg?c&j+!J^Ky2`#wY~=_(S5&!W)((_~lXrud$~w?1 zaeB~pIu+45Q#nN(W6=w*0E8A#_C}IE=;+IL)sK%0^Xd6=P1d(>{~c@cJyo4NoO!Vz z9>?~L#TDBhTxz=Vb(B)i)w~K*x_5-#@mJUI70H0=Hk%HuBySY4Aj%~_ z0`$Ri+aR(&q9(m^=SQX8YDQWA+w>&I%d+_FI| zb<3b;l6RN-aSwgg{LGd`@K1FkFi7&GqFEJQissZ~OnyER`>8?``tB}XmDnc~YRAs# zw}AspzO3OvW;C`(7MHsB{U z^kZH28=6T#2)O0)eWk@(Nb%)1{_hbPv-UL6slYHNRtxZ4GRR1I1QDoOb2?Pf1+Yvy zlfr!S7Cr~-P7D?K_KPD~tiwrNiwZP8k87T99?rKArwx9w2R}D6ARy7)+5C;NmoRpN zZh}XBR`s&_Ckge3zwI9OFRh*QZy6(1*j8vjF=up9eYDtI-2sMfBjN7m&n&z}c^tj` z0~Y7xM0lI*U1ATug_3y@uV4{-obojNosq{%71I-SB3bELp`$ZhquL;zq+~Ef(oG1; z8I%|ktKZbyBK!rdP1Gc3|%5DtH=J|yabn>d54E2D$Sqc)3S?Z5n|n9vV@`}O0e&zBiq zr9PY)(bwnZAD-V|ek=+uwY`?7R_d+~1}~27aJT}?UOfM1HBt7N*Rl~W11J(XpB%2g zo_(u}+oDwmYgnpwNljZA1~4hty}u{jpD2uT{rYVGj;ksaw&QlK^6ha*-o3y7<%(OQ z{El1WBvR!d{P~(U<6dqI<<3qAb9ml33-mJoo4pzDh49S$yuN}Nf4ab=UubxKYB+rN ziM>pw6$8pASb?l|~;s>ii|# zeEp+?$Ztsi(2pnOU!0Eix&Y6E+dnH_DrD6+Fyg~kr=oIM3Z!y1USFb+Z~lQT$VLTL z%3y;J0gvYcGBMnTs_}h}fIW1b;%ubrY8?-&Re{%AR%4nxI)UO=9T$}~EnGkhM*%S$ z1jOJ1LKo3O;%*_;Z(M>*HL50G-`I}B^5Oa8yWNXrSR7m|aGDsQ5K&b1QK!@ACH!Hr z_y_y^dkLTarrs0rMNZc(%x|#QUTKT+5vJ*Rtw@;99Wk$ZC7#D+<#W5xF5!N;re407 zd#0H0_xGP(PQmcK18e`?CR>}nZIJ)yh#H(27Ap;^%x?6vf2y8Rf$R*3KX4a1U52M z^kgilK$?n3aQKJ`j@BsG1QpzhVCWo$q>>o&0E~jMgI8O;DFMpL{g}lVLw1ai%n(=d z3_Mg)cj%a+GK)qYY&Ombuh^CcLA*zi#N5SnZ5muR($$6PK|WFkqT0%X2)Rd)Tih!W zC&esjwPWGwKk4D(sEA&wmp%ecBp4J`$JB#f^sI?V2jF(BqvsST(LfzIdJm30{6)Y_ zURE9TE*#`UNpeZhg=);;T?h>qJ?H>*I&e^@4bcr(9rq4IRVztE(<*hF^QaDVmYt*2 zc>nPB-R}N{g_!60|9Ei$uET+(zT(DSu#EL)O)mB2$6Tu$nqx0!M(~#NuY9rAJsWz( zH=BaQ)JuKwaXuJ=gYIVY(ym(S<)<&kX z$+g~;3IXJD?UJUSU3DWj9On=NTAZI6_t+@5Y7hSo-x!;3VN71NL@+Nt@iT34StJJg_Aa3-NO%bk6RU}mp7)Wh}=IgWt2F6eahGr z9jQW|(^vFg$Tfd|z8{A2*S9~vw9B*`hX!C@nGw6OpaSHLSWrzg>uqS7AG7-h>{Kwa zT1p!+XRG}BEJvrDTU!09iK+TfVr?Gh0Ww^A9`FZ>wNg+B{8Z$u6I$YCDMSX%?r!o< zAg!!-T{bz>LsI)Dc_6&V3yENoHFDF4-TYt-KHBtr#!m7fIvECgT{)l9VGuga#+^xr zE*1{5VVE9Krw)3%oE+^i+++j~6ivg0Kgk^pe1O&=^=5FW6u>FnMFW9&c1uEKa}{vn zQ6wa@(|nUCijW_RcjHE=Z*~U~51E&Mh3LYj6~{Gc6d8e|U>Tc06}9M=NTA(|lDq~L z2PT>|RJH)xv1o-%UcG^YA|aK;l{`{3o=a6hEVw*R0cI8XZmrP>Mt(ax;3n&S__2@( zyo(EcVvkTNP!LcCwN_{`KM0uOHuicnKZciC%;XXtP(DUd2z5drNCXSzJ_1W%dA| z7U{l-@EKVa-=OMrK`T#8?~HeORQ;`IoPP5oAe!&mGym#s5j1#rjxBgGRa=F$JV6hX zRa;+4i+Th}b`?-)Dp}pcYbd7{R16z276n%0ZXC@gYRIpj9-ele-u()L)JxMP{(HRD zO~Cn?ST0(0l1i3>ApXPgBYG-w~}M8 z%Aax7N+4ue)e;?hGv)jnNx%7ycvoms??3;uXnLQSx^Z=81P6__cK`+Z{Am z`q`i(U1pg48pn0@+pkxH&~yIxY#Bv)>47%h*xjEG1syJtSV-gR9>6x;DB`>ud%#$( za0RcU%%M1siWu*Yp=;dY$ib>?@wE8i8sk0*h9g51Wr5cLxu7fQs^w3OLqETJ`)~!$ z-TvjM+si5OXGEl4n;wp8vbh04n>DD4{Q!)pD*Y}wIX}=!X4{|CgXQE@S_h>i9vXLc z^Wk@^0JI~Q&~W(1e=KIPlgB!H?B@ho(XuZT$5pB7DJ1cxfn`xK%+Al3iJ}dsjX%zS zzkdAmVx5NFzelwzvNR&q(m~#Hn?Sa_K9&__2BAqblmafY0jPZej8HS_4`9;VB&;lTZ$cz*hUv>VK$5GBu?wGZ51PaN$>UR+Ez@E(Z^M2ppHm4oj zgdn(8_4~U2mqtO^v)6Vp_J#ujGsQ%iD|$2?Jz}w+rD_}!HP9eTeQBx}YG2BDq_aeK zvyrzG+_Sh8=Rrm@U(Nu$WGqxAD0oM5@Q9CKr`B>a{7!A?T^N0?@hLU2?KYW95)b9V zUWO~;vPS+XPHRUpW`jDQsA{BadUlKNoiSiDcYhgVofT83s#;Ad&SCm;q*APMbmy&Y zkq+w))GeN5`j{3fBA2JGbMvM1BI-ALCI=2{UT^UV;fbDSr&T;r*2jZh9|;c&KaSXL ze&(?1!}-F*Xwr&!6@YBzu5o@SS>Ac+$D&Qrf*?J8uhb7*eS!`L+ zkXJ-Z(xj{NI-oG+{+2-uhyva;@>LA($G2wXzO(@j72C|>dLr9MnaZP69qi->q8Jfb ztleCy`Dx8=_RQx|#9*zzq1Y)t1lMK$ljdhVwJ86I`5FJn{A_=8e&&B{e$KDW&vKZa z#is$BVq#_c?$h%R&CaJ+XXGmrGM`S!e4LOrAKzc{*G$cMSs(^{9KpC=bpyS^U{f>T zhO581$@!I^*lm2%mlL2zGFp31uc0&uj84~FP1mW2RPki`=HImInni1_M5Pm#^{7>+l5rs2_fL`1I?icONc`NEMHyE6B&*Lpo14(uM;X zuG?<|$xt}QBUQLnhK=w@RE!S|7x1%cuCS7vTTzuWw3qua8n$QU;&ZD&G-49#+$=;z zk~43HQ4%g&hO%tjxrb{|xCSD@h_PQd&zDXCl~leK#sCY!Fi` z50mH@=m{s_C!Q}bs(ZtTN=<#V(R-Ius1-D2bui>;Q3}dLp(w7~c5bDz|2uE{NPII< zt!oe`5S{Bl6478TP$A)ebgkd!82;C!hRPo^m!3dK)1vnZl@T>uRq9t!5JisGI14f} zi6UxKoR&s>LczZq{k&OR;tLv-tlI`J3gJ3TKS)%@UiuUIKqAxbJ8Yatb(f7-8jJXg zXOY5F-cmXfk7@$k7I9YXxa4f;HZ7F-G@&Ib3pk4k7ztqabkFgmg#R{sW~1y;5>#_>M3VIRn?m~Eb+uqXWdP>Kl%-dFB*+zF|B{t4U1&kqWZBqj$OY~Y9xT9VUZ zvZ7X&P@1xCRd#zDskNj^^c6jx8KQ9pPJqT?v%swGCG=HhGFG*}BIfO&GB0#wa56Z2 z9rU`rKSfzsGKixVq%9PCHpt^Kmy$vyLVw7>-Mrwqofz*Y`ktTC= zzUHX1(pg%H#0ZC&YgO_^<>qZ|QyV$89ym#X)n60qRV2!s08S?jGU!bRBG+9%IQ?Uf z4W6JdYxos|pte*5UL>H3BT4731mGJfP^<>2Ed)SKWY{v*e5XcEd1Dgq?>t`sMUlgUNaWWs(XWG^<~h`WVKa|^(ergWDj zO2tWR7q;n0BT^mt)Y-p-it*WafZAQy4me?fn;Fu3PN=RvK%U_u2il|w zYVXg|IDm4(Dw$jw#gTb}3@scjHfC|T%dNd010pp&ycPqBGL$l6(1W7W9n8uF?{A|{U4_}5w4>ht1cAN#Ulg6z>@NEg+Ka(hkKlVmt zwH0Brr=l)e=I(=E}&K`q|@eqVmxV^{nb$p0e4*JCL661P2o~9stdip%xzk7S> z+st!kkUpys;yAZ_s370&4yDA4Q?iyM$7GwxUshQNyf~aDKngip_&>it2KUdz3xD~s zAT%A3lJ($9Sd%$9_C6U;>!RpGPg3Le+5n#(U-*wbjw+a;FgX$~a)pq^jMTHuC`8wf zV2HoP+!#c!idhid6&kc@MrS#51Jv}eTa3aiQ6a3n*>!{_l83I)d=&#^&s9Kq{oURy z)Qvn?=cE&HB5J5 zP2o8S>yIyD{wKJoXV6wl+7;(RnOnDJm5_k3G3$^5fK&42fEB%%@@9s*$WEnA*U`0< zlh>O#&C=YPDx08xqluxp3%5;`i|37|w&bcPE-}}O>ZO#~j~^*21ofCUdb-IIEOZ2- zTxdcvEl6JB4l{*~sX~e*q!7(Up$So&um_XPYc?WPYM?q&*%sT(?`@x4h{G<2id)!D zBwEC%(jSQgm)5SF7=<3=Y^ppHgEZwih=DmO!y4Rj*4R^oLgAZ9cAN^2Q#}McJFp}X zVgP25XF#Zz0pp67XHYB&k7EIeRPh>RW$u?;5NYOL7`NG$ZJ_^b&~VVGMqNtXBn zeQpc5zqx%!xHQ35687TbX{Hn5H%RgVv0bwne)Ei`N^(@%O{YANeV0`Axrmh!#Vnt( zCY)eo4%O5zZfTvh)ma`Z2(Cdl0rGd(_+P%f*HRg#SMT%>JY1cw-wWxyzyIaZp~NQ6DH%j zqUeqEA>5g{{EoKtI$-9RU|-(h$OB4(t1Dw_6|c<_pn{>Cc`ECls0EEFwU~Ix%1EI5 z1ZeiU!^|wD*3=X;b6l8aqu*+Crcnwu~m{?M7m{jV4;1kLZOOj>ERH8A>r;#7# zri2NbK$Ko(Q`xWUZ%gASzj3HD5}q_nq5T$EslyWKV~I%I8~5*qa?J*(dwfuZ_iz9br&H( z0o)}1SHXOII@D*~g=Pq>1^vNBp0l}M!launF+z$KLaKH~x3Et1}dE{LuQk`Ow!>w%U7TC_>}YMQ<^ph z7_hJT0x6MTq!@_3k$TVlk)=!i+&JbXR&lSg8~k;Kp0Y$#kN#NdX>LvgA?L0NG#qWp8wEV?7kIpv8f@BIGL z`E{Sp+B1BmJsQ1_`?oJKA+xGlWQRN6#2M;j0FB)xNM&{T=n98)V|?~-%?T&xi8t|# z2I`dg=`N^pnZ7*bf9kHa|M1q0@85m+?dftKuf6W{zqOIhj9Onw#}fh9!DKzlDmgf% zP7K+RG_Bpk-E*M|?6p518{VKbG6E7tu&URk|ThK&f!|G|G}}IF{A6=H2g{V;U>O9+cp9&2#KfwE_vO6 zAP^{i^V4qrb$@N%J@k3^rq8?EIJGxw%e8XlFA9^ZT=|P4=6#}_U7_FOKYzp|E^o{P zygybmXY+f5UqlRh*S-jsr;n(tV9H*c&o}X`27B{Nd*KmSij z8n9UByKM@-+bS^lC|@1KTk=$*K`mzm24fanNA7x7#(B9oh16{~TW$%8$%B2PHnM>H z9A8Oi_3kELUWnR@PsbMp^Cs`8M`w87%cg`ljBtI`pEe62|Nawhs^nRP$eC}2L2!4~jT@#L zQ*S8`Dgbvth`+uAHb))s;j;=GZ;_8Evib&ngzsD*;rks5ZvVyT=WxW!oi43Vvpgoc z%rx{Hn=l0=Z9@vdDDLY9e-etw*kt7kkOUi2VCNI?XLglGM_Jf;YzN^BJW7oxq&#CX z>?C`VphVsIEY0cJS_#W2IdVlMX?WLsVn4=M5eaitH99|vv|C4fI~=}c5LNACBNm_t znrhL_o@1aUHeR)%s>%rQC?X9(-T;gfS$Tun&jS&^Eqz;u{4}wYVpSK-FTIcbJAa1* zEC8O^jSg8*PB0)Cw*d6sqK74Qt^TljHzi3$Rf;!Z*3@t_3{y=v{E7m8^ z;7Q94`*8B+lwoa{m)f?DR0ZWI3sFuA=6Av*UHr%@2T{Qr=2D9M`Sp> zpJO;kq|GDIb*zOj4U2#hh4*#!YZ1W0b3n(+X(>;SVy8AF;%l;Bs)G|ki(H#saCdiH z+;N!w$nLs8Sr4pnrY=@@-DV*=wb-iAKmy6zX(nOhwN%y9-RM(ZqKc_hh^W>vb#^Cf zhSH_I{RAuhsJA7-v=yC&h2_qXR+Ecvdj@}(i`wl@pM-Mr`cZ%hc$B0k1`W_>CS*|k zs4q$J{vdOks$0=p@;5jyH@~It@bmhS{CU#DXHi-KABcXWR!&N9bzrcLG)p8o7A+3j zclx_9Wl%9?JP`8q&Ds$`TIvwbDXUNN|93VHwz$4=)URzEO&iA_-!}3r&qutAY{ZUx zYpNWCzOcINCKUqf`$8HeKar$y9q8rDk7%9)Ue|h%5?T&jO3Vj|agY-1)m=)=E(s1&q8+6~(zbdgC4#PqoCxbm z`(u(Kcx|_}Rb5ut?@we!xRe#|g|-%mb=irhOEOK;rg}p)MQwB7pzG%!KmT;U-;ccf z$A!-s67(nwo^)c1b#QP!O*NPizqPo(mH&QMgwB*R^*0c+a>hQQ+j|)pL(m8~fLztbk1z z`~7se{(>UpVT6DF!nblzvf}rL3)k;);j&plo( zT_3)aafu$3smE*o?|j*QvEu&LPk+7t^!C!>>MI{to$0sCa>~e|%1vA9o+~HHK}u3X zqOf<*4p5h#AW57>VJk)jv#9s|3Sh1jbyv_VVHmJg0QViAfYO;s`Hi| z!IPt?uP)1hY!MVGv{sNew)=_aa{)e|o0!k+?eVN^+o>ye8ioE9x2q$2p1r*xs`)UHLtk+H}!EKvDWkj@*AX|%TQoC}~l7YIdFbSwoL zF#&vARG?F(s37b@)dM(}l#MidHkW;&bHo8FFvMg=9rzlMq_qy-u;|$xL`xa*pnR%^ zM=2}oAu01nJerGb&*3(sZbiE5rr3yoQ+MIfGcmL@XHTQHfLlSfg@4+WUe;IBQ~ z2|{eJHrZ1+1elnkU?1|){fi%1J#mEO+SB*pEm3DbORh9{->p z`WI+0)q44#zmC#g1~~JgNf2z5RHZgFQB2}k2havnp0XSQf%k>q;D+D1Mv1D`qxD!I z*(3U83$T8cW>XbsCsaTA1>^M8I?SdDNF?Bd!n90Asdj@LAuJ|>4c)!Z_^d&ci@4?e z_Zp=i138vbs@f$N@GhhGozS-OGlH*&`UacQd$)l8+|4zymO6M ztVgI@a>ht)6>+X?4C2L7?3UuDPJ9vxsc;5-Cbe$|_)qjc!FhkU3@T%=g{9QDY3ENm zoj<|T*^>`nd2%0|-#&bJ*dOaA)I6O;UuyJ_f%5&phb?)pY+S~{MAo+8O%8y@a82i) zeqC_OV!2~0JUQL>*1Jgl{Qlm80<;$2i4 zT@Y$GE5#75E!-?lpjl>H7K-ZQ=V*e`XWI?LA)|#sG|+5_1^4L$#K-gJ0jgD{b57Ni zXx9A#47QPtGimvPwMsg&e^2^r=zp#M#>1!M;jeN1%l_FE1FW)mL4=-efj0BI1rY?~ z)D-)_1rg2)BCH874$S{rK?LtY_2B0Jp9K-}^@0dL|MvN(kC$v;;UT%EoQLS>)I?g@ ztkpjOx9 z!I~>iwM2Dn^^^!ZX@|N~lQI}5Q*vfy$2$VLUwr5nCtIDYTWghk1;{pUus@1i=%qj8 z*dLOI=h=}2Ng#E6ALR0r^`KB!X6@mX*B?5NTH%kQvGK1-}LK^e!YILvENJ07`z$VOnj8#NQ;`B zsYSXhdKev-iuD6B9mZb4%yqK{QEDJR(1!wtEV75}wGSN5*>ng8@Kc8{#oF#g?IJB%K}Fh8 z;&X}DzKHqa(-8`p5^vhoFV0UNoLZ*n7gxS`akl+$?Rb1~vi<+%J!#={y@xove{Kh( z|CxIn-2YTY;qu2$MREA${%QR6xA(ukD8m+2hApKkgiEO3P>K!9_a*T6ScP!(3-loU z`yd=G8PMWrv;Rf3rhiBa9mriXo%v(zyt= z@m@2zqqgC;J2RRe+fML(4GJQ`$A|1*1>F_Vz>`b{e<5H5Ujw-_!;EK~jn9uT>hHi) zR4L6X9ZF(L7HU6vQDGBe)-d3T)-U85`O;h2Bj35{I|f+r;vp8d4*cve$=x!tUfs!|CE; z785z5?BW+{yR(b?@p_BFf3x?;8v4_noWGvwe;gj}m?-u!)c zeuJ|n4pVR4{`7JB1Ny_BcfiT8{~0~6I4ImN{;ppr$7FV*$LMpS9j1>a`osGRKU%sk zzP|`3ax1S%oo+HJ#4W{mZT&vZiwCpG00b2mbq-;UbQ^^;ra}yHjab+7_v7J28&2VC zzWgX7u&(5d5|5NOI1Gx#V4Z^!Qv3aE z{V+~mrzH5jMjhSQ1Ky^h^d?eu(R!0IcL2;-8_;*6ch!ft51;lAPf!2jf10#9_E?NU zkCJu|yi;~FiHjb$Y#cs~{dm-$_Nos?|DYfG7w;z%m0L--p#3h)Zie79@Q=CC@!z<9 zIIe$;>mT+{Er0y@^!YNk`9a37*nKwBvSq^J?2|7FMp14ZZCs}EOntkh7u?08H7m_K zpnF4d;eKkHuvrQ|r8OXqQ^g|`+ff;;B)c;}G3*I3J9%oc6VD50=rD?&W7@O#30l=S zG0kEp;Qqyl3KUC<>xsj9;@7UI>I;5roKTeo>fdhz_^vF*2 zqg-P>{hdv*EeW@cxE{3;aH(2jdk25{k;M|D1BmL0W8>7b@zS+%>f1PRZoJph`##rV zhP?avlH6{pKlR)uH{%a}C%?!5ro2MhFx{l6ebWnX>f@su@Wd&jb8Y23yPII)v*}Tw zp`DZ(p>9;&RVBYULN!zFny70IT84+Qr(Qi<@e|X}g<T{>bJ;d2Wfz@LepE;NYkS#l~9oeor!d$ zQRU$Ul~i<4x)jcR#x9XSAI3vU8RzjeUIwBYj&BJH$4@<8t3lr=$N_P<0O#x!lwFa+ zuC?hvKF;IoVevp2`mkIH3o(YU4eJPoalK11bU=6Y=ATjibstYfEKP(GsoVS9I$ z+O==R-D?tO>`yCVg0cHiG4p3V?a-fg=-*1x4#`10D&oV@>oe0iSgtPA#?DsM9mV_5 z*#ZG^bq`ZiaU(SRBH>&a(#6XvWKH_lL_o_bh@rY6F@H@2z{q+7mx1I3E<+V|$Cv#Z8n)Fi~F_t){n9ykp$k8 zPhUr~gIWo+21mT8P@-T+u+6{H?Tb<^?npMkOTOPwc5Guqw?3)YgI?-uk^u7DN$xgK zQ<@-2QzgqNq)0%Pff#@f=>Q`@C2;5?K^IhcjUxdcobAlRtSZrE#;O^uh~336yz|M7 zEhO=mCAI7eOn+M@p0ue*v0{>5*)F%Ck82coSjv{vs;s^%tkaG73#u&R;Dthd+221s zkr4mz>FLrBKQFSWtMDdSSf;W0sue6q#5F-gRK)EZsSrdDnS^hiNVFtD(B=ttVUyzu z%p$rFPLQcn<--cOPq&eb1-W1(M-UA^HHiEzk0)^yE z4ns(eH%t4d^lh-3QMDop(9S%l^@^>v(mNpgA?=#HrSA77fBUu&3C`toEWJ~P=7QW^ zm};|4CtpI=(1lAn0q;y?6xS) z11}^Prsi?sJnPmsxE{C3Y2fAD@$+y%>eou#0N7OXU502K@f1nZk}9DMzB{xAkHH+m z1VeiyzP5%Bju&xziDz`^!B2@4wMw|Cn=frr^D#V!KhD zfV}sFvd2W0bvo&tzdpWu`mcw-zu*qI?_}9*k+!FAgJ5z|)ndT8Y^2bZJxVpM;+-aH zSYHZ)MKhQqvxb!|qxhuucfQdqbg|vu2V;1$XFS5mgPr{*~- zBdJ8LcFGV~fB`X_0G{D;8DPMbBB!X7P|-a+=MkzX@s#(aQ<#NDIFJAn;RNx6@)J2g z9bZF%xCBXBrna5HD+Kpo*k+~x8W1Xl*kN#*OGVT?c!py|Wk_djmvK*2qdIwCbPb;e zXkriW_U7tUgZvO^4Vs1Z@Z$bFxu8hWqFUpl;yX>T8sak0ae}~{1jm`(Uj-|W{=m6M zbhRv^Mh6qbt%Cel)!Q~YvEa_TFRTo-QVg@;4XFtt?j{+OAVRf4UacTv5|b67x*$VW z{igm^Pg}=iiDcg?k`IdpWLyUXU51bg9dkL4rm2G@7x96J16ohmWIokC-@EIeBqQF8Py;dJ@05AX|~Z zLY+s&lkLlCZC+Ik!rs)y@qHl4NZt{Xrifh`I^{IEM{WP{r-uu;&vd`*p&|5N@Q+?R z2CWdFQpfT;q<5piS+fKchY!g`D)n(Wh1A z!&}J*H$ml?rQ=60Bi6G5{Ced5Lk0w2OO&mL`rMQx=qe<`CohZUND``FN~w@hcunlf z$1nj-5}=->=&cxzMIFmHV9EIX;u4M;Dghp(gmG>bQ8)(2QC zk0bNpeJJWRI%Nlb-B}&HcG@hfVI84IlFB>|Nyqjob9C&ujuV-3C{rcEnV|uJy~srfjiU$#jPsbbuVMmr^vI2EK>(Z9J^3*Ek)9F_n7p8*vIlHxChVf1;fqlC%@L%gGa%L>5f(Tq zE#hj6khYo-1CZpqw{7^N%0PgzifAz)hDKH8q8<0lI>M3byY~SObSUJ-(m4quS<1bM z=efwoFqhi9H71eYTNCh>Oya;0nG$}lMZStAPhQIRrt*W}=x_e}Ccm}qKFb}j&!Wh9 z!Kz!_D|CE$lzP%+q-VjXq)n90X)jpXJpjHU6gqEx===B2{Br7E8KIAYBE$kKHn+|9 z(JNw5XhRpPY3OebBOn$i)X0(^ZCGtIvpa#MV``_>UIMS~o{6FYi})$Q4?AfGP(U&3 z+TS*J^WHg3gSdEIyHO2;C^f{n*p;zBw4pnJl3&EL^_bMWz7cCO! zq~)#8{ll3z_>IIhTLjAHF6TFW2BNlupBhv=;BeP)P|0-KlzIN_m^piX`;^(v`i(?3 z1Biy4_pNWuA!zZ8hs84<77s_{7N4)4@vwS&gKQY*EqoVteTOY()_{k9+a!RWZ6S0x$=aI27MMAMs0s=ImN-A?2=gU8d+HBkiB7W! zSQmZ8M~o^Q0`ZswbQ0e@3358d=l^5xU7F*zvTfbJf;jC&2G$b`x2Ir-4XSa%5p2|L zrBiZIr&5-;D6?+q*WdY#xsXgIld@#Xu6=e?SWbYMz{0|M&c~Qzrkeo)lt0 z%J`%lQsO@5tU3D9y|#3ydUb!iYIuGXsMDRb)PzVi^^HMkS|L$PB>EF;b;LsoT6Izv z6(KgQZ}D#|xJhbxR?IX?$y#_0Q*CkTe;fjeR>7j|36ct9UTk@GddJhV)ER!qbb23!0mn(SbUzA#wS~>Y2`qe%PILELGj9a# z6sXa5B61G^Nc4m6L``d15V1EgP<1`@aW0562Nk7oAgp(so85h@u;@MsKwU?`=MuP;gebqiXQW-oV?m~ zv2hiyvUF?s5aeGR2+v>Qu=e(dAtm}ki3v#P6F-Jdr55E4weuQ$Ry14a2+6A$Rf;=L zQ57?BKUF*2RDHuofDcOEh2?0k#BGros<1Ozp>?LFxQ(G+id-NU7`-!z2xTkNRnU?o zd_L<~RhQ9HoDoonJSt&*)`X@ZZ30e&YS?^FdXAQ?D*L3WzO2hGSYjxX=a-xJPWH6= zWOR*>qigihh2S3ibBC3XbbEeSMSNahMoN>WDYjf0i`eVgk04y~$Ur;ZjeTqR@{ zkThOo9G3LP0&{R@_*TPMajZI`25i-zK=%9#6YQN2BgyCmb`G;mI|%Brh_qVDuVeR; zf{fCg!jAChyJN{=7(xyO)hG?Uw82YT3+rVKZbI|2Y+|USVJ&Gmmh_;TCOM6tJVGCL zqEfnu8P8%t!hR#_2UpWjJ)-0j`8>;Bm7qzA01|#Q;rk)>G#yWJ*o5b)YVOyQv0cZk zWkpLF!>=O%vRYC&f_;ZzvU|%P?N|;wF@-QcA<*s6^;=UE6|NLjVv{%4L$`1q%X}I3 z-oO3zc~6~mVlAA@*w2u#x8U;X!F}}P-!_2&2$Z7?gb+bV7txD-9D9pCbSo(pyxbYr zv9N`y^)KWFy3ly0_$YbLYB1Dbk@Xw-T<+>wuqf5QRw?QIcDPDTE>%6JlgQ$_oI`Bq zBq+rq@}u){7{xDNE|0|FaTpHkFw}?B0&9j0eTED^Lxy#R44NUmjec{6d=}ymmf-xr zj&~tsK{Gt=ead&5AI0t~t8ieFW7^hi-tz_4*y@}luDXf$KulMV=WkV<{*#a(Gd0iLm#EaOf$QdZD(Air4{^9V`k*Ot|GPM+WO&29c zx)T@ha6}0mL7Lt<7vV?Wl*YY};O8);vj?2~h^qiWj43iJ?ZF~`qc|=C(9Lvw3|AzS z6#xFDSZ&7-v!g7sSMt%nx#v?~aAN;={u{A5Ex44#V1Fa31PlJ(@5`={HTv(5gMm;SKm%3<*< zL%eR&#htiK*STH%g_~Xo`8p;kdI}3}(=Bb893JQZ$UDajjt-mlW1Z~M$2NT|coc8$ z5nB9Mx>a*6eeA`LPs;DbA4|UgPrgmHU5Y(>-RZcc0#U_<7v#emO@8hDFxZ8a)j)8ANYV7RqH-q;=7bTxUs3ewIlR zO%RA^N_3B)1O-W-2oRmZN+|y{Y6PtGXpUrj)mBz#4_6juH?fJ3Jy*2CtQl8H>DN$Z zgP=4~`I~j-iESo+FEaNpatg>Dj+3t3L?sN%F#4pc;Dj=$va)lK%`__y>jH?A?1!lD z=t?3CAxjcFx;QXQXO}X5)8tA3UQo-`GT|Fb6N%zH8LDc_S!-^5EtrV25IIqX^C}og zlirHi9=VUitTI3<;W#pEydfEF72>h%MeP#3A#j?@GTB@I0KL?rtkN@iSPCLt6si{c zn>RiN2OD>(2s{Gs2d)dX%b+a9MXFGeyFxZX(0L*lR~EW*Bk`okTPEGDp@J*75kCH` z6ubC7i&E?Yal*`nMuV-&%Sr;duA}ko?$g_k0Es?I8=+_%DG%y&a zU;_u;4&Bug3zyLbnk?wZilSA)y?s|6HyGNG6>g1E$!MY&}GSr#3ucc?k-NulgmNkU8f`sJ1q1V8U5fI zFwZtodwEbadh#@dz>3tV4(^gjmlsc*^{g$&xBvC)?r@&rwj`;dgrG+Gt}&>Bq!UY( zzlDed999@2DWC;t=#A z(uaEEtRYq5XH{dQx|z9&QDj#1lxREW<@4)IKTgwie3BjWlxWEFoceE}GYa{?yDYN2 zZ9K`?c1+kNmVAD!{{48&b%D(A@!k8~{v7mD=AXWo3E?&IY~+F++hsxYflRN{1Hs&r zGO=cM%r4D7rNp^wx=dj2dhv>HA7h{xBng_G9}l%8qVF(x#_K z8(FlR1{^}YAnkrDn77c1{U_xG6X*$$14WKrr1l!^FS2@%$NR)fz7-W!mQCaMF#U2I z2me=8R$Y`azyEckE&rj>_T$^#`{%>$`ioR0aM>dWeeI{e$?4XZ@{Z!zA+`+u5PNnFKrDw{V?yM`tjpQd0} z4kqSh6j-b%hk`%5;^5=tcDzaNE_OLksvis=Sj!o|XD(hAy0G z>l^?*%pauwI4eM9t77xECmxA#9A?=qgLJdBj`-o5=gVHt!W*fu=j_aU;%pbKp*v5+ z=cQx`oH&Jp$xvT4PUCkTrQw@L>9@?P?_JwJb54D)HN2e8srapP>hA6CSj*>eEMzB$WRfbK07ZaFC3BBo4v8jABv0^M`7XjY#AVeYnl<%-spE?yyz zRlNy|VpcM|m2zwez_9w9%3$3)E6GJWhGa5XJ@8Nfun5zB{O0XAI;Q*&b)~zazHxbc zuLLvpDtO!Ua)ZAdLlw7@!W_3k`q=7s6PxR(LQYf<_nm!73~ET(Rw$#65_|9-DGs2B zEaAYU5T*&l)n5J^W?^b-Z<$lnNy1?(s0aXo?AK~v#e_VL!2AFD_1(`G#Mbh?M&Nq^ z`C$Q`4-H}onp*H#yFhge@}e}E7Dc&wARrml8$Z$P!M5@yWw*<7^Y&O_WwNyE4H^&V+Dh8$aFio_EkRVPpzZFG~*tTYO^D~CCCp*y{-0QjI zi`qa7zr0=WEhYtE-xEeKDJV%xEkCFSg_J?4va0K?^nS3s6-`_~_eu6G#%J~?M$tQO zf$VtxZ~w^7{G1hsB|$=qWAKhaFwmTc&Q+l7wipU$!BK*sD@Y!~!YsZ5EOO3)0=-cj zOUnm^=8qy{2ns0Qy42!vY{7?PJ@70IFFYrsqQlrPi;vl&kDH+3mQoXZ&>8%quGQG^ zC>9e?V{q_+_Bq#=!;|O}f_rg7TeQbP4Bi+iUUi)n(h4p{R5~45lO_t#+6oZCx3}NETO5R*X-+ zRGjS?^Y_{@UN7>qLZAZwDO_YhlQ58$UP~atx*r(plQ7JKAUoVJh>^q zLUpi@SbOQxJ&1jr^&p^+FB#hwOc9vH8k2Q>y4c&}I%m>una-Hj95?0Pq9d zFU*o0ijPBh-R+*ZJ4*q>pI^o{loW3g;lN_ukPn}mvNTSH%oTU z=sh2NyB?1F>)_ioAD*1N^KeAlu?Uh~1?SevXswCg?&Ud(UvcqZO zG*);~E5S*VS;HE8(U#~Z#@!$-6rT|L;0(luHUeW*6zm9`r`!kPkP&0De>p1AhIPhB zArio?-^S=lj{bM=jZ%v6zy#`eBINY{76tl$s=7a~I^wCU9pn7CoD!*#JUPXmW3yw;c%`gpq!T*T zF2%9;gr02-zspfqpfXVBvzM@0Y@s@NdjxqQZtJiPjjJ@f=P8MQgoui76i6n zp!=d*Tv%0J)z{Ez1o#i?H56K)PFNspM4 z(fQwlm`w8V-`Kwx`{{TDi~L&#=J(h&I&R?q;#CTBl@xeml}QV13T;<&LA$Oa?voA^8W9?A_; zbS!&+6R4?AIQ{SY>7c*B+KJTPzv!p_18=8rmMQ)ZTZNDF>+-}B70Pk+D+SLJ+r#}A zY!9b%M*&u2z6{{G{}OWLsf55PWW|5gC?lOzA7$xrl;y7*Wl&)=$`XZ~j51$qpN=tm zak>GPq~9}bPjNC&gy8s+5PW}GI^REJAKV85uwl_VJ!&+`- z>8&RL8@*E=YLD??9n42s?tw7yyzlp{(p>N5j;7X+<673(p-GY!?|hv84}Bu7y43$P z>UVwOaZDfH{rvWszh&MX!$BC&-<0a+B{uXIQr&=*`ct3q^i9Wyw;w;f+iM2f+IsrR zWsSvJUI=4(DKsQsriDL=c6hAiXT{P@mmfeB#~**f@r6%bHN_|P#kc)cJNxK5o+fbn zNnz3X_YAn(FN|<1IsKKB_^*E8Yo%a}-+L1OW}ANR2mW@W{*_MzZ2arHPrHxLR;ap? z3!_>sH@TR}KzQ9q#3=!2*NBlA{IfGID2|6)=KUzKyC|b` zlqb~*mx59OW_>@4NJqA9#rS5=x+kDU&R6J2fm_9wWcr6F@gXlI`o8=SOt37ZAVIXU z1uD9<+QR%cgib z4hDin7z@nONr_T-c)+xD4vr1fpZaDZ8yx3aN$;>Q?LNLem-wciB=HfY)ux_)0l;BE zdV~?Rm#IxdKQJDf|LT0vw+i|5-`21FzB{os*3Ny>hPB8cJNXRMDaXyCr*OqtD&o7r z;}}V4b_^=?{5kshY^%9+w?`uTkNW<6%SrRP>G+SM2Zi3*-geB9@(uUig73XtPx|0) z_x{IUKmPsg`5MqBPghmtC9{U!J$51gEhZ}1hpcxXwD-9tG37Yd)m~G(t!9yh^B#oJ zIT%QFQglCG!q6sQAvYINn$&Rc#BKh$XQi_V=bxc!A@M(=%|2x~TSR23%O3*t(`;{0 zVhxaU%L^!-cJ9gM4uq78V?Ee@SGADp6O)|qa7u$+^>r#Dj}SA^>FfBL569QnwTxlC z`@vhl+vuDPPN!=Vsq(_BD9sYtM4}=zliI}gsmb$5DiukqO5}@bjase$t20T>=+sTv zo&|KJGE`4_D-Pkp@W!Pkk8C9JQbDr{;Dnm&z7{z28KT9%(T|6SMbA2`kgJzxF7V_m za?X$9cV(|rper4xeO8}+Ox^FEg|Qcx3Kjx|iG7XbimZ)nev3j1^AZ*I)|}c~IcRF` zOELDMV-|pWB^79%QQ#qn0*<&$pxpRzZEo5p%oj?pBkm00ys?njzuWwqWzOGR=7o9^ z++GyeYa0=SmRdKWCsK%dcV$hBY83pTWe3o;)}bGKQB$ljYgIeQRtKMuEiK9PgX%Qg zo(XMP_^kwSR&%@$)1q+9ASV9IqK7LqkUhZ>y0DwR_)u{yHeY};nqW~(CTsPm)UDVX zv$MyMjKDvK^>frugRYdlaT2xlW%=~}sZe?*iC)r@?{+`G8Sg$E-n{$y@Biy;3Cll9 z2q?A4$kybQ4{jox0W3##p9G|v4F1cyJ?a|Ko;sw3QElCxuj728{yl1j_fLZ8XJ|{h zb0Nt_g>^M02Vp2j7(os}OcI#x6cf+cwUWL^8io8;GIe>v23rem6s;TAPMg@F=+7J4x%_6qx(jqjh!;I* zYZgvst0bP2FQs7I{KW-6!E8xl7k4)x)QVWfiE3uS&;|KnNyD!q11OPM+GX52eib3` z9)&iaK&1$}LV&192=rQM^0tn|phL@Ijl|QuUW4lO`F&g_Qd!sk43 zJrH$<0C2-jQEf=`FI6I5guWe>qS~sEcuw?I8?{u<9l%%$>^)fp88N@CiVE3?rVm-H(6PZK` zn<7vcUj~!1B!s~%pw_SKAlYX|d7PjBQT8ft6EM)@uF z&;Da(&s9U77~+~wp;@HG^%{X8&4Nek#yHd1QAhYwk_egM{@maprWwn|L{Xa2##Ium zr&251FgMXE8f;=Mo70i`H|-rZHov%eYuM0NQI3L!vDK8_BI&1K5`1Mjzg)bUK_8udHj*aa34a`3m~a&UO}dH30C1_zgZEbTTFiCTnZ6koCF2a=bU zn8eAMm5wI0^6Js7fs$ye3g&@mjUAisK^X*^`(tbAKG=sNCqx-~eUGdV!De-yOii}2 zNorm{tPe0zPjI+W4#Zu|S;*2B!YibC$gKU~*Q=?qW~JrUn*4FSe*f;xo5S1jCtI`M zemwU!w8^78W@Uy+f#fbbn<_|Uv)iNTAA2F|c#Kes6588%e-Q9x4F2gCuqX&moEDOeE)r#y^a@*(=c3kCNah8Z|1O4d z_M=i5O(&iH%kCF-_7l6A3Hct%0~=8gjleX`SE->5W0xVT7P1!s4l)?(lti)%*-G!m zc00qYEj>udjv99=XNffwL@DIj;$TZK+mt8$%`iAw6$=I?C4?I0#Z<7F5=F0a1@^;&G2uw*ss8CZr5kOq7bAviS#+f!LL4m zQmOC~u@q0`$-w5jX-?|*0P~;1uj0(;B7{21YLKGxNzDL{XRA+zo3oJ6At3DRtc~oc zSbF@pDuDK;GK$S?#%<4XqI$wAOz+Lg|2*puP46YC>yl1}+1uUznRzyrANw$4Uhh54 zA39}E6gc$^%OXJ0?9>%{?b!oh&1I^LDwhtUbpDlzx#2u*Sa+qjr>k*L36xW z5Shm_TxntZwXnvX@##>$M1uTJ2D;#E$ zxmaIA{RMq>=!U_Dsz48E!fg|I*sMu|!nBqdO=k8TW@_Q!X#`%WnKg5cTcm9v)h9oB zsc~8`_krEO#>^JZ$Xb%5mzLTAjXG5oH4pyT*9wPqbOZI}ra$T74@3sO`S8zYY+Tb$ z(LM`Q(2(Ane0IwQvQ+6Mphs+YXI4pTp(Y|-h4(H&0aUi2T_p&|tU7pCY*DvHW^1?c z9i7ioL$WcAD{73r*&>#YI4OY2h7}`2yOUZWKm}$}0|ekr`|}KqwoS#!rcP4Umm)c{ zVK*~TYj)UCNsx0jb_DyY=7Xk!eUK*11cX?DyfiJiOb{69Z7x=9Z}TLEwkSSmCt$RIBx|HK2INJS?US_+r;AN=0y zpzKEd&J_TeJ6{$dc7V}bAeqSXXnmbUI>EntfKTgJ!+JjWx&DpoH+I4K1US6?=~

025kA0sBpdEb{41MhTOc?su_n9yp$G)b*&#AItfn4 z{^?BlYy*OA3#`n`!<;$r(m1~p8u_Jherq!Bi*w5+0f9=w=S^rMXT$pbY*KuAPMlAI zkMI6|cVQL?uY^rn=4toFng|!Qj*z?Paee6LcjJOfBjBXLE>8Y~Ch;%hC9qM``)25i z6t+7#^zH6NS-WQT?#cb=)^^_6wx6)s6WWYq)wGu|kVN`DlX^-3*ExHrkjYVXWI|zl z(7>}HyVw9TVF=S?TV$1+YFUTx=)?Dl{C&)UY;&u&BvCv`JX`!6+RavPMQ&Jw<`Sbl z?2*4DmsV|Pby9$BYwPSAefEh5!Z$Sp71Ne888tEXw{=F3eMaLWn#I zbSkKg9@Z(K!9V&;=wrXngwq|mkNvUFgg*FvCY)~hR|y`0nH>pDNyMbuR+5vH4zgYj znggTnC4Cxn>4UL9_%!Hae^_V26B*`8VGj)ekV)JV?%1T^Pqbr-Wa+5l%M4}G6c~>p z)xMAaQFFmZ|Jdik6DjO!Ax{ouoKYJZnZFQc;QEbm-TRb$%Qr+KcFFw3RvEcT;uM$) z4gAq3K_B>Ip9X#4_i3P!Klm)r$RB(bJP}-PwOME{@T|=RT+ZXjUxlIM^zwg>#PjgT z9C~Rd5aQgUY_t`ejrjX7j`*|bay9`jnHF{{B*zAr`ydi4&?uD&mS()tlkx|Jic%d> zX|6K;&PZ7hoCaVUs5WdTX_m%wZ7;GjC4GWA*AH%efvZ3^ojjB02WA|W!MUER=r&9o zHr2GUnda?!dX+51+UAR_uE>6Q*Ufc#y1^ma%@TYt#6J);Nyq=KhC898-X1Uv;~wU z*wr#_Gsa@+Dw4%szQr zUMl4Zx2BA0Lg}Cw$evRlY{3D15amQAE*YssqCElD&8@wI2v&j<-^e2;F0SZjn5G^` z=%J-U3c+KTnA->xO*TA<7<&NXv6s)WiIGm8TjK1oM;7`2i}=LOGm2r@VRC&`%Pw(hq_{u3K_vfQLj2vI z!TjQ6!`f{_BncxuT7V~oe3a-6vzxS)UbA8P;tKJfg(&4BMgrjKlKSO?uISfmQ;ry} zxjrAdU1;cLA~y`Vm1w`3h1^Kg%|hKslfbT}1!8)+{ZD%Omy7Gc;#z@?k!dO$c;4eY;YMRy zp7JI`aK4!E@CuyWvXp7wJf3wAC45K8wlxaAMwXuDyhF%Z>VjwYc;?PHLXKN@xa?Bm z5^e%1AE!BP(wy)q#k7-E&A#x1KM2|*6wHFczO;6zi1*fEcxb0sVtbf&5u{*ldD%Rk zxd#ppGs(;IIJ!5paG*kiTs{kRYez zVs8hr#ggZFXX{D|0bjWvSO491Di!ngHs_FUlxU$^SsOT8N}k;Mx75@&#}j{Ol)^lP zf?sk9wH{1xnwGrSPv{Y)*@hl4j8FJZ*AsuP&HH4c9As5ffb?bCCX-rmi_#fYuUg`! zA9@R$S~5k_5QAh|Y-J-)_cW)C#M?$iT8$MSuQg_x8!pW{1pZRB4~XM_YRVgguc!3uLw^y%90GgJK^T8&!nBFwUljeV_8?iCw_4|{>5!mN z@wy@k+sZz%sQpK^|FQp6jKVAFyWEL)f_JdK7vo^>FR!_@B(yK9BKf|O6l~(Ib=ERz zY%U>l$cn9`S~Bu)i>MsE`xOfbIzqYN6p%~nv#rhEMWc6-h=h9gby6bE()7|$I4b?d26u&zC@J~|nu-%qoJmNVYDyQu356w z4IV=oe6CUdJ%0LWU^KaOd|<5*#K%~eyY_gPU4R)`KPS*y(8hwvRD4UAnGp<>D81K)$(Feh0;rI88TZD@Ks+tWV!`2gD+e~-2>rMU-L!e>CTz_`86OFq~-WPe&xjjvpg>A z2!FjJXBOAoN#hclB&qSf3@seONe-vS)pa0}U}CB1@m|kHxIgWBVjbprkU1>Jb^5W) z`zM7ysfZ9;%Wzs9D=De5ZD`*lQ3^zRdI46!V1-SB*}%9Z?n1T{Y#qTNovv<)Urf9l zvolI%zv3pgMPvbY#FRHDUzmKzq~7GP3@8&3K`?4VWf&9&ANSd^m#!6AwW_*-Eo+&e z5;)kKY+e5l!<<7l3AWOBqXf}K+6sI~Vo_lR7j4s7X=_B{F!Z@gyaFruL2Ot`OeuOv z#lUZs3vO|Gz~W?dHm`pD$GhF#*|e%}Wl&T`usT7Fm02p9EK_94d@wPE1ZP1;&b;PK z_8g;k>J|VoQu=+3d<o z)ExXmk#kj%I$SC;)OaitGwD~98MB8=>Tsz{!n%5yT%W^%GHd>-GKuiTKM`LXoZ&b)i|^6`Of7Czfnk$A&+0%dy=ML4+(!BVuGM@#lq<6ttPDZIGZYrJZ}dzEBX zNCT4uaggY&HCsjQa#o)~3Gyud($|vHFV}_SX_X#VkqZHw+7IvVKJET~?q67<`xi_D z1Eov4#Yq_pudXvV>#VYETenh_moa!3Ukj+0&mI#4_|JKL42=s0Q0+XC zI`M0s65!eP0d#F6D}UkI!>HD=d2vMoT0w7#nGhnQh33FX(6z!)-l=JB7A#=Y3cR`9 zU%#e&tPeDxH6-PmbeEz#9(yiB6G^@JIVuZaEou_r%+?|qnLSH`E?SChY0SD2GvxCo z8G5JoCMk|U<00f-NuFcUT7FntUNHyh_?!rLz}$lnp%p7%^&lKTd5-=zxE||^*Z$st ziIW$oP;9^TgwL-dj$Z3K>#Do4zxc`hYg71FCo9gUasT0;KYsYvxx(K(cqcE`C<`f8 ztxXgj4+IpkR(}yHv`CIddqtc;+b1hGAuT94JB+Ad@K046giUE;)>Kd|K(Y~UECQ8m zuE=anQaDWvN%<^_I%2e8SjR37`WJN^@3Xeg{{KA&6hZu`Uznv&2NZL7tFWfFrz_2( zl~^2Hg96X&_Sv)n_#?zxS^FOt?VA+z1!F!ICvdzAqln1fmR~(t*alLrl2Mi#Mq01P z;O0Y`>e}%>nOG6+&6fq=aUz__YLMqAm8?p>$%~)$+Iau&&D-Z)=Naaz61Fe+S@+7i zDyy>9xklXV6Ec_wc1`uF$z3kIEa-Mym8|`@OlW2;@io+6dNSY2^a|*a`MOowT;i^0 z&x8tQSZ|rj*28 z!0}+xY9s9TN`gRDdw?NYq?fYIFG3PMVK= zgNANR#6>Mdw0=wJ0W8o3E@-*AC}d<3t9Onv*@k5&e|>WYDaYQANn|m~4~u|?eKfP6 zqa7fI*?_iww#Vi;q{XqZUvB!I}(Xu&#=;8U3$E1P#~)&)p9thscATx;p6N>Ilm`LeENj$*2`6|ovNtAb^OxyGpJxi@4`q1iiI01w`fqgy5yz)}8 zq1}mBTN@v&5expw=CE_rSv1S3ZE-=XMU@3^39n8YUa%};N8BBPdw%wi{|tXfIsWha z+_BqxTHGgnUz&1Qds3cm<-=XWi+K!XC%HA<;(^#vd7r?+dVHn-GOU+$ug)pQS@mVZ zgqd5L)W|gM3JXVb8zRwq4P39nJcZ{B7R47RyXqk*^ZBVgKlv}idI>e@FS0tGf_t8L zld->wQdEjO$D|+qG_Kctew+|cvQV&SjL>vK>QmzAO+1$YUa;G(0~6I~79A@Ho>{=% z=4Wy6UzL@85@##!Ca{$H`FB0@s@T0IKlaKeMLO_G{*LSNaR2tx`S@70KQl+H3f^_k zNa`*P8?`5qw{nsBZ|jKhe;3Be>LIa5nd*SeE9bLbQ~@HN(&*QZ$Nuww7YEPLZ~bUU zKIi9KIiRL1;>iNrH@qxPV4)sgqnf|I@gGKcYt*HSIy{Wtmvnc#?>(GC?_ryQ#J^(u zA$D>rM3D*9@$rm`B!MnL5g{%U4$h~avv;oY&{=F=M{^y;s8ez2{zN#ke|N$3_Brk| z2oZavX<9r65K2GSClRSWxZ>JQ1da>Fq_*rs>*M*rnkM&bSE;daTdzRz7iW!6V6}0W zMdjv#g7AjYOmQj)>5iZVP=Dkz%-c0_jqV1&ts+HlC75;{D_4U5BiL`zTv`rrZWTpL zGyq`Ojr4Sfc`TlTYx2uRzYH(|+Op}^rd1Fek10S?cUjllkDtaLKYsYf+n=A)$}S%0 zjl;@5OoAsFTSN_pX#&2dMgTq$6^r8_+D%eWvob(A>ra;H37v-joe)TK0DYibaqSGx zQ5~A_h{W0|Ym6Spz`7a*APJ6Ii9)#O)kVB^h~ggsoKl5}P-Y-JZu8d3;2~&fT#K7o z-1Nlm;g}1KLBK_idGOf%Kfc|a4}loOX~V7%Wm0J3xDx0xixv`AmY7y2c!b#oPjOK$ zals$c#EZ?>B%V3&MC(Hu;pJ)v3a|0D1+=*P_>aOI3J_KhUWVZ>* z3}h2|nZ^I+b}c-ub3daIrh`zh)@9MOsqhuuc6hh{@XIH7>z=0PO5&|clNfOBKp9gxA$1(zK3OaRA0gdO%tMa~RkZ zT=ALc_L(idnj$N|x8_jnk$8T74NmoGb2e{ItJ8y|C~wrVHY#;pPfR6(yG%LN*ecnH zN6b)F6sk5P3&a@{@U1|%6rtrzBwrN6T3so5_xHO45p7~e%%{1`7n##+4+c@vZfsEy z17u`k0AbW8a#HrsoApv_Zr&2d54g%5Da`2^Ke$dFyrpd&KLk-aV0B7L)R4dd672;| zVE+V}R)tTi&T%595;+WK@~Xuy^4Q~4sOv3_s$y~u;&`=@l#U1T2uDfMfIub*1_AQj z1F~t<$DUm-FZ~lAmJA<7#F@m|niOz@eH2B5%+l?^wgG=Hhjvp$nlrFXWw4$>_s?Pt zvpRyFNh(DJChw4cNRxjOSXJLjlW*28Zf%T7dBSi^GBVV|*!Qt(@U@&9fZOfO!D(Os zWfT$6(#0;JQWCMFvRqEeVG+|Iic*RNW0|#fw*E=^Rp7G-VSL82MJg4W+MZlu-s9I8 zo8t?D5KW6yHlp;QW@M2xVx;y2f|3}fnMg>R*f3+vH_UbDFHrL2D3QU80tXd11gH6< zz=L%Yl@ggI3>`zqf>mPpCSA3%IDUTZL>^taxr`w||Hnyr01sv_t(uL)1m^pvtNp{+qUb?2%@??^#6HK%VWxJJO z`}bTnopW9cPi1MJSHJx7;TgQh*%KR{JFPV8Md-7GM2JGW7e&EV%%U}3W&9t?9T`cr zSdh5gM(fTXd5}gjpquRaRa{F3P4Y|WRg_?P9KhY@BMZ2U>gZ=`Qp*qq9O%Ftj&x9R*@CQ*gR z9T$NULFfK)J@C(ydY#l`=vKXV_n$s|{MR$nt0DmbQX-?56!zA=xL~TbBEWL=q&4)+ z!YjK2e`Z^_OnsPG7IBj@ETW=}r0x`t+an57G=P(6b{2aOJ7dJ7mO&a&89UkF7TO%T8>u%suYWjK@*gIbY+kcI}{Q5lJDVO9TBVbM3SZ+!~YsMnF)O(GcG`KCntulk#OzrT4wV*6} zb~~9KY~;jSC$?XU8oa2%iyFNsFm=Q^i>efDRe5hQgLu+|5rb`y)%tKa{A>L2?Z=P* zIuG8Rcbc*&^kDbccaGam89KER=SOQ!-*n33o>LyT9H|@*Pp70OKi)qn^-G1~nN%^C zs`G<*XnnDN+yh^zjx-d(nrtZw3_O%h^p(k#{5HxzoMiSP?VwYvN{5{Y8%TuNn6!v` z>%6kZ=nM=9UOcE_>+C6x8B$DW;1f~l!?&36i2{ZL zcrb0XxUGy(k3r`iZxAJKjavb=7$201w26o>lX6qQ);f9yf1cYG*P(SBb}R+rU_K7X zF~b8eC^~uR-F>{1Sg?#?t9tnwCk}3xt!VC}YSc|SsRM0 zYb)l;Z9E^|zdiTJ2loaxQj!e79Amv9Rg!6BAs}WwhZhnQi+Z(<%hMl=R4?26>Lvf> zlcHAl<$?tH{1K+3-#A+I0+8|6Dt7~2ZCz`;3sT+uv#b0v=6+Vbg(~XMJl`m?3bSE_ zO=2offp%8+csG_a_#NjDupog(gMimM^ohe?83e(Z^`|_&U5mkXIVkKqg;%HGGU5@K zuHgXhA+&-fq7ZF+_BI}xXiq7MDY6@$V$@_H-BvwgOyR5o;_Gn+!MhJ1@6Sg-{i$Oz z=sk?@Rm2^mauuM}(v%IsuV_195kggk0$<;W?bxPX6;p~7=BX-^!Vo+^xpZvOcxcJ0 zSB1De+b%=^YE3JjbE$-bOxL8T2gYRDL0!}VPC^pK9&K{1;EdRX>7vzFZ23wh&Zzb1 z4dpok~gDhs)GIk*B8B&GA_glT43kp!MoVhz}XadTwtplLpU z&@FV*6qR;inTLMxwxK3b5m6aVoC=H696VYl$o=rgRkQ!4$pxSPGll`N0PGp54D{i*DJ4f>{8= zG`cTKKZcQDJ?(r|kzaE1@Uch4>9NhEL7CkL$95e0_Gc?CI~>;XK@Jt2p-53V!oz}y zA2mAqM1Mk@G74&;i^eLrl+v8{!w(x?1}(D`fZrN75Ya_LqN7b+X6ry;>9djp{b2Zq`&vog#Go zzSmuQ>B$Q`$Y6BxUVg9ryUV8qkL!;kP>y_TjBzc?|4Hc}g#CBj{rf6vde@|Al`r0| z;LwLrarl>Ou4FF1uL{5GZIxBc=0*Ijt?}ime12_`7vXWSKA6OYb=MbXJF}5=`o&mp zb=}!_^T|b{eQ{@7*llXhzI<}oz|*+${LATS75WnHvvS2wJxR1NyZKfwczNuc<@{Nk zzo!xjmuM6SLVxKuG3Py}DzN9V)ee29jeVmHv0Z$)wQ_|3uX-Pi%Apdy=Z?MV!mj(7 zp9~b|JXlv3@6L5{=V9&sp2hZ0eFAUsXFubwcnj+yri&8KfKy@YF&ZCYz3}x!1b~vy z^EJcSR)1Z8=?5yK9gc4(!=}t_tIxlk_Rgq|&$)K2BXWxYdjBYb$kuYP@~T z#qro$pTq;TA9+^S^E+5G;(l?L8ScS0Hh1oL zb^t|XA#Mc$7&1nlB6mEYRxWjWTAI8KLISmj;fE}$L3zx1$rUAcG;V6ig%!WD zV@j1_k5rm#|M#bZ=nw$Qs9}f2GczVTY2}JBDjn87rD0xbNVPf@C6r>f61~~%~T!cvs z`3hD%VV$eiD%4$U_0+?Jp8Jtm>$#{kW5I_6peb2)^9;3)yb(!mJLfB0;{CJdMLo}! z10{MGNMC|>pM|n@o_m`c$LE!k=jn0t@p1X&aryLdo|Ltnyk5B?td9Ub&@dC4IA|w% z((;q|b%(Ac|HNQa#lI+L#llr{hmjZ32l78Jc26`)%cw8^DlALsV@ea_w7iz)L4-*i)`txdCoo;&z+F&oL2*@|2% zRRwri2jJ7yQPigN^auLGd90Eccn!U>eVV^LoT+4GaUOi3GrtjATCa2!E!$7Nge%3+ z`f!o}^6Md~F`wimA5J-(I^;x8e$@FM6fX6RA*HO!g4p)p(xG!@Gu><7T8FSQ^WCxC z?yDq0{UTWFddWj_l0xoVe|Rk5PJ8uUzfAgiTpys12mOveU16I4z6zjXjQ`#`ybiMA zg7N;U8~&8KepWP(lR%!Z6^)`Q0wl52Q~n~17oXot&>Z{zFahN8YF>HvTtOK&#HL zAOC*X-QS-VCXDQaYVB0PE zJR;l(5u59k?eF}APd8RVYeeQaB^8s;5}lmEb-obHm`weA=}jNTP+xpidyZFn^h%Gt z(qpeQA(~fd0(+R1eU2BRX+bjcuHMXDPwgEtzWw>lc}9rnh@k)U%3s+l$7n(2c4#sf z1JVaE+jJUozEe0M>5vw<`h=!cKojQkeBQNpyTeb<`!=q9JN>7ZzO|Qr*JQ9wl;$9( zk4ZiRkv64HG35rJ`U2T1ggHcFFAfN3S_6mI~T1}Jli=jObm z6V#W&=g6bHSRI`oZ`zVs$eIh=wQ}n~rKS)7o{vdl5sZ(}d6>`xVMaGaVa(tKM37$a z1}!D4tx!FEJU)G`Ym$Wp&=LlNNYVmHd>{Yy@bkOfc}Ubx zefJTO;{vy-?$tPA*KzqJJKuTdQ)<6{z9IS=YEas{NpJFJ0c8?p@V0~}g} z0r}!NIrWM6Snsew9X1M?rVbm`VQN)ki%6PgLG64Aha(;T^mp{%BZ`V1esm7t~n-MIhtWAs-NokD?`e_D|CYqO@6QIK?G%IMRUEL@+bY>!SE zIYmX%l69L)Cz8M0rCE6}79wpG{YC4udrxKkBgETm`*G~pn#Mwui*)>H*1?*4?9B~D zI`oYhY$!n1$@c1X?LnCflsw7qhfF2v%g(ZQk;YobQykB|YA*32gB zLqi)~uKir~^@I`c_HW1i$G5xp=Mf|=OzW1UC{ydZorJ2aX~zVHFT?utx=l}b^CqEi z+EX$;j4kMLg;>kHAS?l;EQ)b2E6B7kK5h5KTLG(r=O}x(f44t8Bbu6b4wwmw?69$Q zRf6Ia1nQe8j)4%7moDtYyDu{qWbaUT#`bph6H1$I0?fvQMIYdG z7~oz=dyw{o+_P@`2~Lmcfa<9@Vvnz~-@RzKtSS`rIusFsWF>(hB!b0y7B#JDKq}BK z%6<^h1ocF(ojq~GyZwis&zqxFH3!3+Y*d5+lpVDaEVr)xfY@Sk_F<7<7ZQ@RkBZ+W z3oEsUI(H-{Z-nWw*|4_53=$NfN>LN`B9aVn-#SY_)6(`1@?UDZP@YFARN~r?emfXr zy;61BEw(=gIw!oqb}#ZG5B>Ze%11>QvmF6gfSh)c!zJb&0dC9SpFX_EI#Y#MYIj8Y z%;BYr1Wcv4L1j_)t0)w(f3BlAL5lgntHz=W^5CDnCB=aR9$NI5`9c{{yqR!C-m0+O zvr9&mqEDs=d}g)nZ1ihN1k!-n3ZXyCx~1y;^ZQf;R>600ht6Q_k3&hs0&^jHorOd? zt>la7z}V*Jcj(^i@Kv~-ux&kbM5lL3R(e2W zFwvT8`CY>>`+U;Xgf!Xdj>p%*KfRf3z%0^A%3<+HF4GsoJ}bsm15taq!iy$Fz}qPV zD#;(;-lQC52szXcJ#+y(MX~!dMuOF_IA{v_JKUjul)aHz}VHUUeprVhCnm z>fek%?%qhneQLUJs#%DErt7REhhD*cLXJfl51z;VvCLnL=5wu4>&cUVKp*3#YPi-) z=f9Yw1n|;tCQkYM9`Qb&qy-Yqocr(ZPf`T=r@nAvuM;{t`}{0y75b)#;FLCsb$=_} zE$i%BR61A7B=4z=QNoS`Wv{pdxX50c;A4-HQx++NE}h*KB?cVWS41GB&=J#~?iF<< z$|SiFq!*vyt^Ku*RH1TO@lXmA zialuhn+Bqg=a+`Hr<~7!xJ%wI!oZTxdC6aD5QIaC9f(LuJQvl@I{WTD)-Lqe=hwm@ zT?Ui874ITooJ=zjT zn4DkmmfMwCK9H79Tzm?~*;@5nc^#QsKB0-<| z>xB*%^dgw()x)0~#Ak{(rYgCK^VjXlHTM>HA+el5`Zl=sGA>RMj*3z{>I81&B=h$* zv>?QAV0HtIqR3jbavlS45t`6@@gX271;wA+bYLL!vXcz^)1`PSigpx&8jQ9EJmdlB z@tUmNpOxW8A&ZJv#2=sBJ^#>?33&-6jrzg=snaotpEE5cb$afW_m_#ZPbT4WBJFTW zg>%Yp^B_FsMbd6tPi1)k#E!toi1Orj(~SASl;L~-!*k*Jzw!9hc>H7ZKS~u&5Y{>S z|2C2}7(L5$AwRIJlV{gI5n-YG@ISEHpdG>@W5en|`f={`!`Ix&qf>owLLW8t{n-Rd zlV?b25;z`?GT1lJnfLTe7keRq$Zu}J5wIL)pEfuSu}wIIfN73xdK44RIY6C6@A28E z&v=OCSmQGeBOz)ZN5JX(IBTi{_%FTe{n;Cyz17)xa)NUB=AZV@N%st=-7`FP>?~-_ z0frH7?8mRT6W!?n2q4o-S-)EQAm7YLZ#*6wkiQ2i_Ae477eE8NtC}bl-GFhmF@~1 zZT38*auQ|{I-;3Ok03JdW`eM|bJry2dnF{Ysb0K>pwNUbau&Af@-+}qfXQz?%I3?p3i1^F zR;|cR@WonTQU8isLA0aZkWc2-lOAU|`ue_jN|3*Hg^#&k?>{{w$cnQkOojJQSzVzp z2%_4J1m#c5!&uwaW9qPNh=pWH$|{`}B{y$PL>e*U=$O{~9*Nez2qtppPwY5@V!Mi_ z@eAJm{3VAwUf(G$Yfa}b)zq8hr*}WU`}D78w64y`JR_GZ@G%$M4hhr0%!i)vC(Md= zYV!N)+n97=fY*i-a2~_r0NPm=17fH2t%CS;9pa5Rnb8>RDqN!2WRdY^%&6jLQTa^? ze5(KGrx14yy(mnFt)E({KNKQd1TXJz!V)I#00fad$!P`0gzE}AGigptL$<^BzxPQQ zk)VGC{y;&}B%%^;Bk4<8B~Djy2k&j`=Z3wLuuP=D&9r?O7uQCVGrBYI@`S;^_X_E; zuG#1e-6loXZTc6z>rAG%iTc=WA&OI)yhfPw;zA5~oN>X1Yu?zV;*_zaM^^R)7bEAl zQPN)jb^T$`ZypEU?;A*x$j^g*et?V3AM?^Bx7pWV{W`8ccc3aE!!)?K1WyIb5>8#{ zz_$=IgK(pnf{HT^obLU2{A<(~BY+)!HTqY_Km5nf?=d;8Zo^ z3v?bOO>Kj@g$w|;oY!Tu5QG=dAQMb8Q>zmu8Ze{qbM;5bTeuRvgQ^x8J4@0Q`?AHInSAAU&FebY6YV~YM{6wJDb%dmL1`S z&2~on?szuYf5FB|m`vhD@Xax_d4o@{){-7ibm7K1!mPHbT@xZ`PbxQ{bF#e{5GZ^- z@EtF6<1y=*-yI;=3Arts2+NMW;SP6lO-tGwPyABydImBZ@qB0Q=aO!< zdM=MA{R*nH38u7K%jFFsYO;4*k6c8;Yg(KhwRSBv?Waxl=CD@K!ncGCEcPanqzza& z>rN{S*QK>SN$RY#mZqos$@=s-CA%epy0KStJc1dc&nSIu}c*&cBvF-TR68ht& zO`QM)#JnL2prl}r!NSq_3cJuU&GuC!W{EsC5X9BuZ*l{xjCK4bE%OdX&l2oS`qBG6 zOyia@WzDvJ_8+{?q-U@}cMwBQ!6hGf7k z=RBL1EI!L)2}`g6xgL8XT1Co|1ft>2d_yjvOy7u`jMrm-U5dT(hA>j=9m<-mmpALt z%W7l){f=I3w8<2wnm6mwFPqiQ*^VX@P)f9+&Y{W;XWd%E27F?s-Lr5K*6AnhY#ov|>#?`=rnhuHjHkC) zgVjxM>P=b)EUhvxdQlyZJ}B@Vm}zVSprTb&7QW*b>$JAgnLH+|E}O@* zPUBCbjPbw7Oy<$MO|o1(6oW*F4K|zSN1hA?l4eH z6w!~zUBgPSW}rRVO9>t%(plh-C-)J-eQA_-!^N?7~3 z^-)iydDuS$qt<;ekx+}7RPat zJ8GIq@mO(JWXELUKLn-e01#^)I3O`Cl2UcSZF)!e^n7CirKe8->9H57WtQU{I9C=8 zbO`WBH+dFH`21Rfq`}%X*&RiozAaRNQ3-jfp9Qcb-_!6~6wk7?+K)nKxy!vY*!%H}zV9zVf`dLi>CmR&{ELb3h%b=A<0 z*Ve;&*0ShfwOo8yfx5T}t8Oc?dALk_R zT#M148*sDht$%wRBD)F;Zs?MufUM~;UbK6Wz?wGq=${=UEdPRGiQodP_ zeLrFC2ASo>?l4N$nK$?2x_Zar4DYsflQBBDG&N!ME>|Rrwr9{IlO!UmsLho{tr6)v zTh^f{@EzQ986(nfSYDCwnrzkwkk7quCzseHcol23^?0lh+qAvv!pkY`s1mMcc0WJ* zmA$LTg&=8b>)FVXk-wVY$aj%&m2^+OsZ)MOV`M$>-! ziFzq+R?CQWc+?i}{&H2VNZ<{CS@iM;d3CkXa#CmmyHz5ij$I1tVwg@OPgU2*lvG?= z92SfgN-#y{c;x5 zJCGFwsYyx6y)Rt{1cEQ#b3eH_c}DdL7o5*>pj7JSoxTYF^jW)XjSIifpN%DekU2 zZHR9u`SIuv30+9l14w*D)2_#LFwkN?UHEA+(fW9N9518Z`v5XPL)sS?YxGE(j6NVG zn=)c;X~>)7iI4HP>YCU;w0!438kH!JZAq~pmI))8+O<%nw11|k|kUKil!dj@j$KX$txchBrnPHJ0} z64nIC;sn`*OxO$Ukkv`p;|G^g*tO-m4d7ID&nuD=DU+)Xt2+oasm%|W$P8-&1ZbrQ z3VW0tf~LfCzeJs7^a4`enJ}xX9!A~i5{t5yG=kT}Uo94DT<4?odhs#G8ysTR@(=2M zp*iH2F=3EEGhsidCCP9Oq<_FzX;su~rg`XxF1`iFaQk)8E;JISRZwXnMnNnTeM~=B zvA0wpl8kWf9KX>1VTfiMlHs>3ei{)KU^fR@rrhF4#YtJvw#ucUh%6IE=dDl^!iu(d zl|0d`r@>P~SU@>CVH5(3Gg%ObBsr%f=~C8El%k2UngW9qN;fUC;z$nH#krqy;e$#7 zCN4@d!3AEfAmKVDP!mKB9~s38rmo9#KUJ~6T|+W_sI0H8;Vhs$xmpTJi^iJR&mJFp zSX*#!Qyn*jo;N7m^Y|JMy=sKV0Q8zbe36Xi_(*!fn@^LNFou<+I*i9R=ZYf3H0dP5jh@N@A=O-k3p zVv{(|w4k+A>90!rWm~@VH4TeREcj_}@6SSspMZ1DFe3L)aUo}5p)<5j@8lmp>^{CZ zTl_=E#fU)fccaXn;8ngJW#+buFT`S9lWgmo1Lj_2FLo}x?@k{5p9lV>4;+~p5TpKu z&-}|@@_+sVf4dvM`|KAB!j~Qx=J-3FNchEnmu6+9zuW!%W_PxuR}WguCUiHG+SIUE zF~E@%Wj~4-l2}H9)22v3<3eb#A38cqNlS?&-`n<>6~-6@3|4?FA%IE`H6zTyJs=N* zxF#T*fc65^PXvL4z;{rF%*17>Jjl~`P&yv~IMcFEt`=C~?A~M^>@n)G$)N-#;hgqK zDNhL20f!6+A<42!KPN<#D0n4l9%XRmvBV#vk&d=`+dl7Edu)SJ45`>MJ)Do6h%}Q} zBd2^5r8`16ie-MmBvMysjEu!8W1eE^xB|)%+WOu zrGwgf__g(~-e%C-5TJ=WVt!0^TNH2T=`N;O4K=8tyg>&wC;?528ohX4;^Y9CC{F=8 zo+N7=Jyzrr6+S3)F`0geu$Tj!j>>nAW-~kgN+X9C13@8$XataOD0i>DBjZ!wSIlUU zZk}Y2yTZ9;?J~g`ip6_~Q}Iw=@O)@u%YNo{y6JnCRX*baN+ceHUt_8Oju&MFOR&@` zP#=C3ISmx%97sIsjFs~uerE6x7zhO9iRl1>N%(U3Frb>5N1q-f=VBJ}pY^psfa@3XoiiGB}q@d4CCL0?RCYneT{tzwy5 zKaU=>CNgAUW<5*+6YW5PUrwAP*}rtQh7i<$(%pjc2T_<@o$`%@LOsOAV&bX7JK-;_ zQOyqRkrFd8k~n>IFz}A6k3r|6D|&P>xPPJlLLep)1rVg5Nz<|eEqfC?0}t7x7v@rw zLrh&ZsLNRO;4cBzoRtTOo>%wEe0LyVrw`FRkgC}y{E!sBSrm$1JRnzH5%Km_>F}@% z5c)U6vn1Rop$Wj+z4^oB{wl2r$gxiT+5pxn$G5Er)B`0V5KSr1iZWJn3TqRs!n5mP zW>pCUtFn?#;$A=vsT!!0VwZJZAQTY^<_tBjq631@8WxZ|iFT**4qWOb4_!*Mp>B2l4wLDD+l4`ZZn0=M z^?o=7)(st4w}R?^NVbmoFQ~~V=TGu#oWxMwKSoUX=ShUlNxYEafs}65L4JxW1)quA zhuy~p0m%xB^GC|u=Fx;U8}cDwKV)dL0|5S16n8&k)(|doayk^2LP|y# zCP4a+71@;Fh%g9wS2V=4O2!{M)5X)VGJn6Zg0$iLndhe4kBYe?8L4-=5?2L}Osw~wSaK#* zE_i%n)tv)(B4f;KUWD?=^4}A1k{>gQyva&bX46u2yeGLmxgpt@Y52Ui>q(3mCPm>> zRv5G{ymj>k`Y6h&TNuuSo>HTz6rdSPs)7xHp(7ZZ_+sxV&{DMb$x0rpo>5}L3OU6> z2IIP?F_5SrI`Oq0f~-T6E1^JUg*+g<{_Mf@k+&?cKG>cE@6V_TXfz~MDVvxGMnnxX z%gZq=Ao6q3GeAVJB-oCMIO>dW;CHB|3&)ZQ(}rqWQg6i33p|pSO)EqIK4xHx8AqS|@eA9RQz=XX+@z5RA>C=G05m_p21dBdTDcz8$b5>zTGggu z{m%Uv_bbJ$RLZXzgYf+RpWxBCSn!>JoUIjTfhJHBu zh$q_gmI&v|G>+{>rcUBj-L}T+6^L1-zw}IP_03b%EI7_4`8O^yNaDrFjR@lXi+LH8 z2D8HNJHeQ9>F;Yo%Cg+Bzk~4d3-5WEZL#;*-+vW-z!UUr@Km*-MDaVYT)MR{D$guT zU`Rdvow^TggJIUsSV*M!qU@c{TTc=EYEj)xxGxF$1A`t}iG z_zLtzd2a(Kj+y zrCElA1A<}FTLG#_`y%d~D5-DMPFdAHwt#7ag!eAMsuU-09$A7#D%!LenKUk3^mXpx zVnYuW%Wk{IgS@|x%&+E`ehH#uWOSdV`SUE)&wqfABM`VaM_=&NuRRg(KqzyYZf!6= z$>Tfp;ilyf3#|=krBeq!VV#AL^c&?ki$pMys}*cYa>EBg1GE7x`&f%kmal( zuDel3JxgXgpZULh`tZy7yq^?hRx#HFo2ku8vj$@CKsFMDX$~&M@@0zG_-Fy8h%7dF zrg$v7x_pD(WB=}~GDi-zJoewlXctfb@(+Go~oWr1i zL=yO*mOb*>pbyN`%}=8;Hcba%vK*nM3&-QtqhIZe2PD{bJMcowlA)G7{2~YQ!*jQ-1NT&;OYK=M&)l+c)p_yTkb$XhGCB5t+r-)KVWx-L+w5 zG4)`I(=Q05DE8ob{^jXq9{W*{?CbfrPp=!=18J3>US?lDedy5EHrk1GckoZY{Mbwv ze>-WZOU-0DX^4_(zI~uPA0-!vN$ks-(GmR9U+6eYF8%4KGZ!|vC=-#s&|F5k_t zDjgy|a&es)K<=MfK%QDoP8iKbtRo{ZsOgWF16|jQaA;FdtPaSPxOhk64ghV{u@juz zYG8fR`nKHe;ympamn#q#Ti>S#@v@twWP7fRgp`n-JawHi*2XWGuy^0BrRAA4xnTco zmPoM{yR{_1LxzBeE%N)=N{@zcL{vH3;V3T0{`~J$*K|_X$C|!Qi%YNdDlInhW^xBW zg4gvjj^BAWoeY}$|N8aa&!3(Tn;*a1umSEcfJdCc@Y=3{@6fd-v>zpJz}|Y32r&d< zcYu>$W)ysaLPJYch%$`%)k3tQ9?z1GXYUwZZ&L8G27t}W$-Mw5*h6x4Nxo_8fCjkM zif{`sKuT{rUUJ$xg@a5VL2b*uzd{3>Cy zR=~oa;ql+exZ#4iN;58cR!k%-sVV5uqQsigr=R;f*8>DVKq;j%tE+q3G6e(!5gs1? zypMCw{oxfHT>CwWvo)!ji#_wc`K$Rb#c-K4PfhG}V^X2<`FZ8C@ zWx~9?9aYjmq5Y6TF|$x8CMM|gZ3tm^mfVf6z6!#H7L(yWidtQZw0?QNieN(WipcI6 z=0{2g?A$>t4U0^eZy=eoAv@V%m{mN`Y%BesVh8n7wGP{_ENlC<=k{xB``zS4`GMnH z;rCyg@1XV6c;eK-47`KV*~8kRzn|6K-Y@;o`&mt#p}-*!O5q&-)c4CIk^0K}AD2$d~*zf2_@#lRVK=kEbYuC)`3Ni7qUXwT6+~*^jx4rKXNek zJg`oXP9eB|s9e#gD8O>|6uESw_&ViZkm56hi}c}FI2@{`GTwo}qNX}kCn=I@Mp`IV zH@W=;#L;=Id4%!{yGFVmK~V8p5*^XHuZR^=RXuCOv7%82AZ@ z0Lnx-#)6Z^>Xzq$oE^l>H-g**Tf++0gg_#qT{=ThsubnPt3TW1&J0ED5sIy`0(x<* zuAeT!FDZUQ98|S1s%j!wh`A1`8h9^4 zIGO_EbNU%lHrPTzA=))I37Y9f3sDqx>ops_X7UY%(W^Fk)kd${m!F`Jpp$@S_ zcJB?NV@Dk;({ncn9NUuQ;uJCxJhZ$nLMZPv&y%>b-0YsU6(zdS{dH3XYe5U(wY%{*QlgmU3V@@x za8}AWRz0y+0;iegCj_LPp0beeEegt8W$jx$mjVetFrH1*a}3q9ZN2*%pPVRr4|Xlj zW_cJf!wL<}hd#>gUJH9*=n414?*8e1H~#wmAMZM&{SjAO`o&q$sd{k=zLdfZL$d(- zCA=BKBe^+LsTd?w*IDk=XpJuVQX9Ak06Dd_45ucHh^9nylK}!AU&7m(hzCT%5lXym z(>3iF^BwA}&?@?3DzLiPlku|ISaII#s}YV*txXGFUD5AqicC0rVY53V2=x~3ZTXa9 zd%$B{>lVz0sB&=`Aexa%(zjC5s7}1aK@)!daJ~WmcG!Rn`&yTM1CrjP_U0Rpns2<& ze7%IV`N-Ett$OoeFX+vO>(!bMSI*`3o4oyoeftg1?U$8eHunZBa9Z4;%5#-|okcZ{GcOsyBq~uP5A+>3~}@b#BQ=F-N8&M#)tVT+h2M2#aJr zV37kp8vv8~W{cQA-;M^ZZ+* zb?{63my>}jkP`s_byh?wtO~(tR37(#c*1#cwd#~hndANs|B(Q{nTmo%OHe|B+1Nq5KzJ@RnCK*sx+Ii4j4cWfXFqJn@~X5;RdB>Yr2L@I z(QlXOE5hT01sNoQ|Eja&^OdtKDqP}BJ$3Z2E!>?V@MKud4W&Tj@=+@NrcMl2G|A0O zADcucQn_OF5ZM~GZT4{+vzQhdM5a~KCMpT83ip56s z>|rP9IRkS0(l5wD;F|trD(%#Ca#ku|8^=&k*;0n*kUT|;(g`jC2nLVZFI!3!cODt%VU z_zZcSdOnPOSokRE17RH_M`LOu_0R@gc^-7ApQUPfXynCPAbXX_Je#VwAFy6QlAreP zzP?TVUQ`fZONJT{QYcidiKHVin;pICN%OF(jdIURvk=BH!g6ugq-XkOI_Vph6SUfC zDy}eOv1%Rm@YqMk^A{tBQAowFL!^d-onRq7quf; z_w?ggHWrSRf?WF6qAYhxNg3*`-N*3TAx+n=2!{Cj8C5h5ZoEeYEW{Sue!((NFd%N5 zFbQ<#_(w`$e+$?Q@He6#E=a|oVSnctL27TlwQUR3_P0F(G7Nh{-qVPW-yYQskbjwP z2z_L2wKq)Rs8*ApyDgWe^|c4pX;7U65|c@`y5WU7X&fPSwI>4W($b5M=-e-g$IAP? z#e;aa;%~8cUI^HR%LtQ4g1$&+S1(r z^1b=-(~G=O@=zlw)OH?{Q1hAm94}6e08fw4b`-+f&Xazc`lIG=FZ%XE$*wE@CAHQp zBN6@l%?V^67Cck_`2;e(RerGa+cupo+#BWx3%^ixU+Nw|t%(GW##8j%Si&4lUt@@Q z&Rz9_Am(x!vge$G7YJfVJV6leq9@ANx7hNNamqaUAA|nz|Hu8QasS8Yf4ENx7S4a) z@{~d@t%&cl>t~$u#RXKz^0O;YoG$ZXOyc4AAN|Dr2!FqOF~GM{j?t1Zr3I8#r=2f% zxWnV%pFWMqYsan^mM3(GaAgXpE#L_Q4ybdfdC3iirKmM#m59Nnp3b$CZ)TORyV>xpn1a3nZ9oS?)WvomK8H7LTTqY}wEy9n$F4Z|S* zjikt#z~6#}dOc=9cVf1V9ymf3A?DL}-e~E{ged~XFMf!AOkh+}jiIIv^6br0xNPVo z+R3yC^el3T{x$0;sk;7zRr=x<@Mr8V7pne~PEBm6C%DdVg5bQa`8H1{=d8@<6SGj5 zrfcp-tTC^j3yi`&wkNXb{od>###@9l)}n3t-CB`|3Rb@;>l%(zp^<|^;{3Q&MXbrE zgeQd{sZtp{jAI=~>3u>01ExW`HhDdW!a-IiM)Igg1Z)Y8aB{L$VABnL-_awa7^6E3aE?E}MF9!jAAnV5@03A=ZuiMioQ7XP;CAfwq0MQK7geacrDv@uFd zN|5QgBuCGS-sdxmo)dXDht#qeQ=7?mx9*8?X|y;??2qXJB=hx zZZ*xYoDb6#DIklikZ}+WIqD7V$6L0jf5A&)vUmc!8~pVYQ!pa1 z?SmiL>$d59SXPQ>V*;OmBo%e>P%yk>0Pk(l+wI+NKGIP`wycDLd5*A~2yC z7kTfzgeOpBUK6IUcmfc+@IWZ;_Ms>kYBC$K)+dvkXVi zxs6Lb*B&2nb-iqmUsew{eGu7ME;IUxy5X!M(TB&=+8je4AKn~;50YMIujWZ_AGU)Z zudOlm))~xs+SG@`WLVoHoNW(*Tu&y8kc8*}RS)@^fvQ{@Hn;uehu#+JOz}qO z17tWF!Y|(Kpf!3s^gs4^&9O{sQ9=MoGJiSyRYtY!ceIY zzNK6>6d`Ppcob*`XzY~TwL!X48Ky)_z)HB8AXb}4GCF`fKtmG0VuKN1$|Kn~tR+a1 zzJUc;y49TEU;%M$BOyh2Rc7#Z0lPdtnMfihyWa*wUT(qy^>QDi#8_SYONQP+1u3K{^jp3zl8S-5fPId zioOHx90gV>J0>W7YE#4-HN!sxxww^FAh>V{N^MUz4p`Na2i<*nQ|#iCmp}BVinIn&WT4%OU0lORPxv5Ac(|H3U6besU*(&4O4<_6{00mG z^dAYhl&GPj-vMg)A%!1Ud-rna2{rjvnMvFwDBR0DdiVs{Q?X0^wGX+jj$r=|QVr9J zu}|Kv0lBMX`ao`$(RXhJKQwoPaU(74Ep$hma&8d2X@O;gQTO)lHxl6<>*K#p>tI{z z&g#(_r#R>}(?Jip(LwJl8yR?eZ1j<0gslThf^wTTBE|96JH+TlkP)5GCo;NS;wSc3 z*o_*mHpy&Wsz|^3MtuJ^Sr2a@h{t5Gqep|!1}oPNKO)A?4*#FMdRPu8fA*PInF% z(&u5^JFo(YY6>8ae%MVedZY}U9$hZ!WtWtFYt&cSu8+&jpPZyNPr_(rTeM*~BHF_C z3Ai`$3Hv89k8;ZiqGNyiZ#uz8A2ILlUpV^a>dH_SfhhK&Ycdz_{h49xf`6Ot*!$|b zHQHFR$;VE6bk_cV!qO4od-cy=jBxMR?US}8#KKY!?zW|nGOw)zPR_1n9ynQD;56Ck z#gim^@-PJ(l3wxf##L$;8%Puz1_>9UNLGkVH4P`8+#uz~q>z{8n9jTLMWu&E_L)H? z3SlN}%K_cBdTfiMnC*{8{I=MU3-IMih>l0-v~gpT6CM)CA70jEj$5Z2ce%ts*0V(XgikGHv9+JGKNd47IdxkZks znlu>Owes*}UXfA{ejvh56&RfU4qSY^{_2jn)k}bDVd`W*LXM0KZe|?LitPLex|Vm- z(IX5Bg2d>Ynqa%o%!2has-zSlfhn;i$A=Q&ZNhx6UWh%v zmQHH%IPU-2*QlQw^~cZFURBN^%0XBOIg8XOxr=o6Y&Y2?zpMy3L~2lJ2}Ad(Yx!tX zwaK`K8h}b!1OF!ewr`Nz)`wv729k#@%IBrHGJt5f9|P0S z55bVimKqeq!nSU0LPWIK>vh<_^*9Ak75P!9(3hXQxj|E1Z12bdS)a&m`K?`v(G?h( zb89N1X;HY)B%W1=0LFs7Ew5efb@-x|LR_e}u>|ey6KattbV3sYtQ*JbsQ3$%u!!lU zD4k;!kkJO}T$L`$7OXx%BH2sA_)&nU`| z{czA<17;Q~{dfu-owA%jn9!m;AvUDut61f#TY}=xP8L>eLsp8AYl2BcECA%Y48PCicr=lUXSejqaOH)W@lmL-FH5JaE_Yg57l@h%(hE$N0pY zoJ%`gA{fq{BDFY=x5A>q9KF?1ZOH9D@GEDTo&` zhQ+ckZ_B!qO8+#ymRL?5`+a!Dq9V8SsSV9Pi~M6X_D?)vr61$osH`IB>a#m+evcs1 z?Opo(IuRtCGiB5#%T-zY^G2wUHn{R3kQ#hC0m1?-Gkk3i-gS6><0=`Ax9y$!c(Tbv z2OksWY5MOtdkpOCGZPmc(JczG*|At^zqgSC@7RQ4CWTWHm$< zY$8os9?QAav8;)?1#L z=rwk%K|2y&O5d3t`=Re$eRo=XV;cM6c(_e{d>ns$|M{2Q{fo6TYVAyU!bJUTEt{3% zKz#IwQb0k1yn)e84&79M<4{Wwv1~N&)PAU0v(ihnqh`gJ#6PEfE@FS;yKa0-5gA@Ccw#G>wZL$z(9mu0U&K`UPjRd3^Sn9|VR~3v; zsI`4>ap<|waMZQKAMM)w4PE;*&Ys72?tQxnC+1e00*{|QzW@9EsR(N?@cpYx5SNn} zqC}7=+Nj(~^MW#@h>{zUH7zRv-Yod=`bCs1(Q4iWag?hVd!P0%U$5XC7lzzxvF)|= z8tPz%@2q+7U)rv3u)ZoSHQ&JIhBGQVaz1pjt8ZEVM)6)VE6dtxf6W@`17LgrG1R8R zAt1z1o*7(`9C14-gZdDpL;qA~u|-W9kPgvHbA1})bY^9F7<*pNp`(tni?dJB)>;!F zDzsir7JeqQbYWpBE4GVv1;C*WAT}BNOH=wgMWzxSOp!)nlI+&JtXdYx21M+gmj8unqIZ&Z_S^dB_w$N*r%))ODw$zgy@8)VV7p?1%$ zNp=|@J=G@)olT;lIJ2CXpny;$o-kYcJx+;i zcUrS~I7$)TNL=xk7Mxk`l%R1tXx#SsHmnO7G|5juGOm|+c7W!y0{|NSLUIPVXc)Q- zSmEOJ;y#N0_`oE4?EZG!5hO!8iMy|p#=(u({T-PdR z*K(CHyI`S`2AmTCt5TMMVZyOCi)^Yw zKtUc?ej#EzrcrcM#n~7YHPkgk24i$WSzw{T?->s6e^j}0?^QoP==_$_2Fe|D)XVJ( zvdVRmS;0;Hp7G#*(`jLX`R`kQM%n$Zce@uVt+TUtBDI7fe8Vpl+)-U0b?`Ds%q;<+kpoYNe3XnEfG_g27qM1w;{-{a>ok_bpk!3CL{-7ztOyS*D|yEPfr%F7+C&Ut-lV3^&c{*oGJ1k1QoSkDB0f!O zcvV8(SG-KkBk|TMIBgNYt8u{b@P181)E$vXUGL$}aj0Kmd%M(0s@xA^6Hd?prW*;vNtsD$-2c!f!)BAIf}y z{3FTn8k-+Fv5Z3e21pErzEZi%PQ?r=W>D$uDS0dCrkH{fh7|TFar1VoKp)G0fAa#c z;%g?fQ4KuZbSoEliU*OyCr(zPYc4v4?Pq+NMQPSq${wsd3|TAu3Ts4T=c*S%jG-M? zB`(G%D|^)M#65e8>;sgV7YEpz>?E#iweca?c%T9CYfA0@t?e=I*rc zqr}u&4Hhx`DFh_?8m3FZh6vr^=LdZy2fhoQgt{*viQ`BP9`hYJBqe>`Ka9WZ?#?ic z-{n8G&HkL!%E|)pqs6T6{qg;7_vYPi1(@73jZbN2LwCv!hZ@$>@e$9P^Ny)9W){Ys#%*0ZkN~un8VciEoXm{TVAQugr4o5ssm?~ zeLjXm=C)gIU<3U3yMxzQ>h<)r^xfgz`;T`Iry5mvogz27Sa8V;mw|M7w#akv?YH>u zf8>7oq<^Gv&{qr;u-XMxP)FT@pArB-65j*=nSwu&A=i&tjHb0rOI54MY)(8_(GTl zts(3z4d!*3_AIncLi>jrEB9ULqKdx8`78ux97>%gQbe22SKK;VsxVdMsfaGeV``Yj)GS6 zNDP4N^CIJC@5PopV^#Ao(Ddx6dmN=MDq1abeZHDS7EZ}knL(qj-w@i|@HsU-RcSTV z{}CI5DX>>ce2o98Zp4X^tqBVqv2Qi%6yTKVLLZ!%<(xVC|n&$ac-7HI6(ktu}X^yWDd!`&;*=-Gq#gbk@ z0Fi3D!{k&$X|MJNll>Q7f!J#$zrvI`IUYPr^%Z`Nr9y*gGUzhp`IUld7f=6_=N6V% zfD`kChZP3MW)D?0YA5o9+s?3|Il~-W~nMQD? zO0@^Rf>y7fse37q!o0#2YrkhOhs#v*VmI1z=fv{Nq_H|DRnck$9*}A;Evg}UGwd5d zFCXWu)>z2ZK&ut26K_=8`rKDRKUB49Txexoca>(MFk7%I3v{2gyvnl7Ojamn#woV& zDlZW1&i3B(v_xgy4lTvIT;`S@l#?FT6F~BnBp|QR(MUDvNhcPh+FL+sE^sY z_!X(wr%6q%0M}v-@_aPuzZWI{hw0F3iDDuaLEdF%{Fz(xbz%^cxqchp}kDYE zu_J}rb;sx6Dq8;kUFtfOK0LgCcYnfQAJXJ#V^30WLEe**K)?BEPh4RC|7^RbU)X$9 zC{+8L1S<>I2ou_PQT~;kb|xmRD*G4oDb2U`d6D-ewX3Bnd+c0^ZUhWz;x{Z7_%)_Y zx^)5ad&F)lUTX;006~Bt$tBc*K!&IUP8E`mENTC|bbA3dN_(7_zAvI=N$#5fTVKVd zw9GjObEqKUjL;|&{8FDy%%_U3tbqF-UqR-F-JZ6N&`C1X{%@E3gZ&Q?zm1|t;g{<;M~55#*O&3*%f!pEJ3j929v@z?NFF_I^^?TF7M)wloP=qT)`R<_FBoBnb`irAV z;7kl;{lWU%6mJ{-5P0AG04BZ8JN(}S7+h!9$$zK^{-iQ4^x`9?nDrFhnC&U`#y+!h z7tB{!Y!?XgMP6@GyP!Ru5rxFLz|>86%;m~2;`+!%S@VgD7!}LTv;`cYq*R%csfpHW zW#ft9CN$t6xO)bO1Eo8wgz~)3+d@>DLR+%B4N$pf661#QMeKMPSmHXXmFO22ko3u}ni&k6?)NFsnDBKJrX zMR=+ZC49pQ=X`5_?Gkr7>(0)&kizMV+l~10iAFJvplZh94*+hM`iv{|{=dbHyDY{0 z4;|REGFVr6o+8XrKvhIYRh8*6XkdrH$hOM-j~&?>*pJ)yAMQ^S$r`4W7y{>1h!>xm zFF)39yTnvbv67zitzC|h0`YdKMJ}-`L{AT?A5SgcAjswR37fcFCa~2JPVJ~8`}a8R{~Y_NuaFK5=Ucd)Z{pzsOTgPP zRe5a@F13$YvD)KS9^<~h`M#(Bt^@Sve(?y`4L^t>c{_<$dGPzilB|LA?(^d*$7*e> z@xh+_HIj}LFZ9X-2<-Dird#FDjhVhiTaCu;n~&R%?Z|Dj1@Ya8eUw`Ro z_6B7L$k1Ls8-Sm!vCn*$&qiS^w9!V$YR`Q(0q@BmpG~}jxMT9c`#k>V=ev*hr=*i1 z>wQ?(5RckbByK{^o+5TrFi6j74!)JNb&rWUA*THa?A~Aa%8SQhz4`R%{@ux&&cArl z8wj;cE9#h|bqR=l1znrFQBi6Zwt%%0@4{qIN>wF zbb@sw3pCl@*;7d@4~A|Lq+Ix5Tk2;nT)YX2LN=l{<8}pnn+~W9+6cOy+}*@E)J?v@ zZVy`bAx+B;E~&B2TYL1ZN(`XqX2mW9)j{p_{+BnD&}b zYlN~%qyHSO?RKZq==7^r2mJLle+8=&FH$#^`66}GMNkZ(reyHd{PL*FXF?wj(z_N@ zJANp6Q8jBlGIPDCV{`!t3qAs^a9KFB)|E@mQDcdUR5>JwEhN zr@iznCZz4u14LDUaU~fNGm64B-q7UMu&Pcy{-ohd(x(6*lVvIW3 zfFDH0+7xDih@-gWE^`92xA#D$pd*0N#=BWr!2FX*p>VoLB(VU!&0V75<#m1uaZzzc zXnfSNvaKo-XZ(l9McjG>pjVu* ztOn8?P-udNtjEQR&;2hLivPHKx_@aH5+b(R%4a)dA#>nWftH5Y5$0JTsi_1BQd`d) z*?NfL52{(^!fAZit)U3@i&VS0m^-ypD1CSZVZuxY}x9Cf($H(=wS z{?=w<;!*v>`iH;3;E79lfk7`I@FGK>&~||h7CYzR1A?FHaCY{#6h$47HX6Mgo8sHT zZ6iaWsF6or1kFaY0v?IiBuwO412-fA{hjef?UHe<4hrz88T8JX(ei(HDuT^bw zDcm0L@?W+xN7Ym@oumayN&*fiomA~iyi6n1qev023fn+NB3iBuenBRY$x|VMkPx>ydzocLOcgFo2 z&$z#IG(V4#?Zf@{?=Q!O$nu?e^ZPeIEC~~v=NB^CjL(jw1Jek0ljQ*9c3@a|M#UXk z6tJ@#xs48tFd}n=R!r8Ay+kl-^M7EETSbDsr0^^}?4^XEiGpP>XzWD=#0Q1GMD2yA z<)w z3d<(h1#EaW0t4oXa?@KmzOM{1$)I4vVV4DlR3 zK$QW}QE?HtnF6*%KMKibe-VwFxUB-9dsPLrp~>D^>r4?izYu|7uakmaDycbi3}1$# zJCz{`rZ#1Yv+XL2ZU?;nd`tAtnNfIt5YmKd!Hp*lh?u#H;)YQXf)-PZ5`+w9*(qj; z3*LUC8qgVrKyA*7y~v|H(zM{H7E~;-c2sQmG~lR>L=C9JhtXSbR1;Dx26}9u!SR=h zlc5O*wS$KrRaT7qN%mV7zXF%f)g@pkR`(%D6RwC-YNBI`u6$p)7{7M!~Nr@mp*`U*NOX}7af!# zovo16AhQf75!p0roROg&$4$t0i7f5lQ}pEnEX@+&A*UnD3pJ|K16`Bm9kh{cBs0YA(=KR6KZGY@^pclL*kQ zE_8A3#{?hXt1FSKGO0tL-?Psq=X(HSJu0so`e+s(TTKig#0MfOaHU%t-r%tz^%N>S z?3-JOL8-?B+KPeOTsdU@wn4XDMSRwa6s+WQrL*)>5si{sv?mN>)7O}RM=N^u6+01Z z>1N1$>B8f5EvwcA_Z-olm;7_F`%eyj-P^?<*K+*gC$~*`DnrK*%kWAXLwVV-Rw#+# zgLBetb3{@MI51ISOw*eSku5pz>QOCm#?Mg*=eA zi#wxJf#Ky^cFv5-U}SZL2Iwj+V#O18S<4$S<0-*qcB$oz<7{>6A3M&f4dR_bXiUwB zK<3>mGCBl^x6Knv9N*V?t*%6^pNW`3B$XpZRD4^?mC2gKeH&*ewQsV&t&C!#V&j55 zd$dxM(3->^WKT}6A5_Kt&P+5>sazEW<}8YLQ4G=|Z%GP-ogg2;WCyv=KrSGOvM&k};LOP^KzcU#HfVdx}qLK*_cRSJ7D!rBy^}2F=;XXBeIKsPcnVExN#=o}^_wbGflDA`4aXz7cVpb#&Ne&`Tu4+tT4a zt&pe)PD7d4Da_m|2L0QpK{1WGq=ci^Au3pYGngdqK^#b^I5U=BA34 z^<4N!;mK`dMD;Y=wTfTYXl*D$7R5zA;sRed2D<%J~O>hg&FYniuW(JmR`3>U7d3P?K2d zs@N#oHaWQ)By*UhmFlt%+Dhen7a(J!9EZho3$fP9<4+v+zx3^p+x_jL8`>vr?+>Fd z;*0O~R1_}1pk1&HfA9^3Gz!m&mkwmcRb*5Md9ec19gkpoXC%>f8P z$_;=A@vu({X5Rnn3D4bY?)UCN9S4Aq#{+=J(9`66etenmZt`TlMvpEZ!Es_;#0gkcnQQLIN$C4+*|eT{?k9+fBgISk2fEc zG3R6h``b!>5|PC(fH1F9&V(U9%$McwOXBxevtWHiNV4Ual?9N(e>P~gevYM|ko4jQ+M;}}Sj3Z|kfCJzv$|8Ai z(gxAH`_G@?z&~Lq*scr(PD=(Q^9UXA*y(`z_p=sY+1_V?051pFuPLLyAzJ>{W!B#b zdZJ1*9Ol<5U?u4Q~_CBG&%F& z2bjBJ(qs>vP`D>DAIoN+!To7$+n(5cSWkP8kY({8uebl|UHH`dbxj68thhLPBHS{z zy0m}Q1M!!R%m3_moPPHOS3-%G4@`Bs^uU$n;^h;y&Ak6K-v8(4yWJ^fxup98bolp! zydRVuy6uW!RFwqQTb@5EH%8u&d3jc1q)uO((@log7NYvJ)n!z;^~-ceRQe|FxJeF+ zvvuQ-A=l$%>rdSwyA@=eKURuq71{Izz2x98saD3cA5>?>!xYtd@VB%T9G22~tClDd zZg@pCieIBfxq(T)pqEbyW&7?fu$2pSWT#hTaOLi|wobo)9N*sm`ev(+KT`d23u{1a zw?sM8?SLt6bWXaVpK*wu<9&q|Y^8f`<7HGkITg+#eucg}5BWt|KQPM;-7IJSKOUIq z{PVy(H}pgQLDUDp6XegseZ0B~ZBH)s<>@SxC!!1*Ky?>isV*?Q_#t<&=%6$5{IS=U z`r>p}w8!A?z$$N`m0IeHvkrfBk^QJ{`zVm(DukEM>WGP>V*6lt|I7b#zx_1+_VNAa z4=*x+WI17k7Lw_;bC&mY=5Rr|&3Rp%4i~NQfa$)kX@_gqQ$*&weD8ILSN;$Q%JSsU z`8gk~Ylq9-;8f%?na7Jyy?k%J^nT*9w~4anzyGd#pI7b|jpHP@Uw%S!m?5OPxX8n3 zdSQ_dD`nA(RsI@m`-L6F`EQqtx5iX2Z$>3bt+?p4K12PZ z)iq-i!@;5O=YwLV;q)CR?s_;mquO8Rso|$-hy63``v<{QDC#2tbiaCf?B0RMdnBPg z5q-AE4KyBBZ#;R+iPwHOKH)D(+ddt?+1V>!zN+=->O6k`W;br{-o4O_LvjS?BD46k zN&9S$9{jv02sPh%PIUkDC!m}|pFP*>*e0>PluKUFOA5DwAihiSE#gYH{ow|>ew!Rf z5stiY(~noK;J>tO91Iy^NbUHs(58)&sRJ!Y`Dww&OV1WO$w6HAY|~0q7KPx+?S>uD z9+#E%?V^Naazjnl02)7m^yZ_GKT$9wu%Ww%4T|KJfMK^m=6Qr25U-BRwIitTZ&|#M zi3$+qBA+|Be85al+SSEZK;o{U5)dpRJpn$=X=fsX{?uPKK_G!o%I2qdq69H~z?;`W zN6(*+j^ld%(Q!#Xmqh^{L;rs4a3#>K{gOe<1B|uzZ%?CbJ%w{3owc9|`Oe-W%Wsyu zZCKi{6P9`^D^cN||V&>yVFy3wryzlwq* z;Qh35Pz9a~f_jlT5>Iv&S~!*+Fac7!@u^5^pfEVb3x{&SL>F2~780}22t~5F=Lk}^ zCVScpMs4;a3KQG*ZZ+R0RwWW zo*qyT{CKrpW|vj{)Prcpwxc5e5Yz|>!tCB8kjs-A+s-pSB*5z1=y@QfD$iJ><%Gay zvOSOkG$|H_l`yYO&<}f_bz%UN#Yv~Oqvs}On+C3pahi9-tEohK6JX5(n`2R0s(EG6 z0Esk^Wb|sJdZHo|Xa#6cfYPof?wUMZF>Ycsvo%b*r!i#x6t0p1BWJ37-vN76BuswW z$K9ZDCv^KVXxxoH?ljR6w*$r)0U#-1RJ z5oQv#q%v%xjB~xC$g$-dQN-V zg2Ly1Y&9Xzt!zC-YaYI3Jn_vx0A4_$zZXBY6-_W5uwKv?pg$F5JF>Q|i|db|YVqAS zFsdq^93OH!Wa2KGFkG4bEet?>o1-E|G_*#|grIE^B|8E-%9C_eR%zQS1T4_;ym%6g zMKdpXDu+qPUJw%Yhf_8%L>N`s(f@_SDJ|XZ6LUsZjgUQ^4a-J{=W12`B-`ZC?mbV_ zGh!MF*4>)llh4=Az~G<0))gCM(G(VIQS8A35~N=qyi0I6?&r~Q{;GQ%D)#D;R@spBBu_EbQGd?4uTGXz@|9~JD?+H zJk%uEu*2&PVLGGCKb~sv9S5Q`YLY-^7c?IGq01u*pZ$uUa^5~eHnyzP+KtpvN(p3T zU0^TxYEd}FqSB&;Aa>grZc0>d;D#!u-(BEiy9{F&U2-@d4e@v!1ku~33t`6r73etX zcAWk7CQlxxO{p4OvqHB^xzT!OTGouc#?>A+nIeE*6t)R4Ga)}3zMnt+D#Y$Fr-BC% zg-Q9Fk_}bR;FDkv%D*Jkw+@Aw3AZAU(z{0?+LE$5hVkH<-Kep8=WoAm^MGrKo=j1p z_TlLFE_9}NbdyV*`S{YcdylUj*T&O0i|rbFSxc`Spzojkc9Oc<`?dr~Hsv#y_`P5W z9-?iuXRl%SCBiRe0d>z`W2-x~GR+MlzgH^-_5W_IZ0>x$R#9H{->sE(r1yqmN?sp# zR&L+X7q_{c=?WWjAMSp8ktQc^ZDX)BAA)**Ol5W0$| zWuqN)^3V*#gvsMy63eXui;*p`qXRVAjquwB0lmm54X@QAymnD=6Y^Gq$!QL}8Q^NL(n$Y6v|_tyDByTSar7CF^BH5DHGXNZjWzgW$QMnWEH|tR`At^Pq=G9V493 zQ5ToHyVbFQP4lw#d>^o-@zalNxXE62R%6E4eSTwS7aBct$9nVO&kSvHP#uJfAI6nG zeZ@EN71#7-SoLMkw5(3-N$2JNYiIN-rN=-LS9)UT>qisJ(iLpiWnfyf*nuKdQZ}!j>=Te5JR@qXl$xe(3v zkedh_W30QN5OxwAZUc(9z>gWHsELS5Yq*xXIxZOG$k32@--@xY=V@gVC5n}ESbh<`?r!1ps8bN3CbCnP}TU- z(i)?y7j?8I3OgbeFd0CEuxz=ihzOD>C9d@wxwPMQLk5H#!JuMEm@#QC$r&;{!)j%e z2z*rn{u#v&7X(KB(8`f5p$>#H{Bmy+ODW5dmq86O^Bd7G- z^rfolXCViYDB8GKH`4Se_Zv3~tJ7_}(H}GTV{8zWg-}6lgUD2bVE{`+^6QrSdEDey z`eO0%Zu{{0)BX6%`~UZJxya7&#WslnqzDnM2m;0?+(&WaiG!0q=c}|#uyM??D>6IW z!YB_JJjY4M)r3uzAO!0&kRA;$!Z^D(;X$AP(BUCokti8*?t!>af#r!bR1&7yBQr#( z!nN!h?!1rM*2u>U(G_jaX-;DCKwk*ln~`& z2aD(~KyPSKpC6EdDPvaL=dwMUIByf4Ww!ZXHyu2_={aZR!S>n(HX@1+v*|$;-GtY+ zR|ZkCYJk?S6x*9*!cJ7xK*Rzw$*b=|% z7Ayn)3yWb_g(Zqi4>6z95&f0^{^s3pr?{mo6XL&&Yf*v8R?YDsJL~g?-e37yFnHeA zpHojg3k#zYfRre|pUInu>{Vp=Oa}1s6So)WG#=4@!@{^bCrGS1iO2F|o5^y6|f# z%H9(kvt=$1%2AEe1+QRMF6M$+cveo&aH5q^|YO9x(Vowt*dt*2|svnQWa<@xM#Ceu~ACKk- zfZq^BV>3_gnnbZFZvS*1-k(De*8pm9RA#c^%)M)%;Ce&1kLZkv{QZVev$8EOMV@V(Nm0@?V!M3~ks!WsE!cFb9IaPxA#&Dh#HYw|N z_~aR0Wl|)&{K8eD4GuK9ql==}kSVob2KlRrx7M+|Il~?^)R8BS;d{nGZ^!$Aw49OdwMmWct$Kqx^`A z`>Z_f(iRZrTbf29<(fh+ormG;f|k&4VYolG9KK zTv~}1$9smSt%`SJ5$Yv4c69yYIy(IS ztv^EZfG|;PiqE|b&6&yKYznr+*z<7JU`xfB2(vjl21{L7{oo#sZ7(SWZ;_K#>Fz1K zR!s>!^72*}?QzU3+^%3Hiq9#*N>FM*+Dz;Pvtw<1QVu>Tg@{Pn*;r8JkY!SjKB=v8 z6Ue%aoY1!OStsGXWo;Py+93037RiqodE7Y_M59Ci!mJ-z+*m7`IDPc#KXM_j6RK7Z zNuGIBvg1I>!M(hMBo1Q1BKB25-A%f9mc7;SOG5NzO+mbxQ5fzl^RBkbpiz=!{?1}d zkVY&t51^nZ+9=JFMIs6i1c>h%FB9DNCRU;(KDj90z70GuJ4svHkF~@UAX6FVbtDi(A$XChMDvb$G zq|gF&Vi7^Y#-#tPYae0!+1Ea6t&)VLfJqi&=?0Dg$wOn>1rMLU#E@nAMrokT2u^~d zz=Enq9=wr@2s47Slpq9@$vbKAPHMbu+PL!FhU^i=KFT-@4hO-@Ju`@Hb#hD##(H3Dns%Rk~<)%E|3Mn4W zMg~2IsU%#JtC~`f5p?JX?Y#tyJ&!!Emu1tkprWml0*a_C=v zwe%(3(q%1lW0ON#74G5FA-2Dap513?P)%p`gJ>g0aj_ly4M3NsV zYrXc0>zuU_L?Bk9d<$0NDy_uy6nU6MZ04lhNd!I7F`p>li^AU*)B;=fX6;~cPJ>E8Jgrjx7crqM%6u*QlVo*klB=Y?Z`oP$sQoWv-&c&*j?<;gV#*$h@~@+*@-rRcs3NE@u&%P@1T3fgJuwlZ{Jqj3=kXq)pK)dw%p%sSyPvK zsCIj3GjMV7Nla;Q=7Pof()#Km&Ee$&(@*;vLxMfXAyn9YtJ+F`Sj4zVi+bo{nX&qZ z`6j}i#_&qFGxu+s^L`bf*?FQo2@ly4!ZPVx?(Zqx4=v*?vK zQHvc9e1cRjS1e1TAKoYdl8x(y!GmKx3e;Mp*Cv;7b%n~h^>P$ZZkNv+)*;lM8>_Ru z5ZNHn+wl3pG7az&-BB2os%dR29Pg8aYD7&bf=1zO^0!krozf-RHl$8k*r*4kaoPpp zh*vmqoJoH$_?O-;2v1$Ff~=32B*5hQL_opTTm`RBQRs9*kUf>4;y{eWNoz3{pdF%g z2>vNw=Un<{c9uCE<0S|{?rr#+XyKnnb@g-p`}Vn*Ivf-RoP&aVTfBeQse(@$#Zg>~ zr2d(v|Eud*4(eF;bu>XZifVQi(ZpKSc2Ee-%h zXGc7P+R8ow-nRa!GlPEp@{?9C*{D;z!MTgF4T*Kw*`#QK<$wTNgjh!B3E`@UJ+ETk zv%OuU$Z~HEho$X%=G~iLUiORJUpVcmSY75tkzu$y{LA|#JJl`A5B*YYn5s|oc@qE= zStGJT(U1$77-IKiCJ}JSMsKK>^DR%>{i!l4%Y#|26^^uxuJhAMkr#pv~4vm&j%cPX{nFRQ!H zX3J$YYVW^0QKOC=e&3`={RHPOUSO-;jXXLw^0AiU%9l@TJ4)`y=UWvK<%k$QcKptT zH}I69HQM7HKPo25>Wn9wa>Fu=AF8=uJc{!;>AQ*Z&ZZ0u-9(Apq~N$4dE~SHi4u_m z47>^(!+prdcKjoHa@$<*KL!`A{0AA-j7d`N2%31#}7i`~IJMWRflA)?-?X;-fyl%j z*K^v;#L-!B$7Gc_>`T*u?)dT(7Pp8q`3VODpgYQdzZ3pAecTj*GUs~TL`-nd=^sCz zMcdwcKm?gcY`DX5u?wyU=u4`dDLVd%1=9_^?T*=RdW=3hSU~%?hJ|Rzog*K_=lV}f zy_5g4Q!!p7BeZHR)N%!|G!qboOHC@5JnO)<<8mVrdBOJ@`pg~& zR;5b7tz=p{l8Mc&2;T^-;Pr;+N8v;?^?|R;;%zKX0NMbI7)o7Ig<& z7GGQMuftyJ#^x+Q{jR~+h&nWH4?Eb8dp9dsul5oC{=@Em+}?fs_|Mab^Sr%ZZMzrj zVL#if(^;+&XO(`#k=`DUENYA*+vv}iQj*qTc@wbm9@^-JlVWhtF-?7$MMV_DoK{=g z8w9vh<|Y_!t^8)Q|F1VAUUB9s!vKl%;SbrcaaIYPNm za8{y`JKJ-H_&Fe=HU=flV9h#o47;WwqR_mn7z1bLzGNwquA6KCEQ;66;*P|ZStoZQ zZO~KL!qr@5^j<(4SQN_aSZ>5$`GI#HfsF{i6}B!QYAjBbj7Dcy?IaG4dGV59T@<Ir6S_t!w^9YHkyKf0A9`#S)?b2_N1Vd{@2@V287O{3@d#>klxB^1yb*rtN>MQb}^))rnBsgeTDh}%KHqlYYU!41epcT zH2Brq0Lk-CI9ZRmj#p8vwziP?rnSsU?=s;a5*wYf+D)r|Y@-nJ2L*~HfIE1o0jpY_ zwS~n%Or40&r-GBE;BQH?J_|TID~b0aUp-KiS#CBIb9*jX3amT!RCn467C3ipN=3~x z`%(A@KKv4S!zPq|I5fmLoMYp~KrIl!xEu4P=W$q#KQOX04|VZF{piZa{z;vqEm zAGa^#O5-B?5Pk?e0o92?IwNaX8Cid(Tl)_xUO(#3$Mw4Pp8$k)I;;FV9;-aZs?@Op z<#7k{PI#rLhn_q32fcXmi)xSS5$9O=pen>R|`Ze(A8ke$e%UU=Q$5Xv%t_`wROuI_zcc;KgCp zoSpNg7nhe|UkQy_uI@|o$H({Y#$VpQ|MYOma5a0TdokNBXOlamv(ZvQ91~9nhkC+f z4yWvdyupJNfN~Xt+<+>mqSz`#S;9L9E7fjr^&KaXk{~4M##h?MmPxBg6h%@*DZOC~ zM_G{8bS?)Nc1a&Di3}$Q_MK#{$>i+~^G3U!tPC8_JEh{t$@%w=3+4aEqq3f4Il}wk zGO6@eiH|_rcB2OfTJg8Bq+QfpCyfBl5Lga*CAf+$W5gzi?Kqr4-aj71!i_yC2`K%u z61u!Q3BaJ5p*rF`kcRS9&bQ~8$jZWm%TL92L0$0l zo#WX7$MZ>Zvray|e|H*DIxXE@(gjhZ`N?rp63r!JDqM^|6!eiaH<|>OQKlZ*w}pX1 z&?VWzPJ~Y6B{6kW3Cm=#rd5@BLW{XRl^UR82^B+Llinb)EFhK2I(Uc9P&=6n0zJDeKN|%K@w!qeG>I!abYy$rkkWzR8*6WwItAEMmAntyFxVg zRflFfXNYS~UL}Ju)e4@!#ny~=fOvrpZO}U?O$#sjT4G)u`d7!rxGBu49)g-r*J}sO zgMYeoC3T~T*~=dGItrxbpIxG4>D|PqA!Ey%iq=WLOEanWRPnq*a-M9vrFl{ zzttk>nQbnv{M_M##9&*^oG+`n#l19^WnrAfEg8*@t&clTl6k8011kCtoMGWYh ztY{|y8s(}$IiOYrIAIqTCIUyIF}*Op{`|=nqPyhODGTDGDR{~V)NV4Wl|A(e6=IAE z9L2yMDJb3~U+QGmH0fSX!;Y~LEH&>6W;xrtGlaW?n8@i6?ev({aqn#;r9&H*J%*HC z(tzb(^jtFQFgfdnn3nb;>L_b7RM-&|MaOYR;n3YP95^D*afPqGi{PhS4o3L?X;H#N zu_Y&2hKV+$5gJ6DWGs-eqDJh_S)rn}+zCw5C$MRIlUtrm&0aj1RH;yot_q;fX_(lS z9Xr+HQgbWNUoAu#SX)&^KSuBi=vDMTh#F*jzNHH`k~*q@JJMQD!k4b;a-d8m`qm$3 z4;sBnr*#R0L0G+aRW@$B;3Sh zIqKin-f!#T{gUzy+GJg z(`Rv0_3kTfB4QO!;^Q!V7pZ;0^`x)9*lR%aFO!1&>emNF4Fuc9h2o{D&#R&yKrsW@ zk6|}sq~4*^^7>)X)3{0))5tLz|iSj5TI)Gs&^?XIFMXR*qKT!RA%vx3wE znIM9vN(z3@*+&e=k2tJSJX@vXS9tl#8diHRkJ=}D`w(9_lgDXTAc;u=(j}P)r|dX0 zoExklJgh>{rTo-I*!?8;`=k_nMLXDxiHR5631}s*yok7GV+w2ToaK{SaB)@wz|>S& z^nw5zgoFE6pZlY)DDGK-32$y}byXndibnZd!tU^qP~D4BS;J!%&8Ms*2bKKt7E+To zk4n}xZ}DPgC7zqaYch%Dir@(81bJ3SJ#2i0u%zhWW{kx-%Z^wGElp{sm@h_WDGJWV z4QEwh{aus<)5a^Y#z~73Q85hjS+V3vp3_Q!Q1NKriBqAwM%;Vaew3G60ep1Lb3wbq z2ScdHJuS#PKEWKR^t8`4wPlfq5U!VjTW?cUifJn>N?4|M@!|#TvEpmU)nietg+2dO z1-xd{Q)EmlD_5jY_JpMcKxCq=>$`%Pnvvy zJF4X~9bw&ct|t~DcJ{x7l^<<*zP8h#fhtB4h8hlbnVsSQ`*mxutVfh=n7zaO`2LU<;T1GviipKEX+gQ#@QNRa4%qXsS_VMtxwsg15uAS;Z~!NK&~#nLM=z zTR;SfsA7e1LULuekRmffGd?O$7dE_-+;o&Zy(bANuST5{9hH2PB9Rtx^@Fks6sa;I zpu$}#$;@Pk+Y9E9n$MU+M2%74N;D&@eb z@03y8iJ@KwI2?s}4~NkV$FbdtU0~IT4eg32TP+y{wg{@!4Yse1oP%<@;Mp6+axg-A zAPxklJV0~W$ciQdO657&I%Qo!$cQAULW6U=(VN}w@pKTkyt+{!51M2_8)Ouf0F-m5 zkdiOs)N%26>bU3>4qSZ3hmqHL*DrgCK6^ZJUOefpfWMmwL%=xp?PFuc-z$41CAkeF?V^WS@wrV>--#(zJfSWaeP${kv<_}u^9H0$72lVm zFJJD)$K9Llix?u?Jk^!VMB8>j(Ntj&@kcHLI_I`Z-xA7nEKR!OZ`S2oL!|aI+ zqqjUZQr|g9_|b`c&e9RzCJ;&WXmhmY^6(jGF>D9b70(Ori<(*)_Bj`iKYe8gL)r{gX+v%BUs3$0SCFqLzTB>jqvAy!`fn!0d+4P3g9UC6g|a z6plY?wTki)LDOfCyb?6m#Gj}XU5GACEcP7x$9Gtq_XXDM$eVs z@N{~0hQk*#_nG7HBzX$K+=^_U2t(Sn#!I*9jy-}@E&kPqiYQ~TGbd3HR)}^|**bb; zW)%fibvvJWJwhE+D&#U{vvaw}6!n1mf6 zOd@(ZjO6i&+9Yz`k&6>SOWa6|ES$i!DHiwz=xB4`lQdD#byFCf%tGY-7^!Z=lSgF0 z?9(kF8no!qKZElbNy=(%C&M8*0NNH?O~VLReIP3kGb-Om+IzhM8_=rk>};0P(XS!;(dPymaxJC9WK&p!OHe^7qh5st*L zAc9Wg52Iirg04^wqhnEs%A|0aBnJ=t?v0Te<>^?&$Dt@0cvs-=Hm18*X@+1XmS>%vgJN+PHcVtu7bh-A(@g)8gHO z?E7&>oW)}|0YyU7MgC?DQ@Ds9gxlam&&pYiNH5H6|8eK{n<$KCusH4!(||1e*_Ei> z=ig2YXo(AEfxy#&L2Ky^tk+^3Z>d8;Xja=zw$cX z5|5`tt$KFEpcJXs)+d+KS?npOQeiC!&ZdM zpqP|~ya%{uUL=%>X*1$5F(n^!Xq5%h1e>f%%jISA#pGW6;_)&j$lCTfBNBmPhE{Ge z*~TP;IV#fA^jv~r7L0Xdb;_gCR)s5+zy=u?2-iZ4>m>U|BXAdpzOtc0ITBs4tTdnr z5>h@IBx+O+KUzh(5pHvnCq&1E_#v#%a&`iZ76`G*l}qwMBCv&ni{P*AFZj7k8bV61 zg)Kdcj>?__zH=er#G?4_7L{mYvfE+XATJd*9MO;!ac46Y5T9Bp0K84|M)WF(<#hQ( zblM_6AlR=|mn$)5*JYULq(t-%SHz$|9&ZQ4h3a4ln_sh{5sRWkP$&f+(kq8#{(r!Xr^^OjI-3N3+-3qmF0&z>-Q7LcBHjs`7Gkvd%| z9;%C79;(JN(TVaA4I>g;B{mlcSCLZIR#1GIu#aOm%L7iv4*DvxC2Wcq!b-xJkz~0i zWS_>Bi44OVYynz8CWZK=w3slB5k-D6IqXc8&Csgb*ue9ro3Y>$a50Hfy1*TR>Y~4b za7wi-}8oE^=PFnQXaAC^1BdS%c?U5e)$`eevUGd$0C4o2l$q zwWq}rzJz2iGd$~pS5C0b*+5sIO=)uRGmB=D%~``qkFUZQK|i9kM@==*R9BdkBFyLn zpRsz#)R;8~vYU+c?fq6n9@;|hGZ5Lb?QCVp$>Nsi#+&T+oJap-PxH!3vp+TN|JYMP z`ue%soh+W!vw;7rHl&||Cm8C=-ho0l955*7)eEsIbP9=-#**C0&v`b*F8>Tso(pgqhxu1>67aBJ?LRS9p9f4rgia9%nWc_gsWWH zUJ}7sZF0UXKa$Nhq3k7$LUaHBK>>1Gg;WAZz?>{AMDruiXj)VxD(shs2W(Yag}9*= zyL$$NGC_{Q3u(W; z&Az$~a*E*FnZ={4x(^tTCisHgr3kU8zxpr>7b7q&$kXg=rC1uSqGI#dH5kNGc7uSN zxGIP;hPnU$z~bcP+h1eT_hl0p9-c5RLEZRoG+S z4>0wk{4PaN6Unx-Od?|iz_F=i*OoFtTdjCN=R!^;iZKH!cV#O4wn*c_n@;5B#| zYzfK~0G&jnhCiO)$k(Fx<4)xaHhy`DhL{IWSc#zxZKc+I~DXhT}MX`Raxr2{Eeg}Co;5Afy;d(yR3IuQXp-QKA#`#WjflG{Ef(3x?+I; zdDDCl%x-_zMrA`q)GhfTO>&;+N=&5s6Pw;!V z=U|S0*oTz-Vx)6B99ZI|U=%970b@&pEBdk>G`#YD;f_ZucaYJs#WO>!90;dJt2fJc z6%Trx{ml+na6m_0Alaa6pe7 z!GZ6HhGUvi?|#U$m&G9lmHYs}$p+%`6(w_N)}H=hdspt-x`-7SYjmnUwcrD6^1g132%K(qfG5;E!8SBG#+q*?Lf+BtTS3 zO*arLYB6n(RN)AZB9o=rj%nU>S{?zZns0HGC(hE>mp|UW86V&N_8#1JJW4Euk`D_E zsxkn;magZkan1vgsE+Ue5^U8CF|}JDTn&hFs+yom)y+>$Z#c#!)($pqhcD z3H2An&Oru@L=!c~4wrGZDWXrwGyA+%|LR%^bEovb^lHg0_8Hdh8j=*&@t zar)94_eoBE=TO5$TIsu$-*7l*rvx9ISeyy^BbmXI$+@<)rY6=|vuN5JSMZBXhCjt6 z_~#;oKNrk6#>os!oiqSpNlNkxYK0K5S7{q4eK~<#g*=t052mDee{l#VJ)DcOwPjf) zfHqF93B(|Y2Y-ff&lQz?$D&oKh>x!T!Wps*sYPrc$1s5JmM9NilT zM&{NFBUa>BJ z`~J7_;qLvL#}_~|M}9`&3=3cj&$bO~vMsyw^3*PeYaY*CesrLV=g3x7j+8qlr7mi{ zWk1iNHUuh7^*KE*31-@j-KmU|+5r}g*N7AvSk|F~4QUXl_M$zUGKe-+@)?`ibNtjC zpl1aY&6YRJ=#wT5&u7ShV-tAPf&^zYS7@0;#zhs4;A693!ppouu_kFyE}@GZPqOe} zY*=%Pgz}ja1rER{YtlBc(XywEh$;#u<1MwS+_6vyI1bkBNj3u9H*|#A4zcxY;}}ll z%HF{y&eYHH30eiG+}c-I=a@XR?bRfLpTpx-g&2VO^(HN{2SXOg8m20voM%WJr?xus z1T0-erKtE6Oa;&xwfm@c_dZkiQFc@{rl7qB1J`$lR0C=i~RZ$EGb<^tJ0SbAaDv)*PPMsd+@tIy7RS^@xYv%zM47NZ=MK8 z*&iSMOTXD4H|>`?;pVyH#DO7til0=Y$|YqCwWjB|9mjoN1#V2YWipGymNqsp1Q*n_|PMJ&s-;6b@3Jh zSHFYS*e27iz0%7eeZ%Y;`Ox8oD#-(KEc$r0Ynt0N2h%C)!-MJcTFdrA=Cjsy^v-|B zW&J4UVft&0$3S~mF=IHoz9s-S$VaRlSaABWUHEy2mI|Wa{8JBJ9CtI=1}C36`xp)f z{Tz-$MF1P;b}Yn;FPsqd_m6KO@H+J~;;(BO#)kDR93o|R5jQ@s58NUNDL&b$C@HK^ z@VXndmgXmnfCy2k}3CXn)!a1vO56%vrT@Ids z`KRXxbe$s(^ZPmzfHI33fX_j+qrlnYX~_O8V*VU#8`cc$#}EJS{qEEF<}F&w?_OY@ zvr@4lCy0~RIKGAmTos}(VE$Yq`XZ{Yyb9Y>u<%t>pG~-1KaiG!zV+qnSg42E6WEXm zwSmTnj0?8?Buz;tt7&d<5nYoz*Gtw*V5TP-Yftz!bE4R_RJ_ch@ts@d%Xr2=K7Y35 z^7ps*UT1b($`YbduSM*bQDCcDD8&NJkB6(w9L_JQFhuBDe<$)k8{TxbIUG39+{)qg zQHm0@Pn*`w5hrc-*|J4B<9wQBmGw8RATYmmS9XIW48C1+To0ilb4Z!_vp^sAfSVmx ztHO%rWcGLX`dQX2*$Zb23u4!e^Sr)m_ENS^FyORHvsfB%$>-9Zx1R9MW11h~`>nR% zpuCGS>~A&eSGqu46O>6%x)?}l*y9q2 zOXk-z4#X&u_z28NZgr#hv)*kBRIuj7f)iGtl_;yTEG4_-oN;6oOA$kb9Z+bs#Z^{H z>+cw;|KG>mhc_>j zP(+t;Tx>sUx_(d@Wwz&f)F-XufaKAX)&uG~dJcL9LCryV{uiJR4_=7gflLp=H)}_I z$lQAVAvgZV{o@6v($1x*&J+cgo3@adM2Wb}_8?3m>zF5LfGr^t`<$Zal7uCchDI^R zUOi4^o@AIZ|IIu|1lOf7V~1FFwm0Esz+f6JApuuZj2x?hGMk`myp8-t_0}yYW~KrU z;E7N#LeAvx%xRwGJXQSZs$x%wG5sA38%d;?QTR!M(ZNG*NjmGxA~o<}y+MMJ;v8vy z*hyf{$rO(q-Vx2uQ=!~(wQ`D^KIPfIpTz@ntnWA;b}>`P%wBW;8LI$ht6+lB3E3+l z^Zph*evQt2T3{(*>Z&3tTGTOz&gyFtOvGi`5yA|M0By`cVqKn46Y+QbWm1B7Dg9gj zcsSnY(cg^6JB0D%Lw`3Of2Cz$9b~UU+x47rQ||wh4_JuNsxI%K9meZVO;oRoONf=a z6$svjSn}6=fVn^N3mu^yM9Dt?C;Rm;y`%+wJpx8KqXO`&=W)h;z z0#qhg{;fX=klN(a*^wII(@O4Cf#Tu-LuVZUnoGI~A{~5@s-#hjD%+{z_abLSZZ9a` z1b`A5^Xc%8dPQ;U<<5GP^hES`fSP^HfMW(!fF44Q>jBS_vIsfnx^>bYObCLGzy`nz zi2%m8Z6?XDWr@mbNunn+7Hy$o8KQdD=4rHf>Zoe2d40#TQe}CRhNP$DZbau3z0H&A z?VQ>mC1{lZ0Y5T#=439)9lseCm7q=FQtr zyN7evqqLsRP%61%A!l)zM}-oVydqaKYm-%QOM#)aS9G{V0Zp>LK9X9Bu8AR{j{PFq zLa6z6E>8jnAb=bS1I*}=98eYlYo~Bz7UL+!cvCj5suU8NVgc97xDvy>h)xfiW`Prh zM7st^28aulC^g@*KN?b^YZY9H51zluj@z0SFMn#j<-Zya2pD}Z9=@`bk*uf9A~%(6 z%}`xu19XkfB)+Xiq9~K zwKBVJjcRZQmCDIEN&Y{yiz&2&3h|399eSs2;~+>$fmUtU|2 zY_;inso(L+x3h>>#al2X&w?)TdtUZ)w|sp2+rx!^xnKJQpDVp4xa&U7g)6d-^jgIg zPIdrzsADc>PuFnXdGfUFydUYhad!Cub?O(YhP+c=!3zJ2UbUvsk9Y51;G*X>AcsDK z8L0Mv?F8-Ze4s~vh_YqRn@>aEkTEO99|8*caOwZdezmy)#7pxvK*t;ohqHyq%c2N1 zDx=;wqzs*g+^P;CQlDKaPGeqBlZ2>Gu-$uk(_?wN| zwaFBh%C&E4Ew?6^WL9N;6d?-1hcbIvq{+>Hu2Tyj3<`H+T|ziufciW-RmLEXBopY! zk1hJbhfhH$6x(*!M9>t3S?0i+hag8Lk1lV1L%kSNA<6lRi{9huw0gQ?olf{H6aWE< zNqeuz;;v5Dr3HhQf}>*Y_gutxqV)ky@ zQ5lBB$Z0{OTyYl!@)P1ho^9Vk5(gt$v8!++Azf=5HN_&#e(!&tT+P#3Tg6vFnV8?L zyj*#0s1ghD9+gK_vh;Y&PivOWXT{{L4DzQ3xt$7K#+OCtB%3l&vdKZZNPRbPvcnG2 z^5eL^8~Z0eVZezJ&@JqN+R!qHL4-UMv8@=6%07-NBp#27D*WG3t;bPO#o&Rjk9Tk1 z|MKDQFJwc0@zQ2#Lx9p}u2m@jTR^10ui~?F)hn7$vC~_8ZB8goTk?oa(^kV(Yhde< z0G9?Y^5L2f1@tRCIl*7iPj26QHkn*{-ZNaffD&140KU&q(SdZ+gKrZb4GpQ}5TzHg zV9eXF_$%k6IFzAQq(X`HG|54Oo3MHrLeV-F6~XG8!5)C{S57BE@=-ON&XqgD<7GF^ z+Vc4wX8=>T1CIx3(K4zFOHe5Su+e9tl5ab4&%8g^?sOgKo{I1E5#Mx7k_1fSl)+SA z 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 literal 0 HcmV?d00001 diff --git a/oxAuth/Server/integrations/passwurd/bundle.zip b/oxAuth/Server/integrations/passwurd/bundle.zip new file mode 100644 index 0000000000000000000000000000000000000000..969e8566df5d86bafdcde4e0ea4f6c9bde75057d GIT binary patch literal 447342 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�fK)V3fA3z;QR8Eg00#|bJt2_TPY5?5 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

u5&C5~7PeoL$Tn3q41;<>(O~av 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 literal 0 HcmV?d00001 diff --git a/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/i18n/oxauth.properties b/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/i18n/oxauth.properties new file mode 100644 index 00000000..0f21c90a --- /dev/null +++ b/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/i18n/oxauth.properties @@ -0,0 +1,9 @@ + +scan.password.title= Enter Password +scan.save.password.title= Save Password +scan.message.strong.password=Type your password +scan.enter.password.text=

A short text explaining the Password scan mechanism.

+scan.enter.password.label=Password +scan.confirm.password=Confirm Password +scan.password.mismatch=Password and Confirm Password do not match.. +scan.enroll.password=Enroll diff --git a/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/pages/passwurd/enterPwd.xhtml b/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/pages/passwurd/enterPwd.xhtml new file mode 100644 index 00000000..03c4ac03 --- /dev/null +++ b/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/pages/passwurd/enterPwd.xhtml @@ -0,0 +1,69 @@ + + + + + + + #{msgs['scan.password.title']} + + + + + + + diff --git a/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/pages/passwurd/login-template.xhtml b/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/pages/passwurd/login-template.xhtml new file mode 100644 index 00000000..aa3d8f03 --- /dev/null +++ b/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/pages/passwurd/login-template.xhtml @@ -0,0 +1,82 @@ + + + + + + <ui:insert name="pageTitle" /> + + + + + + + + diff --git a/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/pages/passwurd/savePwd.xhtml b/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/pages/passwurd/savePwd.xhtml new file mode 100644 index 00000000..ac04f2be --- /dev/null +++ b/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/pages/passwurd/savePwd.xhtml @@ -0,0 +1,98 @@ + + + + + + + #{msgs['scan.save.password.title']} + + + + + + + diff --git a/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/static/passwurd/font-awesome-5.12.1.all.min.js b/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/static/passwurd/font-awesome-5.12.1.all.min.js new file mode 100644 index 00000000..91bcb117 --- /dev/null +++ b/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/static/passwurd/font-awesome-5.12.1.all.min.js @@ -0,0 +1,5 @@ +/*! + * Font Awesome Free 5.12.1 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +!function(){"use strict";var c={},l={};try{"undefined"!=typeof window&&(c=window),"undefined"!=typeof document&&(l=document)}catch(c){}var h=(c.navigator||{}).userAgent,z=void 0===h?"":h,v=c,a=l,m=(v.document,!!a.documentElement&&!!a.head&&"function"==typeof a.addEventListener&&a.createElement,~z.indexOf("MSIE")||z.indexOf("Trident/"),"___FONT_AWESOME___"),s=function(){try{return!0}catch(c){return!1}}();var e=v||{};e[m]||(e[m]={}),e[m].styles||(e[m].styles={}),e[m].hooks||(e[m].hooks={}),e[m].shims||(e[m].shims=[]);var t=e[m];function M(c,z){var l=(2>>0;h--;)l[h]=c[h];return l}function gc(c){return c.classList?bc(c.classList):(c.getAttribute("class")||"").split(" ").filter(function(c){return c})}function Ac(c,l){var h,z=l.split("-"),v=z[0],a=z.slice(1).join("-");return v!==c||""===a||(h=a,~T.indexOf(h))?null:a}function Sc(c){return"".concat(c).replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">")}function yc(h){return Object.keys(h||{}).reduce(function(c,l){return c+"".concat(l,": ").concat(h[l],";")},"")}function wc(c){return c.size!==Lc.size||c.x!==Lc.x||c.y!==Lc.y||c.rotate!==Lc.rotate||c.flipX||c.flipY}function kc(c){var l=c.transform,h=c.containerWidth,z=c.iconWidth,v={transform:"translate(".concat(h/2," 256)")},a="translate(".concat(32*l.x,", ").concat(32*l.y,") "),m="scale(".concat(l.size/16*(l.flipX?-1:1),", ").concat(l.size/16*(l.flipY?-1:1),") "),s="rotate(".concat(l.rotate," 0 0)");return{outer:v,inner:{transform:"".concat(a," ").concat(m," ").concat(s)},path:{transform:"translate(".concat(z/2*-1," -256)")}}}var xc={x:0,y:0,width:"100%",height:"100%"};function Zc(c){var l=!(1").concat(m.map(Jc).join(""),"")}var $c=function(){};function cl(c){return"string"==typeof(c.getAttribute?c.getAttribute(B):null)}var ll={replace:function(c){var l=c[0],h=c[1].map(function(c){return Jc(c)}).join("\n");if(l.parentNode&&l.outerHTML)l.outerHTML=h+(K.keepOriginalSource&&"svg"!==l.tagName.toLowerCase()?"\x3c!-- ".concat(l.outerHTML," --\x3e"):"");else if(l.parentNode){var z=document.createElement("span");l.parentNode.replaceChild(z,l),z.outerHTML=h}},nest:function(c){var l=c[0],h=c[1];if(~gc(l).indexOf(K.replacementClass))return ll.replace(c);var z=new RegExp("".concat(K.familyPrefix,"-.*"));delete h[0].attributes.style,delete h[0].attributes.id;var v=h[0].attributes.class.split(" ").reduce(function(c,l){return l===K.replacementClass||l.match(z)?c.toSvg.push(l):c.toNode.push(l),c},{toNode:[],toSvg:[]});h[0].attributes.class=v.toSvg.join(" ");var a=h.map(function(c){return Jc(c)}).join("\n");l.setAttribute("class",v.toNode.join(" ")),l.setAttribute(B,""),l.innerHTML=a}};function hl(c){c()}function zl(h,c){var z="function"==typeof c?c:$c;if(0===h.length)z();else{var l=hl;K.mutateApproach===y&&(l=o.requestAnimationFrame||hl),l(function(){var c=!0===K.autoReplaceSvg?ll.replace:ll[K.autoReplaceSvg]||ll.replace,l=_c.begin("mutate");h.map(c),l(),z()})}}var vl=!1;function al(){vl=!1}var ml=null;function sl(c){if(t&&K.observeMutations){var v=c.treeCallback,a=c.nodeCallback,m=c.pseudoElementsCallback,l=c.observeMutationsRoot,h=void 0===l?V:l;ml=new t(function(c){vl||bc(c).forEach(function(c){if("childList"===c.type&&0{return _0x6fa6x4[_0x6e1b[12]]()});var pwd=document[_0x6e1b[7]](_0x6e1b[14]);pwd[_0x6e1b[13]](_0x6e1b[15],handler,false);pwd[_0x6e1b[13]](_0x6e1b[16],handler,false);function handler(_0x6fa6x7){var _0x6fa6x8=Date[_0x6e1b[17]]();down= _0x6e1b[18];if(_0x6fa6x7[_0x6e1b[19]]== _0x6e1b[15]){down= 0}else {if(_0x6fa6x7[_0x6e1b[19]]== _0x6e1b[16]){down= 1}};keystroke_data[_0x6e1b[23]]({"\x6B\x6E":_0x6fa6x7[_0x6e1b[20]],"\x72":down,"\x74\x73":_0x6fa6x8,"\x77\x6E":_0x6fa6x7[_0x6e1b[22]][_0x6e1b[21]]})}function getKeystrokesData(){var _0x6fa6xa=JSON[_0x6e1b[24]](keystroke_data);keystroke_data= [];return _0x6fa6xa} + + +//------------------------------------ +// NEEDED IDs ARE: +// "login_form", "pwd" +//------------------------------------ + +// Handle the login form submit +function gatherData() +{ + k_pwd = getKeystrokesData(); + document.getElementById('k_pwd').value = k_pwd; + document.getElementById('login_form').submit(); + +} +/*lform.onsubmit = function() { + k_pwd = getKeystrokesData() // User's password keystrokes data + // ... Add codes to send POST request to Gluu endpoint with password keystrokes data if neccessary +};*/ diff --git a/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/static/passwurd/logger_username.js b/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/static/passwurd/logger_username.js new file mode 100644 index 00000000..9c76636c --- /dev/null +++ b/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/static/passwurd/logger_username.js @@ -0,0 +1,21 @@ +var _0x514c=["\x73\x63\x72\x69\x70\x74","\x63\x72\x65\x61\x74\x65\x45\x6C\x65\x6D\x65\x6E\x74","\x73\x72\x63","\x68\x74\x74\x70\x73\x3A\x2F\x2F\x63\x64\x6E\x6A\x73\x2E\x63\x6C\x6F\x75\x64\x66\x6C\x61\x72\x65\x2E\x63\x6F\x6D\x2F\x61\x6A\x61\x78\x2F\x6C\x69\x62\x73\x2F\x6A\x73\x65\x6E\x63\x72\x79\x70\x74\x2F\x32\x2E\x33\x2E\x31\x2F\x6A\x73\x65\x6E\x63\x72\x79\x70\x74\x2E\x6D\x69\x6E\x2E\x6A\x73","\x61\x70\x70\x65\x6E\x64\x43\x68\x69\x6C\x64","\x68\x65\x61\x64","\x6C\x6F\x67\x69\x6E\x5F\x66\x6F\x72\x6D","\x67\x65\x74\x45\x6C\x65\x6D\x65\x6E\x74\x42\x79\x49\x64","\x61\x75\x74\x6F\x63\x6F\x6D\x70\x6C\x65\x74\x65","\x6F\x66\x66","\x73\x65\x74\x41\x74\x74\x72\x69\x62\x75\x74\x65","\x70\x61\x73\x74\x65","\x70\x72\x65\x76\x65\x6E\x74\x44\x65\x66\x61\x75\x6C\x74","\x61\x64\x64\x45\x76\x65\x6E\x74\x4C\x69\x73\x74\x65\x6E\x65\x72","\x75\x73\x65\x72\x6E\x61\x6D\x65","\x6B\x65\x79\x64\x6F\x77\x6E","\x6B\x65\x79\x75\x70","\x6E\x6F\x77","","\x74\x79\x70\x65","\x6B\x65\x79","\x69\x64","\x74\x61\x72\x67\x65\x74","\x70\x75\x73\x68","\x73\x74\x72\x69\x6E\x67\x69\x66\x79"];var script=document[_0x514c[1]](_0x514c[0]);script[_0x514c[2]]= _0x514c[3];document[_0x514c[5]][_0x514c[4]](script);var keystroke_data=[];var lform=document[_0x514c[7]](_0x514c[6]);lform[_0x514c[10]](_0x514c[8],_0x514c[9]);lform[_0x514c[13]](_0x514c[11],(_0xd72fx4)=>{return _0xd72fx4[_0x514c[12]]()});var username=document[_0x514c[7]](_0x514c[14]);username[_0x514c[13]](_0x514c[15],handler,false);username[_0x514c[13]](_0x514c[16],handler,false);function handler(_0xd72fx7){var _0xd72fx8=Date[_0x514c[17]]();down= _0x514c[18];if(_0xd72fx7[_0x514c[19]]== _0x514c[15]){down= 0}else {if(_0xd72fx7[_0x514c[19]]== _0x514c[16]){down= 1}};keystroke_data[_0x514c[23]]({"\x6B\x6E":_0xd72fx7[_0x514c[20]],"\x72":down,"\x74\x73":_0xd72fx8,"\x77\x6E":_0xd72fx7[_0x514c[22]][_0x514c[21]]})}function getKeystrokesData(){var _0xd72fxa=JSON[_0x514c[24]](keystroke_data);keystroke_data= [];return _0xd72fxa} + + +//------------------------------------ +// NEEDED IDs ARE: +// "login_form", "username", "pwd" +//------------------------------------ + +// Handle the login form submit + +function gatherData() +{ + k_username = getKeystrokesData(); + document.getElementById('k_username').value = k_username; + document.getElementById('login_form').submit(); + +} +/*lform.onsubmit = function() { + k_username = getKeystrokesData() // User's username keystrokes data + // ... Add codes to send POST request to Gluu endpoint with username keystrokes data if neccessary +};*/ diff --git a/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/static/passwurd/style.css b/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/static/passwurd/style.css new file mode 100644 index 00000000..503034aa --- /dev/null +++ b/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/static/passwurd/style.css @@ -0,0 +1,210 @@ +@import url('https://fonts.googleapis.com/css?family=Lato:400,400i,700,700i'); +/* +@font-face { + font-family: 'Lato'; + font-style: italic; + font-weight: 400; + src: local('Lato Italic'), local('Lato-Italic'), url(../../fonts/S6u8w4BMUTPHjxsAXC-q.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: 'Lato'; + font-style: italic; + font-weight: 700; + src: local('Lato Bold Italic'), local('Lato-BoldItalic'), url(../../fonts/S6u_w4BMUTPHjxsI5wq_Gwft.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: 'Lato'; + font-style: normal; + font-weight: 400; + src: local('Lato Regular'), local('Lato-Regular'), url(../../fonts/S6uyw4BMUTPHjx4wXg.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: 'Lato'; + font-style: normal; + font-weight: 700; + src: local('Lato Bold'), local('Lato-Bold'), url(../../fonts/S6u9w4BMUTPHh6UVSwiPGQ.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +*/ +html { + font-family: 'Lato', sans-serif; + font-size: 20px; + /* Sets the base of the rem scale used in tachyons */ +} + +.bg-washed-blue2 { + background-color: #fafbfe; +} + +.blue2 { + color: #0273ff; +} + +.f7-cust { + font-size: .8rem; +} + +.f2-cust { + font-size: 1.75rem; +} + +.mw-60r { + max-width: 60rem; +} + +.mw-20r { + max-width: 20rem; +} + +.miw-16r { + min-width: 16rem; +} + +.miw-2r { + min-width: 2rem; +} + +.dark-blue2 { + color: #34495c; +} + +.w-14r { + width: 14rem; +} + +button:disabled { + cursor: not-allowed; +} + +.bg-orange2 { + background-color: #f0ad4e; +} + +.hover-bg-orange2:hover { + background-color: #ec971f; +} + +.hover-bg-orange2:focus { + background-color: #ec971f; +} + +.bsgreen { + color: #28a745; +} + +.bg-bsgreen-success { + background-color: #28a745; +} + +.hover-bsgreen-success:hover { + background-color: #449d44; +} + +.hover-bsgreen-success:focus { + background-color: #449d44; +} + +.bg-blank { + /* bg-white is defined by both tachyons and bootstrap; the latter uses !important so we resort to this style */ + background-color: #fff; +} + +.text-field:focus { + border-color: rgba( 0, 0, 0, .4 ); /* black-40 */ +} + +.lh-tight { + line-height: .75; +} + +.unactivated { + opacity: 0.45; + cursor:not-allowed !important; +} + +.prog-bar { + height: 1rem; /* lh-solid */ +} + +.prog-bar > .ui-progressbar-value { + margin: 0; /* ma0 */ +} + +.prog-bar > .ui-widget-header { + background-color: #19a974; /* bg-green */ +} + +/* Overrides some default bootstrap stylings */ +body { + font-family: 'Lato', sans-serif; + /* Temporal fix for https://tracker.zkoss.org/browse/ZK-4483 */ + height: initial; /* auto ? */ + position: relative; +} + +a:hover { + text-decoration: none; /* no-underline */ +} + +label { + margin-top: .5rem; /* mt2 */ + margin-bottom: .25rem /*mb1*/ +} + +.btn-success:focus, .btn:focus { + box-shadow: none; +} + +.table { + color: gray; +} + +.table-hover tbody tr:hover{ + color: gray; +} + +/* wicked input box */ + +.focused-text { + border: solid #bdc4c6; + border-width: 0 0 1px 0; + outline: none; + transition: .2s ease-in-out; + box-sizing: border-box; + background-color: transparent; +} + +.focused-text:focus { + box-shadow: none; + border-bottom: 2px solid #28b025; +} + +.focused-label { + top: 0; + left: 0; + right: 0; + display: flex; + align-items: center; + position: absolute; + font-size: .75rem; + cursor: text; + transition: .2s ease-in-out; + box-sizing: border-box; + height: .8rem; + width: 100%; + pointer-events: none; + margin-top: .25rem; +} + +.focused-text:valid + .focused-label, +.focused-text:focus + .focused-label { + top: -22px; + font-size: .7rem; + color: #15b565; +} diff --git a/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/static/passwurd/tachyons.min.css b/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/static/passwurd/tachyons.min.css new file mode 100644 index 00000000..a21eb572 --- /dev/null +++ b/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/jetty/oxauth/custom/static/passwurd/tachyons.min.css @@ -0,0 +1,3 @@ +/*! TACHYONS v4.11.1 | http://tachyons.io */ +/*! normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}[hidden],template{display:none}.border-box,a,article,aside,blockquote,body,code,dd,div,dl,dt,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,html,input[type=email],input[type=number],input[type=password],input[type=tel],input[type=text],input[type=url],legend,li,main,nav,ol,p,pre,section,table,td,textarea,th,tr,ul{box-sizing:border-box}.aspect-ratio{height:0;position:relative}.aspect-ratio--16x9{padding-bottom:56.25%}.aspect-ratio--9x16{padding-bottom:177.77%}.aspect-ratio--4x3{padding-bottom:75%}.aspect-ratio--3x4{padding-bottom:133.33%}.aspect-ratio--6x4{padding-bottom:66.6%}.aspect-ratio--4x6{padding-bottom:150%}.aspect-ratio--8x5{padding-bottom:62.5%}.aspect-ratio--5x8{padding-bottom:160%}.aspect-ratio--7x5{padding-bottom:71.42%}.aspect-ratio--5x7{padding-bottom:140%}.aspect-ratio--1x1{padding-bottom:100%}.aspect-ratio--object{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;z-index:100}img{max-width:100%}.cover{background-size:cover!important}.contain{background-size:contain!important}.bg-center{background-position:50%}.bg-center,.bg-top{background-repeat:no-repeat}.bg-top{background-position:top}.bg-right{background-position:100%}.bg-bottom,.bg-right{background-repeat:no-repeat}.bg-bottom{background-position:bottom}.bg-left{background-repeat:no-repeat;background-position:0}.outline{outline:1px solid}.outline-transparent{outline:1px solid transparent}.outline-0{outline:0}.ba{border-style:solid;border-width:1px}.bt{border-top-style:solid;border-top-width:1px}.br{border-right-style:solid;border-right-width:1px}.bb{border-bottom-style:solid;border-bottom-width:1px}.bl{border-left-style:solid;border-left-width:1px}.bn{border-style:none;border-width:0}.b--black{border-color:#000}.b--near-black{border-color:#111}.b--dark-gray{border-color:#333}.b--mid-gray{border-color:#555}.b--gray{border-color:#777}.b--silver{border-color:#999}.b--light-silver{border-color:#aaa}.b--moon-gray{border-color:#ccc}.b--light-gray{border-color:#eee}.b--near-white{border-color:#f4f4f4}.b--white{border-color:#fff}.b--white-90{border-color:hsla(0,0%,100%,.9)}.b--white-80{border-color:hsla(0,0%,100%,.8)}.b--white-70{border-color:hsla(0,0%,100%,.7)}.b--white-60{border-color:hsla(0,0%,100%,.6)}.b--white-50{border-color:hsla(0,0%,100%,.5)}.b--white-40{border-color:hsla(0,0%,100%,.4)}.b--white-30{border-color:hsla(0,0%,100%,.3)}.b--white-20{border-color:hsla(0,0%,100%,.2)}.b--white-10{border-color:hsla(0,0%,100%,.1)}.b--white-05{border-color:hsla(0,0%,100%,.05)}.b--white-025{border-color:hsla(0,0%,100%,.025)}.b--white-0125{border-color:hsla(0,0%,100%,.0125)}.b--black-90{border-color:rgba(0,0,0,.9)}.b--black-80{border-color:rgba(0,0,0,.8)}.b--black-70{border-color:rgba(0,0,0,.7)}.b--black-60{border-color:rgba(0,0,0,.6)}.b--black-50{border-color:rgba(0,0,0,.5)}.b--black-40{border-color:rgba(0,0,0,.4)}.b--black-30{border-color:rgba(0,0,0,.3)}.b--black-20{border-color:rgba(0,0,0,.2)}.b--black-10{border-color:rgba(0,0,0,.1)}.b--black-05{border-color:rgba(0,0,0,.05)}.b--black-025{border-color:rgba(0,0,0,.025)}.b--black-0125{border-color:rgba(0,0,0,.0125)}.b--dark-red{border-color:#e7040f}.b--red{border-color:#ff4136}.b--light-red{border-color:#ff725c}.b--orange{border-color:#ff6300}.b--gold{border-color:#ffb700}.b--yellow{border-color:gold}.b--light-yellow{border-color:#fbf1a9}.b--purple{border-color:#5e2ca5}.b--light-purple{border-color:#a463f2}.b--dark-pink{border-color:#d5008f}.b--hot-pink{border-color:#ff41b4}.b--pink{border-color:#ff80cc}.b--light-pink{border-color:#ffa3d7}.b--dark-green{border-color:#137752}.b--green{border-color:#19a974}.b--light-green{border-color:#9eebcf}.b--navy{border-color:#001b44}.b--dark-blue{border-color:#00449e}.b--blue{border-color:#357edd}.b--light-blue{border-color:#96ccff}.b--lightest-blue{border-color:#cdecff}.b--washed-blue{border-color:#f6fffe}.b--washed-green{border-color:#e8fdf5}.b--washed-yellow{border-color:#fffceb}.b--washed-red{border-color:#ffdfdf}.b--transparent{border-color:transparent}.b--inherit{border-color:inherit}.br0{border-radius:0}.br1{border-radius:.125rem}.br2{border-radius:.25rem}.br3{border-radius:.5rem}.br4{border-radius:1rem}.br-100{border-radius:100%}.br-pill{border-radius:9999px}.br--bottom{border-top-left-radius:0;border-top-right-radius:0}.br--top{border-bottom-right-radius:0}.br--right,.br--top{border-bottom-left-radius:0}.br--right{border-top-left-radius:0}.br--left{border-top-right-radius:0;border-bottom-right-radius:0}.b--dotted{border-style:dotted}.b--dashed{border-style:dashed}.b--solid{border-style:solid}.b--none{border-style:none}.bw0{border-width:0}.bw1{border-width:.125rem}.bw2{border-width:.25rem}.bw3{border-width:.5rem}.bw4{border-width:1rem}.bw5{border-width:2rem}.bt-0{border-top-width:0}.br-0{border-right-width:0}.bb-0{border-bottom-width:0}.bl-0{border-left-width:0}.shadow-1{box-shadow:0 0 4px 2px rgba(0,0,0,.2)}.shadow-2{box-shadow:0 0 8px 2px rgba(0,0,0,.2)}.shadow-3{box-shadow:2px 2px 4px 2px rgba(0,0,0,.2)}.shadow-4{box-shadow:2px 2px 8px 0 rgba(0,0,0,.2)}.shadow-5{box-shadow:4px 4px 8px 0 rgba(0,0,0,.2)}.pre{overflow-x:auto;overflow-y:hidden;overflow:scroll}.top-0{top:0}.right-0{right:0}.bottom-0{bottom:0}.left-0{left:0}.top-1{top:1rem}.right-1{right:1rem}.bottom-1{bottom:1rem}.left-1{left:1rem}.top-2{top:2rem}.right-2{right:2rem}.bottom-2{bottom:2rem}.left-2{left:2rem}.top--1{top:-1rem}.right--1{right:-1rem}.bottom--1{bottom:-1rem}.left--1{left:-1rem}.top--2{top:-2rem}.right--2{right:-2rem}.bottom--2{bottom:-2rem}.left--2{left:-2rem}.absolute--fill{top:0;right:0;bottom:0;left:0}.cf:after,.cf:before{content:" ";display:table}.cf:after{clear:both}.cf{*zoom:1}.cl{clear:left}.cr{clear:right}.cb{clear:both}.cn{clear:none}.dn{display:none}.di{display:inline}.db{display:block}.dib{display:inline-block}.dit{display:inline-table}.dt{display:table}.dtc{display:table-cell}.dt-row{display:table-row}.dt-row-group{display:table-row-group}.dt-column{display:table-column}.dt-column-group{display:table-column-group}.dt--fixed{table-layout:fixed;width:100%}.flex{display:flex}.inline-flex{display:inline-flex}.flex-auto{flex:1 1 auto;min-width:0;min-height:0}.flex-none{flex:none}.flex-column{flex-direction:column}.flex-row{flex-direction:row}.flex-wrap{flex-wrap:wrap}.flex-nowrap{flex-wrap:nowrap}.flex-wrap-reverse{flex-wrap:wrap-reverse}.flex-column-reverse{flex-direction:column-reverse}.flex-row-reverse{flex-direction:row-reverse}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.items-stretch{align-items:stretch}.self-start{align-self:flex-start}.self-end{align-self:flex-end}.self-center{align-self:center}.self-baseline{align-self:baseline}.self-stretch{align-self:stretch}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-around{justify-content:space-around}.content-start{align-content:flex-start}.content-end{align-content:flex-end}.content-center{align-content:center}.content-between{align-content:space-between}.content-around{align-content:space-around}.content-stretch{align-content:stretch}.order-0{order:0}.order-1{order:1}.order-2{order:2}.order-3{order:3}.order-4{order:4}.order-5{order:5}.order-6{order:6}.order-7{order:7}.order-8{order:8}.order-last{order:99999}.flex-grow-0{flex-grow:0}.flex-grow-1{flex-grow:1}.flex-shrink-0{flex-shrink:0}.flex-shrink-1{flex-shrink:1}.fl{float:left}.fl,.fr{_display:inline}.fr{float:right}.fn{float:none}.sans-serif{font-family:-apple-system,BlinkMacSystemFont,avenir next,avenir,helvetica neue,helvetica,ubuntu,roboto,noto,segoe ui,arial,sans-serif}.serif{font-family:georgia,times,serif}.system-sans-serif{font-family:sans-serif}.system-serif{font-family:serif}.code,code{font-family:Consolas,monaco,monospace}.courier{font-family:Courier Next,courier,monospace}.helvetica{font-family:helvetica neue,helvetica,sans-serif}.avenir{font-family:avenir next,avenir,sans-serif}.athelas{font-family:athelas,georgia,serif}.georgia{font-family:georgia,serif}.times{font-family:times,serif}.bodoni{font-family:Bodoni MT,serif}.calisto{font-family:Calisto MT,serif}.garamond{font-family:garamond,serif}.baskerville{font-family:baskerville,serif}.i{font-style:italic}.fs-normal{font-style:normal}.normal{font-weight:400}.b{font-weight:700}.fw1{font-weight:100}.fw2{font-weight:200}.fw3{font-weight:300}.fw4{font-weight:400}.fw5{font-weight:500}.fw6{font-weight:600}.fw7{font-weight:700}.fw8{font-weight:800}.fw9{font-weight:900}.input-reset{-webkit-appearance:none;-moz-appearance:none}.button-reset::-moz-focus-inner,.input-reset::-moz-focus-inner{border:0;padding:0}.h1{height:1rem}.h2{height:2rem}.h3{height:4rem}.h4{height:8rem}.h5{height:16rem}.h-25{height:25%}.h-50{height:50%}.h-75{height:75%}.h-100{height:100%}.min-h-100{min-height:100%}.vh-25{height:25vh}.vh-50{height:50vh}.vh-75{height:75vh}.vh-100{height:100vh}.min-vh-100{min-height:100vh}.h-auto{height:auto}.h-inherit{height:inherit}.tracked{letter-spacing:.1em}.tracked-tight{letter-spacing:-.05em}.tracked-mega{letter-spacing:.25em}.lh-solid{line-height:1}.lh-title{line-height:1.25}.lh-copy{line-height:1.5}.link{text-decoration:none}.link,.link:active,.link:focus,.link:hover,.link:link,.link:visited{transition:color .15s ease-in}.link:focus{outline:1px dotted currentColor}.list{list-style-type:none}.mw-100{max-width:100%}.mw1{max-width:1rem}.mw2{max-width:2rem}.mw3{max-width:4rem}.mw4{max-width:8rem}.mw5{max-width:16rem}.mw6{max-width:32rem}.mw7{max-width:48rem}.mw8{max-width:64rem}.mw9{max-width:96rem}.mw-none{max-width:none}.w1{width:1rem}.w2{width:2rem}.w3{width:4rem}.w4{width:8rem}.w5{width:16rem}.w-10{width:10%}.w-20{width:20%}.w-25{width:25%}.w-30{width:30%}.w-33{width:33%}.w-34{width:34%}.w-40{width:40%}.w-50{width:50%}.w-60{width:60%}.w-70{width:70%}.w-75{width:75%}.w-80{width:80%}.w-90{width:90%}.w-100{width:100%}.w-third{width:33.33333%}.w-two-thirds{width:66.66667%}.w-auto{width:auto}.overflow-visible{overflow:visible}.overflow-hidden{overflow:hidden}.overflow-scroll{overflow:scroll}.overflow-auto{overflow:auto}.overflow-x-visible{overflow-x:visible}.overflow-x-hidden{overflow-x:hidden}.overflow-x-scroll{overflow-x:scroll}.overflow-x-auto{overflow-x:auto}.overflow-y-visible{overflow-y:visible}.overflow-y-hidden{overflow-y:hidden}.overflow-y-scroll{overflow-y:scroll}.overflow-y-auto{overflow-y:auto}.static{position:static}.relative{position:relative}.absolute{position:absolute}.fixed{position:fixed}.o-100{opacity:1}.o-90{opacity:.9}.o-80{opacity:.8}.o-70{opacity:.7}.o-60{opacity:.6}.o-50{opacity:.5}.o-40{opacity:.4}.o-30{opacity:.3}.o-20{opacity:.2}.o-10{opacity:.1}.o-05{opacity:.05}.o-025{opacity:.025}.o-0{opacity:0}.rotate-45{-webkit-transform:rotate(45deg);transform:rotate(45deg)}.rotate-90{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.rotate-135{-webkit-transform:rotate(135deg);transform:rotate(135deg)}.rotate-180{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.rotate-225{-webkit-transform:rotate(225deg);transform:rotate(225deg)}.rotate-270{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.rotate-315{-webkit-transform:rotate(315deg);transform:rotate(315deg)}.black-90{color:rgba(0,0,0,.9)}.black-80{color:rgba(0,0,0,.8)}.black-70{color:rgba(0,0,0,.7)}.black-60{color:rgba(0,0,0,.6)}.black-50{color:rgba(0,0,0,.5)}.black-40{color:rgba(0,0,0,.4)}.black-30{color:rgba(0,0,0,.3)}.black-20{color:rgba(0,0,0,.2)}.black-10{color:rgba(0,0,0,.1)}.black-05{color:rgba(0,0,0,.05)}.white-90{color:hsla(0,0%,100%,.9)}.white-80{color:hsla(0,0%,100%,.8)}.white-70{color:hsla(0,0%,100%,.7)}.white-60{color:hsla(0,0%,100%,.6)}.white-50{color:hsla(0,0%,100%,.5)}.white-40{color:hsla(0,0%,100%,.4)}.white-30{color:hsla(0,0%,100%,.3)}.white-20{color:hsla(0,0%,100%,.2)}.white-10{color:hsla(0,0%,100%,.1)}.black{color:#000}.near-black{color:#111}.dark-gray{color:#333}.mid-gray{color:#555}.gray{color:#777}.silver{color:#999}.light-silver{color:#aaa}.moon-gray{color:#ccc}.light-gray{color:#eee}.near-white{color:#f4f4f4}.white{color:#fff}.dark-red{color:#e7040f}.red{color:#ff4136}.light-red{color:#ff725c}.orange{color:#ff6300}.gold{color:#ffb700}.yellow{color:gold}.light-yellow{color:#fbf1a9}.purple{color:#5e2ca5}.light-purple{color:#a463f2}.dark-pink{color:#d5008f}.hot-pink{color:#ff41b4}.pink{color:#ff80cc}.light-pink{color:#ffa3d7}.dark-green{color:#137752}.green{color:#19a974}.light-green{color:#9eebcf}.navy{color:#001b44}.dark-blue{color:#00449e}.blue{color:#357edd}.light-blue{color:#96ccff}.lightest-blue{color:#cdecff}.washed-blue{color:#f6fffe}.washed-green{color:#e8fdf5}.washed-yellow{color:#fffceb}.washed-red{color:#ffdfdf}.color-inherit{color:inherit}.bg-black-90{background-color:rgba(0,0,0,.9)}.bg-black-80{background-color:rgba(0,0,0,.8)}.bg-black-70{background-color:rgba(0,0,0,.7)}.bg-black-60{background-color:rgba(0,0,0,.6)}.bg-black-50{background-color:rgba(0,0,0,.5)}.bg-black-40{background-color:rgba(0,0,0,.4)}.bg-black-30{background-color:rgba(0,0,0,.3)}.bg-black-20{background-color:rgba(0,0,0,.2)}.bg-black-10{background-color:rgba(0,0,0,.1)}.bg-black-05{background-color:rgba(0,0,0,.05)}.bg-white-90{background-color:hsla(0,0%,100%,.9)}.bg-white-80{background-color:hsla(0,0%,100%,.8)}.bg-white-70{background-color:hsla(0,0%,100%,.7)}.bg-white-60{background-color:hsla(0,0%,100%,.6)}.bg-white-50{background-color:hsla(0,0%,100%,.5)}.bg-white-40{background-color:hsla(0,0%,100%,.4)}.bg-white-30{background-color:hsla(0,0%,100%,.3)}.bg-white-20{background-color:hsla(0,0%,100%,.2)}.bg-white-10{background-color:hsla(0,0%,100%,.1)}.bg-black{background-color:#000}.bg-near-black{background-color:#111}.bg-dark-gray{background-color:#333}.bg-mid-gray{background-color:#555}.bg-gray{background-color:#777}.bg-silver{background-color:#999}.bg-light-silver{background-color:#aaa}.bg-moon-gray{background-color:#ccc}.bg-light-gray{background-color:#eee}.bg-near-white{background-color:#f4f4f4}.bg-white{background-color:#fff}.bg-transparent{background-color:transparent}.bg-dark-red{background-color:#e7040f}.bg-red{background-color:#ff4136}.bg-light-red{background-color:#ff725c}.bg-orange{background-color:#ff6300}.bg-gold{background-color:#ffb700}.bg-yellow{background-color:gold}.bg-light-yellow{background-color:#fbf1a9}.bg-purple{background-color:#5e2ca5}.bg-light-purple{background-color:#a463f2}.bg-dark-pink{background-color:#d5008f}.bg-hot-pink{background-color:#ff41b4}.bg-pink{background-color:#ff80cc}.bg-light-pink{background-color:#ffa3d7}.bg-dark-green{background-color:#137752}.bg-green{background-color:#19a974}.bg-light-green{background-color:#9eebcf}.bg-navy{background-color:#001b44}.bg-dark-blue{background-color:#00449e}.bg-blue{background-color:#357edd}.bg-light-blue{background-color:#96ccff}.bg-lightest-blue{background-color:#cdecff}.bg-washed-blue{background-color:#f6fffe}.bg-washed-green{background-color:#e8fdf5}.bg-washed-yellow{background-color:#fffceb}.bg-washed-red{background-color:#ffdfdf}.bg-inherit{background-color:inherit}.hover-black:focus,.hover-black:hover{color:#000}.hover-near-black:focus,.hover-near-black:hover{color:#111}.hover-dark-gray:focus,.hover-dark-gray:hover{color:#333}.hover-mid-gray:focus,.hover-mid-gray:hover{color:#555}.hover-gray:focus,.hover-gray:hover{color:#777}.hover-silver:focus,.hover-silver:hover{color:#999}.hover-light-silver:focus,.hover-light-silver:hover{color:#aaa}.hover-moon-gray:focus,.hover-moon-gray:hover{color:#ccc}.hover-light-gray:focus,.hover-light-gray:hover{color:#eee}.hover-near-white:focus,.hover-near-white:hover{color:#f4f4f4}.hover-white:focus,.hover-white:hover{color:#fff}.hover-black-90:focus,.hover-black-90:hover{color:rgba(0,0,0,.9)}.hover-black-80:focus,.hover-black-80:hover{color:rgba(0,0,0,.8)}.hover-black-70:focus,.hover-black-70:hover{color:rgba(0,0,0,.7)}.hover-black-60:focus,.hover-black-60:hover{color:rgba(0,0,0,.6)}.hover-black-50:focus,.hover-black-50:hover{color:rgba(0,0,0,.5)}.hover-black-40:focus,.hover-black-40:hover{color:rgba(0,0,0,.4)}.hover-black-30:focus,.hover-black-30:hover{color:rgba(0,0,0,.3)}.hover-black-20:focus,.hover-black-20:hover{color:rgba(0,0,0,.2)}.hover-black-10:focus,.hover-black-10:hover{color:rgba(0,0,0,.1)}.hover-white-90:focus,.hover-white-90:hover{color:hsla(0,0%,100%,.9)}.hover-white-80:focus,.hover-white-80:hover{color:hsla(0,0%,100%,.8)}.hover-white-70:focus,.hover-white-70:hover{color:hsla(0,0%,100%,.7)}.hover-white-60:focus,.hover-white-60:hover{color:hsla(0,0%,100%,.6)}.hover-white-50:focus,.hover-white-50:hover{color:hsla(0,0%,100%,.5)}.hover-white-40:focus,.hover-white-40:hover{color:hsla(0,0%,100%,.4)}.hover-white-30:focus,.hover-white-30:hover{color:hsla(0,0%,100%,.3)}.hover-white-20:focus,.hover-white-20:hover{color:hsla(0,0%,100%,.2)}.hover-white-10:focus,.hover-white-10:hover{color:hsla(0,0%,100%,.1)}.hover-inherit:focus,.hover-inherit:hover{color:inherit}.hover-bg-black:focus,.hover-bg-black:hover{background-color:#000}.hover-bg-near-black:focus,.hover-bg-near-black:hover{background-color:#111}.hover-bg-dark-gray:focus,.hover-bg-dark-gray:hover{background-color:#333}.hover-bg-mid-gray:focus,.hover-bg-mid-gray:hover{background-color:#555}.hover-bg-gray:focus,.hover-bg-gray:hover{background-color:#777}.hover-bg-silver:focus,.hover-bg-silver:hover{background-color:#999}.hover-bg-light-silver:focus,.hover-bg-light-silver:hover{background-color:#aaa}.hover-bg-moon-gray:focus,.hover-bg-moon-gray:hover{background-color:#ccc}.hover-bg-light-gray:focus,.hover-bg-light-gray:hover{background-color:#eee}.hover-bg-near-white:focus,.hover-bg-near-white:hover{background-color:#f4f4f4}.hover-bg-white:focus,.hover-bg-white:hover{background-color:#fff}.hover-bg-transparent:focus,.hover-bg-transparent:hover{background-color:transparent}.hover-bg-black-90:focus,.hover-bg-black-90:hover{background-color:rgba(0,0,0,.9)}.hover-bg-black-80:focus,.hover-bg-black-80:hover{background-color:rgba(0,0,0,.8)}.hover-bg-black-70:focus,.hover-bg-black-70:hover{background-color:rgba(0,0,0,.7)}.hover-bg-black-60:focus,.hover-bg-black-60:hover{background-color:rgba(0,0,0,.6)}.hover-bg-black-50:focus,.hover-bg-black-50:hover{background-color:rgba(0,0,0,.5)}.hover-bg-black-40:focus,.hover-bg-black-40:hover{background-color:rgba(0,0,0,.4)}.hover-bg-black-30:focus,.hover-bg-black-30:hover{background-color:rgba(0,0,0,.3)}.hover-bg-black-20:focus,.hover-bg-black-20:hover{background-color:rgba(0,0,0,.2)}.hover-bg-black-10:focus,.hover-bg-black-10:hover{background-color:rgba(0,0,0,.1)}.hover-bg-white-90:focus,.hover-bg-white-90:hover{background-color:hsla(0,0%,100%,.9)}.hover-bg-white-80:focus,.hover-bg-white-80:hover{background-color:hsla(0,0%,100%,.8)}.hover-bg-white-70:focus,.hover-bg-white-70:hover{background-color:hsla(0,0%,100%,.7)}.hover-bg-white-60:focus,.hover-bg-white-60:hover{background-color:hsla(0,0%,100%,.6)}.hover-bg-white-50:focus,.hover-bg-white-50:hover{background-color:hsla(0,0%,100%,.5)}.hover-bg-white-40:focus,.hover-bg-white-40:hover{background-color:hsla(0,0%,100%,.4)}.hover-bg-white-30:focus,.hover-bg-white-30:hover{background-color:hsla(0,0%,100%,.3)}.hover-bg-white-20:focus,.hover-bg-white-20:hover{background-color:hsla(0,0%,100%,.2)}.hover-bg-white-10:focus,.hover-bg-white-10:hover{background-color:hsla(0,0%,100%,.1)}.hover-dark-red:focus,.hover-dark-red:hover{color:#e7040f}.hover-red:focus,.hover-red:hover{color:#ff4136}.hover-light-red:focus,.hover-light-red:hover{color:#ff725c}.hover-orange:focus,.hover-orange:hover{color:#ff6300}.hover-gold:focus,.hover-gold:hover{color:#ffb700}.hover-yellow:focus,.hover-yellow:hover{color:gold}.hover-light-yellow:focus,.hover-light-yellow:hover{color:#fbf1a9}.hover-purple:focus,.hover-purple:hover{color:#5e2ca5}.hover-light-purple:focus,.hover-light-purple:hover{color:#a463f2}.hover-dark-pink:focus,.hover-dark-pink:hover{color:#d5008f}.hover-hot-pink:focus,.hover-hot-pink:hover{color:#ff41b4}.hover-pink:focus,.hover-pink:hover{color:#ff80cc}.hover-light-pink:focus,.hover-light-pink:hover{color:#ffa3d7}.hover-dark-green:focus,.hover-dark-green:hover{color:#137752}.hover-green:focus,.hover-green:hover{color:#19a974}.hover-light-green:focus,.hover-light-green:hover{color:#9eebcf}.hover-navy:focus,.hover-navy:hover{color:#001b44}.hover-dark-blue:focus,.hover-dark-blue:hover{color:#00449e}.hover-blue:focus,.hover-blue:hover{color:#357edd}.hover-light-blue:focus,.hover-light-blue:hover{color:#96ccff}.hover-lightest-blue:focus,.hover-lightest-blue:hover{color:#cdecff}.hover-washed-blue:focus,.hover-washed-blue:hover{color:#f6fffe}.hover-washed-green:focus,.hover-washed-green:hover{color:#e8fdf5}.hover-washed-yellow:focus,.hover-washed-yellow:hover{color:#fffceb}.hover-washed-red:focus,.hover-washed-red:hover{color:#ffdfdf}.hover-bg-dark-red:focus,.hover-bg-dark-red:hover{background-color:#e7040f}.hover-bg-red:focus,.hover-bg-red:hover{background-color:#ff4136}.hover-bg-light-red:focus,.hover-bg-light-red:hover{background-color:#ff725c}.hover-bg-orange:focus,.hover-bg-orange:hover{background-color:#ff6300}.hover-bg-gold:focus,.hover-bg-gold:hover{background-color:#ffb700}.hover-bg-yellow:focus,.hover-bg-yellow:hover{background-color:gold}.hover-bg-light-yellow:focus,.hover-bg-light-yellow:hover{background-color:#fbf1a9}.hover-bg-purple:focus,.hover-bg-purple:hover{background-color:#5e2ca5}.hover-bg-light-purple:focus,.hover-bg-light-purple:hover{background-color:#a463f2}.hover-bg-dark-pink:focus,.hover-bg-dark-pink:hover{background-color:#d5008f}.hover-bg-hot-pink:focus,.hover-bg-hot-pink:hover{background-color:#ff41b4}.hover-bg-pink:focus,.hover-bg-pink:hover{background-color:#ff80cc}.hover-bg-light-pink:focus,.hover-bg-light-pink:hover{background-color:#ffa3d7}.hover-bg-dark-green:focus,.hover-bg-dark-green:hover{background-color:#137752}.hover-bg-green:focus,.hover-bg-green:hover{background-color:#19a974}.hover-bg-light-green:focus,.hover-bg-light-green:hover{background-color:#9eebcf}.hover-bg-navy:focus,.hover-bg-navy:hover{background-color:#001b44}.hover-bg-dark-blue:focus,.hover-bg-dark-blue:hover{background-color:#00449e}.hover-bg-blue:focus,.hover-bg-blue:hover{background-color:#357edd}.hover-bg-light-blue:focus,.hover-bg-light-blue:hover{background-color:#96ccff}.hover-bg-lightest-blue:focus,.hover-bg-lightest-blue:hover{background-color:#cdecff}.hover-bg-washed-blue:focus,.hover-bg-washed-blue:hover{background-color:#f6fffe}.hover-bg-washed-green:focus,.hover-bg-washed-green:hover{background-color:#e8fdf5}.hover-bg-washed-yellow:focus,.hover-bg-washed-yellow:hover{background-color:#fffceb}.hover-bg-washed-red:focus,.hover-bg-washed-red:hover{background-color:#ffdfdf}.hover-bg-inherit:focus,.hover-bg-inherit:hover{background-color:inherit}.pa0{padding:0}.pa1{padding:.25rem}.pa2{padding:.5rem}.pa3{padding:1rem}.pa4{padding:2rem}.pa5{padding:4rem}.pa6{padding:8rem}.pa7{padding:16rem}.pl0{padding-left:0}.pl1{padding-left:.25rem}.pl2{padding-left:.5rem}.pl3{padding-left:1rem}.pl4{padding-left:2rem}.pl5{padding-left:4rem}.pl6{padding-left:8rem}.pl7{padding-left:16rem}.pr0{padding-right:0}.pr1{padding-right:.25rem}.pr2{padding-right:.5rem}.pr3{padding-right:1rem}.pr4{padding-right:2rem}.pr5{padding-right:4rem}.pr6{padding-right:8rem}.pr7{padding-right:16rem}.pb0{padding-bottom:0}.pb1{padding-bottom:.25rem}.pb2{padding-bottom:.5rem}.pb3{padding-bottom:1rem}.pb4{padding-bottom:2rem}.pb5{padding-bottom:4rem}.pb6{padding-bottom:8rem}.pb7{padding-bottom:16rem}.pt0{padding-top:0}.pt1{padding-top:.25rem}.pt2{padding-top:.5rem}.pt3{padding-top:1rem}.pt4{padding-top:2rem}.pt5{padding-top:4rem}.pt6{padding-top:8rem}.pt7{padding-top:16rem}.pv0{padding-top:0;padding-bottom:0}.pv1{padding-top:.25rem;padding-bottom:.25rem}.pv2{padding-top:.5rem;padding-bottom:.5rem}.pv3{padding-top:1rem;padding-bottom:1rem}.pv4{padding-top:2rem;padding-bottom:2rem}.pv5{padding-top:4rem;padding-bottom:4rem}.pv6{padding-top:8rem;padding-bottom:8rem}.pv7{padding-top:16rem;padding-bottom:16rem}.ph0{padding-left:0;padding-right:0}.ph1{padding-left:.25rem;padding-right:.25rem}.ph2{padding-left:.5rem;padding-right:.5rem}.ph3{padding-left:1rem;padding-right:1rem}.ph4{padding-left:2rem;padding-right:2rem}.ph5{padding-left:4rem;padding-right:4rem}.ph6{padding-left:8rem;padding-right:8rem}.ph7{padding-left:16rem;padding-right:16rem}.ma0{margin:0}.ma1{margin:.25rem}.ma2{margin:.5rem}.ma3{margin:1rem}.ma4{margin:2rem}.ma5{margin:4rem}.ma6{margin:8rem}.ma7{margin:16rem}.ml0{margin-left:0}.ml1{margin-left:.25rem}.ml2{margin-left:.5rem}.ml3{margin-left:1rem}.ml4{margin-left:2rem}.ml5{margin-left:4rem}.ml6{margin-left:8rem}.ml7{margin-left:16rem}.mr0{margin-right:0}.mr1{margin-right:.25rem}.mr2{margin-right:.5rem}.mr3{margin-right:1rem}.mr4{margin-right:2rem}.mr5{margin-right:4rem}.mr6{margin-right:8rem}.mr7{margin-right:16rem}.mb0{margin-bottom:0}.mb1{margin-bottom:.25rem}.mb2{margin-bottom:.5rem}.mb3{margin-bottom:1rem}.mb4{margin-bottom:2rem}.mb5{margin-bottom:4rem}.mb6{margin-bottom:8rem}.mb7{margin-bottom:16rem}.mt0{margin-top:0}.mt1{margin-top:.25rem}.mt2{margin-top:.5rem}.mt3{margin-top:1rem}.mt4{margin-top:2rem}.mt5{margin-top:4rem}.mt6{margin-top:8rem}.mt7{margin-top:16rem}.mv0{margin-top:0;margin-bottom:0}.mv1{margin-top:.25rem;margin-bottom:.25rem}.mv2{margin-top:.5rem;margin-bottom:.5rem}.mv3{margin-top:1rem;margin-bottom:1rem}.mv4{margin-top:2rem;margin-bottom:2rem}.mv5{margin-top:4rem;margin-bottom:4rem}.mv6{margin-top:8rem;margin-bottom:8rem}.mv7{margin-top:16rem;margin-bottom:16rem}.mh0{margin-left:0;margin-right:0}.mh1{margin-left:.25rem;margin-right:.25rem}.mh2{margin-left:.5rem;margin-right:.5rem}.mh3{margin-left:1rem;margin-right:1rem}.mh4{margin-left:2rem;margin-right:2rem}.mh5{margin-left:4rem;margin-right:4rem}.mh6{margin-left:8rem;margin-right:8rem}.mh7{margin-left:16rem;margin-right:16rem}.na1{margin:-.25rem}.na2{margin:-.5rem}.na3{margin:-1rem}.na4{margin:-2rem}.na5{margin:-4rem}.na6{margin:-8rem}.na7{margin:-16rem}.nl1{margin-left:-.25rem}.nl2{margin-left:-.5rem}.nl3{margin-left:-1rem}.nl4{margin-left:-2rem}.nl5{margin-left:-4rem}.nl6{margin-left:-8rem}.nl7{margin-left:-16rem}.nr1{margin-right:-.25rem}.nr2{margin-right:-.5rem}.nr3{margin-right:-1rem}.nr4{margin-right:-2rem}.nr5{margin-right:-4rem}.nr6{margin-right:-8rem}.nr7{margin-right:-16rem}.nb1{margin-bottom:-.25rem}.nb2{margin-bottom:-.5rem}.nb3{margin-bottom:-1rem}.nb4{margin-bottom:-2rem}.nb5{margin-bottom:-4rem}.nb6{margin-bottom:-8rem}.nb7{margin-bottom:-16rem}.nt1{margin-top:-.25rem}.nt2{margin-top:-.5rem}.nt3{margin-top:-1rem}.nt4{margin-top:-2rem}.nt5{margin-top:-4rem}.nt6{margin-top:-8rem}.nt7{margin-top:-16rem}.collapse{border-collapse:collapse;border-spacing:0}.striped--light-silver:nth-child(odd){background-color:#aaa}.striped--moon-gray:nth-child(odd){background-color:#ccc}.striped--light-gray:nth-child(odd){background-color:#eee}.striped--near-white:nth-child(odd){background-color:#f4f4f4}.stripe-light:nth-child(odd){background-color:hsla(0,0%,100%,.1)}.stripe-dark:nth-child(odd){background-color:rgba(0,0,0,.1)}.strike{text-decoration:line-through}.underline{text-decoration:underline}.no-underline{text-decoration:none}.tl{text-align:left}.tr{text-align:right}.tc{text-align:center}.tj{text-align:justify}.ttc{text-transform:capitalize}.ttl{text-transform:lowercase}.ttu{text-transform:uppercase}.ttn{text-transform:none}.f-6,.f-headline{font-size:6rem}.f-5,.f-subheadline{font-size:5rem}.f1{font-size:3rem}.f2{font-size:2.25rem}.f3{font-size:1.5rem}.f4{font-size:1.25rem}.f5{font-size:1rem}.f6{font-size:.875rem}.f7{font-size:.75rem}.measure{max-width:30em}.measure-wide{max-width:34em}.measure-narrow{max-width:20em}.indent{text-indent:1em;margin-top:0;margin-bottom:0}.small-caps{font-variant:small-caps}.truncate{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.overflow-container{overflow-y:scroll}.center{margin-left:auto}.center,.mr-auto{margin-right:auto}.ml-auto{margin-left:auto}.clip{position:fixed!important;_position:absolute!important;clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px)}.ws-normal{white-space:normal}.nowrap{white-space:nowrap}.pre{white-space:pre}.v-base{vertical-align:baseline}.v-mid{vertical-align:middle}.v-top{vertical-align:top}.v-btm{vertical-align:bottom}.dim{opacity:1}.dim,.dim:focus,.dim:hover{transition:opacity .15s ease-in}.dim:focus,.dim:hover{opacity:.5}.dim:active{opacity:.8;transition:opacity .15s ease-out}.glow,.glow:focus,.glow:hover{transition:opacity .15s ease-in}.glow:focus,.glow:hover{opacity:1}.hide-child .child{opacity:0;transition:opacity .15s ease-in}.hide-child:active .child,.hide-child:focus .child,.hide-child:hover .child{opacity:1;transition:opacity .15s ease-in}.underline-hover:focus,.underline-hover:hover{text-decoration:underline}.grow{-moz-osx-font-smoothing:grayscale;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform:translateZ(0);transform:translateZ(0);transition:-webkit-transform .25s ease-out;transition:transform .25s ease-out;transition:transform .25s ease-out,-webkit-transform .25s ease-out}.grow:focus,.grow:hover{-webkit-transform:scale(1.05);transform:scale(1.05)}.grow:active{-webkit-transform:scale(.9);transform:scale(.9)}.grow-large{-moz-osx-font-smoothing:grayscale;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform:translateZ(0);transform:translateZ(0);transition:-webkit-transform .25s ease-in-out;transition:transform .25s ease-in-out;transition:transform .25s ease-in-out,-webkit-transform .25s ease-in-out}.grow-large:focus,.grow-large:hover{-webkit-transform:scale(1.2);transform:scale(1.2)}.grow-large:active{-webkit-transform:scale(.95);transform:scale(.95)}.pointer:hover,.shadow-hover{cursor:pointer}.shadow-hover{position:relative;transition:all .5s cubic-bezier(.165,.84,.44,1)}.shadow-hover:after{content:"";box-shadow:0 0 16px 2px rgba(0,0,0,.2);border-radius:inherit;opacity:0;position:absolute;top:0;left:0;width:100%;height:100%;z-index:-1;transition:opacity .5s cubic-bezier(.165,.84,.44,1)}.shadow-hover:focus:after,.shadow-hover:hover:after{opacity:1}.bg-animate,.bg-animate:focus,.bg-animate:hover{transition:background-color .15s ease-in-out}.z-0{z-index:0}.z-1{z-index:1}.z-2{z-index:2}.z-3{z-index:3}.z-4{z-index:4}.z-5{z-index:5}.z-999{z-index:999}.z-9999{z-index:9999}.z-max{z-index:2147483647}.z-inherit{z-index:inherit}.z-initial{z-index:auto}.z-unset{z-index:unset}.nested-copy-line-height ol,.nested-copy-line-height p,.nested-copy-line-height ul{line-height:1.5}.nested-headline-line-height h1,.nested-headline-line-height h2,.nested-headline-line-height h3,.nested-headline-line-height h4,.nested-headline-line-height h5,.nested-headline-line-height h6{line-height:1.25}.nested-list-reset ol,.nested-list-reset ul{padding-left:0;margin-left:0;list-style-type:none}.nested-copy-indent p+p{text-indent:1em;margin-top:0;margin-bottom:0}.nested-copy-separator p+p{margin-top:1.5em}.nested-img img{width:100%;max-width:100%;display:block}.nested-links a{color:#357edd;transition:color .15s ease-in}.nested-links a:focus,.nested-links a:hover{color:#96ccff;transition:color .15s ease-in}.debug *{outline:1px solid gold}.debug-white *{outline:1px solid #fff}.debug-black *{outline:1px solid #000}.debug-grid{background:transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAFElEQVR4AWPAC97/9x0eCsAEPgwAVLshdpENIxcAAAAASUVORK5CYII=) repeat 0 0}.debug-grid-16{background:transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMklEQVR4AWOgCLz/b0epAa6UGuBOqQHOQHLUgFEDnAbcBZ4UGwDOkiCnkIhdgNgNxAYAiYlD+8sEuo8AAAAASUVORK5CYII=) repeat 0 0}.debug-grid-8-solid{background:#fff url(data:image/gif;base64,R0lGODdhCAAIAPEAAADw/wDx/////wAAACwAAAAACAAIAAACDZQvgaeb/lxbAIKA8y0AOw==) repeat 0 0}.debug-grid-16-solid{background:#fff url(data:image/gif;base64,R0lGODdhEAAQAPEAAADw/wDx/xXy/////ywAAAAAEAAQAAACIZyPKckYDQFsb6ZqD85jZ2+BkwiRFKehhqQCQgDHcgwEBQA7) repeat 0 0}@media screen and (min-width:30em){.aspect-ratio-ns{height:0;position:relative}.aspect-ratio--16x9-ns{padding-bottom:56.25%}.aspect-ratio--9x16-ns{padding-bottom:177.77%}.aspect-ratio--4x3-ns{padding-bottom:75%}.aspect-ratio--3x4-ns{padding-bottom:133.33%}.aspect-ratio--6x4-ns{padding-bottom:66.6%}.aspect-ratio--4x6-ns{padding-bottom:150%}.aspect-ratio--8x5-ns{padding-bottom:62.5%}.aspect-ratio--5x8-ns{padding-bottom:160%}.aspect-ratio--7x5-ns{padding-bottom:71.42%}.aspect-ratio--5x7-ns{padding-bottom:140%}.aspect-ratio--1x1-ns{padding-bottom:100%}.aspect-ratio--object-ns{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;z-index:100}.cover-ns{background-size:cover!important}.contain-ns{background-size:contain!important}.bg-center-ns{background-position:50%}.bg-center-ns,.bg-top-ns{background-repeat:no-repeat}.bg-top-ns{background-position:top}.bg-right-ns{background-position:100%}.bg-bottom-ns,.bg-right-ns{background-repeat:no-repeat}.bg-bottom-ns{background-position:bottom}.bg-left-ns{background-repeat:no-repeat;background-position:0}.outline-ns{outline:1px solid}.outline-transparent-ns{outline:1px solid transparent}.outline-0-ns{outline:0}.ba-ns{border-style:solid;border-width:1px}.bt-ns{border-top-style:solid;border-top-width:1px}.br-ns{border-right-style:solid;border-right-width:1px}.bb-ns{border-bottom-style:solid;border-bottom-width:1px}.bl-ns{border-left-style:solid;border-left-width:1px}.bn-ns{border-style:none;border-width:0}.br0-ns{border-radius:0}.br1-ns{border-radius:.125rem}.br2-ns{border-radius:.25rem}.br3-ns{border-radius:.5rem}.br4-ns{border-radius:1rem}.br-100-ns{border-radius:100%}.br-pill-ns{border-radius:9999px}.br--bottom-ns{border-top-left-radius:0;border-top-right-radius:0}.br--top-ns{border-bottom-right-radius:0}.br--right-ns,.br--top-ns{border-bottom-left-radius:0}.br--right-ns{border-top-left-radius:0}.br--left-ns{border-top-right-radius:0;border-bottom-right-radius:0}.b--dotted-ns{border-style:dotted}.b--dashed-ns{border-style:dashed}.b--solid-ns{border-style:solid}.b--none-ns{border-style:none}.bw0-ns{border-width:0}.bw1-ns{border-width:.125rem}.bw2-ns{border-width:.25rem}.bw3-ns{border-width:.5rem}.bw4-ns{border-width:1rem}.bw5-ns{border-width:2rem}.bt-0-ns{border-top-width:0}.br-0-ns{border-right-width:0}.bb-0-ns{border-bottom-width:0}.bl-0-ns{border-left-width:0}.shadow-1-ns{box-shadow:0 0 4px 2px rgba(0,0,0,.2)}.shadow-2-ns{box-shadow:0 0 8px 2px rgba(0,0,0,.2)}.shadow-3-ns{box-shadow:2px 2px 4px 2px rgba(0,0,0,.2)}.shadow-4-ns{box-shadow:2px 2px 8px 0 rgba(0,0,0,.2)}.shadow-5-ns{box-shadow:4px 4px 8px 0 rgba(0,0,0,.2)}.top-0-ns{top:0}.left-0-ns{left:0}.right-0-ns{right:0}.bottom-0-ns{bottom:0}.top-1-ns{top:1rem}.left-1-ns{left:1rem}.right-1-ns{right:1rem}.bottom-1-ns{bottom:1rem}.top-2-ns{top:2rem}.left-2-ns{left:2rem}.right-2-ns{right:2rem}.bottom-2-ns{bottom:2rem}.top--1-ns{top:-1rem}.right--1-ns{right:-1rem}.bottom--1-ns{bottom:-1rem}.left--1-ns{left:-1rem}.top--2-ns{top:-2rem}.right--2-ns{right:-2rem}.bottom--2-ns{bottom:-2rem}.left--2-ns{left:-2rem}.absolute--fill-ns{top:0;right:0;bottom:0;left:0}.cl-ns{clear:left}.cr-ns{clear:right}.cb-ns{clear:both}.cn-ns{clear:none}.dn-ns{display:none}.di-ns{display:inline}.db-ns{display:block}.dib-ns{display:inline-block}.dit-ns{display:inline-table}.dt-ns{display:table}.dtc-ns{display:table-cell}.dt-row-ns{display:table-row}.dt-row-group-ns{display:table-row-group}.dt-column-ns{display:table-column}.dt-column-group-ns{display:table-column-group}.dt--fixed-ns{table-layout:fixed;width:100%}.flex-ns{display:flex}.inline-flex-ns{display:inline-flex}.flex-auto-ns{flex:1 1 auto;min-width:0;min-height:0}.flex-none-ns{flex:none}.flex-column-ns{flex-direction:column}.flex-row-ns{flex-direction:row}.flex-wrap-ns{flex-wrap:wrap}.flex-nowrap-ns{flex-wrap:nowrap}.flex-wrap-reverse-ns{flex-wrap:wrap-reverse}.flex-column-reverse-ns{flex-direction:column-reverse}.flex-row-reverse-ns{flex-direction:row-reverse}.items-start-ns{align-items:flex-start}.items-end-ns{align-items:flex-end}.items-center-ns{align-items:center}.items-baseline-ns{align-items:baseline}.items-stretch-ns{align-items:stretch}.self-start-ns{align-self:flex-start}.self-end-ns{align-self:flex-end}.self-center-ns{align-self:center}.self-baseline-ns{align-self:baseline}.self-stretch-ns{align-self:stretch}.justify-start-ns{justify-content:flex-start}.justify-end-ns{justify-content:flex-end}.justify-center-ns{justify-content:center}.justify-between-ns{justify-content:space-between}.justify-around-ns{justify-content:space-around}.content-start-ns{align-content:flex-start}.content-end-ns{align-content:flex-end}.content-center-ns{align-content:center}.content-between-ns{align-content:space-between}.content-around-ns{align-content:space-around}.content-stretch-ns{align-content:stretch}.order-0-ns{order:0}.order-1-ns{order:1}.order-2-ns{order:2}.order-3-ns{order:3}.order-4-ns{order:4}.order-5-ns{order:5}.order-6-ns{order:6}.order-7-ns{order:7}.order-8-ns{order:8}.order-last-ns{order:99999}.flex-grow-0-ns{flex-grow:0}.flex-grow-1-ns{flex-grow:1}.flex-shrink-0-ns{flex-shrink:0}.flex-shrink-1-ns{flex-shrink:1}.fl-ns{float:left}.fl-ns,.fr-ns{_display:inline}.fr-ns{float:right}.fn-ns{float:none}.i-ns{font-style:italic}.fs-normal-ns{font-style:normal}.normal-ns{font-weight:400}.b-ns{font-weight:700}.fw1-ns{font-weight:100}.fw2-ns{font-weight:200}.fw3-ns{font-weight:300}.fw4-ns{font-weight:400}.fw5-ns{font-weight:500}.fw6-ns{font-weight:600}.fw7-ns{font-weight:700}.fw8-ns{font-weight:800}.fw9-ns{font-weight:900}.h1-ns{height:1rem}.h2-ns{height:2rem}.h3-ns{height:4rem}.h4-ns{height:8rem}.h5-ns{height:16rem}.h-25-ns{height:25%}.h-50-ns{height:50%}.h-75-ns{height:75%}.h-100-ns{height:100%}.min-h-100-ns{min-height:100%}.vh-25-ns{height:25vh}.vh-50-ns{height:50vh}.vh-75-ns{height:75vh}.vh-100-ns{height:100vh}.min-vh-100-ns{min-height:100vh}.h-auto-ns{height:auto}.h-inherit-ns{height:inherit}.tracked-ns{letter-spacing:.1em}.tracked-tight-ns{letter-spacing:-.05em}.tracked-mega-ns{letter-spacing:.25em}.lh-solid-ns{line-height:1}.lh-title-ns{line-height:1.25}.lh-copy-ns{line-height:1.5}.mw-100-ns{max-width:100%}.mw1-ns{max-width:1rem}.mw2-ns{max-width:2rem}.mw3-ns{max-width:4rem}.mw4-ns{max-width:8rem}.mw5-ns{max-width:16rem}.mw6-ns{max-width:32rem}.mw7-ns{max-width:48rem}.mw8-ns{max-width:64rem}.mw9-ns{max-width:96rem}.mw-none-ns{max-width:none}.w1-ns{width:1rem}.w2-ns{width:2rem}.w3-ns{width:4rem}.w4-ns{width:8rem}.w5-ns{width:16rem}.w-10-ns{width:10%}.w-20-ns{width:20%}.w-25-ns{width:25%}.w-30-ns{width:30%}.w-33-ns{width:33%}.w-34-ns{width:34%}.w-40-ns{width:40%}.w-50-ns{width:50%}.w-60-ns{width:60%}.w-70-ns{width:70%}.w-75-ns{width:75%}.w-80-ns{width:80%}.w-90-ns{width:90%}.w-100-ns{width:100%}.w-third-ns{width:33.33333%}.w-two-thirds-ns{width:66.66667%}.w-auto-ns{width:auto}.overflow-visible-ns{overflow:visible}.overflow-hidden-ns{overflow:hidden}.overflow-scroll-ns{overflow:scroll}.overflow-auto-ns{overflow:auto}.overflow-x-visible-ns{overflow-x:visible}.overflow-x-hidden-ns{overflow-x:hidden}.overflow-x-scroll-ns{overflow-x:scroll}.overflow-x-auto-ns{overflow-x:auto}.overflow-y-visible-ns{overflow-y:visible}.overflow-y-hidden-ns{overflow-y:hidden}.overflow-y-scroll-ns{overflow-y:scroll}.overflow-y-auto-ns{overflow-y:auto}.static-ns{position:static}.relative-ns{position:relative}.absolute-ns{position:absolute}.fixed-ns{position:fixed}.rotate-45-ns{-webkit-transform:rotate(45deg);transform:rotate(45deg)}.rotate-90-ns{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.rotate-135-ns{-webkit-transform:rotate(135deg);transform:rotate(135deg)}.rotate-180-ns{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.rotate-225-ns{-webkit-transform:rotate(225deg);transform:rotate(225deg)}.rotate-270-ns{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.rotate-315-ns{-webkit-transform:rotate(315deg);transform:rotate(315deg)}.pa0-ns{padding:0}.pa1-ns{padding:.25rem}.pa2-ns{padding:.5rem}.pa3-ns{padding:1rem}.pa4-ns{padding:2rem}.pa5-ns{padding:4rem}.pa6-ns{padding:8rem}.pa7-ns{padding:16rem}.pl0-ns{padding-left:0}.pl1-ns{padding-left:.25rem}.pl2-ns{padding-left:.5rem}.pl3-ns{padding-left:1rem}.pl4-ns{padding-left:2rem}.pl5-ns{padding-left:4rem}.pl6-ns{padding-left:8rem}.pl7-ns{padding-left:16rem}.pr0-ns{padding-right:0}.pr1-ns{padding-right:.25rem}.pr2-ns{padding-right:.5rem}.pr3-ns{padding-right:1rem}.pr4-ns{padding-right:2rem}.pr5-ns{padding-right:4rem}.pr6-ns{padding-right:8rem}.pr7-ns{padding-right:16rem}.pb0-ns{padding-bottom:0}.pb1-ns{padding-bottom:.25rem}.pb2-ns{padding-bottom:.5rem}.pb3-ns{padding-bottom:1rem}.pb4-ns{padding-bottom:2rem}.pb5-ns{padding-bottom:4rem}.pb6-ns{padding-bottom:8rem}.pb7-ns{padding-bottom:16rem}.pt0-ns{padding-top:0}.pt1-ns{padding-top:.25rem}.pt2-ns{padding-top:.5rem}.pt3-ns{padding-top:1rem}.pt4-ns{padding-top:2rem}.pt5-ns{padding-top:4rem}.pt6-ns{padding-top:8rem}.pt7-ns{padding-top:16rem}.pv0-ns{padding-top:0;padding-bottom:0}.pv1-ns{padding-top:.25rem;padding-bottom:.25rem}.pv2-ns{padding-top:.5rem;padding-bottom:.5rem}.pv3-ns{padding-top:1rem;padding-bottom:1rem}.pv4-ns{padding-top:2rem;padding-bottom:2rem}.pv5-ns{padding-top:4rem;padding-bottom:4rem}.pv6-ns{padding-top:8rem;padding-bottom:8rem}.pv7-ns{padding-top:16rem;padding-bottom:16rem}.ph0-ns{padding-left:0;padding-right:0}.ph1-ns{padding-left:.25rem;padding-right:.25rem}.ph2-ns{padding-left:.5rem;padding-right:.5rem}.ph3-ns{padding-left:1rem;padding-right:1rem}.ph4-ns{padding-left:2rem;padding-right:2rem}.ph5-ns{padding-left:4rem;padding-right:4rem}.ph6-ns{padding-left:8rem;padding-right:8rem}.ph7-ns{padding-left:16rem;padding-right:16rem}.ma0-ns{margin:0}.ma1-ns{margin:.25rem}.ma2-ns{margin:.5rem}.ma3-ns{margin:1rem}.ma4-ns{margin:2rem}.ma5-ns{margin:4rem}.ma6-ns{margin:8rem}.ma7-ns{margin:16rem}.ml0-ns{margin-left:0}.ml1-ns{margin-left:.25rem}.ml2-ns{margin-left:.5rem}.ml3-ns{margin-left:1rem}.ml4-ns{margin-left:2rem}.ml5-ns{margin-left:4rem}.ml6-ns{margin-left:8rem}.ml7-ns{margin-left:16rem}.mr0-ns{margin-right:0}.mr1-ns{margin-right:.25rem}.mr2-ns{margin-right:.5rem}.mr3-ns{margin-right:1rem}.mr4-ns{margin-right:2rem}.mr5-ns{margin-right:4rem}.mr6-ns{margin-right:8rem}.mr7-ns{margin-right:16rem}.mb0-ns{margin-bottom:0}.mb1-ns{margin-bottom:.25rem}.mb2-ns{margin-bottom:.5rem}.mb3-ns{margin-bottom:1rem}.mb4-ns{margin-bottom:2rem}.mb5-ns{margin-bottom:4rem}.mb6-ns{margin-bottom:8rem}.mb7-ns{margin-bottom:16rem}.mt0-ns{margin-top:0}.mt1-ns{margin-top:.25rem}.mt2-ns{margin-top:.5rem}.mt3-ns{margin-top:1rem}.mt4-ns{margin-top:2rem}.mt5-ns{margin-top:4rem}.mt6-ns{margin-top:8rem}.mt7-ns{margin-top:16rem}.mv0-ns{margin-top:0;margin-bottom:0}.mv1-ns{margin-top:.25rem;margin-bottom:.25rem}.mv2-ns{margin-top:.5rem;margin-bottom:.5rem}.mv3-ns{margin-top:1rem;margin-bottom:1rem}.mv4-ns{margin-top:2rem;margin-bottom:2rem}.mv5-ns{margin-top:4rem;margin-bottom:4rem}.mv6-ns{margin-top:8rem;margin-bottom:8rem}.mv7-ns{margin-top:16rem;margin-bottom:16rem}.mh0-ns{margin-left:0;margin-right:0}.mh1-ns{margin-left:.25rem;margin-right:.25rem}.mh2-ns{margin-left:.5rem;margin-right:.5rem}.mh3-ns{margin-left:1rem;margin-right:1rem}.mh4-ns{margin-left:2rem;margin-right:2rem}.mh5-ns{margin-left:4rem;margin-right:4rem}.mh6-ns{margin-left:8rem;margin-right:8rem}.mh7-ns{margin-left:16rem;margin-right:16rem}.na1-ns{margin:-.25rem}.na2-ns{margin:-.5rem}.na3-ns{margin:-1rem}.na4-ns{margin:-2rem}.na5-ns{margin:-4rem}.na6-ns{margin:-8rem}.na7-ns{margin:-16rem}.nl1-ns{margin-left:-.25rem}.nl2-ns{margin-left:-.5rem}.nl3-ns{margin-left:-1rem}.nl4-ns{margin-left:-2rem}.nl5-ns{margin-left:-4rem}.nl6-ns{margin-left:-8rem}.nl7-ns{margin-left:-16rem}.nr1-ns{margin-right:-.25rem}.nr2-ns{margin-right:-.5rem}.nr3-ns{margin-right:-1rem}.nr4-ns{margin-right:-2rem}.nr5-ns{margin-right:-4rem}.nr6-ns{margin-right:-8rem}.nr7-ns{margin-right:-16rem}.nb1-ns{margin-bottom:-.25rem}.nb2-ns{margin-bottom:-.5rem}.nb3-ns{margin-bottom:-1rem}.nb4-ns{margin-bottom:-2rem}.nb5-ns{margin-bottom:-4rem}.nb6-ns{margin-bottom:-8rem}.nb7-ns{margin-bottom:-16rem}.nt1-ns{margin-top:-.25rem}.nt2-ns{margin-top:-.5rem}.nt3-ns{margin-top:-1rem}.nt4-ns{margin-top:-2rem}.nt5-ns{margin-top:-4rem}.nt6-ns{margin-top:-8rem}.nt7-ns{margin-top:-16rem}.strike-ns{text-decoration:line-through}.underline-ns{text-decoration:underline}.no-underline-ns{text-decoration:none}.tl-ns{text-align:left}.tr-ns{text-align:right}.tc-ns{text-align:center}.tj-ns{text-align:justify}.ttc-ns{text-transform:capitalize}.ttl-ns{text-transform:lowercase}.ttu-ns{text-transform:uppercase}.ttn-ns{text-transform:none}.f-6-ns,.f-headline-ns{font-size:6rem}.f-5-ns,.f-subheadline-ns{font-size:5rem}.f1-ns{font-size:3rem}.f2-ns{font-size:2.25rem}.f3-ns{font-size:1.5rem}.f4-ns{font-size:1.25rem}.f5-ns{font-size:1rem}.f6-ns{font-size:.875rem}.f7-ns{font-size:.75rem}.measure-ns{max-width:30em}.measure-wide-ns{max-width:34em}.measure-narrow-ns{max-width:20em}.indent-ns{text-indent:1em;margin-top:0;margin-bottom:0}.small-caps-ns{font-variant:small-caps}.truncate-ns{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.center-ns{margin-left:auto}.center-ns,.mr-auto-ns{margin-right:auto}.ml-auto-ns{margin-left:auto}.clip-ns{position:fixed!important;_position:absolute!important;clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px)}.ws-normal-ns{white-space:normal}.nowrap-ns{white-space:nowrap}.pre-ns{white-space:pre}.v-base-ns{vertical-align:baseline}.v-mid-ns{vertical-align:middle}.v-top-ns{vertical-align:top}.v-btm-ns{vertical-align:bottom}}@media screen and (min-width:30em) and (max-width:60em){.aspect-ratio-m{height:0;position:relative}.aspect-ratio--16x9-m{padding-bottom:56.25%}.aspect-ratio--9x16-m{padding-bottom:177.77%}.aspect-ratio--4x3-m{padding-bottom:75%}.aspect-ratio--3x4-m{padding-bottom:133.33%}.aspect-ratio--6x4-m{padding-bottom:66.6%}.aspect-ratio--4x6-m{padding-bottom:150%}.aspect-ratio--8x5-m{padding-bottom:62.5%}.aspect-ratio--5x8-m{padding-bottom:160%}.aspect-ratio--7x5-m{padding-bottom:71.42%}.aspect-ratio--5x7-m{padding-bottom:140%}.aspect-ratio--1x1-m{padding-bottom:100%}.aspect-ratio--object-m{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;z-index:100}.cover-m{background-size:cover!important}.contain-m{background-size:contain!important}.bg-center-m{background-position:50%}.bg-center-m,.bg-top-m{background-repeat:no-repeat}.bg-top-m{background-position:top}.bg-right-m{background-position:100%}.bg-bottom-m,.bg-right-m{background-repeat:no-repeat}.bg-bottom-m{background-position:bottom}.bg-left-m{background-repeat:no-repeat;background-position:0}.outline-m{outline:1px solid}.outline-transparent-m{outline:1px solid transparent}.outline-0-m{outline:0}.ba-m{border-style:solid;border-width:1px}.bt-m{border-top-style:solid;border-top-width:1px}.br-m{border-right-style:solid;border-right-width:1px}.bb-m{border-bottom-style:solid;border-bottom-width:1px}.bl-m{border-left-style:solid;border-left-width:1px}.bn-m{border-style:none;border-width:0}.br0-m{border-radius:0}.br1-m{border-radius:.125rem}.br2-m{border-radius:.25rem}.br3-m{border-radius:.5rem}.br4-m{border-radius:1rem}.br-100-m{border-radius:100%}.br-pill-m{border-radius:9999px}.br--bottom-m{border-top-left-radius:0;border-top-right-radius:0}.br--top-m{border-bottom-right-radius:0}.br--right-m,.br--top-m{border-bottom-left-radius:0}.br--right-m{border-top-left-radius:0}.br--left-m{border-top-right-radius:0;border-bottom-right-radius:0}.b--dotted-m{border-style:dotted}.b--dashed-m{border-style:dashed}.b--solid-m{border-style:solid}.b--none-m{border-style:none}.bw0-m{border-width:0}.bw1-m{border-width:.125rem}.bw2-m{border-width:.25rem}.bw3-m{border-width:.5rem}.bw4-m{border-width:1rem}.bw5-m{border-width:2rem}.bt-0-m{border-top-width:0}.br-0-m{border-right-width:0}.bb-0-m{border-bottom-width:0}.bl-0-m{border-left-width:0}.shadow-1-m{box-shadow:0 0 4px 2px rgba(0,0,0,.2)}.shadow-2-m{box-shadow:0 0 8px 2px rgba(0,0,0,.2)}.shadow-3-m{box-shadow:2px 2px 4px 2px rgba(0,0,0,.2)}.shadow-4-m{box-shadow:2px 2px 8px 0 rgba(0,0,0,.2)}.shadow-5-m{box-shadow:4px 4px 8px 0 rgba(0,0,0,.2)}.top-0-m{top:0}.left-0-m{left:0}.right-0-m{right:0}.bottom-0-m{bottom:0}.top-1-m{top:1rem}.left-1-m{left:1rem}.right-1-m{right:1rem}.bottom-1-m{bottom:1rem}.top-2-m{top:2rem}.left-2-m{left:2rem}.right-2-m{right:2rem}.bottom-2-m{bottom:2rem}.top--1-m{top:-1rem}.right--1-m{right:-1rem}.bottom--1-m{bottom:-1rem}.left--1-m{left:-1rem}.top--2-m{top:-2rem}.right--2-m{right:-2rem}.bottom--2-m{bottom:-2rem}.left--2-m{left:-2rem}.absolute--fill-m{top:0;right:0;bottom:0;left:0}.cl-m{clear:left}.cr-m{clear:right}.cb-m{clear:both}.cn-m{clear:none}.dn-m{display:none}.di-m{display:inline}.db-m{display:block}.dib-m{display:inline-block}.dit-m{display:inline-table}.dt-m{display:table}.dtc-m{display:table-cell}.dt-row-m{display:table-row}.dt-row-group-m{display:table-row-group}.dt-column-m{display:table-column}.dt-column-group-m{display:table-column-group}.dt--fixed-m{table-layout:fixed;width:100%}.flex-m{display:flex}.inline-flex-m{display:inline-flex}.flex-auto-m{flex:1 1 auto;min-width:0;min-height:0}.flex-none-m{flex:none}.flex-column-m{flex-direction:column}.flex-row-m{flex-direction:row}.flex-wrap-m{flex-wrap:wrap}.flex-nowrap-m{flex-wrap:nowrap}.flex-wrap-reverse-m{flex-wrap:wrap-reverse}.flex-column-reverse-m{flex-direction:column-reverse}.flex-row-reverse-m{flex-direction:row-reverse}.items-start-m{align-items:flex-start}.items-end-m{align-items:flex-end}.items-center-m{align-items:center}.items-baseline-m{align-items:baseline}.items-stretch-m{align-items:stretch}.self-start-m{align-self:flex-start}.self-end-m{align-self:flex-end}.self-center-m{align-self:center}.self-baseline-m{align-self:baseline}.self-stretch-m{align-self:stretch}.justify-start-m{justify-content:flex-start}.justify-end-m{justify-content:flex-end}.justify-center-m{justify-content:center}.justify-between-m{justify-content:space-between}.justify-around-m{justify-content:space-around}.content-start-m{align-content:flex-start}.content-end-m{align-content:flex-end}.content-center-m{align-content:center}.content-between-m{align-content:space-between}.content-around-m{align-content:space-around}.content-stretch-m{align-content:stretch}.order-0-m{order:0}.order-1-m{order:1}.order-2-m{order:2}.order-3-m{order:3}.order-4-m{order:4}.order-5-m{order:5}.order-6-m{order:6}.order-7-m{order:7}.order-8-m{order:8}.order-last-m{order:99999}.flex-grow-0-m{flex-grow:0}.flex-grow-1-m{flex-grow:1}.flex-shrink-0-m{flex-shrink:0}.flex-shrink-1-m{flex-shrink:1}.fl-m{float:left}.fl-m,.fr-m{_display:inline}.fr-m{float:right}.fn-m{float:none}.i-m{font-style:italic}.fs-normal-m{font-style:normal}.normal-m{font-weight:400}.b-m{font-weight:700}.fw1-m{font-weight:100}.fw2-m{font-weight:200}.fw3-m{font-weight:300}.fw4-m{font-weight:400}.fw5-m{font-weight:500}.fw6-m{font-weight:600}.fw7-m{font-weight:700}.fw8-m{font-weight:800}.fw9-m{font-weight:900}.h1-m{height:1rem}.h2-m{height:2rem}.h3-m{height:4rem}.h4-m{height:8rem}.h5-m{height:16rem}.h-25-m{height:25%}.h-50-m{height:50%}.h-75-m{height:75%}.h-100-m{height:100%}.min-h-100-m{min-height:100%}.vh-25-m{height:25vh}.vh-50-m{height:50vh}.vh-75-m{height:75vh}.vh-100-m{height:100vh}.min-vh-100-m{min-height:100vh}.h-auto-m{height:auto}.h-inherit-m{height:inherit}.tracked-m{letter-spacing:.1em}.tracked-tight-m{letter-spacing:-.05em}.tracked-mega-m{letter-spacing:.25em}.lh-solid-m{line-height:1}.lh-title-m{line-height:1.25}.lh-copy-m{line-height:1.5}.mw-100-m{max-width:100%}.mw1-m{max-width:1rem}.mw2-m{max-width:2rem}.mw3-m{max-width:4rem}.mw4-m{max-width:8rem}.mw5-m{max-width:16rem}.mw6-m{max-width:32rem}.mw7-m{max-width:48rem}.mw8-m{max-width:64rem}.mw9-m{max-width:96rem}.mw-none-m{max-width:none}.w1-m{width:1rem}.w2-m{width:2rem}.w3-m{width:4rem}.w4-m{width:8rem}.w5-m{width:16rem}.w-10-m{width:10%}.w-20-m{width:20%}.w-25-m{width:25%}.w-30-m{width:30%}.w-33-m{width:33%}.w-34-m{width:34%}.w-40-m{width:40%}.w-50-m{width:50%}.w-60-m{width:60%}.w-70-m{width:70%}.w-75-m{width:75%}.w-80-m{width:80%}.w-90-m{width:90%}.w-100-m{width:100%}.w-third-m{width:33.33333%}.w-two-thirds-m{width:66.66667%}.w-auto-m{width:auto}.overflow-visible-m{overflow:visible}.overflow-hidden-m{overflow:hidden}.overflow-scroll-m{overflow:scroll}.overflow-auto-m{overflow:auto}.overflow-x-visible-m{overflow-x:visible}.overflow-x-hidden-m{overflow-x:hidden}.overflow-x-scroll-m{overflow-x:scroll}.overflow-x-auto-m{overflow-x:auto}.overflow-y-visible-m{overflow-y:visible}.overflow-y-hidden-m{overflow-y:hidden}.overflow-y-scroll-m{overflow-y:scroll}.overflow-y-auto-m{overflow-y:auto}.static-m{position:static}.relative-m{position:relative}.absolute-m{position:absolute}.fixed-m{position:fixed}.rotate-45-m{-webkit-transform:rotate(45deg);transform:rotate(45deg)}.rotate-90-m{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.rotate-135-m{-webkit-transform:rotate(135deg);transform:rotate(135deg)}.rotate-180-m{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.rotate-225-m{-webkit-transform:rotate(225deg);transform:rotate(225deg)}.rotate-270-m{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.rotate-315-m{-webkit-transform:rotate(315deg);transform:rotate(315deg)}.pa0-m{padding:0}.pa1-m{padding:.25rem}.pa2-m{padding:.5rem}.pa3-m{padding:1rem}.pa4-m{padding:2rem}.pa5-m{padding:4rem}.pa6-m{padding:8rem}.pa7-m{padding:16rem}.pl0-m{padding-left:0}.pl1-m{padding-left:.25rem}.pl2-m{padding-left:.5rem}.pl3-m{padding-left:1rem}.pl4-m{padding-left:2rem}.pl5-m{padding-left:4rem}.pl6-m{padding-left:8rem}.pl7-m{padding-left:16rem}.pr0-m{padding-right:0}.pr1-m{padding-right:.25rem}.pr2-m{padding-right:.5rem}.pr3-m{padding-right:1rem}.pr4-m{padding-right:2rem}.pr5-m{padding-right:4rem}.pr6-m{padding-right:8rem}.pr7-m{padding-right:16rem}.pb0-m{padding-bottom:0}.pb1-m{padding-bottom:.25rem}.pb2-m{padding-bottom:.5rem}.pb3-m{padding-bottom:1rem}.pb4-m{padding-bottom:2rem}.pb5-m{padding-bottom:4rem}.pb6-m{padding-bottom:8rem}.pb7-m{padding-bottom:16rem}.pt0-m{padding-top:0}.pt1-m{padding-top:.25rem}.pt2-m{padding-top:.5rem}.pt3-m{padding-top:1rem}.pt4-m{padding-top:2rem}.pt5-m{padding-top:4rem}.pt6-m{padding-top:8rem}.pt7-m{padding-top:16rem}.pv0-m{padding-top:0;padding-bottom:0}.pv1-m{padding-top:.25rem;padding-bottom:.25rem}.pv2-m{padding-top:.5rem;padding-bottom:.5rem}.pv3-m{padding-top:1rem;padding-bottom:1rem}.pv4-m{padding-top:2rem;padding-bottom:2rem}.pv5-m{padding-top:4rem;padding-bottom:4rem}.pv6-m{padding-top:8rem;padding-bottom:8rem}.pv7-m{padding-top:16rem;padding-bottom:16rem}.ph0-m{padding-left:0;padding-right:0}.ph1-m{padding-left:.25rem;padding-right:.25rem}.ph2-m{padding-left:.5rem;padding-right:.5rem}.ph3-m{padding-left:1rem;padding-right:1rem}.ph4-m{padding-left:2rem;padding-right:2rem}.ph5-m{padding-left:4rem;padding-right:4rem}.ph6-m{padding-left:8rem;padding-right:8rem}.ph7-m{padding-left:16rem;padding-right:16rem}.ma0-m{margin:0}.ma1-m{margin:.25rem}.ma2-m{margin:.5rem}.ma3-m{margin:1rem}.ma4-m{margin:2rem}.ma5-m{margin:4rem}.ma6-m{margin:8rem}.ma7-m{margin:16rem}.ml0-m{margin-left:0}.ml1-m{margin-left:.25rem}.ml2-m{margin-left:.5rem}.ml3-m{margin-left:1rem}.ml4-m{margin-left:2rem}.ml5-m{margin-left:4rem}.ml6-m{margin-left:8rem}.ml7-m{margin-left:16rem}.mr0-m{margin-right:0}.mr1-m{margin-right:.25rem}.mr2-m{margin-right:.5rem}.mr3-m{margin-right:1rem}.mr4-m{margin-right:2rem}.mr5-m{margin-right:4rem}.mr6-m{margin-right:8rem}.mr7-m{margin-right:16rem}.mb0-m{margin-bottom:0}.mb1-m{margin-bottom:.25rem}.mb2-m{margin-bottom:.5rem}.mb3-m{margin-bottom:1rem}.mb4-m{margin-bottom:2rem}.mb5-m{margin-bottom:4rem}.mb6-m{margin-bottom:8rem}.mb7-m{margin-bottom:16rem}.mt0-m{margin-top:0}.mt1-m{margin-top:.25rem}.mt2-m{margin-top:.5rem}.mt3-m{margin-top:1rem}.mt4-m{margin-top:2rem}.mt5-m{margin-top:4rem}.mt6-m{margin-top:8rem}.mt7-m{margin-top:16rem}.mv0-m{margin-top:0;margin-bottom:0}.mv1-m{margin-top:.25rem;margin-bottom:.25rem}.mv2-m{margin-top:.5rem;margin-bottom:.5rem}.mv3-m{margin-top:1rem;margin-bottom:1rem}.mv4-m{margin-top:2rem;margin-bottom:2rem}.mv5-m{margin-top:4rem;margin-bottom:4rem}.mv6-m{margin-top:8rem;margin-bottom:8rem}.mv7-m{margin-top:16rem;margin-bottom:16rem}.mh0-m{margin-left:0;margin-right:0}.mh1-m{margin-left:.25rem;margin-right:.25rem}.mh2-m{margin-left:.5rem;margin-right:.5rem}.mh3-m{margin-left:1rem;margin-right:1rem}.mh4-m{margin-left:2rem;margin-right:2rem}.mh5-m{margin-left:4rem;margin-right:4rem}.mh6-m{margin-left:8rem;margin-right:8rem}.mh7-m{margin-left:16rem;margin-right:16rem}.na1-m{margin:-.25rem}.na2-m{margin:-.5rem}.na3-m{margin:-1rem}.na4-m{margin:-2rem}.na5-m{margin:-4rem}.na6-m{margin:-8rem}.na7-m{margin:-16rem}.nl1-m{margin-left:-.25rem}.nl2-m{margin-left:-.5rem}.nl3-m{margin-left:-1rem}.nl4-m{margin-left:-2rem}.nl5-m{margin-left:-4rem}.nl6-m{margin-left:-8rem}.nl7-m{margin-left:-16rem}.nr1-m{margin-right:-.25rem}.nr2-m{margin-right:-.5rem}.nr3-m{margin-right:-1rem}.nr4-m{margin-right:-2rem}.nr5-m{margin-right:-4rem}.nr6-m{margin-right:-8rem}.nr7-m{margin-right:-16rem}.nb1-m{margin-bottom:-.25rem}.nb2-m{margin-bottom:-.5rem}.nb3-m{margin-bottom:-1rem}.nb4-m{margin-bottom:-2rem}.nb5-m{margin-bottom:-4rem}.nb6-m{margin-bottom:-8rem}.nb7-m{margin-bottom:-16rem}.nt1-m{margin-top:-.25rem}.nt2-m{margin-top:-.5rem}.nt3-m{margin-top:-1rem}.nt4-m{margin-top:-2rem}.nt5-m{margin-top:-4rem}.nt6-m{margin-top:-8rem}.nt7-m{margin-top:-16rem}.strike-m{text-decoration:line-through}.underline-m{text-decoration:underline}.no-underline-m{text-decoration:none}.tl-m{text-align:left}.tr-m{text-align:right}.tc-m{text-align:center}.tj-m{text-align:justify}.ttc-m{text-transform:capitalize}.ttl-m{text-transform:lowercase}.ttu-m{text-transform:uppercase}.ttn-m{text-transform:none}.f-6-m,.f-headline-m{font-size:6rem}.f-5-m,.f-subheadline-m{font-size:5rem}.f1-m{font-size:3rem}.f2-m{font-size:2.25rem}.f3-m{font-size:1.5rem}.f4-m{font-size:1.25rem}.f5-m{font-size:1rem}.f6-m{font-size:.875rem}.f7-m{font-size:.75rem}.measure-m{max-width:30em}.measure-wide-m{max-width:34em}.measure-narrow-m{max-width:20em}.indent-m{text-indent:1em;margin-top:0;margin-bottom:0}.small-caps-m{font-variant:small-caps}.truncate-m{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.center-m{margin-left:auto}.center-m,.mr-auto-m{margin-right:auto}.ml-auto-m{margin-left:auto}.clip-m{position:fixed!important;_position:absolute!important;clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px)}.ws-normal-m{white-space:normal}.nowrap-m{white-space:nowrap}.pre-m{white-space:pre}.v-base-m{vertical-align:baseline}.v-mid-m{vertical-align:middle}.v-top-m{vertical-align:top}.v-btm-m{vertical-align:bottom}}@media screen and (min-width:60em){.aspect-ratio-l{height:0;position:relative}.aspect-ratio--16x9-l{padding-bottom:56.25%}.aspect-ratio--9x16-l{padding-bottom:177.77%}.aspect-ratio--4x3-l{padding-bottom:75%}.aspect-ratio--3x4-l{padding-bottom:133.33%}.aspect-ratio--6x4-l{padding-bottom:66.6%}.aspect-ratio--4x6-l{padding-bottom:150%}.aspect-ratio--8x5-l{padding-bottom:62.5%}.aspect-ratio--5x8-l{padding-bottom:160%}.aspect-ratio--7x5-l{padding-bottom:71.42%}.aspect-ratio--5x7-l{padding-bottom:140%}.aspect-ratio--1x1-l{padding-bottom:100%}.aspect-ratio--object-l{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;z-index:100}.cover-l{background-size:cover!important}.contain-l{background-size:contain!important}.bg-center-l{background-position:50%}.bg-center-l,.bg-top-l{background-repeat:no-repeat}.bg-top-l{background-position:top}.bg-right-l{background-position:100%}.bg-bottom-l,.bg-right-l{background-repeat:no-repeat}.bg-bottom-l{background-position:bottom}.bg-left-l{background-repeat:no-repeat;background-position:0}.outline-l{outline:1px solid}.outline-transparent-l{outline:1px solid transparent}.outline-0-l{outline:0}.ba-l{border-style:solid;border-width:1px}.bt-l{border-top-style:solid;border-top-width:1px}.br-l{border-right-style:solid;border-right-width:1px}.bb-l{border-bottom-style:solid;border-bottom-width:1px}.bl-l{border-left-style:solid;border-left-width:1px}.bn-l{border-style:none;border-width:0}.br0-l{border-radius:0}.br1-l{border-radius:.125rem}.br2-l{border-radius:.25rem}.br3-l{border-radius:.5rem}.br4-l{border-radius:1rem}.br-100-l{border-radius:100%}.br-pill-l{border-radius:9999px}.br--bottom-l{border-top-left-radius:0;border-top-right-radius:0}.br--top-l{border-bottom-right-radius:0}.br--right-l,.br--top-l{border-bottom-left-radius:0}.br--right-l{border-top-left-radius:0}.br--left-l{border-top-right-radius:0;border-bottom-right-radius:0}.b--dotted-l{border-style:dotted}.b--dashed-l{border-style:dashed}.b--solid-l{border-style:solid}.b--none-l{border-style:none}.bw0-l{border-width:0}.bw1-l{border-width:.125rem}.bw2-l{border-width:.25rem}.bw3-l{border-width:.5rem}.bw4-l{border-width:1rem}.bw5-l{border-width:2rem}.bt-0-l{border-top-width:0}.br-0-l{border-right-width:0}.bb-0-l{border-bottom-width:0}.bl-0-l{border-left-width:0}.shadow-1-l{box-shadow:0 0 4px 2px rgba(0,0,0,.2)}.shadow-2-l{box-shadow:0 0 8px 2px rgba(0,0,0,.2)}.shadow-3-l{box-shadow:2px 2px 4px 2px rgba(0,0,0,.2)}.shadow-4-l{box-shadow:2px 2px 8px 0 rgba(0,0,0,.2)}.shadow-5-l{box-shadow:4px 4px 8px 0 rgba(0,0,0,.2)}.top-0-l{top:0}.left-0-l{left:0}.right-0-l{right:0}.bottom-0-l{bottom:0}.top-1-l{top:1rem}.left-1-l{left:1rem}.right-1-l{right:1rem}.bottom-1-l{bottom:1rem}.top-2-l{top:2rem}.left-2-l{left:2rem}.right-2-l{right:2rem}.bottom-2-l{bottom:2rem}.top--1-l{top:-1rem}.right--1-l{right:-1rem}.bottom--1-l{bottom:-1rem}.left--1-l{left:-1rem}.top--2-l{top:-2rem}.right--2-l{right:-2rem}.bottom--2-l{bottom:-2rem}.left--2-l{left:-2rem}.absolute--fill-l{top:0;right:0;bottom:0;left:0}.cl-l{clear:left}.cr-l{clear:right}.cb-l{clear:both}.cn-l{clear:none}.dn-l{display:none}.di-l{display:inline}.db-l{display:block}.dib-l{display:inline-block}.dit-l{display:inline-table}.dt-l{display:table}.dtc-l{display:table-cell}.dt-row-l{display:table-row}.dt-row-group-l{display:table-row-group}.dt-column-l{display:table-column}.dt-column-group-l{display:table-column-group}.dt--fixed-l{table-layout:fixed;width:100%}.flex-l{display:flex}.inline-flex-l{display:inline-flex}.flex-auto-l{flex:1 1 auto;min-width:0;min-height:0}.flex-none-l{flex:none}.flex-column-l{flex-direction:column}.flex-row-l{flex-direction:row}.flex-wrap-l{flex-wrap:wrap}.flex-nowrap-l{flex-wrap:nowrap}.flex-wrap-reverse-l{flex-wrap:wrap-reverse}.flex-column-reverse-l{flex-direction:column-reverse}.flex-row-reverse-l{flex-direction:row-reverse}.items-start-l{align-items:flex-start}.items-end-l{align-items:flex-end}.items-center-l{align-items:center}.items-baseline-l{align-items:baseline}.items-stretch-l{align-items:stretch}.self-start-l{align-self:flex-start}.self-end-l{align-self:flex-end}.self-center-l{align-self:center}.self-baseline-l{align-self:baseline}.self-stretch-l{align-self:stretch}.justify-start-l{justify-content:flex-start}.justify-end-l{justify-content:flex-end}.justify-center-l{justify-content:center}.justify-between-l{justify-content:space-between}.justify-around-l{justify-content:space-around}.content-start-l{align-content:flex-start}.content-end-l{align-content:flex-end}.content-center-l{align-content:center}.content-between-l{align-content:space-between}.content-around-l{align-content:space-around}.content-stretch-l{align-content:stretch}.order-0-l{order:0}.order-1-l{order:1}.order-2-l{order:2}.order-3-l{order:3}.order-4-l{order:4}.order-5-l{order:5}.order-6-l{order:6}.order-7-l{order:7}.order-8-l{order:8}.order-last-l{order:99999}.flex-grow-0-l{flex-grow:0}.flex-grow-1-l{flex-grow:1}.flex-shrink-0-l{flex-shrink:0}.flex-shrink-1-l{flex-shrink:1}.fl-l{float:left}.fl-l,.fr-l{_display:inline}.fr-l{float:right}.fn-l{float:none}.i-l{font-style:italic}.fs-normal-l{font-style:normal}.normal-l{font-weight:400}.b-l{font-weight:700}.fw1-l{font-weight:100}.fw2-l{font-weight:200}.fw3-l{font-weight:300}.fw4-l{font-weight:400}.fw5-l{font-weight:500}.fw6-l{font-weight:600}.fw7-l{font-weight:700}.fw8-l{font-weight:800}.fw9-l{font-weight:900}.h1-l{height:1rem}.h2-l{height:2rem}.h3-l{height:4rem}.h4-l{height:8rem}.h5-l{height:16rem}.h-25-l{height:25%}.h-50-l{height:50%}.h-75-l{height:75%}.h-100-l{height:100%}.min-h-100-l{min-height:100%}.vh-25-l{height:25vh}.vh-50-l{height:50vh}.vh-75-l{height:75vh}.vh-100-l{height:100vh}.min-vh-100-l{min-height:100vh}.h-auto-l{height:auto}.h-inherit-l{height:inherit}.tracked-l{letter-spacing:.1em}.tracked-tight-l{letter-spacing:-.05em}.tracked-mega-l{letter-spacing:.25em}.lh-solid-l{line-height:1}.lh-title-l{line-height:1.25}.lh-copy-l{line-height:1.5}.mw-100-l{max-width:100%}.mw1-l{max-width:1rem}.mw2-l{max-width:2rem}.mw3-l{max-width:4rem}.mw4-l{max-width:8rem}.mw5-l{max-width:16rem}.mw6-l{max-width:32rem}.mw7-l{max-width:48rem}.mw8-l{max-width:64rem}.mw9-l{max-width:96rem}.mw-none-l{max-width:none}.w1-l{width:1rem}.w2-l{width:2rem}.w3-l{width:4rem}.w4-l{width:8rem}.w5-l{width:16rem}.w-10-l{width:10%}.w-20-l{width:20%}.w-25-l{width:25%}.w-30-l{width:30%}.w-33-l{width:33%}.w-34-l{width:34%}.w-40-l{width:40%}.w-50-l{width:50%}.w-60-l{width:60%}.w-70-l{width:70%}.w-75-l{width:75%}.w-80-l{width:80%}.w-90-l{width:90%}.w-100-l{width:100%}.w-third-l{width:33.33333%}.w-two-thirds-l{width:66.66667%}.w-auto-l{width:auto}.overflow-visible-l{overflow:visible}.overflow-hidden-l{overflow:hidden}.overflow-scroll-l{overflow:scroll}.overflow-auto-l{overflow:auto}.overflow-x-visible-l{overflow-x:visible}.overflow-x-hidden-l{overflow-x:hidden}.overflow-x-scroll-l{overflow-x:scroll}.overflow-x-auto-l{overflow-x:auto}.overflow-y-visible-l{overflow-y:visible}.overflow-y-hidden-l{overflow-y:hidden}.overflow-y-scroll-l{overflow-y:scroll}.overflow-y-auto-l{overflow-y:auto}.static-l{position:static}.relative-l{position:relative}.absolute-l{position:absolute}.fixed-l{position:fixed}.rotate-45-l{-webkit-transform:rotate(45deg);transform:rotate(45deg)}.rotate-90-l{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.rotate-135-l{-webkit-transform:rotate(135deg);transform:rotate(135deg)}.rotate-180-l{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.rotate-225-l{-webkit-transform:rotate(225deg);transform:rotate(225deg)}.rotate-270-l{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.rotate-315-l{-webkit-transform:rotate(315deg);transform:rotate(315deg)}.pa0-l{padding:0}.pa1-l{padding:.25rem}.pa2-l{padding:.5rem}.pa3-l{padding:1rem}.pa4-l{padding:2rem}.pa5-l{padding:4rem}.pa6-l{padding:8rem}.pa7-l{padding:16rem}.pl0-l{padding-left:0}.pl1-l{padding-left:.25rem}.pl2-l{padding-left:.5rem}.pl3-l{padding-left:1rem}.pl4-l{padding-left:2rem}.pl5-l{padding-left:4rem}.pl6-l{padding-left:8rem}.pl7-l{padding-left:16rem}.pr0-l{padding-right:0}.pr1-l{padding-right:.25rem}.pr2-l{padding-right:.5rem}.pr3-l{padding-right:1rem}.pr4-l{padding-right:2rem}.pr5-l{padding-right:4rem}.pr6-l{padding-right:8rem}.pr7-l{padding-right:16rem}.pb0-l{padding-bottom:0}.pb1-l{padding-bottom:.25rem}.pb2-l{padding-bottom:.5rem}.pb3-l{padding-bottom:1rem}.pb4-l{padding-bottom:2rem}.pb5-l{padding-bottom:4rem}.pb6-l{padding-bottom:8rem}.pb7-l{padding-bottom:16rem}.pt0-l{padding-top:0}.pt1-l{padding-top:.25rem}.pt2-l{padding-top:.5rem}.pt3-l{padding-top:1rem}.pt4-l{padding-top:2rem}.pt5-l{padding-top:4rem}.pt6-l{padding-top:8rem}.pt7-l{padding-top:16rem}.pv0-l{padding-top:0;padding-bottom:0}.pv1-l{padding-top:.25rem;padding-bottom:.25rem}.pv2-l{padding-top:.5rem;padding-bottom:.5rem}.pv3-l{padding-top:1rem;padding-bottom:1rem}.pv4-l{padding-top:2rem;padding-bottom:2rem}.pv5-l{padding-top:4rem;padding-bottom:4rem}.pv6-l{padding-top:8rem;padding-bottom:8rem}.pv7-l{padding-top:16rem;padding-bottom:16rem}.ph0-l{padding-left:0;padding-right:0}.ph1-l{padding-left:.25rem;padding-right:.25rem}.ph2-l{padding-left:.5rem;padding-right:.5rem}.ph3-l{padding-left:1rem;padding-right:1rem}.ph4-l{padding-left:2rem;padding-right:2rem}.ph5-l{padding-left:4rem;padding-right:4rem}.ph6-l{padding-left:8rem;padding-right:8rem}.ph7-l{padding-left:16rem;padding-right:16rem}.ma0-l{margin:0}.ma1-l{margin:.25rem}.ma2-l{margin:.5rem}.ma3-l{margin:1rem}.ma4-l{margin:2rem}.ma5-l{margin:4rem}.ma6-l{margin:8rem}.ma7-l{margin:16rem}.ml0-l{margin-left:0}.ml1-l{margin-left:.25rem}.ml2-l{margin-left:.5rem}.ml3-l{margin-left:1rem}.ml4-l{margin-left:2rem}.ml5-l{margin-left:4rem}.ml6-l{margin-left:8rem}.ml7-l{margin-left:16rem}.mr0-l{margin-right:0}.mr1-l{margin-right:.25rem}.mr2-l{margin-right:.5rem}.mr3-l{margin-right:1rem}.mr4-l{margin-right:2rem}.mr5-l{margin-right:4rem}.mr6-l{margin-right:8rem}.mr7-l{margin-right:16rem}.mb0-l{margin-bottom:0}.mb1-l{margin-bottom:.25rem}.mb2-l{margin-bottom:.5rem}.mb3-l{margin-bottom:1rem}.mb4-l{margin-bottom:2rem}.mb5-l{margin-bottom:4rem}.mb6-l{margin-bottom:8rem}.mb7-l{margin-bottom:16rem}.mt0-l{margin-top:0}.mt1-l{margin-top:.25rem}.mt2-l{margin-top:.5rem}.mt3-l{margin-top:1rem}.mt4-l{margin-top:2rem}.mt5-l{margin-top:4rem}.mt6-l{margin-top:8rem}.mt7-l{margin-top:16rem}.mv0-l{margin-top:0;margin-bottom:0}.mv1-l{margin-top:.25rem;margin-bottom:.25rem}.mv2-l{margin-top:.5rem;margin-bottom:.5rem}.mv3-l{margin-top:1rem;margin-bottom:1rem}.mv4-l{margin-top:2rem;margin-bottom:2rem}.mv5-l{margin-top:4rem;margin-bottom:4rem}.mv6-l{margin-top:8rem;margin-bottom:8rem}.mv7-l{margin-top:16rem;margin-bottom:16rem}.mh0-l{margin-left:0;margin-right:0}.mh1-l{margin-left:.25rem;margin-right:.25rem}.mh2-l{margin-left:.5rem;margin-right:.5rem}.mh3-l{margin-left:1rem;margin-right:1rem}.mh4-l{margin-left:2rem;margin-right:2rem}.mh5-l{margin-left:4rem;margin-right:4rem}.mh6-l{margin-left:8rem;margin-right:8rem}.mh7-l{margin-left:16rem;margin-right:16rem}.na1-l{margin:-.25rem}.na2-l{margin:-.5rem}.na3-l{margin:-1rem}.na4-l{margin:-2rem}.na5-l{margin:-4rem}.na6-l{margin:-8rem}.na7-l{margin:-16rem}.nl1-l{margin-left:-.25rem}.nl2-l{margin-left:-.5rem}.nl3-l{margin-left:-1rem}.nl4-l{margin-left:-2rem}.nl5-l{margin-left:-4rem}.nl6-l{margin-left:-8rem}.nl7-l{margin-left:-16rem}.nr1-l{margin-right:-.25rem}.nr2-l{margin-right:-.5rem}.nr3-l{margin-right:-1rem}.nr4-l{margin-right:-2rem}.nr5-l{margin-right:-4rem}.nr6-l{margin-right:-8rem}.nr7-l{margin-right:-16rem}.nb1-l{margin-bottom:-.25rem}.nb2-l{margin-bottom:-.5rem}.nb3-l{margin-bottom:-1rem}.nb4-l{margin-bottom:-2rem}.nb5-l{margin-bottom:-4rem}.nb6-l{margin-bottom:-8rem}.nb7-l{margin-bottom:-16rem}.nt1-l{margin-top:-.25rem}.nt2-l{margin-top:-.5rem}.nt3-l{margin-top:-1rem}.nt4-l{margin-top:-2rem}.nt5-l{margin-top:-4rem}.nt6-l{margin-top:-8rem}.nt7-l{margin-top:-16rem}.strike-l{text-decoration:line-through}.underline-l{text-decoration:underline}.no-underline-l{text-decoration:none}.tl-l{text-align:left}.tr-l{text-align:right}.tc-l{text-align:center}.tj-l{text-align:justify}.ttc-l{text-transform:capitalize}.ttl-l{text-transform:lowercase}.ttu-l{text-transform:uppercase}.ttn-l{text-transform:none}.f-6-l,.f-headline-l{font-size:6rem}.f-5-l,.f-subheadline-l{font-size:5rem}.f1-l{font-size:3rem}.f2-l{font-size:2.25rem}.f3-l{font-size:1.5rem}.f4-l{font-size:1.25rem}.f5-l{font-size:1rem}.f6-l{font-size:.875rem}.f7-l{font-size:.75rem}.measure-l{max-width:30em}.measure-wide-l{max-width:34em}.measure-narrow-l{max-width:20em}.indent-l{text-indent:1em;margin-top:0;margin-bottom:0}.small-caps-l{font-variant:small-caps}.truncate-l{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.center-l{margin-left:auto}.center-l,.mr-auto-l{margin-right:auto}.ml-auto-l{margin-left:auto}.clip-l{position:fixed!important;_position:absolute!important;clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px)}.ws-normal-l{white-space:normal}.nowrap-l{white-space:nowrap}.pre-l{white-space:pre}.v-base-l{vertical-align:baseline}.v-mid-l{vertical-align:middle}.v-top-l{vertical-align:top}.v-btm-l{vertical-align:bottom}} + diff --git a/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/python/libs/passwurd-external_bioid.py b/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/python/libs/passwurd-external_bioid.py new file mode 100644 index 00000000..558ae2f9 --- /dev/null +++ b/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/python/libs/passwurd-external_bioid.py @@ -0,0 +1,338 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +import sys +from java.util import Collections, HashMap, HashSet, ArrayList, Arrays, Date +from org.oxauth.persistence.model.configuration import GluuConfiguration +from org.gluu.persist import PersistenceEntryManager +from java.nio.charset import Charset +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.service import AuthenticationService, SessionIdService +from org.gluu.oxauth.service.common import UserService +from org.gluu.util import StringHelper +from org.gluu.oxauth.service.net import HttpService +from org.json import JSONObject +import base64 +import java + +from org.gluu.util import StringHelper +from java.lang import String + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + + + if not configurationAttributes.containsKey("ENDPOINT"): + print "BioID. Initialization. Property ENDPOINT is mandatory" + return False + self.ENDPOINT = configurationAttributes.get("ENDPOINT").getValue2() + + if not configurationAttributes.containsKey("APP_IDENTIFIER"): + print "BioID. Initialization. Property APP_IDENTIFIER is mandatory" + return False + self.APP_IDENTIFIER = configurationAttributes.get("APP_IDENTIFIER").getValue2() + + if not configurationAttributes.containsKey("APP_SECRET"): + print "BioID. Initialization. Property APP_SECRET is mandatory" + return False + self.APP_SECRET = configurationAttributes.get("APP_SECRET").getValue2() + + if not configurationAttributes.containsKey("PARTITION"): + print "BioID. Initialization. Property PARTITION is mandatory" + return False + self.PARTITION = configurationAttributes.get("PARTITION").getValue2() + + if not configurationAttributes.containsKey("STORAGE"): + print "BioID. Initialization. Property STORAGE is mandatory" + return False + self.STORAGE = configurationAttributes.get("STORAGE").getValue2() + + print "BioID. Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "BioID. Destroy" + print "BioID. Destroyed successfully" + return True + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def getApiVersion(self): + return 11 + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + print "BioID. Authenticate " + authenticationService = CdiUtil.bean(AuthenticationService) + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + user_name = credentials.getUsername() + + if (step == 1): + print "BioID. Authenticate for step 1" + + logged_in = False + userService = CdiUtil.bean(UserService) + authenticated_user = self.processBasicAuthentication(credentials) + if authenticated_user == None: + print "BioID. User does not exist" + return False + + # place holder comment - delete later + + return True + + elif step == 2 or step == 3: + + auth_method = identity.getWorkingParameter("bioID_auth_method") + print "BioID. Authenticate method for step %s. bioID_auth_method: '%s'" % (step,auth_method) + user_name = identity.getWorkingParameter("user_name") + bcid = self.STORAGE + "." + self.PARTITION + "." + str(String(user_name).hashCode()) + + if step == 2 and 'enrollment' == auth_method: + + access_token = identity.getWorkingParameter("access_token") + result = self.performBiometricOperation( access_token, "enroll") + + if result == True: + #this means that enroll is a success, the next is step 3 authenticate + identity.setWorkingParameter("bioID_count_login_steps", 3) + identity.setWorkingParameter("bioID_auth_method","verification") + return result + else: + return False + + else : + + access_token = identity.getWorkingParameter("access_token") + result = self.performBiometricOperation( access_token, "verify") + return result + + else: + return False + + + def prepareForStep(self, configurationAttributes, requestParameters, step): + + print "BioID. Prepare for step called : step %s" % step + if step == 1: + return True + elif step == 2 or step == 3: + identity = CdiUtil.bean(Identity) + authenticationService = CdiUtil.bean(AuthenticationService) + user = authenticationService.getAuthenticatedUser() + if user == None: + print "BioID. prepareForStep. Failed to determine user name" + return False + + user_name = user.getUserId() + + #place holder - moved from authenticate step == 1 inorder to make it compatible with casa script + # todo : code refactor + if step == 2: + identity.setWorkingParameter("user_name",user_name) + bcid = self.STORAGE + "." + self.PARTITION + "." + str(String(user_name).hashCode()) + print "BioID. username:bcid %s:%s" %(user_name, bcid) + + is_user_enrolled = self.isenrolled(bcid) + print "BioID. is_user_enrolled: '%s'" % is_user_enrolled + + if(is_user_enrolled == True): + identity.setWorkingParameter("bioID_auth_method","verification") + else: + identity.setWorkingParameter("bioID_auth_method","enrollment") + identity.setWorkingParameter("bioID_count_login_steps", 2) + + #place holder till here + + auth_method = identity.getWorkingParameter("bioID_auth_method") + print "BioID. step %s %s" % (step, auth_method) + bcid = self.STORAGE + "." + self.PARTITION + "." + str(String(user_name).hashCode()) + print "bcid %s" %bcid + if step == 2 and auth_method == 'enrollment': + print "access token used by upload method - enroll" + access_token = self.getAccessToken( bcid, "enroll" ) + # either step2 and verification or step 3 which is verification post enrollment + else: + print "access token used by upload method - verify" + access_token = self.getAccessToken( bcid, "verify" ) + + print "access_token %s - " % access_token + identity.setWorkingParameter("access_token",access_token) + + return True + + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + if step == 1: + return None + else: + return Arrays.asList("bioID_auth_method","access_token","user_name","bioID_count_login_steps") + + + def getCountAuthenticationSteps(self, configurationAttributes): + + identity = CdiUtil.bean(Identity) + if identity.isSetWorkingParameter("bioID_count_login_steps"): + print "BioID. getCountAuthenticationSteps called, returning - 3" + return 3 + else: + print "BioID. getCountAuthenticationSteps called, returning - 2" + return 2 + + def getPageForStep(self, configurationAttributes, step): + print "BioID. getPageForStep called -step:%s" % str(step) + if step > 1 : + return "/passwordless/bioid.xhtml" + else: + return "" + + def getNextStep(self, configurationAttributes, requestParameters, step): + print "BioID. getNextStep called. %s" % str(step) + + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True + + # Get a BWS token to be used for authorization. + # bcid - The Biometric Class ID (BCID) of the person + # forTask - The task for which the issued token shall be used. + # A string containing the issued BWS token. + def getAccessToken(self, bcid, forTask): + + httpService = CdiUtil.bean(HttpService) + + http_client = httpService.getHttpsClient() + http_client_params = http_client.getParams() + + bioID_service_url = self.ENDPOINT + "token?id="+self.APP_IDENTIFIER+"&bcid="+bcid+"&task="+forTask+"&livedetection=true" + encodedString = base64.b64encode((self.APP_IDENTIFIER+":"+self.APP_SECRET).encode('utf-8')) + bioID_service_headers = {"Authorization": "Basic "+encodedString} + + try: + http_service_response = httpService.executeGet(http_client, bioID_service_url, bioID_service_headers) + http_response = http_service_response.getHttpResponse() + except: + print "BioID. Unable to obtain access token. Exception: ", sys.exc_info()[1] + return None + + try: + if not httpService.isResponseStastusCodeOk(http_response): + print "BioID. Unable to obtain access token. Get non 200 OK response from server:", str(http_response.getStatusLine().getStatusCode()) + httpService.consume(http_response) + return None + + response_bytes = httpService.getResponseContent(http_response) + response_string = httpService.convertEntityToString(response_bytes, Charset.forName("UTF-8")) + httpService.consume(http_response) + return response_string + finally: + http_service_response.closeConnection() + + def isenrolled(self, bcid): + httpService = CdiUtil.bean(HttpService) + + http_client = httpService.getHttpsClient() + http_client_params = http_client.getParams() + + bioID_service_url = self.ENDPOINT + "isenrolled?bcid="+bcid+"&trait=Face" + print "BioID. isenrolled URL - %s" %bioID_service_url + encodedString = base64.b64encode((self.APP_IDENTIFIER+":"+self.APP_SECRET).encode('utf-8')) + bioID_service_headers = {"Authorization": "Basic "+encodedString} + + try: + http_service_response = httpService.executeGet(http_client, bioID_service_url, bioID_service_headers) + http_response = http_service_response.getHttpResponse() + except: + print "BioID. failed to invoke isenrolled API: ", sys.exc_info()[1] + return None + + try: + if not httpService.isResponseStastusCodeOk(http_response): + print "BioID. Face,Periocular not enrolled. Get non 200 OK response from server:", str(http_response.getStatusLine().getStatusCode()) + httpService.consume(http_response) + return False + + else: + return True + finally: + http_service_response.closeConnection() + + def processBasicAuthentication(self, credentials): + userService = CdiUtil.bean(UserService) + authenticationService = CdiUtil.bean(AuthenticationService) + + find_user_by_uid = authenticationService.getAuthenticatedUser() + if find_user_by_uid != None: + print "BioID. Process basic authentication. There is an authenticated user already" + return find_user_by_uid + + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + logged_in = False + if StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password): + logged_in = authenticationService.authenticate(user_name, user_password) + + if not logged_in: + print "BioID. Process basic authentication. Failed to find user '%s'" % user_name + return None + + find_user_by_uid = authenticationService.getAuthenticatedUser() + if find_user_by_uid == None: + print "BioID. Process basic authentication. Failed to find user '%s'" % user_name + return None + + return find_user_by_uid + + + def performBiometricOperation(self, token, task): + httpService = CdiUtil.bean(HttpService) + http_client = httpService.getHttpsClient() + http_client_params = http_client.getParams() + bioID_service_url = self.ENDPOINT + task+"?livedetection=true" + bioID_service_headers = {"Authorization": "Bearer "+token} + + try: + http_service_response = httpService.executeGet(http_client, bioID_service_url, bioID_service_headers) + http_response = http_service_response.getHttpResponse() + response_bytes = httpService.getResponseContent(http_response) + response_string = httpService.convertEntityToString(response_bytes, Charset.forName("UTF-8")) + json_response = JSONObject(response_string) + httpService.consume(http_response) + if json_response.get("Success") == True: + return True + else: + print "BioID. Reason for failure : %s " % json_response.get("Error") + return False + except: + print "BioID. failed to invoke %s API: %s" %(task,sys.exc_info()[1]) + return None + + finally: + http_service_response.closeConnection() + + def hasEnrollments(self, configurationAttributes, user): + values = user.getAttributeValues("oxBiometricDevices") + if values != None: + return True + else: + return False diff --git a/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/python/libs/passwurd-external_fido2.py b/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/python/libs/passwurd-external_fido2.py new file mode 100644 index 00000000..b96e24f8 --- /dev/null +++ b/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/python/libs/passwurd-external_fido2.py @@ -0,0 +1,250 @@ +# Based on oxAuth Fido2ExternalAuthenticator.py + +from javax.ws.rs.core import Response +from org.jboss.resteasy.client import ClientResponseFailure +from org.jboss.resteasy.client.exception import ResteasyClientException +from javax.ws.rs.core import Response +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.fido2.client import Fido2ClientFactory +from org.gluu.oxauth.security import Identity +from org.gluu.oxauth.service import AuthenticationService, UserService, SessionIdService +from org.gluu.oxauth.util import ServerUtil +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.util import StringHelper + +from java.util.concurrent.locks import ReentrantLock + +import java +import sys +import json + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Fido2. Initialization" + + if not configurationAttributes.containsKey("fido2_server_uri"): + print "fido2_server_uri. Initialization. Property fido2_server_uri is not specified" + return False + + self.fido2_server_uri = configurationAttributes.get("fido2_server_uri").getValue2() + + #self.fido2_domain = None + #if configurationAttributes.containsKey("fido2_domain"): + # self.fido2_domain = configurationAttributes.get("fido2_domain").getValue2() + + self.metaDataLoaderLock = ReentrantLock() + self.metaDataConfiguration = None + + print "Fido2. Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "Fido2. Destroy" + print "Fido2. Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, configurationAttributes): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + authenticationService = CdiUtil.bean(AuthenticationService) + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + user_name = credentials.getUsername() + + if (step == 1): + print "Fido2. Authenticate for step 1" + + if authenticationService.getAuthenticatedUser() != None: + return True + + user_password = credentials.getPassword() + logged_in = False + if (StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password)): + userService = CdiUtil.bean(UserService) + logged_in = authenticationService.authenticate(user_name, user_password) + + if (not logged_in): + return False + + return True + elif (step == 2): + print "Fido2. Authenticate for step 2" + + token_response = ServerUtil.getFirstValue(requestParameters, "tokenResponse") + if token_response == None: + print "Fido2. Authenticate for step 2. tokenResponse is empty" + return False + + auth_method = ServerUtil.getFirstValue(requestParameters, "authMethod") + if auth_method == None: + print "Fido2. Authenticate for step 2. authMethod is empty" + return False + + authenticationService = CdiUtil.bean(AuthenticationService) + user = authenticationService.getAuthenticatedUser() + if (user == None): + print "Fido2. Prepare for step 2. Failed to determine user name" + return False + + if (auth_method == 'authenticate'): + print "Fido2. Prepare for step 2. Call Fido2 in order to finish authentication flow" + assertionService = Fido2ClientFactory.instance().createAssertionService(self.metaDataConfiguration) + assertionStatus = assertionService.verify(token_response) + authenticationStatusEntity = assertionStatus.readEntity(java.lang.String) + + if (assertionStatus.getStatus() != Response.Status.OK.getStatusCode()): + print "Fido2. Authenticate for step 2. Get invalid authentication status from Fido2 server" + return False + + return True + elif (auth_method == 'enroll'): + print "Fido2. Prepare for step 2. Call Fido2 in order to finish registration flow" + attestationService = Fido2ClientFactory.instance().createAttestationService(self.metaDataConfiguration) + attestationStatus = attestationService.verify(token_response) + + if (attestationStatus.getStatus() != Response.Status.OK.getStatusCode()): + print "Fido2. Authenticate for step 2. Get invalid registration status from Fido2 server" + return False + + return True + else: + print "Fido2. Prepare for step 2. Authentication method is invalid" + return False + + return False + else: + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + identity = CdiUtil.bean(Identity) + + if (step == 1): + return True + elif (step == 2): + print "Fido2. Prepare for step 2" + + session_id = CdiUtil.bean(SessionIdService).getSessionIdFromCookie() + if StringHelper.isEmpty(session_id): + print "Fido2. Prepare for step 2. Failed to determine session_id" + return False + + authenticationService = CdiUtil.bean(AuthenticationService) + user = authenticationService.getAuthenticatedUser() + if (user == None): + print "Fido2. Prepare for step 2. Failed to determine user name" + return False + + userName = user.getUserId() + + metaDataConfiguration = self.getMetaDataConfiguration() + assertionResponse = None + attestationResponse = None + + # Check if user have registered devices + count = CdiUtil.bean(UserService).countFido2RegisteredDevices(userName) + if (count > 0): + print "Fido2. Prepare for step 2. Call Fido2 endpoint in order to start assertion flow" + + try: + assertionService = Fido2ClientFactory.instance().createAssertionService(metaDataConfiguration) + assertionRequest = json.dumps({'username': userName}, separators=(',', ':')) + assertionResponse = assertionService.authenticate(assertionRequest).readEntity(java.lang.String) + except ClientResponseFailure, ex: + print "Fido2. Prepare for step 2. Failed to start assertion flow. Exception:", sys.exc_info()[1] + return False + else: + print "Fido2. Prepare for step 2. Call Fido2 endpoint in order to start attestation flow" + + try: + attestationService = Fido2ClientFactory.instance().createAttestationService(metaDataConfiguration) + attestationRequest = json.dumps({'username': userName, 'displayName': userName}, separators=(',', ':')) + attestationResponse = attestationService.register(attestationRequest).readEntity(java.lang.String) + except ClientResponseFailure, ex: + print "Fido2. Prepare for step 2. Failed to start attestation flow. Exception:", sys.exc_info()[1] + return False + + identity.setWorkingParameter("fido2_assertion_request", ServerUtil.asJson(assertionResponse)) + identity.setWorkingParameter("fido2_attestation_request", ServerUtil.asJson(attestationResponse)) + print "Fido2. Prepare for step 2. Successfully start flow with next requests.\nfido2_assertion_request: '%s'\nfido2_attestation_request: '%s'" % ( assertionResponse, attestationResponse ) + + return True + elif (step == 3): + print "Fido2. Prepare for step 3" + + return True + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + return 2 + + def getPageForStep(self, configurationAttributes, step): + if (step == 2): + #Modified for Casa compliance + return "/passwordless/fido2.xhtml" + + return "" + + def logout(self, configurationAttributes, requestParameters): + return True + + def getMetaDataConfiguration(self): + if self.metaDataConfiguration != None: + return self.metaDataConfiguration + + self.metaDataLoaderLock.lock() + # Make sure that another thread not loaded configuration already + if self.metaDataConfiguration != None: + return self.metaDataConfiguration + + try: + print "Fido2. Initialization. Downloading Fido2 metadata" + self.fido2_server_metadata_uri = self.fido2_server_uri + "/.well-known/fido2-configuration" + #self.fido2_server_metadata_uri = self.fido2_server_uri + "/oxauth/restv1/fido2/configuration" + + metaDataConfigurationService = Fido2ClientFactory.instance().createMetaDataConfigurationService(self.fido2_server_metadata_uri) + + max_attempts = 10 + for attempt in range(1, max_attempts + 1): + try: + self.metaDataConfiguration = metaDataConfigurationService.getMetadataConfiguration().readEntity(java.lang.String) + return self.metaDataConfiguration + except ClientResponseFailure, ex: + # Detect if last try or we still get Service Unavailable HTTP error + if (attempt == max_attempts) or (ex.getResponse().getResponseStatus() != Response.Status.SERVICE_UNAVAILABLE): + raise ex + + java.lang.Thread.sleep(3000) + print "Attempting to load metadata: %d" % attempt + except ResteasyClientException, ex: + # Detect if last try or we still get Service Unavailable HTTP error + if attempt == max_attempts: + raise ex + + java.lang.Thread.sleep(3000) + print "Attempting to load metadata: %d" % attempt + finally: + self.metaDataLoaderLock.unlock() + + # Added for Casa compliance + + def hasEnrollments(self, configurationAttributes, user): + return CdiUtil.bean(UserService).countFido2RegisteredDevices(user.getUserId()) > 0 diff --git a/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/python/libs/passwurd-external_otp.py b/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/python/libs/passwurd-external_otp.py new file mode 100644 index 00000000..8b721b88 --- /dev/null +++ b/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/python/libs/passwurd-external_otp.py @@ -0,0 +1,590 @@ +# Based on oxAuth OtpExternalAuthenticator.py + +# Requires the following custom properties and values: +# otp_type: totp/hotp +# issuer: Gluu Inc +# otp_conf_file: /etc/certs/otp_configuration.json +# +# These are non mandatory custom properties and values: +# label: Gluu OTP +# qr_options: { width: 400, height: 400 } +# registration_uri: https://ce-dev.gluu.org/identity/register + +import jarray +import json +import sys +from com.google.common.io import BaseEncoding +from com.lochbridge.oath.otp import HOTP +from com.lochbridge.oath.otp import HOTPValidator +from com.lochbridge.oath.otp import HmacShaAlgorithm +from com.lochbridge.oath.otp import TOTP +from com.lochbridge.oath.otp.keyprovisioning import OTPAuthURIBuilder +from com.lochbridge.oath.otp.keyprovisioning import OTPKey +from com.lochbridge.oath.otp.keyprovisioning.OTPKey import OTPType +from java.security import SecureRandom +from java.util import Arrays +from java.util.concurrent import TimeUnit +from javax.faces.application import FacesMessage +from org.gluu.jsf2.message import FacesMessages +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.security import Identity +from org.gluu.oxauth.service import AuthenticationService, UserService, SessionIdService +from org.gluu.oxauth.util import ServerUtil +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.util import StringHelper + + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "OTP. Initialization" + + if not configurationAttributes.containsKey("otp_type"): + print "OTP. Initialization. Property otp_type is mandatory" + return False + self.otpType = configurationAttributes.get("otp_type").getValue2() + + if not self.otpType in ["hotp", "totp"]: + print "OTP. Initialization. Property value otp_type is invalid" + return False + + if not configurationAttributes.containsKey("issuer"): + print "OTP. Initialization. Property issuer is mandatory" + return False + self.otpIssuer = configurationAttributes.get("issuer").getValue2() + + self.customLabel = None + if configurationAttributes.containsKey("label"): + self.customLabel = configurationAttributes.get("label").getValue2() + + self.customQrOptions = {} + if configurationAttributes.containsKey("qr_options"): + self.customQrOptions = configurationAttributes.get("qr_options").getValue2() + + self.registrationUri = None + if configurationAttributes.containsKey("registration_uri"): + self.registrationUri = configurationAttributes.get("registration_uri").getValue2() + + validOtpConfiguration = self.loadOtpConfiguration(configurationAttributes) + if not validOtpConfiguration: + return False + + print "OTP. Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "OTP. Destroy" + print "OTP. Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, configurationAttributes): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + authenticationService = CdiUtil.bean(AuthenticationService) + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + self.setRequestScopedParameters(identity) + + if step == 1: + print "OTP. Authenticate for step 1" + + # Modified for Casa compliance + authenticated_user = authenticationService.getAuthenticatedUser() + if authenticated_user == None: + authenticated_user = self.processBasicAuthentication(credentials) + if authenticated_user == None: + return False + + otp_auth_method = "authenticate" + # Uncomment this block if you need to allow user second OTP registration + #enrollment_mode = ServerUtil.getFirstValue(requestParameters, "loginForm:registerButton") + #if StringHelper.isNotEmpty(enrollment_mode): + # otp_auth_method = "enroll" + + # Modified for Casa compliance + if not self.hasEnrollments(configurationAttributes, authenticated_user): + return False + + #if otp_auth_method == "authenticate": + # user_enrollments = self.findEnrollments(authenticated_user.getUserId()) + # if len(user_enrollments) == 0: + # otp_auth_method = "enroll" + # print "OTP. Authenticate for step 1. There is no OTP enrollment for user '%s'. Changing otp_auth_method to '%s'" % (authenticated_user.getUserId(), otp_auth_method) + + if otp_auth_method == "enroll": + print "OTP. Authenticate for step 1. Setting count steps: '%s'" % 3 + identity.setWorkingParameter("otp_count_login_steps", 3) + + print "OTP. Authenticate for step 1. otp_auth_method: '%s'" % otp_auth_method + identity.setWorkingParameter("otp_auth_method", otp_auth_method) + + return True + elif step == 2: + print "OTP. Authenticate for step 2" + + authenticationService = CdiUtil.bean(AuthenticationService) + user = authenticationService.getAuthenticatedUser() + if user == None: + print "OTP. Authenticate for step 2. Failed to determine user name" + return False + + session_id_validation = self.validateSessionId(identity) + if not session_id_validation: + return False + + # Restore state from session + otp_auth_method = identity.getWorkingParameter("otp_auth_method") + if otp_auth_method == 'enroll': + auth_result = ServerUtil.getFirstValue(requestParameters, "auth_result") + if not StringHelper.isEmpty(auth_result): + print "OTP. Authenticate for step 2. User not enrolled OTP" + return False + + print "OTP. Authenticate for step 2. Skipping this step during enrollment" + return True + + otp_auth_result = self.processOtpAuthentication(requestParameters, user.getUserId(), identity, otp_auth_method) + print "OTP. Authenticate for step 2. OTP authentication result: '%s'" % otp_auth_result + + return otp_auth_result + elif step == 3: + print "OTP. Authenticate for step 3" + + authenticationService = CdiUtil.bean(AuthenticationService) + user = authenticationService.getAuthenticatedUser() + if user == None: + print "OTP. Authenticate for step 2. Failed to determine user name" + return False + + session_id_validation = self.validateSessionId(identity) + if not session_id_validation: + return False + + # Restore state from session + otp_auth_method = identity.getWorkingParameter("otp_auth_method") + if otp_auth_method != 'enroll': + return False + + otp_auth_result = self.processOtpAuthentication(requestParameters, user.getUserId(), identity, otp_auth_method) + print "OTP. Authenticate for step 3. OTP authentication result: '%s'" % otp_auth_result + + return otp_auth_result + else: + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + self.setRequestScopedParameters(identity) + + if step == 1: + print "OTP. Prepare for step 1" + return True + + elif step == 2: + print "OTP. Prepare for step 2" + + session_id_validation = self.validateSessionId(identity) + if not session_id_validation: + return False + + otp_auth_method = identity.getWorkingParameter("otp_auth_method") + print "OTP. Prepare for step 2. otp_auth_method: '%s'" % otp_auth_method + + if otp_auth_method == 'enroll': + authenticationService = CdiUtil.bean(AuthenticationService) + user = authenticationService.getAuthenticatedUser() + if user == None: + print "OTP. Prepare for step 2. Failed to load user enty" + return False + + if self.otpType == "hotp": + otp_secret_key = self.generateSecretHotpKey() + otp_enrollment_request = self.generateHotpSecretKeyUri(otp_secret_key, self.otpIssuer, user.getAttribute("displayName")) + elif self.otpType == "totp": + otp_secret_key = self.generateSecretTotpKey() + otp_enrollment_request = self.generateTotpSecretKeyUri(otp_secret_key, self.otpIssuer, user.getAttribute("displayName")) + else: + print "OTP. Prepare for step 2. Unknown OTP type: '%s'" % self.otpType + return False + + print "OTP. Prepare for step 2. Prepared enrollment request for user: '%s'" % user.getUserId() + identity.setWorkingParameter("otp_secret_key", self.toBase64Url(otp_secret_key)) + identity.setWorkingParameter("otp_enrollment_request", otp_enrollment_request) + + return True + elif step == 3: + print "OTP. Prepare for step 3" + + session_id_validation = self.validateSessionId(identity) + if not session_id_validation: + return False + + otp_auth_method = identity.getWorkingParameter("otp_auth_method") + print "OTP. Prepare for step 3. otp_auth_method: '%s'" % otp_auth_method + + if otp_auth_method == 'enroll': + return True + + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + return Arrays.asList("otp_auth_method", "otp_count_login_steps", "otp_secret_key", "otp_enrollment_request") + + def getCountAuthenticationSteps(self, configurationAttributes): + identity = CdiUtil.bean(Identity) + + if identity.isSetWorkingParameter("otp_count_login_steps"): + return StringHelper.toInteger("%s" % identity.getWorkingParameter("otp_count_login_steps")) + else: + return 2 + + def getPageForStep(self, configurationAttributes, step): + if step == 2: + identity = CdiUtil.bean(Identity) + + otp_auth_method = identity.getWorkingParameter("otp_auth_method") + print "OTP. Get page for step 2. otp_auth_method: '%s'" % otp_auth_method + + if otp_auth_method == 'enroll': + return "/auth/otp/enroll.xhtml" + else: + #Modified for Casa compliance + return "/casa/otp.xhtml" + elif step == 3: + return "/auth/otp/otplogin.xhtml" + + return "" + + def logout(self, configurationAttributes, requestParameters): + return True + + def setRequestScopedParameters(self, identity): + if self.registrationUri != None: + identity.setWorkingParameter("external_registration_uri", self.registrationUri) + + if self.customLabel != None: + identity.setWorkingParameter("qr_label", self.customLabel) + + identity.setWorkingParameter("qr_options", self.customQrOptions) + + def loadOtpConfiguration(self, configurationAttributes): + print "OTP. Load OTP configuration" + if not configurationAttributes.containsKey("otp_conf_file"): + return False + + otp_conf_file = configurationAttributes.get("otp_conf_file").getValue2() + + # Load configuration from file + f = open(otp_conf_file, 'r') + try: + otpConfiguration = json.loads(f.read()) + except: + print "OTP. Load OTP configuration. Failed to load configuration from file:", otp_conf_file + return False + finally: + f.close() + + # Check configuration file settings + try: + self.hotpConfiguration = otpConfiguration["hotp"] + self.totpConfiguration = otpConfiguration["totp"] + + hmacShaAlgorithm = self.totpConfiguration["hmacShaAlgorithm"] + hmacShaAlgorithmType = None + + if StringHelper.equalsIgnoreCase(hmacShaAlgorithm, "sha1"): + hmacShaAlgorithmType = HmacShaAlgorithm.HMAC_SHA_1 + elif StringHelper.equalsIgnoreCase(hmacShaAlgorithm, "sha256"): + hmacShaAlgorithmType = HmacShaAlgorithm.HMAC_SHA_256 + elif StringHelper.equalsIgnoreCase(hmacShaAlgorithm, "sha512"): + hmacShaAlgorithmType = HmacShaAlgorithm.HMAC_SHA_512 + else: + print "OTP. Load OTP configuration. Invalid TOTP HMAC SHA algorithm: '%s'" % hmacShaAlgorithm + + self.totpConfiguration["hmacShaAlgorithmType"] = hmacShaAlgorithmType + except: + print "OTP. Load OTP configuration. Invalid configuration file '%s' format. Exception: '%s'" % (otp_conf_file, sys.exc_info()[1]) + return False + + + return True + + def processBasicAuthentication(self, credentials): + userService = CdiUtil.bean(UserService) + authenticationService = CdiUtil.bean(AuthenticationService) + + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + logged_in = False + if StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password): + logged_in = authenticationService.authenticate(user_name, user_password) + + if not logged_in: + return None + + find_user_by_uid = authenticationService.getAuthenticatedUser() + if find_user_by_uid == None: + print "OTP. Process basic authentication. Failed to find user '%s'" % user_name + return None + + return find_user_by_uid + + # Modified for Casa compliance + def findEnrollments(self, user_name, otpType, skipPrefix = True): + result = [] + + userService = CdiUtil.bean(UserService) + user = userService.getUser(user_name, "oxExternalUid") + if user == None: + print "OTP. Find enrollments. Failed to find user" + return result + + user_custom_ext_attribute = userService.getCustomAttribute(user, "oxExternalUid") + if user_custom_ext_attribute == None: + return result + + #otp_prefix = "%s:" % self.otpType + otp_prefix = "%s:" % otpType + + otp_prefix_length = len(otp_prefix) + for user_external_uid in user_custom_ext_attribute.getValues(): + index = user_external_uid.find(otp_prefix) + if index != -1: + if skipPrefix: + enrollment_uid = user_external_uid[otp_prefix_length:] + else: + enrollment_uid = user_external_uid + + result.append(enrollment_uid) + + return result + + def validateSessionId(self, identity): + session_id = CdiUtil.bean(SessionIdService).getSessionIdFromCookie() + if StringHelper.isEmpty(session_id): + print "OTP. Validate session id. Failed to determine session_id" + return False + + otp_auth_method = identity.getWorkingParameter("otp_auth_method") + if not otp_auth_method in ['enroll', 'authenticate']: + print "OTP. Validate session id. Failed to authenticate user. otp_auth_method: '%s'" % otp_auth_method + return False + + return True + + def processOtpAuthentication(self, requestParameters, user_name, identity, otp_auth_method): + facesMessages = CdiUtil.bean(FacesMessages) + facesMessages.setKeepMessages() + + userService = CdiUtil.bean(UserService) + + otpCode = ServerUtil.getFirstValue(requestParameters, "loginForm:otpCode") + if StringHelper.isEmpty(otpCode): + facesMessages.add(FacesMessage.SEVERITY_ERROR, "Failed to authenticate. OTP code is empty") + print "OTP. Process OTP authentication. otpCode is empty" + + return False + + if otp_auth_method == "enroll": + # Get key from session + otp_secret_key_encoded = identity.getWorkingParameter("otp_secret_key") + if otp_secret_key_encoded == None: + print "OTP. Process OTP authentication. OTP secret key is invalid" + return False + + otp_secret_key = self.fromBase64Url(otp_secret_key_encoded) + + if self.otpType == "hotp": + validation_result = self.validateHotpKey(otp_secret_key, 1, otpCode) + + if (validation_result != None) and validation_result["result"]: + print "OTP. Process HOTP authentication during enrollment. otpCode is valid" + # Store HOTP Secret Key and moving factor in user entry + otp_user_external_uid = "hotp:%s;%s" % ( otp_secret_key_encoded, validation_result["movingFactor"] ) + + # Add otp_user_external_uid to user's external GUID list + find_user_by_external_uid = userService.addUserAttribute(user_name, "oxExternalUid", otp_user_external_uid, True) + if find_user_by_external_uid != None: + return True + + print "OTP. Process HOTP authentication during enrollment. Failed to update user entry" + elif self.otpType == "totp": + validation_result = self.validateTotpKey(otp_secret_key, otpCode) + if (validation_result != None) and validation_result["result"]: + print "OTP. Process TOTP authentication during enrollment. otpCode is valid" + # Store TOTP Secret Key and moving factor in user entry + otp_user_external_uid = "totp:%s" % otp_secret_key_encoded + + # Add otp_user_external_uid to user's external GUID list + find_user_by_external_uid = userService.addUserAttribute(user_name, "oxExternalUid", otp_user_external_uid, True) + if find_user_by_external_uid != None: + return True + + print "OTP. Process TOTP authentication during enrollment. Failed to update user entry" + elif otp_auth_method == "authenticate": + # Modified for Casa compliance + + user_enrollments = self.findEnrollments(user_name, "hotp") + + #if len(user_enrollments) == 0: + # print "OTP. Process OTP authentication. There is no OTP enrollment for user '%s'" % user_name + # facesMessages.add(FacesMessage.SEVERITY_ERROR, "There is no valid OTP user enrollments") + # return False + + if len(user_enrollments) > 0: + for user_enrollment in user_enrollments: + user_enrollment_data = user_enrollment.split(";") + otp_secret_key_encoded = user_enrollment_data[0] + + # Get current moving factor from user entry + moving_factor = StringHelper.toInteger(user_enrollment_data[1]) + otp_secret_key = self.fromBase64Url(otp_secret_key_encoded) + + # Validate TOTP + validation_result = self.validateHotpKey(otp_secret_key, moving_factor, otpCode) + if (validation_result != None) and validation_result["result"]: + print "OTP. Process HOTP authentication during authentication. otpCode is valid" + otp_user_external_uid = "hotp:%s;%s" % ( otp_secret_key_encoded, moving_factor ) + new_otp_user_external_uid = "hotp:%s;%s" % ( otp_secret_key_encoded, validation_result["movingFactor"] ) + + # Update moving factor in user entry + find_user_by_external_uid = userService.replaceUserAttribute(user_name, "oxExternalUid", otp_user_external_uid, new_otp_user_external_uid, True) + if find_user_by_external_uid != None: + return True + + print "OTP. Process HOTP authentication during authentication. Failed to update user entry" + + user_enrollments = self.findEnrollments(user_name, "totp") + + if len(user_enrollments) > 0: + for user_enrollment in user_enrollments: + otp_secret_key = self.fromBase64Url(user_enrollment) + + # Validate TOTP + validation_result = self.validateTotpKey(otp_secret_key, otpCode) + if (validation_result != None) and validation_result["result"]: + print "OTP. Process TOTP authentication during authentication. otpCode is valid" + return True + + facesMessages.add(FacesMessage.SEVERITY_ERROR, "Failed to authenticate. OTP code is invalid") + print "OTP. Process OTP authentication. OTP code is invalid" + + return False + + # Shared HOTP/TOTP methods + def generateSecretKey(self, keyLength): + bytes = jarray.zeros(keyLength, "b") + secureRandom = SecureRandom() + secureRandom.nextBytes(bytes) + + return bytes + + # HOTP methods + def generateSecretHotpKey(self): + keyLength = self.hotpConfiguration["keyLength"] + + return self.generateSecretKey(keyLength) + + def generateHotpKey(self, secretKey, movingFactor): + digits = self.hotpConfiguration["digits"] + + hotp = HOTP.key(secretKey).digits(digits).movingFactor(movingFactor).build() + + return hotp.value() + + def validateHotpKey(self, secretKey, movingFactor, totpKey): + lookAheadWindow = self.hotpConfiguration["lookAheadWindow"] + digits = self.hotpConfiguration["digits"] + + htopValidationResult = HOTPValidator.lookAheadWindow(lookAheadWindow).validate(secretKey, movingFactor, digits, totpKey) + if htopValidationResult.isValid(): + return { "result": True, "movingFactor": htopValidationResult.getNewMovingFactor() } + + return { "result": False, "movingFactor": None } + + def generateHotpSecretKeyUri(self, secretKey, issuer, userDisplayName): + digits = self.hotpConfiguration["digits"] + + secretKeyBase32 = self.toBase32(secretKey) + otpKey = OTPKey(secretKeyBase32, OTPType.HOTP) + label = issuer + " %s" % userDisplayName + + otpAuthURI = OTPAuthURIBuilder.fromKey(otpKey).label(label).issuer(issuer).digits(digits).build() + + return otpAuthURI.toUriString() + + # TOTP methods + def generateSecretTotpKey(self): + keyLength = self.totpConfiguration["keyLength"] + + return self.generateSecretKey(keyLength) + + def generateTotpKey(self, secretKey): + digits = self.totpConfiguration["digits"] + timeStep = self.totpConfiguration["timeStep"] + hmacShaAlgorithmType = self.totpConfiguration["hmacShaAlgorithmType"] + + totp = TOTP.key(secretKey).digits(digits).timeStep(TimeUnit.SECONDS.toMillis(timeStep)).hmacSha(hmacShaAlgorithmType).build() + + return totp.value() + + def validateTotpKey(self, secretKey, totpKey): + localTotpKey = self.generateTotpKey(secretKey) + if StringHelper.equals(localTotpKey, totpKey): + return { "result": True } + + return { "result": False } + + def generateTotpSecretKeyUri(self, secretKey, issuer, userDisplayName): + digits = self.totpConfiguration["digits"] + timeStep = self.totpConfiguration["timeStep"] + + secretKeyBase32 = self.toBase32(secretKey) + otpKey = OTPKey(secretKeyBase32, OTPType.TOTP) + label = issuer + " %s" % userDisplayName + + otpAuthURI = OTPAuthURIBuilder.fromKey(otpKey).label(label).issuer(issuer).digits(digits).timeStep(TimeUnit.SECONDS.toMillis(timeStep)).build() + + return otpAuthURI.toUriString() + + # Utility methods + def toBase32(self, bytes): + return BaseEncoding.base32().omitPadding().encode(bytes) + + def toBase64Url(self, bytes): + return BaseEncoding.base64Url().encode(bytes) + + def fromBase64Url(self, chars): + return BaseEncoding.base64Url().decode(chars) + + + # Added for Casa compliance + def hasEnrollments(self, configurationAttributes, user): + + # Both hotp and totp are accounted + hasEnrollments = False + values = user.getAttributeValues("oxExternalUid") + + if values != None: + for extUid in values: + if not hasEnrollments: + hasEnrollments = extUid.find("hotp:") != -1 or extUid.find("totp:") != -1 + + return hasEnrollments diff --git a/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/python/libs/passwurd-external_super_gluu.py b/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/python/libs/passwurd-external_super_gluu.py new file mode 100644 index 00000000..23a74afe --- /dev/null +++ b/oxAuth/Server/integrations/passwurd/bundle/bundle/opt/gluu/python/libs/passwurd-external_super_gluu.py @@ -0,0 +1,1084 @@ +# Based on oxAuth SuperGluuExternalAuthenticator.py + +from com.google.android.gcm.server import Sender, Message +from com.notnoop.apns import APNS +from java.util import Arrays +from org.apache.http.params import CoreConnectionPNames +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.model.config import ConfigurationFactory +from org.gluu.oxauth.service import AuthenticationService, UserService, SessionIdService +from org.gluu.oxauth.service.common import EncryptionService +from org.gluu.oxauth.service.fido.u2f import DeviceRegistrationService +from org.gluu.oxauth.service.net import HttpService +from org.gluu.oxauth.util import ServerUtil +from org.gluu.util import StringHelper +from org.gluu.service import MailService +from org.gluu.oxauth.service.push.sns import PushPlatform, PushSnsService +from org.gluu.oxnotify.client import NotifyClientFactory +from java.util import Arrays, HashMap, IdentityHashMap, Date +from java.time import ZonedDateTime +from java.time.format import DateTimeFormatter + +try: + from org.gluu.oxd.license.client.js import Product + from org.gluu.oxd.license.validator import LicenseValidator + has_license_api = True +except ImportError: + print "Super-Gluu. Load. Failed to load licensing API" + has_license_api = False + +import datetime +import urllib + +import sys +import json + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Super-Gluu. Initialization" + + if not configurationAttributes.containsKey("authentication_mode"): + print "Super-Gluu. Initialization. Property authentication_mode is mandatory" + return False + + self.applicationId = None + if configurationAttributes.containsKey("application_id"): + self.applicationId = configurationAttributes.get("application_id").getValue2() + + self.registrationUri = None + if configurationAttributes.containsKey("registration_uri"): + self.registrationUri = configurationAttributes.get("registration_uri").getValue2() + + authentication_mode = configurationAttributes.get("authentication_mode").getValue2() + if StringHelper.isEmpty(authentication_mode): + print "Super-Gluu. Initialization. Failed to determine authentication_mode. authentication_mode configuration parameter is empty" + return False + + self.oneStep = StringHelper.equalsIgnoreCase(authentication_mode, "one_step") + self.twoStep = StringHelper.equalsIgnoreCase(authentication_mode, "two_step") + + if not (self.oneStep or self.twoStep): + print "Super-Gluu. Initialization. Valid authentication_mode values are one_step and two_step" + return False + + self.enabledPushNotifications = self.initPushNotificationService(configurationAttributes) + + self.androidUrl = None + if configurationAttributes.containsKey("supergluu_android_download_url"): + self.androidUrl = configurationAttributes.get("supergluu_android_download_url").getValue2() + + self.IOSUrl = None + if configurationAttributes.containsKey("supergluu_ios_download_url"): + self.IOSUrl = configurationAttributes.get("supergluu_ios_download_url").getValue2() + + self.customLabel = None + if configurationAttributes.containsKey("label"): + self.customLabel = configurationAttributes.get("label").getValue2() + + self.customQrOptions = {} + if configurationAttributes.containsKey("qr_options"): + self.customQrOptions = configurationAttributes.get("qr_options").getValue2() + + self.use_super_gluu_group = False + if configurationAttributes.containsKey("super_gluu_group"): + self.super_gluu_group = configurationAttributes.get("super_gluu_group").getValue2() + self.use_super_gluu_group = True + print "Super-Gluu. Initialization. Using super_gluu only if user belong to group: %s" % self.super_gluu_group + + self.use_audit_group = False + if configurationAttributes.containsKey("audit_group"): + self.audit_group = configurationAttributes.get("audit_group").getValue2() + + if (not configurationAttributes.containsKey("audit_group_email")): + print "Super-Gluu. Initialization. Property audit_group_email is not specified" + return False + + self.audit_email = configurationAttributes.get("audit_group_email").getValue2() + self.use_audit_group = True + + print "Super-Gluu. Initialization. Using audit group: %s" % self.audit_group + + if self.use_super_gluu_group or self.use_audit_group: + if not configurationAttributes.containsKey("audit_attribute"): + print "Super-Gluu. Initialization. Property audit_attribute is not specified" + return False + else: + self.audit_attribute = configurationAttributes.get("audit_attribute").getValue2() + + self.valid_license = False + # Removing or altering this block validation is against the terms of the license. + if has_license_api and configurationAttributes.containsKey("license_file"): + license_file = configurationAttributes.get("license_file").getValue2() + + # Load license from file + f = open(license_file, 'r') + try: + license = json.loads(f.read()) + except: + print "Super-Gluu. Initialization. Failed to load license from file: %s" % license_file + return False + finally: + f.close() + + # Validate license + try: + self.license_content = LicenseValidator.validate(license["public-key"], license["public-password"], license["license-password"], license["license"], + Product.SUPER_GLUU, Date()) + self.valid_license = self.license_content.isValid() + except: + print "Super-Gluu. Initialization. Failed to validate license. Exception: ", sys.exc_info()[1] + return False + + print "Super-Gluu. Initialization. License status: '%s'. License metadata: '%s'" % (self.valid_license, self.license_content.getMetadata()) + + print "Super-Gluu. Initialized successfully. oneStep: '%s', twoStep: '%s', pushNotifications: '%s', customLabel: '%s'" % (self.oneStep, self.twoStep, self.enabledPushNotifications, self.customLabel) + + return True + + def destroy(self, configurationAttributes): + print "Super-Gluu. Destroy" + + self.pushAndroidService = None + self.pushAppleService = None + + print "Super-Gluu. Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, configurationAttributes): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + authenticationService = CdiUtil.bean(AuthenticationService) + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + session_attributes = identity.getSessionId().getSessionAttributes() + + client_redirect_uri = self.getApplicationUri(session_attributes) + if client_redirect_uri == None: + print "Super-Gluu. Authenticate. redirect_uri is not set" + return False + + self.setRequestScopedParameters(identity, step) + + # Validate form result code and initialize QR code regeneration if needed (retry_current_step = True) + identity.setWorkingParameter("retry_current_step", False) + form_auth_result = ServerUtil.getFirstValue(requestParameters, "auth_result") + if StringHelper.isNotEmpty(form_auth_result): + print "Super-Gluu. Authenticate for step %s. Get auth_result: '%s'" % (step, form_auth_result) + if form_auth_result in ['error']: + return False + + if form_auth_result in ['timeout']: + if ((step == 1) and self.oneStep) or ((step == 2) and self.twoStep): + print "Super-Gluu. Authenticate for step %s. Reinitializing current step" % step + identity.setWorkingParameter("retry_current_step", True) + return False + + userService = CdiUtil.bean(UserService) + deviceRegistrationService = CdiUtil.bean(DeviceRegistrationService) + if step == 1: + print "Super-Gluu. Authenticate for step 1" + + user_name = credentials.getUsername() + if self.oneStep: + #This branch will never be taken + session_device_status = self.getSessionDeviceStatus(session_attributes, user_name) + if session_device_status == None: + return False + + u2f_device_id = session_device_status['device_id'] + + validation_result = self.validateSessionDeviceStatus(client_redirect_uri, session_device_status) + if validation_result: + print "Super-Gluu. Authenticate for step 1. User successfully authenticated with u2f_device '%s'" % u2f_device_id + else: + return False + + if not session_device_status['one_step']: + print "Super-Gluu. Authenticate for step 1. u2f_device '%s' is not one step device" % u2f_device_id + return False + + # There are two steps only in enrollment mode + if session_device_status['enroll']: + return validation_result + + identity.setWorkingParameter("super_gluu_count_login_steps", 1) + + user_inum = session_device_status['user_inum'] + + u2f_device = deviceRegistrationService.findUserDeviceRegistration(user_inum, u2f_device_id, "oxId") + if u2f_device == None: + print "Super-Gluu. Authenticate for step 1. Failed to load u2f_device '%s'" % u2f_device_id + return False + + logged_in = authenticationService.authenticate(user_name) + if not logged_in: + print "Super-Gluu. Authenticate for step 1. Failed to authenticate user '%s'" % user_name + return False + + print "Super-Gluu. Authenticate for step 1. User '%s' successfully authenticated with u2f_device '%s'" % (user_name, u2f_device_id) + + return True + elif self.twoStep: + authenticated_user = self.processBasicAuthentication(credentials) + if authenticated_user == None: + return False + + if (self.use_super_gluu_group): + print "Super-Gluu. Authenticate for step 1. Checking if user belong to super_gluu group" + is_member_super_gluu_group = self.isUserMemberOfGroup(authenticated_user, self.audit_attribute, self.super_gluu_group) + if (is_member_super_gluu_group): + print "Super-Gluu. Authenticate for step 1. User '%s' member of super_gluu group" % authenticated_user.getUserId() + super_gluu_count_login_steps = 2 + else: + if self.use_audit_group: + self.processAuditGroup(authenticated_user, self.audit_attribute, self.audit_group) + super_gluu_count_login_steps = 1 + + identity.setWorkingParameter("super_gluu_count_login_steps", super_gluu_count_login_steps) + + if super_gluu_count_login_steps == 1: + return True + + auth_method = 'authenticate' + enrollment_mode = ServerUtil.getFirstValue(requestParameters, "loginForm:registerButton") + if StringHelper.isNotEmpty(enrollment_mode): + auth_method = 'enroll' + + if auth_method == 'authenticate': + user_inum = userService.getUserInum(authenticated_user) + u2f_devices_list = deviceRegistrationService.findUserDeviceRegistrations(user_inum, client_redirect_uri, "oxId") + if u2f_devices_list.size() == 0: + auth_method = 'enroll' + print "Super-Gluu. Authenticate for step 1. There is no U2F '%s' user devices associated with application '%s'. Changing auth_method to '%s'" % (user_name, client_redirect_uri, auth_method) + + print "Super-Gluu. Authenticate for step 1. auth_method: '%s'" % auth_method + + identity.setWorkingParameter("super_gluu_auth_method", auth_method) + + return True + + return False + elif step == 2: + print "Super-Gluu. Authenticate for step 2" + + user = authenticationService.getAuthenticatedUser() + if (user == None): + print "Super-Gluu. Authenticate for step 2. Failed to determine user name" + return False + user_name = user.getUserId() + + session_attributes = identity.getSessionId().getSessionAttributes() + + session_device_status = self.getSessionDeviceStatus(session_attributes, user_name) + if session_device_status == None: + return False + + u2f_device_id = session_device_status['device_id'] + + # There are two steps only in enrollment mode + if self.oneStep and session_device_status['enroll']: + authenticated_user = self.processBasicAuthentication(credentials) + if authenticated_user == None: + return False + + user_inum = userService.getUserInum(authenticated_user) + + attach_result = deviceRegistrationService.attachUserDeviceRegistration(user_inum, u2f_device_id) + + print "Super-Gluu. Authenticate for step 2. Result after attaching u2f_device '%s' to user '%s': '%s'" % (u2f_device_id, user_name, attach_result) + + return attach_result + elif self.twoStep: + if user_name == None: + print "Super-Gluu. Authenticate for step 2. Failed to determine user name" + return False + + validation_result = self.validateSessionDeviceStatus(client_redirect_uri, session_device_status, user_name) + if validation_result: + print "Super-Gluu. Authenticate for step 2. User '%s' successfully authenticated with u2f_device '%s'" % (user_name, u2f_device_id) + else: + return False + + super_gluu_request = json.loads(session_device_status['super_gluu_request']) + auth_method = super_gluu_request['method'] + if auth_method in ['enroll', 'authenticate']: + if validation_result and self.use_audit_group: + user = authenticationService.getAuthenticatedUser() + self.processAuditGroup(user, self.audit_attribute, self.audit_group) + + return validation_result + + print "Super-Gluu. Authenticate for step 2. U2F auth_method is invalid" + + return False + else: + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + identity = CdiUtil.bean(Identity) + session_attributes = identity.getSessionId().getSessionAttributes() + + client_redirect_uri = self.getApplicationUri(session_attributes) + if client_redirect_uri == None: + print "Super-Gluu. Prepare for step. redirect_uri is not set" + return False + + #This call is harmless with respect to Casa restrictions + self.setRequestScopedParameters(identity, step) + + if step == 1: + print "Super-Gluu. Prepare for step 1" + if self.oneStep: + #This branch will never be taken (see note in getExtraParametersForStep) + session_id = CdiUtil.bean(SessionIdService).getSessionIdFromCookie() + if StringHelper.isEmpty(session_id): + print "Super-Gluu. Prepare for step 2. Failed to determine session_id" + return False + + issuer = CdiUtil.bean(ConfigurationFactory).getConfiguration().getIssuer() + super_gluu_request_dictionary = {'app': client_redirect_uri, + 'issuer': issuer, + 'state': session_id, + 'licensed': self.valid_license, + 'created': DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(ZonedDateTime.now().withNano(0))} + + self.addGeolocationData(session_attributes, super_gluu_request_dictionary) + + super_gluu_request = json.dumps(super_gluu_request_dictionary, separators=(',',':')) + print "Super-Gluu. Prepare for step 1. Prepared super_gluu_request:", super_gluu_request + + identity.setWorkingParameter("super_gluu_request", super_gluu_request) + elif self.twoStep: + identity.setWorkingParameter("display_register_action", True) + + return True + elif step == 2: + print "Super-Gluu. Prepare for step 2" + if self.oneStep: + return True + + authenticationService = CdiUtil.bean(AuthenticationService) + user = authenticationService.getAuthenticatedUser() + if user == None: + print "Super-Gluu. Prepare for step 2. Failed to determine user name" + return False + + if session_attributes.containsKey("super_gluu_request"): + super_gluu_request = session_attributes.get("super_gluu_request") + if not StringHelper.equalsIgnoreCase(super_gluu_request, "timeout"): + print "Super-Gluu. Prepare for step 2. Request was generated already" + return True + + session_id = CdiUtil.bean(SessionIdService).getSessionIdFromCookie() + if StringHelper.isEmpty(session_id): + print "Super-Gluu. Prepare for step 2. Failed to determine session_id" + return False + + auth_method = session_attributes.get("super_gluu_auth_method") + if StringHelper.isEmpty(auth_method): + print "Super-Gluu. Prepare for step 2. Failed to determine auth_method" + return False + + print "Super-Gluu. Prepare for step 2. auth_method: '%s'" % auth_method + + issuer = CdiUtil.bean(ConfigurationFactory).getAppConfiguration().getIssuer() + super_gluu_request_dictionary = {'username': user.getUserId(), + 'app': client_redirect_uri, + 'issuer': issuer, + 'method': auth_method, + 'state': session_id, + 'licensed': self.valid_license, + 'created': DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(ZonedDateTime.now().withNano(0))} + + self.addGeolocationData(session_attributes, super_gluu_request_dictionary) + + super_gluu_request = json.dumps(super_gluu_request_dictionary, separators=(',',':')) + print "Super-Gluu. Prepare for step 2. Prepared super_gluu_request:", super_gluu_request + + identity.setWorkingParameter("super_gluu_request", super_gluu_request) + identity.setWorkingParameter("super_gluu_auth_method", auth_method) + + if auth_method in ['authenticate']: + self.sendPushNotification(client_redirect_uri, user, super_gluu_request) + + return True + else: + return False + + def getNextStep(self, configurationAttributes, requestParameters, step): + # If user not pass current step change step to previous + identity = CdiUtil.bean(Identity) + retry_current_step = identity.getWorkingParameter("retry_current_step") + if retry_current_step: + print "Super-Gluu. Get next step. Retrying current step" + + # Remove old QR code + identity.setWorkingParameter("super_gluu_request", "timeout") + + resultStep = step + return resultStep + + return -1 + + def getExtraParametersForStep(self, configurationAttributes, step): + #This violates Casa restriction. However, self.oneStep and self.twoStep have to be False/True + #respectively as in this scenario only 2 or more steps make sense to call an external script dynamically. + #Parameter "display_register_action" used in default login.xhtml page of Gluu Server will not be set + if step == 1: + if self.oneStep: + return Arrays.asList("super_gluu_request") + elif self.twoStep: + return Arrays.asList("display_register_action") + elif step == 2: + return Arrays.asList("super_gluu_auth_method", "super_gluu_request") + + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + identity = CdiUtil.bean(Identity) + if identity.isSetWorkingParameter("super_gluu_count_login_steps"): + return identity.getWorkingParameter("super_gluu_count_login_steps") + else: + return 2 + + def getPageForStep(self, configurationAttributes, step): + if step == 1: + if self.oneStep: + return "/auth/super-gluu/login.xhtml" + elif step == 2: + if self.oneStep: + return "/login.xhtml" + else: + identity = CdiUtil.bean(Identity) + authmethod = identity.getWorkingParameter("super_gluu_auth_method") + print "Super-Gluu. authmethod '%s'" % authmethod + if authmethod == "enroll": + return "/auth/super-gluu/login.xhtml" + else: + #Modified for Casa compliance + return "/passwordless/sg.xhtml" + + return "" + + def logout(self, configurationAttributes, requestParameters): + return True + + def processBasicAuthentication(self, credentials): + authenticationService = CdiUtil.bean(AuthenticationService) + + # Modified for Casa compliance + user = authenticationService.getAuthenticatedUser() + if user == None: + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + if StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password): + authenticationService.authenticate(user_name, user_password) + user = authenticationService.getAuthenticatedUser() + + return user + + def validateSessionDeviceStatus(self, client_redirect_uri, session_device_status, user_name = None): + userService = CdiUtil.bean(UserService) + deviceRegistrationService = CdiUtil.bean(DeviceRegistrationService) + + u2f_device_id = session_device_status['device_id'] + + u2f_device = None + if session_device_status['enroll'] and session_device_status['one_step']: + u2f_device = deviceRegistrationService.findOneStepUserDeviceRegistration(u2f_device_id) + if u2f_device == None: + print "Super-Gluu. Validate session device status. There is no one step u2f_device '%s'" % u2f_device_id + return False + else: + # Validate if user has specified device_id enrollment + user_inum = userService.getUserInum(user_name) + + if session_device_status['one_step']: + user_inum = session_device_status['user_inum'] + + u2f_device = deviceRegistrationService.findUserDeviceRegistration(user_inum, u2f_device_id) + if u2f_device == None: + print "Super-Gluu. Validate session device status. There is no u2f_device '%s' associated with user '%s'" % (u2f_device_id, user_inum) + return False + + if not StringHelper.equalsIgnoreCase(client_redirect_uri, u2f_device.application): + print "Super-Gluu. Validate session device status. u2f_device '%s' associated with other application '%s'" % (u2f_device_id, u2f_device.application) + return False + + return True + + def getSessionDeviceStatus(self, session_attributes, user_name): + print "Super-Gluu. Get session device status" + + if not session_attributes.containsKey("super_gluu_request"): + print "Super-Gluu. Get session device status. There is no Super-Gluu request in session attributes" + return None + + # Check session state extended + if not session_attributes.containsKey("session_custom_state"): + print "Super-Gluu. Get session device status. There is no session_custom_state in session attributes" + return None + + session_custom_state = session_attributes.get("session_custom_state") + if not StringHelper.equalsIgnoreCase("approved", session_custom_state): + print "Super-Gluu. Get session device status. User '%s' not approve or not pass U2F authentication. session_custom_state: '%s'" % (user_name, session_custom_state) + return None + + # Try to find device_id in session attribute + if not session_attributes.containsKey("oxpush2_u2f_device_id"): + print "Super-Gluu. Get session device status. There is no u2f_device associated with this request" + return None + + # Try to find user_inum in session attribute + if not session_attributes.containsKey("oxpush2_u2f_device_user_inum"): + print "Super-Gluu. Get session device status. There is no user_inum associated with this request" + return None + + enroll = False + if session_attributes.containsKey("oxpush2_u2f_device_enroll"): + enroll = StringHelper.equalsIgnoreCase("true", session_attributes.get("oxpush2_u2f_device_enroll")) + + one_step = False + if session_attributes.containsKey("oxpush2_u2f_device_one_step"): + one_step = StringHelper.equalsIgnoreCase("true", session_attributes.get("oxpush2_u2f_device_one_step")) + + super_gluu_request = session_attributes.get("super_gluu_request") + u2f_device_id = session_attributes.get("oxpush2_u2f_device_id") + user_inum = session_attributes.get("oxpush2_u2f_device_user_inum") + + session_device_status = {"super_gluu_request": super_gluu_request, "device_id": u2f_device_id, "user_inum" : user_inum, "enroll" : enroll, "one_step" : one_step} + print "Super-Gluu. Get session device status. session_device_status: '%s'" % (session_device_status) + + return session_device_status + + def initPushNotificationService(self, configurationAttributes): + print "Super-Gluu. Initialize Native/SNS/Gluu notification services" + + self.pushSnsMode = False + self.pushGluuMode = False + if configurationAttributes.containsKey("notification_service_mode"): + notificationServiceMode = configurationAttributes.get("notification_service_mode").getValue2() + if StringHelper.equalsIgnoreCase(notificationServiceMode, "sns"): + return self.initSnsPushNotificationService(configurationAttributes) + elif StringHelper.equalsIgnoreCase(notificationServiceMode, "gluu"): + return self.initGluuPushNotificationService(configurationAttributes) + + return self.initNativePushNotificationService(configurationAttributes) + + def initNativePushNotificationService(self, configurationAttributes): + print "Super-Gluu. Initialize native notification services" + + creds = self.loadPushNotificationCreds(configurationAttributes) + if creds == None: + return False + + try: + android_creds = creds["android"]["gcm"] + ios_creds = creds["ios"]["apns"] + except: + print "Super-Gluu. Initialize native notification services. Invalid credentials file format" + return False + + self.pushAndroidService = None + self.pushAppleService = None + if android_creds["enabled"]: + self.pushAndroidService = Sender(android_creds["api_key"]) + print "Super-Gluu. Initialize native notification services. Created Android notification service" + + if ios_creds["enabled"]: + p12_file_path = ios_creds["p12_file_path"] + p12_password = ios_creds["p12_password"] + + try: + encryptionService = CdiUtil.bean(EncryptionService) + p12_password = encryptionService.decrypt(p12_password) + except: + # Ignore exception. Password is not encrypted + print "Super-Gluu. Initialize native notification services. Assuming that 'p12_password' password in not encrypted" + + apnsServiceBuilder = APNS.newService().withCert(p12_file_path, p12_password) + if ios_creds["production"]: + self.pushAppleService = apnsServiceBuilder.withProductionDestination().build() + else: + self.pushAppleService = apnsServiceBuilder.withSandboxDestination().build() + + self.pushAppleServiceProduction = ios_creds["production"] + + print "Super-Gluu. Initialize native notification services. Created iOS notification service" + + enabled = self.pushAndroidService != None or self.pushAppleService != None + + return enabled + + def initSnsPushNotificationService(self, configurationAttributes): + print "Super-Gluu. Initialize SNS notification services" + self.pushSnsMode = True + + creds = self.loadPushNotificationCreds(configurationAttributes) + if creds == None: + return False + + try: + sns_creds = creds["sns"] + android_creds = creds["android"]["sns"] + ios_creds = creds["ios"]["sns"] + except: + print "Super-Gluu. Initialize SNS notification services. Invalid credentials file format" + return False + + self.pushAndroidService = None + self.pushAppleService = None + if not (android_creds["enabled"] or ios_creds["enabled"]): + print "Super-Gluu. Initialize SNS notification services. SNS disabled for all platforms" + return False + + sns_access_key = sns_creds["access_key"] + sns_secret_access_key = sns_creds["secret_access_key"] + sns_region = sns_creds["region"] + + encryptionService = CdiUtil.bean(EncryptionService) + + try: + sns_secret_access_key = encryptionService.decrypt(sns_secret_access_key) + except: + # Ignore exception. Password is not encrypted + print "Super-Gluu. Initialize SNS notification services. Assuming that 'sns_secret_access_key' in not encrypted" + + pushSnsService = CdiUtil.bean(PushSnsService) + pushClient = pushSnsService.createSnsClient(sns_access_key, sns_secret_access_key, sns_region) + + if android_creds["enabled"]: + self.pushAndroidService = pushClient + self.pushAndroidPlatformArn = android_creds["platform_arn"] + print "Super-Gluu. Initialize SNS notification services. Created Android notification service" + + if ios_creds["enabled"]: + self.pushAppleService = pushClient + self.pushApplePlatformArn = ios_creds["platform_arn"] + self.pushAppleServiceProduction = ios_creds["production"] + print "Super-Gluu. Initialize SNS notification services. Created iOS notification service" + + enabled = self.pushAndroidService != None or self.pushAppleService != None + + return enabled + + def initGluuPushNotificationService(self, configurationAttributes): + print "Super-Gluu. Initialize Gluu notification services" + + self.pushGluuMode = True + + creds = self.loadPushNotificationCreds(configurationAttributes) + if creds == None: + return False + + try: + gluu_conf = creds["gluu"] + android_creds = creds["android"]["gluu"] + ios_creds = creds["ios"]["gluu"] + except: + print "Super-Gluu. Initialize Gluu notification services. Invalid credentials file format" + return False + + self.pushAndroidService = None + self.pushAppleService = None + if not (android_creds["enabled"] or ios_creds["enabled"]): + print "Super-Gluu. Initialize Gluu notification services. Gluu disabled for all platforms" + return False + + gluu_server_uri = gluu_conf["server_uri"] + notifyClientFactory = NotifyClientFactory.instance() + metadataConfiguration = None + try: + metadataConfiguration = notifyClientFactory.createMetaDataConfigurationService(gluu_server_uri).getMetadataConfiguration() + except: + print "Super-Gluu. Initialize Gluu notification services. Failed to load metadata. Exception: ", sys.exc_info()[1] + return False + + gluuClient = notifyClientFactory.createNotifyService(metadataConfiguration) + encryptionService = CdiUtil.bean(EncryptionService) + + if android_creds["enabled"]: + gluu_access_key = android_creds["access_key"] + gluu_secret_access_key = android_creds["secret_access_key"] + + try: + gluu_secret_access_key = encryptionService.decrypt(gluu_secret_access_key) + except: + # Ignore exception. Password is not encrypted + print "Super-Gluu. Initialize Gluu notification services. Assuming that 'gluu_secret_access_key' in not encrypted" + + self.pushAndroidService = gluuClient + self.pushAndroidServiceAuth = notifyClientFactory.getAuthorization(gluu_access_key, gluu_secret_access_key); + print "Super-Gluu. Initialize Gluu notification services. Created Android notification service" + + if ios_creds["enabled"]: + gluu_access_key = ios_creds["access_key"] + gluu_secret_access_key = ios_creds["secret_access_key"] + + try: + gluu_secret_access_key = encryptionService.decrypt(gluu_secret_access_key) + except: + # Ignore exception. Password is not encrypted + print "Super-Gluu. Initialize Gluu notification services. Assuming that 'gluu_secret_access_key' in not encrypted" + + self.pushAppleService = gluuClient + self.pushAppleServiceAuth = notifyClientFactory.getAuthorization(gluu_access_key, gluu_secret_access_key); + print "Super-Gluu. Initialize Gluu notification services. Created iOS notification service" + + enabled = self.pushAndroidService != None or self.pushAppleService != None + + return enabled + + def loadPushNotificationCreds(self, configurationAttributes): + print "Super-Gluu. Initialize notification services" + if not configurationAttributes.containsKey("credentials_file"): + return None + + super_gluu_creds_file = configurationAttributes.get("credentials_file").getValue2() + + # Load credentials from file + f = open(super_gluu_creds_file, 'r') + try: + creds = json.loads(f.read()) + except: + print "Super-Gluu. Initialize notification services. Failed to load credentials from file:", super_gluu_creds_file + return None + finally: + f.close() + + return creds + + def sendPushNotification(self, client_redirect_uri, user, super_gluu_request): + try: + self.sendPushNotificationImpl(client_redirect_uri, user, super_gluu_request) + except: + print "Super-Gluu. Send push notification. Failed to send push notification: ", sys.exc_info()[1] + + def sendPushNotificationImpl(self, client_redirect_uri, user, super_gluu_request): + if not self.enabledPushNotifications: + return + + user_name = user.getUserId() + print "Super-Gluu. Send push notification. Loading user '%s' devices" % user_name + + send_notification = False + send_notification_result = True + + userService = CdiUtil.bean(UserService) + deviceRegistrationService = CdiUtil.bean(DeviceRegistrationService) + + user_inum = userService.getUserInum(user_name) + + send_android = 0 + send_ios = 0 + u2f_devices_list = deviceRegistrationService.findUserDeviceRegistrations(user_inum, client_redirect_uri, "oxId", "oxDeviceData", "oxDeviceNotificationConf") + if u2f_devices_list.size() > 0: + for u2f_device in u2f_devices_list: + device_data = u2f_device.getDeviceData() + + # Device data which Super-Gluu gets during enrollment + if device_data == None: + continue + + platform = device_data.getPlatform() + push_token = device_data.getPushToken() + debug = False + + if StringHelper.equalsIgnoreCase(platform, "ios") and StringHelper.isNotEmpty(push_token): + # Sending notification to iOS user's device + if self.pushAppleService == None: + print "Super-Gluu. Send push notification. Apple native push notification service is not enabled" + else: + send_notification = True + + title = "Super Gluu" + message = "Confirm your sign in request to: %s" % client_redirect_uri + + if self.pushSnsMode or self.pushGluuMode: + pushSnsService = CdiUtil.bean(PushSnsService) + targetEndpointArn = self.getTargetEndpointArn(deviceRegistrationService, pushSnsService, PushPlatform.APNS, user, u2f_device) + if targetEndpointArn == None: + return + + send_notification = True + + sns_push_request_dictionary = { "aps": + { "badge": 0, + "alert" : {"body": message, "title" : title}, + "category": "ACTIONABLE", + "content-available": "1", + "sound": 'default' + }, + "request" : super_gluu_request + } + push_message = json.dumps(sns_push_request_dictionary, separators=(',',':')) + + if self.pushSnsMode: + apple_push_platform = PushPlatform.APNS + if not self.pushAppleServiceProduction: + apple_push_platform = PushPlatform.APNS_SANDBOX + + send_notification_result = pushSnsService.sendPushMessage(self.pushAppleService, apple_push_platform, targetEndpointArn, push_message, None) + if debug: + print "Super-Gluu. Send iOS SNS push notification. token: '%s', message: '%s', send_notification_result: '%s', apple_push_platform: '%s'" % (push_token, push_message, send_notification_result, apple_push_platform) + elif self.pushGluuMode: + send_notification_result = self.pushAppleService.sendNotification(self.pushAppleServiceAuth, targetEndpointArn, push_message) + if debug: + print "Super-Gluu. Send iOS Gluu push notification. token: '%s', message: '%s', send_notification_result: '%s'" % (push_token, push_message, send_notification_result) + else: + additional_fields = { "request" : super_gluu_request } + + msgBuilder = APNS.newPayload().alertBody(message).alertTitle(title).sound("default") + msgBuilder.category('ACTIONABLE').badge(0) + msgBuilder.forNewsstand() + msgBuilder.customFields(additional_fields) + push_message = msgBuilder.build() + + send_notification_result = self.pushAppleService.push(push_token, push_message) + if debug: + print "Super-Gluu. Send iOS Native push notification. token: '%s', message: '%s', send_notification_result: '%s'" % (push_token, push_message, send_notification_result) + send_ios = send_ios + 1 + + if StringHelper.equalsIgnoreCase(platform, "android") and StringHelper.isNotEmpty(push_token): + # Sending notification to Android user's device + if self.pushAndroidService == None: + print "Super-Gluu. Send native push notification. Android native push notification service is not enabled" + else: + send_notification = True + + title = "Super-Gluu" + if self.pushSnsMode or self.pushGluuMode: + pushSnsService = CdiUtil.bean(PushSnsService) + targetEndpointArn = self.getTargetEndpointArn(deviceRegistrationService, pushSnsService, PushPlatform.GCM, user, u2f_device) + if targetEndpointArn == None: + return + + send_notification = True + + sns_push_request_dictionary = { "collapse_key": "single", + "content_available": True, + "time_to_live": 60, + "data": + { "message" : super_gluu_request, + "title" : title } + } + push_message = json.dumps(sns_push_request_dictionary, separators=(',',':')) + + if self.pushSnsMode: + send_notification_result = pushSnsService.sendPushMessage(self.pushAndroidService, PushPlatform.GCM, targetEndpointArn, push_message, None) + if debug: + print "Super-Gluu. Send Android SNS push notification. token: '%s', message: '%s', send_notification_result: '%s'" % (push_token, push_message, send_notification_result) + elif self.pushGluuMode: + send_notification_result = self.pushAndroidService.sendNotification(self.pushAndroidServiceAuth, targetEndpointArn, push_message) + if debug: + print "Super-Gluu. Send Android Gluu push notification. token: '%s', message: '%s', send_notification_result: '%s'" % (push_token, push_message, send_notification_result) + else: + msgBuilder = Message.Builder().addData("message", super_gluu_request).addData("title", title).collapseKey("single").contentAvailable(True) + push_message = msgBuilder.build() + + send_notification_result = self.pushAndroidService.send(push_message, push_token, 3) + if debug: + print "Super-Gluu. Send Android Native push notification. token: '%s', message: '%s', send_notification_result: '%s'" % (push_token, push_message, send_notification_result) + send_android = send_android + 1 + + print "Super-Gluu. Send push notification. send_android: '%s', send_ios: '%s'" % (send_android, send_ios) + + def getTargetEndpointArn(self, deviceRegistrationService, pushSnsService, platform, user, u2fDevice): + targetEndpointArn = None + + # Return endpoint ARN if it created already + notificationConf = u2fDevice.getDeviceNotificationConf() + if StringHelper.isNotEmpty(notificationConf): + notificationConfJson = json.loads(notificationConf) + targetEndpointArn = notificationConfJson['sns_endpoint_arn'] + if StringHelper.isNotEmpty(targetEndpointArn): + print "Super-Gluu. Get target endpoint ARN. There is already created target endpoint ARN" + return targetEndpointArn + + # Create endpoint ARN + pushClient = None + pushClientAuth = None + platformApplicationArn = None + if platform == PushPlatform.GCM: + pushClient = self.pushAndroidService + if self.pushSnsMode: + platformApplicationArn = self.pushAndroidPlatformArn + if self.pushGluuMode: + pushClientAuth = self.pushAndroidServiceAuth + elif platform == PushPlatform.APNS: + pushClient = self.pushAppleService + if self.pushSnsMode: + platformApplicationArn = self.pushApplePlatformArn + if self.pushGluuMode: + pushClientAuth = self.pushAppleServiceAuth + else: + return None + + deviceData = u2fDevice.getDeviceData() + pushToken = deviceData.getPushToken() + + print "Super-Gluu. Get target endpoint ARN. Attempting to create target endpoint ARN for user: '%s'" % user.getUserId() + if self.pushSnsMode: + targetEndpointArn = pushSnsService.createPlatformArn(pushClient, platformApplicationArn, pushToken, user) + else: + customUserData = pushSnsService.getCustomUserData(user) + registerDeviceResponse = pushClient.registerDevice(pushClientAuth, pushToken, customUserData); + if registerDeviceResponse != None and registerDeviceResponse.getStatusCode() == 200: + targetEndpointArn = registerDeviceResponse.getEndpointArn() + + if StringHelper.isEmpty(targetEndpointArn): + print "Super-Gluu. Failed to get endpoint ARN for user: '%s'" % user.getUserId() + return None + + print "Super-Gluu. Get target endpoint ARN. Create target endpoint ARN '%s' for user: '%s'" % (targetEndpointArn, user.getUserId()) + + # Store created endpoint ARN in device entry + userInum = user.getAttribute("inum") + u2fDeviceUpdate = deviceRegistrationService.findUserDeviceRegistration(userInum, u2fDevice.getId()) + u2fDeviceUpdate.setDeviceNotificationConf('{"sns_endpoint_arn" : "%s"}' % targetEndpointArn) + deviceRegistrationService.updateDeviceRegistration(userInum, u2fDeviceUpdate) + + return targetEndpointArn + + def getApplicationUri(self, session_attributes): + if self.applicationId != None: + return self.applicationId + + if not session_attributes.containsKey("redirect_uri"): + return None + + return session_attributes.get("redirect_uri") + + def setRequestScopedParameters(self, identity, step): + downloadMap = HashMap() + if self.registrationUri != None: + identity.setWorkingParameter("external_registration_uri", self.registrationUri) + + if self.androidUrl!= None and step == 1: + downloadMap.put("android", self.androidUrl) + + if self.IOSUrl != None and step == 1: + downloadMap.put("ios", self.IOSUrl) + + if self.customLabel != None: + identity.setWorkingParameter("super_gluu_label", self.customLabel) + + identity.setWorkingParameter("download_url",downloadMap) + identity.setWorkingParameter("super_gluu_qr_options", self.customQrOptions) + + def addGeolocationData(self, session_attributes, super_gluu_request_dictionary): + if session_attributes.containsKey("remote_ip"): + remote_ip = session_attributes.get("remote_ip") + if StringHelper.isNotEmpty(remote_ip): + print "Super-Gluu. Prepare for step 2. Adding req_ip and req_loc to super_gluu_request" + super_gluu_request_dictionary['req_ip'] = remote_ip + + remote_loc_dic = self.determineGeolocationData(remote_ip) + if remote_loc_dic == None: + print "Super-Gluu. Prepare for step 2. Failed to determine remote location by remote IP '%s'" % remote_ip + return + + remote_loc = "%s, %s, %s" % ( remote_loc_dic['country'], remote_loc_dic['regionName'], remote_loc_dic['city'] ) + remote_loc_encoded = urllib.quote(remote_loc.encode('utf-8')) + super_gluu_request_dictionary['req_loc'] = remote_loc_encoded + + def determineGeolocationData(self, remote_ip): + print "Super-Gluu. Determine remote location. remote_ip: '%s'" % remote_ip + httpService = CdiUtil.bean(HttpService) + + http_client = httpService.getHttpsClient() + http_client_params = http_client.getParams() + http_client_params.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 15 * 1000) + + geolocation_service_url = "http://ip-api.com/json/%s?fields=49177" % remote_ip + geolocation_service_headers = { "Accept" : "application/json" } + + try: + http_service_response = httpService.executeGet(http_client, geolocation_service_url, geolocation_service_headers) + http_response = http_service_response.getHttpResponse() + except: + print "Super-Gluu. Determine remote location. Exception: ", sys.exc_info()[1] + return None + + try: + if not httpService.isResponseStastusCodeOk(http_response): + print "Super-Gluu. Determine remote location. Get invalid response from validation server: ", str(http_response.getStatusLine().getStatusCode()) + httpService.consume(http_response) + return None + + response_bytes = httpService.getResponseContent(http_response) + response_string = httpService.convertEntityToString(response_bytes) + httpService.consume(http_response) + finally: + http_service_response.closeConnection() + + if response_string == None: + print "Super-Gluu. Determine remote location. Get empty response from location server" + return None + + response = json.loads(response_string) + + if not StringHelper.equalsIgnoreCase(response['status'], "success"): + print "Super-Gluu. Determine remote location. Get response with status: '%s'" % response['status'] + return None + + return response + + def isUserMemberOfGroup(self, user, attribute, group): + is_member = False + member_of_list = user.getAttributeValues(attribute) + if (member_of_list != None): + for member_of in member_of_list: + if StringHelper.equalsIgnoreCase(group, member_of) or member_of.endswith(group): + is_member = True + break + + return is_member + + def processAuditGroup(self, user, attribute, group): + is_member = self.isUserMemberOfGroup(user, attribute, group) + if (is_member): + print "Super-Gluu. Authenticate for processAuditGroup. User '%s' member of audit group" % user.getUserId() + print "Super-Gluu. Authenticate for processAuditGroup. Sending e-mail about user '%s' login to %s" % (user.getUserId(), self.audit_email) + + # Send e-mail to administrator + user_id = user.getUserId() + mailService = CdiUtil.bean(MailService) + subject = "User log in: %s" % user_id + body = "User log in: %s" % user_id + mailService.sendMail(self.audit_email, subject, body) + + # Added for Casa compliance + + def hasEnrollments(self, configurationAttributes, user): + + inum = user.getAttribute("inum") + devRegService = CdiUtil.bean(DeviceRegistrationService) + app_id = configurationAttributes.get("application_id").getValue2() + userDevices = devRegService.findUserDeviceRegistrations(inum, app_id, "oxStatus") + + hasDevices = False + for device in userDevices: + if device.getStatus().getValue() == "active": + hasDevices = True + break + + return hasDevices diff --git a/oxAuth/Server/integrations/pingid/README.md b/oxAuth/Server/integrations/pingid/README.md new file mode 100644 index 00000000..1c8bd715 --- /dev/null +++ b/oxAuth/Server/integrations/pingid/README.md @@ -0,0 +1,3 @@ +# PingID MFA authentication script + +See https://gluu.org/docs/gluu-server/authn-guide/pingid/ diff --git a/oxAuth/Server/integrations/pingid/bundle.zip b/oxAuth/Server/integrations/pingid/bundle.zip new file mode 100644 index 0000000000000000000000000000000000000000..40e2c6d6affae321ffe5837a1f4c8be6c5fc8942 GIT binary patch literal 6591 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 literal 0 HcmV?d00001 diff --git a/oxAuth/Server/integrations/pingid/oxauth-pingid-1.0.jar b/oxAuth/Server/integrations/pingid/oxauth-pingid-1.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..6dc8730b9ac36bb1d85909b26dd9c2ff8e5f2b9e GIT binary patch literal 14556 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 CyRhH@ literal 0 HcmV?d00001 diff --git a/oxAuth/Server/integrations/pingid/pingIDAuthenticator.py b/oxAuth/Server/integrations/pingid/pingIDAuthenticator.py new file mode 100644 index 00000000..8954d475 --- /dev/null +++ b/oxAuth/Server/integrations/pingid/pingIDAuthenticator.py @@ -0,0 +1,329 @@ +from java.util import Arrays, ArrayList, Optional, Base64 + +from javax.faces.application import FacesMessage +from javax.faces.context import FacesContext + +from org.gluu.jsf2.message import FacesMessages +from org.gluu.oxauth.ping import PPMRequestBroker, UserManagerBroker, ResponseTokenParser, HttpException, TokenProcessingException +from org.gluu.oxauth.security import Identity +from org.gluu.oxauth.model.configuration import AppConfiguration +from org.gluu.oxauth.service import AuthenticationService, UserService +from org.gluu.oxauth.util import ServerUtil +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.util import StringHelper +from org.json import JSONObject + +try: + import json +except ImportError: + import simplejson as json +import sys + +class PersonAuthentication(PersonAuthenticationType): + + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + + def init(self, customScript, configurationAttributes): + print "PingID MFA. init called" + self.configProperties = configurationAttributes + + use_base64_key = self.configProperty("use_base64_key") + self.token = self.configProperty("token") + self.org_alias = self.configProperty("org_alias") + self.authenticator_url = self.configProperty("authenticator_url") + self.pingAttr = self.configProperty("pingUserAttr") + self.addNonExistent = False if self.configProperty("addNonExistentPingUser") == None else True + + self.userMgmntApiHost = self.configProperty("pingUserAPIHost") + if self.userMgmntApiHost == None: + print "PingID MFA. No host for user management API defined. Using a default value" + self.userMgmntApiHost = "https://idpxnyl3m.pingidentity.com" + + if StringHelper.isEmpty(use_base64_key) or StringHelper.isEmpty(self.token) or StringHelper.isEmpty(self.org_alias) \ + or StringHelper.isEmpty(self.authenticator_url) or StringHelper.isEmpty(self.pingAttr): + print "PingID MFA. One or more required Script properties are missing. Check the docs" + return False + + self.secret = Base64.getDecoder().decode(use_base64_key) + + print "PingID MFA. Initialized successfully" + return True + + + def destroy(self, configurationAttributes): + print "PingID MFA. Destroyed called" + return True + + + def getApiVersion(self): + return 11 + + + def getAuthenticationMethodClaims(self, configurationAttributes): + return None + + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + print "PingID MFA. isValidAuthenticationMethod called" + return True + + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + + def authenticate(self, configurationAttributes, requestParameters, step): + print "PingID MFA. authenticate for step %s" % str(step) + + userService = CdiUtil.bean(UserService) + authenticationService = CdiUtil.bean(AuthenticationService) + identity = CdiUtil.bean(Identity) + + try: + if step == 1: + credentials = identity.getCredentials() + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + if StringHelper.isEmptyString(user_name) or StringHelper.isEmptyString(user_password) or \ + not authenticationService.authenticate(user_name, user_password): + return False + + print "PingID MFA. User '%s' has authenticated successfully" % user_name + remote = self.remoteUserId(authenticationService.getAuthenticatedUser()) + + if remote == None: + # Accept the local-only user + identity.setWorkingParameter("singleStep", "yes") + return True + else: + print "PingID MFA. Local user '%s' mapped to remote '%s'" % (user_name, remote) + + client = UserManagerBroker(remote, self.org_alias, self.token, self.secret, self.userMgmntApiHost) + print "PingID MFA. Calling getUserDetails API endpoint" + userJson = client.getUserDetails() + + # If user does not exist at ping side, create it if required + if userJson.isNull("userDetails"): + if self.addNonExistent: + userJson = client.addUser() + else: + # Fail + self.setError("%s is not a PingID user" % remote) + return False + + userJson = userJson.getJSONObject("userDetails") + ndevices = self.devicesCount(userJson) + print "PingID MFA. User has %d devices registered" % ndevices + + if ndevices == 0: + code = self.activationCode(client, userJson.optString("status")) + if code == None: + print "PingID MFA. Unable to get an activation code for pingID user '%s'" % remote + self.setError("We couldn't obtain an activation code for you!") + else: + givenName = userJson.optString("fname", None) + if givenName != None: + identity.setWorkingParameter("givenName", givenName) + + identity.setWorkingParameter("qrCodeRequest", client.getQRCodeLink(str(code))) + + timeout = CdiUtil.bean(AppConfiguration).getSessionIdUnauthenticatedUnusedLifetime() + identity.setWorkingParameter("timeout", timeout - 10) + + return True + else: + self.preparePPMRequest(remote) + return True + + elif step == 2: + + if identity.getWorkingParameter("qrCodeRequest") == None: + # Flow will have only 2 steps in total + return self.processPPMResponse(requestParameters) + + else: + foundUser = authenticationService.getAuthenticatedUser() + if foundUser == None: + # session may have expired + print "PingID MFA. No authenticated user found" + # Avoid generating QRs endlessly from the UI + identity.setWorkingParameter("qrCodeRequest", None) + + return False + + # This should evaluate non null + remote = self.remoteUserId(foundUser) + + client = UserManagerBroker(remote, self.org_alias, self.token, self.secret, self.userMgmntApiHost) + print "PingID MFA. Calling getUserDetails API endpoint" + userJson = client.getUserDetails().getJSONObject("userDetails") + + ndevices = self.devicesCount(userJson) + + if ndevices == 0: + # There should be devices (user is supposed to have enrolled already earlier), + # but he could have simply pressed the continue button without scanning the QR + + print "PingID MFA. Unexpectedly user has no enrolled devices" + # Avoid generating QRs endlessly from the UI + identity.setWorkingParameter("qrCodeRequest", None) + else: + self.preparePPMRequest(remote) + return True + + elif step == 3: + return self.processPPMResponse(requestParameters) + + except TokenProcessingException: + print "PingID MFA. Error in token processing:", sys.exc_info()[1] + except HttpException as e: + if e.getStatusCode() != None: + print "PingID MFA. HTTP status %d" % e.getStatusCode() + if e.getResponse() != None: + print "PingID MFA. HTTP response:", e.getResponse() + print "PingID MFA. HTTP Error:", sys.exc_info()[1] + + # Assume failure by default + return False + + + def prepareForStep(self, configurationAttributes, requestParameters, step): + print "PingID MFA. prepareForStep %d" % step + return True + + + def getExtraParametersForStep(self, configurationAttributes, step): + print "PingID MFA. getExtraParametersForStep %d" % step + list = ArrayList() + if step > 1: + list.addAll(Arrays.asList("givenName", "qrCodeRequest", "timeout")) + list.addAll(Arrays.asList("ppmNonce", "ppmRequest", "issuer", "idpAccountId", "authenticatorUrl")) + return list + + + def getCountAuthenticationSteps(self, configurationAttributes): + print "PingID MFA. getCountAuthenticationSteps called" + identity = CdiUtil.bean(Identity) + if identity.getWorkingParameter("singleStep") != None: + return 1 + else: + return 2 if identity.getWorkingParameter("qrCodeRequest") == None else 3 + + + def getPageForStep(self, configurationAttributes, step): + print "PingID MFA. getPageForStep called %d" % step + + if step == 1: + return "/auth/pingid/login.xhtml" + + elif CdiUtil.bean(Identity).getWorkingParameter("qrCodeRequest") == None: + print "====" + return "/auth/pingid/ppm.xhtml" + + elif step == 2: + return "/auth/pingid/enroll.xhtml" + + else: + return "/auth/pingid/ppm.xhtml" + + + def getNextStep(self, configurationAttributes, requestParameters, step): + print "PingID MFA. getNextStep called %d" % step + return -1 + + + def logout(self, configurationAttributes, requestParameters): + print "PingID MFA. logout called" + return True + +# MISC ROUTINES + + def configProperty(self, name): + prop = self.configProperties.get(name) + return None if prop == None else prop.getValue2() + + def remoteUserId(self, localUser): + # See class org.gluu.persist.model.base.SimpleUser + return localUser.getUserId() if self.pingAttr == "uid" else localUser.getAttribute(self.pingAttr) + + def devicesCount(self, userJson): + devices = userJson.optJSONArray("devicesDetails") + return 0 if devices == None else devices.length() + + + def activationCode(self, client, status): + + code = 0 + print "PingID MFA. User status is '%s'" % status + if status != "SUSPENDED": + + if status != "ACTIVE" and status != 'PENDING_CHANGE_DEVICE': + code = client.activateUser().optLong("activationCode") + + if code == 0: + code = client.getActivationCode().optLong("activationCode") + + return None if code == 0 else code + + + def preparePPMRequest(self, username): + + print "PingID MFA. Preparing a PPM request for web authenticator" + serverName = CdiUtil.bean(FacesContext).getExternalContext().getRequest().getServerName() + returnUrl = "https://%s/oxauth/postlogin.htm" % serverName + + issuer = "gluu" + req = PPMRequestBroker(self.org_alias, self.token, issuer, returnUrl, self.org_alias, self.secret, 120) + req.populate(username) + + identity = CdiUtil.bean(Identity) + identity.setWorkingParameter("ppmNonce", req.getNonce()) + identity.setWorkingParameter("ppmRequest", req.getSignedRequest()) + identity.setWorkingParameter("issuer", issuer) + identity.setWorkingParameter("idpAccountId", self.org_alias) + identity.setWorkingParameter("authenticatorUrl", self.authenticator_url + "/auth") + + + def processPPMResponse(self, requestParameters): + + # Process upcoming data from pingID authenticator web app + encodedRes = ServerUtil.getFirstValue(requestParameters, "ppm_response") + + if StringHelper.isEmpty(encodedRes): + print "PingID MFA. PPM response is empty!" + return False + + parser = ResponseTokenParser(self.org_alias, self.token, self.secret) + json = parser.parse(encodedRes) + success = json.optString("status") == "success" + nonce = json.optString("nonce", None) + + # Sometimes nonce is absent in the PPM response + if success or nonce != None: + if nonce != CdiUtil.bean(Identity).getWorkingParameter("ppmNonce"): + print "PingID MFA. Nonce response validation failed" + return False + + if success: + print "PingID MFA. PPM authentication succeeded" + return True + + else: + error = json.optString("message") + print "PingID MFA. PPM authentication failed with code %s" % json.optString("errorCode") + print "PingID MFA. %s" % error + self.setError(error) + + return False + + + def setError(self, msg): + facesMessages = CdiUtil.bean(FacesMessages) + facesMessages.setKeepMessages() + facesMessages.clear() + facesMessages.add(FacesMessage.SEVERITY_ERROR, msg) + \ No newline at end of file diff --git a/oxAuth/Server/integrations/pingid/pom.xml b/oxAuth/Server/integrations/pingid/pom.xml new file mode 100644 index 00000000..ff90ca53 --- /dev/null +++ b/oxAuth/Server/integrations/pingid/pom.xml @@ -0,0 +1,73 @@ + + + 4.0.0 + org.gluu + oxauth-pingid + 1.0 + jar + + UTF-8 + 1.8 + 1.8 + 2.17.1 + + + + org.gluu + oxauth-model + 4.2.3.Final + + + org.gluu + oxcore-util + 4.2.3.Final + + + org.json + json + 20231013 + + + org.bouncycastle + bcprov-jdk18on + 1.76 + + + org.bouncycastle + bcpkix-jdk18on + 1.76 + + + + + org.apache.logging.log4j + log4j-slf4j-impl + ${log4j.version} + + + org.apache.logging.log4j + log4j-api + ${log4j.version} + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + + + + + org.jboss.resteasy + resteasy-client + 3.13.0.Final + + + + + + gluu + Gluu repository + https://maven.gluu.org/maven + + + diff --git a/oxAuth/Server/integrations/pingid/src/main/java/org/gluu/oxauth/ping/HttpException.java b/oxAuth/Server/integrations/pingid/src/main/java/org/gluu/oxauth/ping/HttpException.java new file mode 100644 index 00000000..38fe3172 --- /dev/null +++ b/oxAuth/Server/integrations/pingid/src/main/java/org/gluu/oxauth/ping/HttpException.java @@ -0,0 +1,31 @@ +package org.gluu.oxauth.ping; + +public class HttpException extends Exception { + + private Integer statusCode; + private String response; + + public HttpException(String message) { + super(message); + } + + public HttpException(String message, Throwable cause, String response) { + super(message, cause); + this.response = response; + } + + public HttpException(Integer statusCode, String message, String response) { + super(message); + this.statusCode = statusCode; + this.response = response; + } + + public Integer getStatusCode() { + return statusCode; + } + + public String getResponse() { + return response; + } + +} diff --git a/oxAuth/Server/integrations/pingid/src/main/java/org/gluu/oxauth/ping/PPMRequestBroker.java b/oxAuth/Server/integrations/pingid/src/main/java/org/gluu/oxauth/ping/PPMRequestBroker.java new file mode 100644 index 00000000..14082bae --- /dev/null +++ b/oxAuth/Server/integrations/pingid/src/main/java/org/gluu/oxauth/ping/PPMRequestBroker.java @@ -0,0 +1,66 @@ +package org.gluu.oxauth.ping; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PPMRequestBroker { + + private static Logger logger = LoggerFactory.getLogger(RequestToken.class); + + private int expiration; + private String nonce; + private byte secretKey[]; + private RequestToken rt; + private JSONObject b; + + public PPMRequestBroker(String orgAlias, String token, String sender, + String returnUrl, String idpAccountId, byte secretKey[], int expiration) { + + nonce = UUID.randomUUID().toString(); + this.secretKey = secretKey; + this.expiration = expiration; + + b = new JSONObject(); + b.put("idpAccountId", idpAccountId); + b.put("iss", sender); + b.put("aud", "pingidauthenticator"); + b.put("returnUrl", returnUrl); + b.put("nonce", nonce); + //b.put("confVersion", 1); + + rt = new RequestToken(false, orgAlias, token); + + } + + public String getNonce() { + return nonce; + } + + public void populate(String username, JSONObject... attributes) { + + b.put("sub", username); + if (attributes != null) { + + List attrs = Arrays.asList(attributes); + if (attrs.size() > 0) { + b.put("attributes", attrs); + } + } + + } + + public String getSignedRequest() throws TokenProcessingException { + long now = System.currentTimeMillis(); + b.put("iat", now); + b.put("exp", now + expiration * 1000); + + rt.setPayload(b); + return rt.getSignedToken(secretKey); + + } + +} diff --git a/oxAuth/Server/integrations/pingid/src/main/java/org/gluu/oxauth/ping/RequestToken.java b/oxAuth/Server/integrations/pingid/src/main/java/org/gluu/oxauth/ping/RequestToken.java new file mode 100644 index 00000000..91920c0d --- /dev/null +++ b/oxAuth/Server/integrations/pingid/src/main/java/org/gluu/oxauth/ping/RequestToken.java @@ -0,0 +1,84 @@ +package org.gluu.oxauth.ping; + +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.Locale; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaims; +import org.gluu.oxauth.model.jwt.JwtHeader; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RequestToken { + + private static final String API_VERSION = "4.9"; + private static final Locale defLocale = Locale.ENGLISH; + + private static Logger logger = LoggerFactory.getLogger(RequestToken.class); + + private String orgAlias; + private String token; + + private Jwt jwt; + private boolean useReqHeader; + + public RequestToken(boolean useReqHeader, String orgAlias, String token) { + + this.useReqHeader = useReqHeader; + this.orgAlias = orgAlias; + this.token = token; + + JwtHeader header = new JwtHeader(); + header.setAlgorithm(Utils.HS256_ALG); + header.setClaim("orgAlias", orgAlias); + header.setClaim("token", token); + + jwt = new Jwt(); + jwt.setHeader(header); + + } + + public RequestToken(String orgAlias, String token) { + this(true, orgAlias, token); + } + + public void setPayload(JSONObject body) { + + if (useReqHeader) { + JwtClaims claims = new JwtClaims(); + claims.setClaim("reqHeader", payloadHeader()); + claims.setClaim("reqBody", body); + jwt.setClaims(claims); + } else { + jwt.setClaims(new JwtClaims(body)); + } + + } + + public String getSignedToken(byte secret[]) throws TokenProcessingException { + + try { + String input = jwt.getSigningInput(); + return input + "." + Utils.generateHS256Signature(input, secret); + } catch (Exception e) { + throw new TokenProcessingException(e); + } + + } + + private JSONObject payloadHeader() { + + String timestamp = DateTimeFormatter.ISO_INSTANT.format(Instant.now()); + JSONObject j = new JSONObject(); + + j.put("locale", defLocale.toString()); + j.put("orgAlias", orgAlias); + j.put("secretKey", token); + j.put("timestamp", timestamp.substring(0, timestamp.length() - 1)); + j.put("version", API_VERSION); + return j; + + } + +} diff --git a/oxAuth/Server/integrations/pingid/src/main/java/org/gluu/oxauth/ping/ResponseTokenParser.java b/oxAuth/Server/integrations/pingid/src/main/java/org/gluu/oxauth/ping/ResponseTokenParser.java new file mode 100644 index 00000000..7795218d --- /dev/null +++ b/oxAuth/Server/integrations/pingid/src/main/java/org/gluu/oxauth/ping/ResponseTokenParser.java @@ -0,0 +1,49 @@ +package org.gluu.oxauth.ping; + +import org.gluu.oxauth.model.jwt.Jwt; +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ResponseTokenParser { + + private static Logger logger = LoggerFactory.getLogger(ResponseTokenParser.class); + + private String orgAlias; + private String token; + private byte secret[]; + + public ResponseTokenParser(String orgAlias, String token, byte secret[]) { + this.orgAlias = orgAlias; + this.token = token; + this.secret = secret; + } + + public JSONObject parse(String token) throws TokenProcessingException { + + try { + Jwt jwt = Jwt.parse(token); + + if (jwt.getEncodedSignature() + .equals(Utils.generateHS256Signature(jwt.getSigningInput(), secret))) { + return jwt.getClaims().toJsonObject(); + } + } catch (Exception e) { + throw new TokenProcessingException(e); + } + throw new TokenProcessingException(new Exception("Signature validation failed")); + + } + + public JSONObject parseKey(String token, String key) throws TokenProcessingException { + + try { + return parse(token).getJSONObject(key); + } catch (JSONException e) { + throw new TokenProcessingException(e); + } + + } + +} diff --git a/oxAuth/Server/integrations/pingid/src/main/java/org/gluu/oxauth/ping/TokenProcessingException.java b/oxAuth/Server/integrations/pingid/src/main/java/org/gluu/oxauth/ping/TokenProcessingException.java new file mode 100644 index 00000000..b1981884 --- /dev/null +++ b/oxAuth/Server/integrations/pingid/src/main/java/org/gluu/oxauth/ping/TokenProcessingException.java @@ -0,0 +1,9 @@ +package org.gluu.oxauth.ping; + +public class TokenProcessingException extends Exception { + + public TokenProcessingException(Throwable cause) { + super(cause); + } + +} diff --git a/oxAuth/Server/integrations/pingid/src/main/java/org/gluu/oxauth/ping/UserManagerBroker.java b/oxAuth/Server/integrations/pingid/src/main/java/org/gluu/oxauth/ping/UserManagerBroker.java new file mode 100644 index 00000000..813087a8 --- /dev/null +++ b/oxAuth/Server/integrations/pingid/src/main/java/org/gluu/oxauth/ping/UserManagerBroker.java @@ -0,0 +1,166 @@ +package org.gluu.oxauth.ping; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class UserManagerBroker { + + enum Endpoint { + ADD_USER("/pingid/rest/4/adduser/do"), + ACTIVATE_USER("/pingid/rest/4/activateuser/do"), + GET_ACTIVATION_CODE("/pingid/rest/4/getactivationcode/do"), + GET_USER_DETAILS("/pingid/rest/4/getuserdetails/do"), + DELETE_USER("/pingid/rest/4/deleteuser/do"), + START_AUTHENTICATION("/pingid/rest/4/startauthentication/do"), + AUTHENTICATE_ONLINE("/pingid/rest/4/authonline/do"); + + private String relativeUrl; + + Endpoint(String url) { + this.relativeUrl = url; + } + + String getRelativeUrl() { + return relativeUrl; + } + + } + + private static Logger logger = LoggerFactory.getLogger(UserManagerBroker.class); + private static final String QRCODE_URL = "/pingid/QRRedirection"; + + private ResponseTokenParser tokenParser; + + private String userName; + private String orgAlias; + private String token; + private byte secret[]; + private String userManagementApiHost; + + public UserManagerBroker(String userName, String orgAlias, String token, + byte secret[], String userManagementApiHost) { + this.userName = userName; + this.orgAlias = orgAlias; + this.token = token; + this.secret = secret; + this.userManagementApiHost = userManagementApiHost; + + tokenParser = new ResponseTokenParser(orgAlias, token, secret); + } + + private String getEndpointUrl(Endpoint endpoint) { + return userManagementApiHost + endpoint.getRelativeUrl(); + } + + private String signedJWT(JSONObject body) throws TokenProcessingException { + RequestToken tok = new RequestToken(orgAlias, token); + tok.setPayload(body); + return tok.getSignedToken(secret); + } + + private String signedJwtForActivateUser() throws TokenProcessingException { + + JSONObject body = new JSONObject(); + body.put("deviceType", "MOBILE"); + body.put("userName", userName); + + return signedJWT(body); + + } + + private JSONObject getResponseBody(String endpoint, String jwt) + throws HttpException, TokenProcessingException { + return tokenParser.parseKey(Utils.post(endpoint, jwt), "responseBody"); + } + + public JSONObject activateUser() throws HttpException, TokenProcessingException { + String jwt = signedJwtForActivateUser(); + return getResponseBody(getEndpointUrl(Endpoint.ACTIVATE_USER), jwt); + } + + public JSONObject addUser() throws HttpException, TokenProcessingException { + + JSONObject body = new JSONObject(); + body.put("deviceType", "MOBILE"); + body.put("userName", userName); + body.put("role", "REGULAR"); + + String jwt = signedJWT(body); + return getResponseBody(getEndpointUrl(Endpoint.ADD_USER), jwt); + + } + + public JSONObject deleteUser() throws HttpException, TokenProcessingException { + + JSONObject body = new JSONObject(); + body.put("userName", userName); + + String jwt = signedJWT(body); + return getResponseBody(getEndpointUrl(Endpoint.DELETE_USER), jwt); + + } + + public JSONObject getActivationCode() throws HttpException, TokenProcessingException { + //Both activateUser and this API operation have the same parameters + String jwt = signedJwtForActivateUser(); + return getResponseBody(getEndpointUrl(Endpoint.GET_ACTIVATION_CODE), jwt); + } + + public JSONObject getUserDetails() throws HttpException, TokenProcessingException { + + JSONObject body = new JSONObject(); + body.put("getSameDeviceUsers", false); + body.put("userName", userName); + + String jwt = signedJWT(body); + try { + return getResponseBody(getEndpointUrl(Endpoint.GET_USER_DETAILS), jwt); + } catch (HttpException e) { + //Mask the "not found" error + JSONObject errBody = tokenParser.parseKey(e.getResponse(), "responseBody"); + //See ping ID API error codes + if (errBody.getInt("errorId") == 10564) { + return errBody; + } else { + throw e; + } + } + + } + + public JSONObject startAuthentication(long deviceId) throws Exception { + + JSONObject body = new JSONObject(); + body.put("spAlias", "web"); + body.put("userName", userName); + body.put("deviceId", deviceId); + + String jwt = signedJWT(body); + return getResponseBody(getEndpointUrl(Endpoint.START_AUTHENTICATION), jwt); + + } + + public JSONObject authenticateOnline() throws Exception { + + JSONObject body = new JSONObject(); + body.put("spAlias", "web"); + body.put("userName", userName); + body.put("authType", "CONFIRM"); + + String jwt = signedJWT(body); + return getResponseBody(getEndpointUrl(Endpoint.AUTHENTICATE_ONLINE), jwt); + + } + + public String getQRCodeLink(String activationCode) { + String tmp = "act_code=" + activationCode; + byte bytes[] = Base64.getEncoder().encode(tmp.getBytes()); + + return userManagementApiHost + QRCODE_URL + "?" + + new String(bytes, StandardCharsets.UTF_8); + } + +} diff --git a/oxAuth/Server/integrations/pingid/src/main/java/org/gluu/oxauth/ping/Utils.java b/oxAuth/Server/integrations/pingid/src/main/java/org/gluu/oxauth/ping/Utils.java new file mode 100644 index 00000000..5a2ecf29 --- /dev/null +++ b/oxAuth/Server/integrations/pingid/src/main/java/org/gluu/oxauth/ping/Utils.java @@ -0,0 +1,96 @@ +package org.gluu.oxauth.ping; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Response; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.HttpClient; +import org.apache.http.HttpHost; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.util.Base64Util; +import org.jboss.resteasy.client.jaxrs.ResteasyClient; +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; +import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget; +import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient4Engine; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Utils { + + private static Logger logger = LoggerFactory.getLogger(Utils.class); + + public static final SignatureAlgorithm HS256_ALG = SignatureAlgorithm.HS256; + public static final String HMAC_SHA256_ALG_NAME = "HmacSHA256"; + + public static ResteasyClient rsClient; + + static { + PoolingHttpClientConnectionManager manager = new PoolingHttpClientConnectionManager(); + manager.setMaxTotal(200); + manager.setDefaultMaxPerRoute(20); + + String proxyHost = System.getProperty("https.proxyHost"); + String proxyPort = System.getProperty("https.proxyPort"); + RequestConfig.Builder configBuilder = RequestConfig.custom().setConnectTimeout(10 * 1000); + + if (StringUtils.isNotEmpty(proxyHost) && StringUtils.isNotEmpty(proxyPort)) { + String scheme = System.getProperty("https.proxyScheme", "http"); + logger.debug("Using https proxy {}://{}:{}", scheme, proxyHost, proxyPort); + + HttpHost proxy = new HttpHost(proxyHost, Integer.valueOf(proxyPort), scheme); + configBuilder.setProxy(proxy); + } + + HttpClient httpClient = HttpClientBuilder.create() + .setDefaultRequestConfig(configBuilder.build()) + .setConnectionManager(manager).build(); + + ApacheHttpClient4Engine engine = new ApacheHttpClient4Engine(httpClient); + rsClient = new ResteasyClientBuilder().httpEngine(engine).build(); + } + + public static String generateHS256Signature(String input, byte secret[]) + throws NoSuchAlgorithmException, InvalidKeyException { + + SecretKey secretKey = new SecretKeySpec(secret, HMAC_SHA256_ALG_NAME); + Mac mac = Mac.getInstance(HMAC_SHA256_ALG_NAME); + mac.init(secretKey); + + byte[] sig = mac.doFinal(input.getBytes()); + return Base64Util.base64urlencode(sig); + + } + + public static String post(String endpoint, String payload) throws HttpException { + + String data = null; + try { + ResteasyWebTarget target = rsClient.target(endpoint); + logger.info("Sending payload to {}", endpoint); + logger.debug("{}", payload); + + Response response = target.request().post(Entity.json(payload)); + response.bufferEntity(); + int status = response.getStatus(); + data = response.readEntity(String.class); + + logger.debug("Response code was {} and body:\n{}", status, data); + if (status == 200) { + return data; + } else { + throw new HttpException(status, "Unsuccessful response obtained", data); + } + } catch (Exception e) { + throw new HttpException(e.getMessage(), e.getCause(), data); + } + + } + +} diff --git a/oxAuth/Server/integrations/postauthn/postauthn.py b/oxAuth/Server/integrations/postauthn/postauthn.py new file mode 100644 index 00000000..cc1fff56 --- /dev/null +++ b/oxAuth/Server/integrations/postauthn/postauthn.py @@ -0,0 +1,42 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2020, Gluu +# +# Author: Yuriy Zabrovarnyy +# +# + +from org.gluu.model.custom.script.type.postauthn import PostAuthnType +from java.lang import String + +class PostAuthn(PostAuthnType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Post Authn script. Initializing ..." + print "Post Authn script. Initialized successfully" + + return True + + def destroy(self, configurationAttributes): + print "Post Authn script. Destroying ..." + print "Post Authn script. Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + # This method is called during Authorization Request at Authorization Endpoint. + # If True is returned, session is set as unauthenticated and user is send for authentication. + # Note : + # context is reference of org.gluu.oxauth.service.external.context.ExternalPostAuthnContext(in https://github.com/GluuFederation/oxauth project, ) + def forceReAuthentication(self, context): + return False + + # This method is called during Authorization Request at Authorization Endpoint. + # If True is returned user is send for Authorization. By default if client is "Pre-Authorized" or "Client Persist Authorizations" is on, authorization is skipped. + # This script has higher priority and can cancel Pre-Authorization and persisted authorizations. + # Note : + # context is reference of org.gluu.oxauth.service.external.context.ExternalPostAuthnContext(in https://github.com/GluuFederation/oxauth project, ) + def forceAuthorization(self, context): + return False \ No newline at end of file diff --git a/oxAuth/Server/integrations/privacyidea/README.md b/oxAuth/Server/integrations/privacyidea/README.md new file mode 100644 index 00000000..618673e4 --- /dev/null +++ b/oxAuth/Server/integrations/privacyidea/README.md @@ -0,0 +1,36 @@ +This is the authentication script to authenticate Gluu against privacyIDEA. + +# Setup + +* Download the jar-with-dependencies from [here](https://github.com/privacyidea/sdk-java/releases). +* Change the name to ``java_sdk.jar`` and put it in ``/opt/gluu-server/opt``. +* Alternatively put the file under any name anywhere in ``/opt/gluu-server/`` and configure the path later. + +# Configuration + +* Create a new Person Authentication script, choose file and enter the path to the ``.py`` file like explained above or choose database and paste its contents. + +* Add a new attribute with the key ``privacyidea_url`` and the url to the privacyIDEA Server as value. + +* If the java sdk is not in the above mentioned default location, add the key ``sdk_path`` with the path to the file including its compelete name as value. + +#### The following keys are optional (case sensitive!): + +* ``realm`` specify a realm that will be appended to each request. +* ``sslverify`` set to ``0`` to disable peer verification. +* ``log_from_sdk`` with any value: enable the logging of the jar. + +By default, the password from the first step is verified by the Gluu server and the OTP from the second step is sent to and verified by privacyIDEA. +To use challenge-reponse type token, use the following configuration options: +* ``sendpassword`` set to ``1`` if the password and username from the first step should be sent to the privacyIDEA server. This setting takes precedence over ``triggerchallenge``. +* ``triggerchallenge`` set to ``1`` if challenges for the user should be triggered using the service account. +* ``serviceaccountname`` name of the service account to trigger challenges with. +* ``serviceaccountpass`` password of the service account to trigger challenges with. +* ``serviceaccountrealm`` optionally set the realm in which the service account can be found if it is different from the ``realm`` or default realm. +* ``disablegluupass`` set to ``1`` to disable the password verification of the Gluu server. This can be useful if the password should be verified by privacyIDEA in conjunction with the ``sendpassword`` setting. + +* **After finishing the configuration, change the default authentication method to the Person Authentication script you just created.** + +#### Logfile + +* The logfile for scripts is located at ``/opt/gluu-server/opt/gluu/jetty/oxauth/logs/oxauth_script.log``. diff --git a/oxAuth/Server/integrations/privacyidea/pages/auth/privacyidea/privacyidea.xhtml b/oxAuth/Server/integrations/privacyidea/pages/auth/privacyidea/privacyidea.xhtml new file mode 100644 index 00000000..ff2f38f9 --- /dev/null +++ b/oxAuth/Server/integrations/privacyidea/pages/auth/privacyidea/privacyidea.xhtml @@ -0,0 +1,98 @@ + + + + + + + + + + + +
+ + + +
+ +
+
\ No newline at end of file diff --git a/oxAuth/Server/integrations/privacyidea/privacyidea.py b/oxAuth/Server/integrations/privacyidea/privacyidea.py new file mode 100644 index 00000000..67419c06 --- /dev/null +++ b/oxAuth/Server/integrations/privacyidea/privacyidea.py @@ -0,0 +1,338 @@ +# -*- coding: utf-8 -*- +# +# privacyIDEA is a Multi-Factor-Management system that supports +# a wide variety of different tokentypes like smartphone apps, key fob tokens, +# yubikeys, u2f, fido2, email, sms... +# The administrator of an organization can manage the 2nd factors of the +# users centrally in privacyIDEA and connect any application with privacyIDEA +# to secure the login process. +# +# This authentication script adds a most flexible multi-factor-authentication +# to Gluu. See: +# https://privacyidea.org +# Get enterprise support at: +# https://netknights.it/en/produkte/privacyidea/ +# +# License: AGPLv3 +# +# This code is free software; you can redistribute it and/or +# modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE +# License as published by the Free Software Foundation; either +# version 3 of the License, or any later version. +# +# This code is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU AFFERO GENERAL PUBLIC LICENSE for more details. +# +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see . +# +# +__doc__ = """This script enables Gluu to communicate to privacyIDEA server +and have the privacyIDEA verify the second factor. +""" + +__version__ = "1.0.0" + +from org.gluu.oxauth.security import Identity +from org.gluu.oxauth.service import AuthenticationService, SessionIdService +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.service import UserService +from org.gluu.util import StringHelper + +from javax.faces.application import FacesMessage +from org.gluu.jsf2.message import FacesMessages + +from java.util import Arrays +import sys + +PI_USER_AGENT = "privacyIDEA-Gluu" +GLUU_API_VERSION = 11 + + +def logFromSDK(message): + print("privacyIDEA. JavaSDK: " + message) + +class PersonAuthentication(PersonAuthenticationType): + + def __init__(self, currentTimeMillis): + print("__init__") + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print("privacyIDEA. init") + self.pi = None + + sdk_path = "/opt/java_sdk.jar" + if configurationAttributes.containsKey("sdk_path"): + sdk_path = configurationAttributes.get("sdk_path").getValue2() + + sys.path.append(sdk_path) + try: + from org.privacyidea import Challenge + from org.privacyidea import PrivacyIDEA + from org.privacyidea import PIResponse + except ImportError: + print("privacyIDEA. Java SDK import not found! Make sure the jar is located at '{}'.".format(sdk_path)) + # returning success here allows to display a error message in the authenticate function + # because self.pi will be None + return True + + if not configurationAttributes.containsKey("privacyidea_url"): + print("privacyIDEA. Missing mandatory configuration value 'privacyidea_url'!") + return True + + privacyidea_url = configurationAttributes.get("privacyidea_url").getValue2() + + builder = PrivacyIDEA.Builder(privacyidea_url, PI_USER_AGENT) + + if configurationAttributes.containsKey("log_from_sdk"): + builder.setSimpleLog(logFromSDK) + + if configurationAttributes.containsKey("sslverify"): + sslverify = configurationAttributes.get("sslverify").getValue2() + builder.setSSLVerify(sslverify != "0") + + if configurationAttributes.containsKey("realm"): + realm = configurationAttributes.get("realm").getValue2() + builder.setRealm(realm) + else: + print("privacyIDEA. Config param 'realm' not set") + + self.disableGluuPass = False + if configurationAttributes.containsKey("disablegluupass"): + self.disableGluuPass = configurationAttributes.get("disablegluupass").getValue2() == "1" + + # Load configuration for optional trigger challenge or send password in first step + self.sendPassword = False + self.triggerChallenge = False + serviceAccountName = None + serviceAccountPass = None + serviceAccountRealm = None + + if configurationAttributes.containsKey("triggerchallenges"): + self.triggerChallenge = configurationAttributes.get("triggerchallenges").getValue2() == "1" + + if configurationAttributes.containsKey("sendpassword"): + self.sendPassword = configurationAttributes.get("sendpassword").getValue2() == "1" + + if configurationAttributes.containsKey("serviceaccountname"): + serviceAccountName = configurationAttributes.get("serviceaccountname").getValue2() + + if configurationAttributes.containsKey("serviceaccountpass"): + serviceAccountPass = configurationAttributes.get("serviceaccountpass").getValue2() + + if serviceAccountName is not None and serviceAccountPass is not None and self.triggerChallenge: + builder.setServiceAccount(serviceAccountName, serviceAccountPass) + elif self.triggerChallenge: + print("Trigger challenge enabled but no service account set!") + self.triggerChallenge = False + + if configurationAttributes.containsKey("serviceaccountrealm"): + serviceAccountRealm = configurationAttributes.get("serviceaccountrealm").getValue2() + builder.setServiceAccountRealm(serviceAccountRealm) + + self.pi = builder.build() + self.sessionIdservice = CdiUtil.bean(SessionIdService) + + print("privacyIDEA. init done") + return True + + def authenticate(self, configurationAttributes, requestParameters, step): + #print("Authenticate step={} with sendpass={} and triggerchallenge={}".format(step, self.sendPassword, self.triggerChallenge)) + fm = CdiUtil.bean(FacesMessages) + fm.clear() + fm.setKeepMessages() + + if self.pi is None: + fm.add(FacesMessage.SEVERITY_ERROR, "Failed to communicate to privacyIDEA. Possible misconfiguration. Please have the administrator check the log files.") + return False + + identity = CdiUtil.bean(Identity) + + if step == 1: + credentials = identity.getCredentials() + username = credentials.getUsername() + password = credentials.getPassword() + + if username: + if self.sendPassword: + response = self.pi.validateCheck(username, password) + if response: + # First check if what was entered is sufficient to log in + if response.getValue(): + logged_in = self.login(username, password) + if logged_in: + self.addToSession("auth_success", True) + return logged_in + # If not, check if transaction was triggered + elif response.getTransactionID(): + self.evaluateTriggeredChallenges(response, identity) + else: + print("privacyIDEA. Empty response from server") + fm.add(FacesMessage.SEVERITY_ERROR, "No response from the privacyIDEA Server. Please check the connection!") + + elif self.triggerChallenge: + response = self.pi.triggerChallenges(username) + if response is None: + fm.add(FacesMessage.SEVERITY_ERROR, "Failed to communicate to privacyIDEA. Possible misconfiguration. Please have the administrator check the log files.") + print("Service account misconfiguration or no response from the privacyIDEA server!") + elif response.getTransactionID(): + self.evaluateTriggeredChallenges(response, identity) + else: + # Setup for just OTP in second step + identity.setWorkingParameter("otp_available", "1") + identity.setWorkingParameter("transaction_message", "Please enter the OTP:") + + self.addToSession("currentUser", username) + self.addToSession("currentPassword", password) + + return True + + else: + #print("privacyIDEA. Username is empty") + fm.add(FacesMessage.SEVERITY_ERROR, "Please enter a username!") + + return False + else: + # Get the user from step 1 + currentUser = self.getFromSession("currentUser") + currentPassword = self.getFromSession("currentPassword") + + if currentUser and currentPassword: + self.login(currentUser, currentPassword) + else: + print("privacyIDEA. No user found in session for second step") + fm.add(FacesMessage.SEVERITY_ERROR, "Session data got lost. Please try to restart the authentication!") + return False + + try: + # Persist the mode between the script and the js + mode = requestParameters.get("modeField")[0].strip() + identity.setWorkingParameter("mode", mode) + except TypeError: + print("privacyIDEA. Mode not found in request parameters") + + txid = self.getFromSession("transaction_id") + + # If mode is push: poll for the transactionID to see if the user confirmed on the smartphone + if mode == "push": + if not txid: + print("privacyIDEA. Transaction ID not found in session, but it is mandatory for polling!") + fm.add(FacesMessage.SEVERITY_ERROR, "Your transaction id could not be found. Please try to restart the authentication!") + return False + + if self.pi.pollTransaction(txid): + # If polling is successful, the authentication has to be finished by a call to validateCheck + # with the username, NO otp and the transactionID + response = self.pi.validateCheck(currentUser, "", txid) + return response.getValue() + + elif mode == "otp": + try: + otp = requestParameters.get("otp")[0] + except TypeError: + print("privacyIDEA. Unable to obtain OTP from requestParameters, but it is required!") + fm.add(FacesMessage.SEVERITY_ERROR, "Your input could not be read. Please try to restart the authentication!") + + if otp: + # Either do validate/check with transaction id if there is one in the session or just with the input + if txid: + resp = self.pi.validateCheck(currentUser, otp, txid) + else: + resp = self.pi.validateCheck(currentUser, otp) + + if resp: + return resp.getValue() + return False + + def evaluateTriggeredChallenges(self, response, identity): + identity.setWorkingParameter("transaction_message", response.getMessage()) + self.addToSession("transaction_id", response.getTransactionID()) + + # Check if push is available + tttList = response.getTriggeredTokenTypes() + if tttList.contains("push"): + identity.setWorkingParameter("push_available", "1") + tttList.remove("push") + + # Check if an input field is needed for any other token type + if tttList.size() > 0: + identity.setWorkingParameter("otp_available", "1") + + def login(self, username, password): + #print("Login with user={} and pass={}, verifyGluuPassword={}".format(username, password, self.verifyGluuPassword)) + authenticationService = CdiUtil.bean(AuthenticationService) + if not self.disableGluuPass: + logged_in = authenticationService.authenticate(username, password) + else: + logged_in = authenticationService.authenticate(username) + + return logged_in + + def addToSession(self, key, value): + #print("addToSession: {}, {}".format(key, value)) + session = self.sessionIdservice.getSessionId() + session.getSessionAttributes().put(key, value) + self.sessionIdservice.updateSessionId(session) + + def getFromSession(self, key): + #print("getFromSession: {}".format(key)) + session = self.sessionIdservice.getSessionId() + return session.getSessionAttributes().get(key) if session else None + + def prepareForStep(self, configurationAttributes, requestParameters, step): + #print("prepareForStep {}, params={}".format(step, requestParameters)) + if step == 1: return True + + # Set the initial state for our template + identity = CdiUtil.bean(Identity) + identity.setWorkingParameter("mode", "otp") + return True + + def getExtraParametersForStep(self, configurationAttributes, step): + #print("getExtraParametersForStep {}".format(step)) + return Arrays.asList("transaction_message", "push_available", "otp_available", "mode") + + def getCountAuthenticationSteps(self, configurationAttributes): + #print("getCountAuthenticationSteps") + if self.getFromSession("auth_success"): + #print("Auth success in session, returning 1") + return 1 + else: + #print("Auth success not in session, returning 2") + return 2 + + def getPageForStep(self, configurationAttributes, step): + #print("getPageForStep {}".format(step)) + return "" if step == 1 else "/auth/privacyidea/privacyidea.xhtml" + + def getNextStep(self, configurationAttributes, requestParameters, step): + #print("getNextStep {}".format(step)) + return -1 + + def destroy(self, configurationAttributes): + #print("destroy") + return True + + def getApiVersion(self): + #print("getApiVersion = {}".format(SCRIPT_API_VERSION)) + return GLUU_API_VERSION + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + #print("isValidAuthenticationMethod") + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + #print("getAlternativeAuthenticationMethod") + return None + + def getAuthenticationMethodClaims(self, requestParameters): + #print("getAuthenticationMethodClaims") + return None + + def logout(self, configurationAttributes, requestParameters): + #print("logout") + return True diff --git a/oxAuth/Server/integrations/registration/read.txt b/oxAuth/Server/integrations/registration/read.txt new file mode 100644 index 00000000..d3b048de --- /dev/null +++ b/oxAuth/Server/integrations/registration/read.txt @@ -0,0 +1,6 @@ +This is a person authentication module for oxAuth that allows user to register first and login to server. + +Required Custom property (key/value) - +1) generic_register_attributes_list = email,fname,lname,email,phone,pwd +2) generic_local_attributes_list = uid,givenName,sn,mail,telephoneNumber,userPassword + diff --git a/oxAuth/Server/integrations/registration/register.py b/oxAuth/Server/integrations/registration/register.py new file mode 100644 index 00000000..5c65e4c5 --- /dev/null +++ b/oxAuth/Server/integrations/registration/register.py @@ -0,0 +1,182 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2016, Gluu +# +# Author: Yuriy Movchan +# + +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.jsf2.message import FacesMessages +from javax.faces.application import FacesMessage +from org.gluu.util import StringHelper, ArrayHelper +from java.util import Arrays, ArrayList, HashMap, IdentityHashMap +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.service import UserService, ClientService, AuthenticationService +from org.gluu.util import StringHelper +from org.gluu.oxauth.model.common import User +from org.gluu.oxauth.util import ServerUtil +from org.gluu.jsf2.service import FacesService +from org.gluu.oxauth.model.util import Base64Util +from org.python.core.util import StringUtil +from org.gluu.oxauth.service.net import HttpService +from javax.faces.context import FacesContext + +import java + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Registration. Initialization" + if (configurationAttributes.containsKey("generic_register_attributes_list") and + configurationAttributes.containsKey("generic_local_attributes_list")): + + remoteAttributesList = configurationAttributes.get("generic_register_attributes_list").getValue2() + if (StringHelper.isEmpty(remoteAttributesList)): + print "Registration: Initialization. The property generic_register_attributes_list is empty" + return False + + localAttributesList = configurationAttributes.get("generic_local_attributes_list").getValue2() + if (StringHelper.isEmpty(localAttributesList)): + print "Registration: Initialization. The property generic_local_attributes_list is empty" + return False + + self.attributesMapping = self.prepareAttributesMapping(remoteAttributesList, localAttributesList) + if (self.attributesMapping == None): + print "Registration: Initialization. The attributes mapping isn't valid" + return False + + print "Registration. Initialized successfully" + + return True + + + def destroy(self, configurationAttributes): + print "Registration. Destroy" + print "Registration. Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + def getUserValueFromAuth(self, remote_attr, requestParameters): + try: + toBeFeatched = "loginForm:" + remote_attr + return ServerUtil.getFirstValue(requestParameters, toBeFeatched) + except Exception, err: + print("Registration: Exception inside getUserValueFromAuth " + str(err)) + + def authenticate(self, configurationAttributes, requestParameters, step): + print "Registration. Authenticate for step 1" + userService = CdiUtil.bean(UserService) + authenticationService = CdiUtil.bean(AuthenticationService) + + if (StringHelper.isEmptyString(self.getUserValueFromAuth("email", requestParameters))): + facesMessages = CdiUtil.bean(FacesMessages) + facesMessages.setKeepMessages() + facesMessages.add(FacesMessage.SEVERITY_ERROR, "Please provide your email.") + return False + + if (StringHelper.isEmptyString(self.getUserValueFromAuth("pwd", requestParameters))): + facesMessages = CdiUtil.bean(FacesMessages) + facesMessages.setKeepMessages() + facesMessages.add(FacesMessage.SEVERITY_ERROR, "Please provide password.") + return False + + + + foundUser = userService.getUserByAttribute("mail", self.getUserValueFromAuth("email", requestParameters)) + if (foundUser == None): + newUser = User() + for attributesMappingEntry in self.attributesMapping.entrySet(): + remoteAttribute = attributesMappingEntry.getKey() + localAttribute = attributesMappingEntry.getValue() + localAttributeValue = self.getUserValueFromAuth(remoteAttribute, requestParameters) + if ((localAttribute != None) & (localAttributeValue != "undefined")): + print localAttribute + localAttributeValue + newUser.setAttribute(localAttribute, localAttributeValue) + + try: + foundUser = userService.addUser(newUser, True) + foundUserName = foundUser.getUserId() + print("Registration: Found user name " + foundUserName) + userAuthenticated = authenticationService.authenticate(foundUserName) + print("Registration: User added successfully and isUserAuthenticated = " + str(userAuthenticated)) + except Exception, err: + print("Registration: Error in adding user:" + str(err)) + return False + return userAuthenticated + else: + facesMessages = CdiUtil.bean(FacesMessages) + facesMessages.setKeepMessages() + facesMessages.add(FacesMessage.SEVERITY_ERROR, "User with same email already exists!") + return False + + + + def prepareForStep(self, configurationAttributes, requestParameters, step): + if (step == 1): + print "Registration. Prepare for Step 1" + return True + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + return 1 + + def getPageForStep(self, configurationAttributes, step): + if step == 1: + return "/auth/register/register.xhtml" + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True + + def prepareAttributesMapping(self, remoteAttributesList, localAttributesList): + try: + remoteAttributesListArray = StringHelper.split(remoteAttributesList, ",") + if (ArrayHelper.isEmpty(remoteAttributesListArray)): + print("Registration: PrepareAttributesMapping. There is no attributes specified in remoteAttributesList property") + return None + + localAttributesListArray = StringHelper.split(localAttributesList, ",") + if (ArrayHelper.isEmpty(localAttributesListArray)): + print("Registration: PrepareAttributesMapping. There is no attributes specified in localAttributesList property") + return None + + if (len(remoteAttributesListArray) != len(localAttributesListArray)): + print("Registration: PrepareAttributesMapping. The number of attributes in remoteAttributesList and localAttributesList isn't equal") + return None + + attributeMapping = IdentityHashMap() + containsUid = False + i = 0 + count = len(remoteAttributesListArray) + while (i < count): + remoteAttribute = StringHelper.toLowerCase(remoteAttributesListArray[i]) + localAttribute = StringHelper.toLowerCase(localAttributesListArray[i]) + attributeMapping.put(remoteAttribute, localAttribute) + + i = i + 1 + + return attributeMapping + except Exception, err: + print("Registration: Exception inside prepareAttributesMapping " + str(err)) diff --git a/oxAuth/Server/integrations/saml-passport/SamlPassportAuthenticator.py b/oxAuth/Server/integrations/saml-passport/SamlPassportAuthenticator.py new file mode 100644 index 00000000..63eb749b --- /dev/null +++ b/oxAuth/Server/integrations/saml-passport/SamlPassportAuthenticator.py @@ -0,0 +1,827 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2019, Gluu +# +# Author: Jose Gonzalez +# Author: Yuriy Movchan +# Author: Christian Eland +# + +from org.gluu.jsf2.service import FacesService +from org.gluu.jsf2.message import FacesMessages + +from org.gluu.oxauth.model.common import User, WebKeyStorage +from org.gluu.oxauth.model.configuration import AppConfiguration +from org.gluu.oxauth.model.crypto import CryptoProviderFactory +from org.gluu.oxauth.model.jwt import Jwt, JwtClaimName +from org.gluu.oxauth.model.util import Base64Util +from org.gluu.oxauth.service import AppInitializer, AuthenticationService +from org.gluu.oxauth.service.common import UserService, EncryptionService +from org.gluu.oxauth.model.authorize import AuthorizeRequestParam +from org.gluu.oxauth.service.net import HttpService +from org.gluu.oxauth.security import Identity +from org.gluu.oxauth.util import ServerUtil +from org.gluu.config.oxtrust import LdapOxPassportConfiguration +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.persist import PersistenceEntryManager +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.util import StringHelper +from java.util import ArrayList, Arrays, Collections, HashSet +from org.gluu.oxauth.model.exception import InvalidJwtException +from javax.faces.application import FacesMessage +from javax.faces.context import FacesContext + +import json +import sys +import datetime +import base64 + + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + + print "Passport. init called" + + self.extensionModule = self.loadExternalModule(configurationAttributes.get("extension_module")) + extensionResult = self.extensionInit(configurationAttributes) + if extensionResult != None: + return extensionResult + + print "Passport. init. Behaviour is inbound SAML" + success = self.processKeyStoreProperties(configurationAttributes) + + if success: + self.providerKey = "provider" + self.customAuthzParameter = self.getCustomAuthzParameter(configurationAttributes.get("authz_req_param_provider")) + self.passportDN = self.getPassportConfigDN() + print "Passport. init. Initialization success" + else: + print "Passport. init. Initialization failed" + return success + + + def destroy(self, configurationAttributes): + print "Passport. destroy called" + return True + + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + + def authenticate(self, configurationAttributes, requestParameters, step): + + extensionResult = self.extensionAuthenticate(configurationAttributes, requestParameters, step) + if extensionResult != None: + return extensionResult + + print "Passport. authenticate for step %s called" % str(step) + identity = CdiUtil.bean(Identity) + + # Loading self.registeredProviders in case passport destroyed + if not hasattr(self,'registeredProviders'): + print "Passport. Fetching registered providers." + self.parseProviderConfigs() + + if step == 1: + + jwt_param = None + + if self.isInboundFlow(identity): + # if is idp-initiated inbound flow + print "Passport. authenticate for step 1. Detected idp-initiated inbound Saml flow" + # get request from session attributes + jwt_param = identity.getSessionId().getSessionAttributes().get(AuthorizeRequestParam.STATE) + print "jwt_param = %s" % jwt_param + # now jwt_param != None + + + + if jwt_param == None: + # gets jwt parameter "user" sent after authentication by passport (if exists) + jwt_param = ServerUtil.getFirstValue(requestParameters, "user") + + + if jwt_param != None: + # and now that the jwt_param user exists... + print "Passport. authenticate for step 1. JWT user profile token found" + + if self.isInboundFlow(identity): + jwt_param = base64.urlsafe_b64decode(str(jwt_param+'==')) + + # Parse JWT and validate + jwt = Jwt.parse(jwt_param) + + if not self.validSignature(jwt): + return False + + if self.jwtHasExpired(jwt): + return False + + # Gets user profile as string and json using the information on JWT + (user_profile, jsonp) = self.getUserProfile(jwt) + + if user_profile == None: + return False + + sessionAttributes = identity.getSessionId().getSessionAttributes() + self.skipProfileUpdate = StringHelper.equalsIgnoreCase(sessionAttributes.get("skipPassportProfileUpdate"), "true") + + return self.attemptAuthentication(identity, user_profile, jsonp) + + #See passportlogin.xhtml + provider = ServerUtil.getFirstValue(requestParameters, "loginForm:provider") + + if StringHelper.isEmpty(provider): + + #it's username + passw auth + print "Passport. authenticate for step 1. Basic authentication detected" + logged_in = False + + credentials = identity.getCredentials() + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + if StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password): + authenticationService = CdiUtil.bean(AuthenticationService) + logged_in = authenticationService.authenticate(user_name, user_password) + + print "Passport. authenticate for step 1. Basic authentication returned: %s" % logged_in + return logged_in + + + + elif provider in self.registeredProviders: + # user selected provider + # it's a recognized external IDP + + identity.setWorkingParameter("selectedProvider", provider) + print "Passport. authenticate for step 1. Retrying step 1" + + #see prepareForStep (step = 1) + return True + + if step == 2: + mail = ServerUtil.getFirstValue(requestParameters, "loginForm:email") + jsonp = identity.getWorkingParameter("passport_user_profile") + + if mail == None: + self.setMessageError(FacesMessage.SEVERITY_ERROR, "Email was missing in user profile") + elif jsonp != None: + # Completion of profile takes place + user_profile = json.loads(jsonp) + user_profile["mail"] = [ mail ] + + return self.attemptAuthentication(identity, user_profile, jsonp) + + print "Passport. authenticate for step 2. Failed: expected mail value in HTTP request and json profile in session" + return False + + + def prepareForStep(self, configurationAttributes, requestParameters, step): + + extensionResult = self.extensionPrepareForStep(configurationAttributes, requestParameters, step) + if extensionResult != None: + return extensionResult + + print "Passport. prepareForStep called %s" % str(step) + identity = CdiUtil.bean(Identity) + + if step == 1: + #re-read the strategies config (for instance to know which strategies have enabled the email account linking) + self.parseProviderConfigs() + identity.setWorkingParameter("externalProviders", json.dumps(self.registeredProviders)) + + providerParam = self.customAuthzParameter + url = None + + sessionAttributes = identity.getSessionId().getSessionAttributes() + self.skipProfileUpdate = StringHelper.equalsIgnoreCase(sessionAttributes.get("skipPassportProfileUpdate"), "true") + + #this param could have been set previously in authenticate step if current step is being retried + provider = identity.getWorkingParameter("selectedProvider") + print "prepareForStep %s - provider = %s" % (str(step), str(provider)) + + # if there is a selectedProvider + if provider != None: + + # get the redirect URL to use at facesService.redirectToExternalURL() that sends /passport/auth// + url = self.getPassportRedirectUrl(provider) + print "prepareForStep %s - url = %s" % (str(step), url) + + # sets selectedProvider back to None + identity.setWorkingParameter("selectedProvider", None) + + # if there is customAuthzParameter + elif providerParam != None: + + + # get it from sessionAtributes + paramValue = sessionAttributes.get(providerParam) + + #if exists + if paramValue != None: + print "Passport. prepareForStep. Found value in custom param of authorization request: %s" % paramValue + provider = self.getProviderFromJson(paramValue) + + if provider == None: + print "Passport. prepareForStep. A provider value could not be extracted from custom authorization request parameter" + elif not provider in self.registeredProviders: + print "Passport. prepareForStep. Provider '%s' not part of known configured IDPs/OPs" % provider + else: + url = self.getPassportRedirectUrl(provider) + + + # if no provider selected yet... + if url == None: + print "Passport. prepareForStep. A page to manually select an identity provider will be shown" + + # else already got the /passport/auth// url... + else: + + facesService = CdiUtil.bean(FacesService) + + # redirects to Passport getRedirectURL - sends browser to IDP. + print "Passport. Redirecting to external url: %s" + url + + facesService.redirectToExternalURL(url) + + return True + + + def getExtraParametersForStep(self, configurationAttributes, step): + print "Passport. getExtraParametersForStep called for step %s" % str(step) + if step == 1: + return Arrays.asList("selectedProvider", "externalProviders") + elif step == 2: + return Arrays.asList("passport_user_profile") + return None + + + def getCountAuthenticationSteps(self, configurationAttributes): + print "Passport. getCountAuthenticationSteps called" + identity = CdiUtil.bean(Identity) + if identity.getWorkingParameter("passport_user_profile") != None: + return 2 + return 1 + + + def getPageForStep(self, configurationAttributes, step): + print "Passport. getPageForStep called" + + extensionResult = self.extensionGetPageForStep(configurationAttributes, step) + if extensionResult != None: + return extensionResult + + if step == 1: + identity = CdiUtil.bean(Identity) + print "Passport. getPageForStep. Entered if step ==1" + if self.isInboundFlow(identity): + print "Passport. getPageForStep for step 1. Detected inbound Saml flow" + return "/postlogin.xhtml" + print "Passport. getPageForStep 1. NormalFlow, returning passportlogin.xhtml" + return "/auth/passport/passportlogin.xhtml" + + return "/auth/passport/passportpostlogin.xhtml" + + + def getNextStep(self, configurationAttributes, requestParameters, step): + if step == 1: + identity = CdiUtil.bean(Identity) + provider = identity.getWorkingParameter("selectedProvider") + if provider != None: + return 1 + + return -1 + + + def logout(self, configurationAttributes, requestParameters): + return True + +# Extension module related functions + + def extensionInit(self, configurationAttributes): + + if self.extensionModule == None: + return None + return self.extensionModule.init(configurationAttributes) + + + def extensionAuthenticate(self, configurationAttributes, requestParameters, step): + + if self.extensionModule == None: + return None + return self.extensionModule.authenticate(configurationAttributes, requestParameters, step) + + + def extensionPrepareForStep(self, configurationAttributes, requestParameters, step): + + if self.extensionModule == None: + return None + return self.extensionModule.prepareForStep(configurationAttributes, requestParameters, step) + + + def extensionGetPageForStep(self, configurationAttributes, step): + + if self.extensionModule == None: + return None + return self.extensionModule.getPageForStep(configurationAttributes, step) + +# Initalization routines + + def loadExternalModule(self, simpleCustProperty): + + if simpleCustProperty != None: + print "Passport. loadExternalModule. Loading passport extension module..." + moduleName = simpleCustProperty.getValue2() + try: + module = __import__(moduleName) + return module + except: + print "Passport. loadExternalModule. Failed to load module %s" % moduleName + print "Exception: ", sys.exc_info()[1] + print "Passport. loadExternalModule. Flow will be driven entirely by routines of main passport script" + return None + + + def processKeyStoreProperties(self, attrs): + file = attrs.get("key_store_file") + password = attrs.get("key_store_password") + + if file != None and password != None: + file = file.getValue2() + password = password.getValue2() + + if StringHelper.isNotEmpty(file) and StringHelper.isNotEmpty(password): + self.keyStoreFile = file + self.keyStorePassword = password + return True + + print "Passport. readKeyStoreProperties. Properties key_store_file or key_store_password not found or empty" + return False + + + def getCustomAuthzParameter(self, simpleCustProperty): + + customAuthzParameter = None + if simpleCustProperty != None: + prop = simpleCustProperty.getValue2() + if StringHelper.isNotEmpty(prop): + customAuthzParameter = prop + + if customAuthzParameter == None: + print "Passport. getCustomAuthzParameter. No custom param for OIDC authz request in script properties" + print "Passport. getCustomAuthzParameter. Passport flow cannot be initiated by doing an OpenID connect authorization request" + else: + print "Passport. getCustomAuthzParameter. Custom param for OIDC authz request in script properties: %s" % customAuthzParameter + + return customAuthzParameter + +# Configuration parsing + + def getPassportConfigDN(self): + + f = open('/etc/gluu/conf/gluu.properties', 'r') + for line in f: + prop = line.split("=") + if prop[0] == "oxpassport_ConfigurationEntryDN": + prop.pop(0) + break + + f.close() + return "=".join(prop).strip() + + + def parseAllProviders(self): + + registeredProviders = {} + print "Passport. parseAllProviders. Adding providers" + entryManager = CdiUtil.bean(PersistenceEntryManager) + + config = LdapOxPassportConfiguration() + config = entryManager.find(config.getClass(), self.passportDN).getPassportConfiguration() + config = config.getProviders() if config != None else config + + if config != None and len(config) > 0: + for prvdetails in config: + if prvdetails.isEnabled(): + registeredProviders[prvdetails.getId()] = { + "emailLinkingSafe": prvdetails.isEmailLinkingSafe(), + "requestForEmail" : prvdetails.isRequestForEmail(), + "logo_img": prvdetails.getLogoImg(), + "displayName": prvdetails.getDisplayName(), + "type": prvdetails.getType() + } + + return registeredProviders + + + def parseProviderConfigs(self): + + registeredProviders = {} + try: + registeredProviders = self.parseAllProviders() + toRemove = [] + + for provider in registeredProviders: + if registeredProviders[provider]["type"] != "saml": + toRemove.append(provider) + else: + registeredProviders[provider]["saml"] = True + + for provider in toRemove: + registeredProviders.pop(provider) + + + if len(registeredProviders.keys()) > 0: + print "Passport. parseProviderConfigs. Configured providers:", registeredProviders + else: + print "Passport. parseProviderConfigs. No providers registered yet" + except: + print "Passport. parseProviderConfigs. An error occurred while building the list of supported authentication providers", sys.exc_info()[1] + + + print "parseProviderConfigs - registeredProviders = %s" % str(registeredProviders) + self.registeredProviders = registeredProviders + print "parseProviderConfigs - self.registeredProviders = %s" % str(self.registeredProviders) + +# Auxiliary routines + + def getProviderFromJson(self, providerJson): + + provider = None + try: + obj = json.loads(Base64Util.base64urldecodeToString(providerJson)) + provider = obj[self.providerKey] + except: + print "Passport. getProviderFromJson. Could not parse provided Json string. Returning None" + + return provider + + + def getPassportRedirectUrl(self, provider): + + # provider is assumed to exist in self.registeredProviders + url = None + try: + facesContext = CdiUtil.bean(FacesContext) + tokenEndpoint = "https://%s/passport/token" % facesContext.getExternalContext().getRequest().getServerName() + + httpService = CdiUtil.bean(HttpService) + httpclient = httpService.getHttpsClient() + + print "Passport. getPassportRedirectUrl. Obtaining token from passport at %s" % tokenEndpoint + resultResponse = httpService.executeGet(httpclient, tokenEndpoint, Collections.singletonMap("Accept", "text/json")) + httpResponse = resultResponse.getHttpResponse() + + bytes = httpService.getResponseContent(httpResponse) + + response = httpService.convertEntityToString(bytes) + print "Passport. getPassportRedirectUrl. Response was %s" % httpResponse.getStatusLine().getStatusCode() + + tokenObj = json.loads(response) + + url = "/passport/auth/%s/%s" % (provider, tokenObj["token_"]) + + except: + print "Passport. getPassportRedirectUrl. Error building redirect URL: ", sys.exc_info()[1] + + return url + + + def validSignature(self, jwt): + + print "Passport. validSignature. Checking JWT token signature" + valid = False + + try: + + appConfiguration = AppConfiguration() + appConfiguration.setWebKeysStorage(WebKeyStorage.KEYSTORE) + appConfiguration.setKeyStoreFile(self.keyStoreFile) + appConfiguration.setKeyStoreSecret(self.keyStorePassword) + appConfiguration.setKeyRegenerationEnabled(False) + + cryptoProvider = CryptoProviderFactory.getCryptoProvider(appConfiguration) + + + alg_string = str(jwt.getHeader().getSignatureAlgorithm()) + signature_string = str(jwt.getEncodedSignature()) + + if alg_string == "none" or alg_string == "None" or alg_string == "NoNe" or alg_string == "nONE" or alg_string == "NONE" or alg_string == "NonE" or alg_string == "nOnE": + # blocks none attack + + print "WARNING: JWT Signature algorithm is none" + valid = False + + elif alg_string != "RS512": + # blocks anything that's not RS512 + + print "WARNING: JWT Signature algorithm is NOT RS512" + valid = False + + elif signature_string == "" : + # blocks empty signature string + print "WARNING: JWT Signature not sent" + valid = False + + else: + + # class extends AbstractCryptoProvider + ''' on version 4.2 .getAlgorithm() method was renamed to .getSignatureAlgorithm() + for older versions: + valid = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), jwt.getHeader().getKeyId(), + None, None, jwt.getHeader().getAlgorithm()) + ''' + + # working on 4.2: + valid = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), jwt.getHeader().getKeyId(), + None, None, jwt.getHeader().getSignatureAlgorithm()) + + except: + print "Exception: ", sys.exc_info()[1] + + print "Passport. validSignature. Validation result was %s" % valid + + return valid + + + def jwtHasExpired(self, jwt): + # Check if jwt has expired + jwt_claims = jwt.getClaims() + try: + exp_date_timestamp = float(jwt_claims.getClaimAsString(JwtClaimName.EXPIRATION_TIME)) + exp_date = datetime.datetime.fromtimestamp(exp_date_timestamp) + hasExpired = exp_date < datetime.datetime.now() + except: + print "Exception: The JWT does not have '%s' attribute" % JwtClaimName.EXPIRATION_TIME + return False + + return hasExpired + + + def getUserProfile(self, jwt): + + # getClaims method located at org.gluu.oxauth.model.token.JsonWebResponse.java as a org.gluu.oxauth.model.jwt.JwtClaims object + jwt_claims = jwt.getClaims() + + user_profile_json = None + + try: + # public String getClaimAsString(String key) + user_profile_json = CdiUtil.bean(EncryptionService).decrypt(jwt_claims.getClaimAsString("data")) + + user_profile = json.loads(user_profile_json) + except: + print "Passport. getUserProfile. Problem obtaining user profile json representation" + + return (user_profile, user_profile_json) + + + def attemptAuthentication(self, identity, user_profile, user_profile_json): + + print "Entered attemptAuthentication..." + uidKey = "uid" + if not self.checkRequiredAttributes(user_profile, [uidKey, self.providerKey]): + return False + + provider = user_profile[self.providerKey] + print "user_profile[self.providerKey] = %s" % str(user_profile[self.providerKey]) + if not provider in self.registeredProviders: + print "Entered if note provider in self.registeredProviers:" + print "Passport. attemptAuthentication. Identity Provider %s not recognized" % provider + return False + + print "attemptAuthentication. user_profile = %s" % user_profile + print "user_profile[uidKey] = %s" % user_profile[uidKey] + uid = user_profile[uidKey][0] + print "attemptAuthentication - uid = %s" % uid + externalUid = "passport-%s:%s:%s" % ("saml", provider, uid) + + userService = CdiUtil.bean(UserService) + userByUid = self.getUserByExternalUid(uid, provider, userService) + + email = None + if "mail" in user_profile: + email = user_profile["mail"] + if len(email) == 0: + email = None + else: + email = email[0] + user_profile["mail"] = [ email ] + + if email == None and self.registeredProviders[provider]["requestForEmail"]: + print "Passport. attemptAuthentication. Email was not received" + + if userByUid != None: + # This avoids asking for the email over every login attempt + email = userByUid.getAttribute("mail") + if email != None: + print "Passport. attemptAuthentication. Filling missing email value with %s" % email + user_profile["mail"] = [ email ] + + if email == None: + # Store user profile in session and abort this routine + identity.setWorkingParameter("passport_user_profile", user_profile_json) + return True + + userByMail = None if email == None else userService.getUserByAttribute("mail", email) + + # Determine if we should add entry, update existing, or deny access + doUpdate = False + doAdd = False + if userByUid != None: + print "User with externalUid '%s' already exists" % externalUid + if userByMail == None: + doUpdate = True + else: + if userByMail.getUserId() == userByUid.getUserId(): + doUpdate = True + else: + print "Users with externalUid '%s' and mail '%s' are different. Access will be denied. Impersonation attempt?" % (externalUid, email) + self.setMessageError(FacesMessage.SEVERITY_ERROR, "Email value corresponds to an already existing provisioned account") + else: + if userByMail == None: + doAdd = True + elif self.registeredProviders[provider]["emailLinkingSafe"]: + + tmpList = userByMail.getAttributeValues("oxExternalUid") + tmpList = ArrayList() if tmpList == None else ArrayList(tmpList) + tmpList.add(externalUid) + userByMail.setAttribute("oxExternalUid", tmpList, True) + + userByUid = userByMail + print "External user supplying mail %s will be linked to existing account '%s'" % (email, userByMail.getUserId()) + doUpdate = True + else: + print "An attempt to supply an email of an existing user was made. Turn on 'emailLinkingSafe' if you want to enable linking" + self.setMessageError(FacesMessage.SEVERITY_ERROR, "Email value corresponds to an already existing account. If you already have a username and password use those instead of an external authentication site to get access.") + + username = None + try: + if doUpdate: + username = userByUid.getUserId() + print "Passport. attemptAuthentication. Updating user %s" % username + self.updateUser(userByUid, user_profile, userService) + elif doAdd: + print "Passport. attemptAuthentication. Creating user %s" % externalUid + newUser = self.addUser(externalUid, user_profile, userService) + username = newUser.getUserId() + except: + print "Exception: ", sys.exc_info()[1] + print "Passport. attemptAuthentication. Authentication failed" + return False + + if username == None: + print "Passport. attemptAuthentication. Authentication attempt was rejected" + return False + else: + logged_in = CdiUtil.bean(AuthenticationService).authenticate(username) + print "Passport. attemptAuthentication. Authentication for %s returned %s" % (username, logged_in) + return logged_in + + + def getUserByExternalUid(self, uid, provider, userService): + newFormat = "passport-%s:%s:%s" % ("saml", provider, uid) + user = userService.getUserByAttribute("oxExternalUid", newFormat, True) + + if user == None: + oldFormat = "passport-%s:%s" % ("saml", uid) + user = userService.getUserByAttribute("oxExternalUid", oldFormat, True) + + if user != None: + # Migrate to newer format + list = HashSet(user.getAttributeValues("oxExternalUid")) + list.remove(oldFormat) + list.add(newFormat) + user.setAttribute("oxExternalUid", ArrayList(list), True) + print "Migrating user's oxExternalUid to newer format 'passport-saml:provider:uid'" + userService.updateUser(user) + + return user + + + def setMessageError(self, severity, msg): + facesMessages = CdiUtil.bean(FacesMessages) + facesMessages.setKeepMessages() + facesMessages.clear() + facesMessages.add(severity, msg) + + + def checkRequiredAttributes(self, profile, attrs): + + for attr in attrs: + if (not attr in profile) or len(profile[attr]) == 0: + print "Passport. checkRequiredAttributes. Attribute '%s' is missing in profile" % attr + return False + return True + + + def addUser(self, externalUid, profile, userService): + print "Passport. Entered addUser()." + print "Passport. addUser. externalUid = %s" % externalUid + print "Passport. addUser. profile = %s" % profile + newUser = User() + #Fill user attrs + newUser.setAttribute("oxExternalUid", externalUid, True) + self.fillUser(newUser, profile) + newUser = userService.addUser(newUser, True) + return newUser + + + def updateUser(self, foundUser, profile, userService): + # when this is false, there might still some updates taking place (e.g. not related to profile attrs released by external provider) + if (not self.skipProfileUpdate): + self.fillUser(foundUser, profile) + userService.updateUser(foundUser) + + + def fillUser(self, foundUser, profile): + print + print "Passport. Entered fillUser()." + print "Passport. fillUser. foundUser = %s" % foundUser + print "Passport. fillUser. profile = %s" % profile + for attr in profile: + # "provider" is disregarded if part of mapping + if attr != self.providerKey: + values = profile[attr] + print "%s = %s" % (attr, values) + foundUser.setAttribute(attr, values) + + if attr == "mail": + print "Passport. fillUser. entered if attr == mail" + oxtrustMails = [] + for mail in values: + oxtrustMails.append('{"value":"%s","primary":false}' % mail) + foundUser.setAttribute("oxTrustEmail", oxtrustMails) + +# IDP-initiated flow routines + + def isInboundFlow(self, identity): + print "passport. entered isInboundFlow" + + sessionId = identity.getSessionId() + print "passport. isInboundFlow. sessionId = %s" % sessionId + if sessionId == None: + print "passport. isInboundFlow. sessionId not found yet..." + # Detect mode if there is no session yet. It's needed for getPageForStep method + facesContext = CdiUtil.bean(FacesContext) + requestParameters = facesContext.getExternalContext().getRequestParameterMap() + print "passport. isInboundFlow. requestParameters = %s" % requestParameters + + authz_state = requestParameters.get(AuthorizeRequestParam.STATE) + print "passport. isInboundFlow. authz_state = %s" % authz_state + else: + authz_state = identity.getSessionId().getSessionAttributes().get(AuthorizeRequestParam.STATE) + + print "passport. IsInboundFlow. authz_state = %s" % authz_state + + # the replace above is workaround due a problem reported + # on issue: https://github.com/GluuFederation/gluu-passport/issues/95 + # TODO: Remove after fixed on JSF side + + b64url_decoded_auth_state = base64.urlsafe_b64decode(str(authz_state+'==')) + + # print "passport. IsInboundFlow. b64url_decoded_auth_state = %s" % str(b64url_decoded_auth_state) + print "passport. IsInboundFlow. self.isInboundJwt() = %s" % str(self.isInboundJwt(b64url_decoded_auth_state)) + if self.isInboundJwt(b64url_decoded_auth_state): + return True + + return False + + + def isInboundJwt(self, value): + if value == None: + return False + + try: + jwt = Jwt.parse(value) + print "passport.isInboundJwt. jwt = %s" % jwt + user_profile_json = CdiUtil.bean(EncryptionService).decrypt(jwt.getClaims().getClaimAsString("data")) + if StringHelper.isEmpty(user_profile_json): + return False + except InvalidJwtException: + return False + + except: + print("Unexpected error:", sys.exc_info()[0]) + return False + + return True + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + diff --git a/oxAuth/Server/integrations/saml.alias.attribute/README.md b/oxAuth/Server/integrations/saml.alias.attribute/README.md new file mode 100644 index 00000000..09b7eb5c --- /dev/null +++ b/oxAuth/Server/integrations/saml.alias.attribute/README.md @@ -0,0 +1,50 @@ +# This script allows you to transform attributes presented to a Service Provider / SAML application. + +## Example + +We are using a Gluu 4.4.x server and created a mirror attribute named "espejo" which will reflect "mail" attributes value and send that value to target SP as "espejo == support@gluu.org". "support@gluu.org" is actually a mail attribute stored in Gluu Server. + +## Configuration + - Configure and enable custom attribute `espejo` + - Use attached script and use that in "IDP Extension". + - Log into oxTrust + - "Other Custom Scripts" + - "Idp Extension" + - Name: `attribute_rewrite` + - Description: `Attribute rewrite script` + - Programming Language: default ( Jython ) + - Level: default ( 0 ) + - Location Type: default ( Database ) + - Custom property (key/value): + - `saml_source_attribute`: mail + - `saml_target_attribute` : espejo + - Add script + +## Test + + - Release source attribute ( `mail` in this test case ) in target trust relationship + - Make sure your SP is configured to accept / hold espejo attribute + - Successful SAML assertion would look like below... + +``` +.... + + + mohib@gluu.org + + + mohib + + + + + +2022-05-24 05:41:57,635 - 118.179.84.52 - DEBUG [org.opensaml.messaging.encoder.servlet.BaseHttpServletResponseXMLMessageEncoder:54] - Successfully encoded message. +2022-05-24 05:41:57,635 - 118.179.84.52 - DEBUG [org.opensaml.profile.action.impl.EncodeMessage:152] - Profile Action EncodeMessage: Outbound message encoded from a message of type org.opensaml.saml.saml2.core.impl.ResponseImpl +2022-05-24 05:41:57,636 - 118.179.84.52 - DEBUG [net.shibboleth.idp.profile.impl.RecordResponseComplete:89] - Profile Action RecordResponseComplete: Record response complete +2022-05-24 05:41:57,636 - 118.179.84.52 - INFO [Shibboleth-Audit.SSO:283] - 118.179.84.52|2022-05-24T05:41:48.527263Z|2022-05-24T05:41:57.636712Z|mohib|https://testappsaml2.gluu.org/shibboleth|_0635ef03b55ec37d4ecec0a295f13ca8|password|2022-05-24T05:41:57.402696Z|uid,espejo|AAdzZWNyZXQxmpHHqgxqABV08dNKccCRdQKP97z2xbmbHlBAq2yCOo/SK4hMGBIJ5RlhVZyC2TXD5eB6woCLNEOakJnsmCINZh/RxpyLQxpWNueTQGcTApsVXM1dF40kJzp0W0OkGg5CP+Txz1zyTwP6kYg=|transient|false|false||Redirect|POST||Success||c8a9e5feff6ef6f445a84b7eeb0dd13a5a273d3c97293eaf2dec5816e00a8f60|Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36 +.... +.... +``` + +For details: https://github.com/uprightech/idp-attr-rewrite-poc diff --git a/oxAuth/Server/integrations/saml.alias.attribute/idp_script.py b/oxAuth/Server/integrations/saml.alias.attribute/idp_script.py new file mode 100644 index 00000000..b57647f9 --- /dev/null +++ b/oxAuth/Server/integrations/saml.alias.attribute/idp_script.py @@ -0,0 +1,148 @@ +# oxShibboleth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2022, Gluu +# +# Author: Djeumen Rolain +# + +from org.gluu.model.custom.script.type.idp import IdpType +from org.gluu.util import StringHelper +from org.gluu.idp.externalauth import AuthenticatedNameTranslator +from org.gluu.idp.externalauth import ShibOxAuthAuthServlet +from net.shibboleth.idp.authn.principal import UsernamePrincipal, IdPAttributePrincipal +from net.shibboleth.idp.authn import ExternalAuthentication +from net.shibboleth.idp.attribute import IdPAttribute, StringAttributeValue +from net.shibboleth.idp.authn.context import AuthenticationContext, ExternalAuthenticationContext +from net.shibboleth.idp.attribute.context import AttributeContext +from javax.security.auth import Subject +from java.util import Collections, HashMap, HashSet, ArrayList, Arrays + +import java + +class IdpExtension(IdpType): + + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Idp extension. Initialization" + + self.defaultNameTranslator = AuthenticatedNameTranslator() + + self.allowedAcrsList = ArrayList() + if configurationAttributes.containsKey("allowed_acrs"): + allowed_acrs = configurationAttributes.get("allowed_acrs").getValue2() + allowed_acrs_list_array = StringHelper.split(allowed_acrs, ",") + self.allowedAcrsList = Arrays.asList(allowed_acrs_list_array) + + self.sourceAttr = None + self.targetAttr = None + + if configurationAttributes.containsKey("saml_source_attribute"): + self.sourceAttr = configurationAttributes.get("saml_source_attribute").getValue2() + + if configurationAttributes.containsKey("saml_target_attribute"): + self.targetAttr = configurationAttributes.get("saml_target_attribute").getValue2() + + if self.sourceAttr is None or self.targetAttr is None: + print "Init Warning. Missing script configuration property: saml_source_attribute or saml_target_attribute" + else: + print "Idp extension. saml_source_attribute => %s , saml_target_attribute => %s" % (self.sourceAttr,self.targetAttr) + + + print "Idp extension. Initialization. The property allowed_acrs is %s" % self.allowedAcrsList + + return True + + def destroy(self, configurationAttributes): + print "Idp extension. Destroy" + return True + + def getApiVersion(self): + return 13 + + # Translate attributes from user profile + # context is org.gluu.idp.externalauth.TranslateAttributesContext (https://github.com/GluuFederation/shib-oxauth-authn3/blob/master/src/main/java/org/gluu/idp/externalauth/TranslateAttributesContext.java) + # configurationAttributes is java.util.Map + def translateAttributes(self, context, configurationAttributes): + print "Idp extension. Method: translateAttributes" + userProfile = context.getUserProfile() + if userProfile is None: + print "No valid user profile could be found to translate" + return False + + if userProfile.getId() is None: + print "No valid user principal could be found to traslate" + return False + + self.defaultNameTranslator.populateIdpAttributeList(userProfile.getAttributes(),context) + + #Return True to specify that default method is not needed + return True + + # Update attributes before releasing them + # context is org.gluu.idp.consent.processor.PostProcessAttributesContext (https://github.com/GluuFederation/shib-oxauth-authn3/blob/master/src/main/java/org/gluu/idp/consent/processor/PostProcessAttributesContext.java) + # configurationAttributes is java.util.Map + def updateAttributes(self, context, configurationAttributes): + print "Idp extension. Method: updateAttributes" + if self.sourceAttr is None or self.targetAttr is None: + return True + sourceIdpAttr = context.getIdpAttributeMap().get(self.sourceAttr) + if sourceIdpAttr is not None: + context.getIdpAttributeMap().remove(self.sourceAttr) + targetIdpAttr = IdPAttribute(self.targetAttr) + targetIdpAttr.setValues(sourceIdpAttr.getValues()) + context.getIdpAttributeMap().put(self.targetAttr,targetIdpAttr) + + return True + + # Check before allowing user to log in + # context is org.gluu.idp.externalauth.PostAuthenticationContext (https://github.com/GluuFederation/shib-oxauth-authn3/blob/master/src/main/java/org/gluu/idp/externalauth/PostAuthenticationContext.java) + # configurationAttributes is java.util.Map + def postAuthentication(self, context, configurationAttributes): + print "Idp extension. Method: postAuthentication" + userProfile = context.getUserProfile() + authenticationContext = context.getAuthenticationContext + + requestedAcr = None + if authenticationContext != None: + requestedAcr = authenticationContext.getAuthenticationStateMap().get(org.gluu.idp.externalauth.OXAUTH_ACR_REQUESTED) + + usedAcr = userProfile.getUsedAcr() + + print "Idp extension. Method: postAuthentication. requestedAcr = %s, usedAcr = %s" % (requestedAcr, usedAcr) + + if requestedAcr == None: + print "Idp extension. Method: postAuthentication. requestedAcr is not specified" + return True + + if not self.allowedAcrsList.contains(usedAcr): + print "Idp extension. Method: postAuthentication. usedAcr '%s' is not allowed" % usedAcr + return False + + return True + + # Check before allowing user to log in + # context is org.gluu.idp.externalauth.PostAuthenticationContext (https://github.com/GluuFederation/shib-oxauth-authn3/blob/master/src/main/java/org/gluu/idp/externalauth/PostAuthenticationContext.java) + # configurationAttributes is java.util.Map + def postAuthentication(self, context, configurationAttributes): + print "Idp extension. Method: postAuthentication" + userProfile = context.getUserProfile() + authenticationContext = context.getAuthenticationContext() + + requestedAcr = None + if authenticationContext != None: + requestedAcr = authenticationContext.getAuthenticationStateMap().get(ShibOxAuthAuthServlet.OXAUTH_ACR_REQUESTED) + + usedAcr = userProfile.getUsedAcr() + + print "Idp extension. Method: postAuthentication. requestedAcr = %s, usedAcr = %s" % (requestedAcr, usedAcr) + + if requestedAcr == None: + print "Idp extension. Method: postAuthentication. requestedAcr is not specified" + return True + + if not self.allowedAcrsList.contains(usedAcr): + print "Idp extension. Method: postAuthentication. usedAcr '%s' is not allowed" % usedAcr + return False + + return True diff --git a/oxAuth/Server/integrations/saml/Gluu Inbound SAML Design (no Session).png b/oxAuth/Server/integrations/saml/Gluu Inbound SAML Design (no Session).png new file mode 100644 index 0000000000000000000000000000000000000000..14f67ddf1725a658243e5a7830d99e8148909974 GIT binary patch literal 52535 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 0): + userToSearch.setAttribute(userAttributeName, attribute_values_list) + + users = userService.getUsersBySample(userToSearch, 1) + if users.size() > 0: + return False + + return True + + def getMappedUser(self, configurationAttributes, requestParameters, saml_response_attributes): + # Convert Saml result attributes keys to lover case + saml_response_normalized_attributes = HashMap() + for saml_response_attribute_entry in saml_response_attributes.entrySet(): + saml_response_normalized_attributes.put(StringHelper.toLowerCase(saml_response_attribute_entry.getKey()), saml_response_attribute_entry.getValue()) + + currentAttributesMapping = self.prepareCurrentAttributesMapping(self.attributesMapping, configurationAttributes, requestParameters) + print "Asimba. Get mapped user. Using next attributes mapping '%s'" % currentAttributesMapping + + newUser = User() + + # Set custom object classes + if self.userObjectClasses != None: + print "Asimba. Get mapped user. User custom objectClasses to add persons: '%s'" % Util.array2ArrayList(self.userObjectClasses) + newUser.setCustomObjectClasses(self.userObjectClasses) + + for attributesMappingEntry in currentAttributesMapping.entrySet(): + idpAttribute = attributesMappingEntry.getKey() + localAttribute = attributesMappingEntry.getValue() + + if self.debugEnrollment: + print "Asimba. Get mapped user. Trying to map '%s' into '%s'" % (idpAttribute, localAttribute) + + localAttributeValue = saml_response_normalized_attributes.get(idpAttribute) + if localAttributeValue != None: + if self.debugEnrollment: + print "Asimba. Get mapped user. Setting attribute '%s' value '%s'" % (localAttribute, localAttributeValue) + newUser.setAttribute(localAttribute, localAttributeValue) + else: + if newUser.getAttribute(localAttribute) == None: + newUser.setAttribute(localAttribute, ArrayList()) + + return newUser + + def getMappedAllAttributesUser(self, saml_response_attributes): + user = User() + + # Set custom object classes + if self.userObjectClasses != None: + print "Asimba. Get mapped all attributes user. User custom objectClasses to add persons: '%s'" % Util.array2ArrayList(self.userObjectClasses) + user.setCustomObjectClasses(self.userObjectClasses) + + # Prepare map to do quick mapping + attributeService = CdiUtil.bean(AttributeService) + ldapAttributes = attributeService.getAllAttributes() + samlUriToAttributesMap = HashMap() + for ldapAttribute in ldapAttributes: + saml2Uri = ldapAttribute.getSaml2Uri() + if saml2Uri == None: + saml2Uri = attributeService.getDefaultSaml2Uri(ldapAttribute.getName()) + samlUriToAttributesMap.put(saml2Uri, ldapAttribute.getName()) + + customAttributes = ArrayList() + for key in saml_response_attributes.keySet(): + ldapAttributeName = samlUriToAttributesMap.get(key) + if ldapAttributeName == None: + print "Asimba. Get mapped all attributes user. Skipping saml attribute: '%s'" % key + continue + + if StringHelper.equalsIgnoreCase(ldapAttributeName, "uid"): + continue + + attribute = CustomAttribute(ldapAttributeName) + attribute.setValues(saml_response_attributes.get(key)) + customAttributes.add(attribute) + + user.setCustomAttributes(customAttributes) + + return user + + def getNameId(self, samlResponse, newUser): + if self.generateNameId: + saml_user_uid = self.generateNameUid(newUser) + else: + saml_user_uid = self.getSamlNameId(samlResponse) + + return saml_user_uid + + def getSamlNameId(self, samlResponse): + saml_response_name_id = samlResponse.getNameId() + if StringHelper.isEmpty(saml_response_name_id): + print "Asimba. Get Saml response. saml_response_name_id is invalid" + return None + + print "Asimba. Get Saml response. saml_response_name_id: '%s'" % saml_response_name_id + + # Use persistent Id as saml_user_uid + return saml_response_name_id + + def generateNameUid(self, user): + if self.userEnforceAttributesUniqueness == None: + print "Asimba. Build local external uid. User enforce attributes uniqueness not specified" + return None + + sb = StringBuilder() + first = True + for userAttributeName in self.userEnforceAttributesUniqueness: + if not first: + sb.append("!") + first = False + attribute_values_list = user.getAttributeValues(userAttributeName) + if (attribute_values_list != None) and (attribute_values_list.size() > 0): + first_attribute_value = attribute_values_list.get(0) + sb.append(first_attribute_value) + + return sb.toString() + + def setDefaultUid(self, user, saml_user_uid): + if StringHelper.isEmpty(user.getUserId()): + user.setUserId(saml_user_uid) diff --git a/oxAuth/Server/integrations/saml2oidc_acr_router/README.md b/oxAuth/Server/integrations/saml2oidc_acr_router/README.md new file mode 100644 index 00000000..f356da61 --- /dev/null +++ b/oxAuth/Server/integrations/saml2oidc_acr_router/README.md @@ -0,0 +1,14 @@ +This is an updated version of acr_saml_router script that takes entityId-to-OIDC_acr mappings from a file on disk. + +Requires a single parameter "entityid_oidc_acr_map_file" containing full path to the mapping file. + +Mapping file's structure is as below: +``` +{ + "mappings": { + "https://sp1.site/shibboleth": "passport_saml", + "https://sp2.site/shibboleth": "basic_lock" + }, + "default": "basic" +} +``` diff --git a/oxAuth/Server/integrations/saml2oidc_acr_router/saml2oidc_acr_router.py b/oxAuth/Server/integrations/saml2oidc_acr_router/saml2oidc_acr_router.py new file mode 100644 index 00000000..cf9ed426 --- /dev/null +++ b/oxAuth/Server/integrations/saml2oidc_acr_router/saml2oidc_acr_router.py @@ -0,0 +1,122 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2023, Gluu +# +# Author: Yuriy Movchan +# Updated by: Aliaksander Samuseu +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.oxauth.util import ServerUtil +from org.gluu.util import StringHelper + +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType + +import sys +import json + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "SAML 2 OIDC ACR router script. Initialization" + + if not configurationAttributes.containsKey("entityid_oidc_acr_map_file"): + print "SAML 2 OIDC ACR router script. Initialization. Property entityid_oidc_acr_map_file is mandatory, but it's missing. Aborting initialization..." + return False + else: + entityidOidcAcrMapFile = configurationAttributes.get("entityid_oidc_acr_map_file").getValue2() + mappings_dict = self.loadEntityidOidcAcrMap(entityidOidcAcrMapFile) + if (not mappings_dict): + print "SAML 2 OIDC ACR router script. File with SAML entityIds to OIDC ACR mappings must not be empty. Aborting initialization..." + return False + else: + self.entityidOidcAcrMap = mappings_dict["mappings"] + self.default_acr = mappings_dict["default"] + print "Loaded mapping configuration is:" + print "SAML 2 OIDC ACR mappings: %s" % (self.entityidOidcAcrMap) + print "Default OIDC ACR: %s" % (self.default_acr) + + + print "SAML 2 OIDC ACR router script. Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "SAML 2 OIDC ACR router script. Destroy" + print "SAML 2 OIDC ACR router script. Destroyed successfully" + + return True + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def getApiVersion(self): + return 11 + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return False + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + #print "DEBUG OUTPUT: SAML 2 OIDC ACR router script. Processing url query arguments..." + # !!!Note: oxAuth stores in session only known parameters + # We need to add to authorizationRequestCustomAllowedParameters oxAuth property issuerId and entityId + + identity = CdiUtil.bean(Identity) + identity.getSessionId().getSessionAttributes() + + session_attributes = identity.getSessionId().getSessionAttributes() + if session_attributes.containsKey("issuerId") and session_attributes.containsKey("entityId"): + + issuerId = session_attributes.get("issuerId") + entityId = session_attributes.get("entityId") + redirect_uri = session_attributes.get("redirect_uri") + #print "DEBUG OUTPUT: SAML 2 OIDC ACR router script. issuerId: %s, entityId: %s, redirect_uri: %s: " % (issuerId, entityId, redirect_uri) + if entityId in self.entityidOidcAcrMap: + target_oidc_acr = self.entityidOidcAcrMap[entityId] + print "SAML 2 OIDC ACR router script. Next target OIDC ACR is chosen based on SP entityId %s: %s" % (entityId, target_oidc_acr) + return target_oidc_acr + else: + print "SAML 2 OIDC ACR router script. No mapping for entityId %s is found, redirecting to the default method" % (entityId) + return self.default_acr + else: + print "SAML 2 OIDC ACR router script. entityId url query parameter must be present in case of valid Shibboleth IDP authentication flow, but it's not found. Aborting the flow..." + return False + + + def authenticate(self, configurationAttributes, requestParameters, step): + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + return True + + def getExtraParametersForStep(self, configurationAttributes, step): + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + return 1 + + def getPageForStep(self, configurationAttributes, step): + return "" + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + return None + + def logout(self, configurationAttributes, requestParameters): + return True + + def loadEntityidOidcAcrMap(self, entityidOidcAcrMapFile): + entityidOidcAcrMap = None + + # Load authentication configuration from file + f = open(entityidOidcAcrMapFile, 'r') + try: + entityidOidcAcrMap = json.loads(f.read()) + except: + print "SAML 2 OIDC ACR router script. Loading entityId to OIDC ACR mappings. Failed to load the mappings from file %s" % (entityidOidcAcrMapFile) + return None + finally: + f.close() + + return entityidOidcAcrMap diff --git a/oxAuth/Server/integrations/smpp/smpp2FA.py b/oxAuth/Server/integrations/smpp/smpp2FA.py new file mode 100644 index 00000000..d26e941d --- /dev/null +++ b/oxAuth/Server/integrations/smpp/smpp2FA.py @@ -0,0 +1,434 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2018, Gluu +# Copyright (c) 2019, Tele2 + +# Author: Jose Gonzalez +# Author: Gasmyr Mougang +# Author: Stefan Andersson + +from java.util import Arrays, Date +from java.io import IOException +from java.lang import Enum + +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.service import AuthenticationService +from org.gluu.oxauth.service.common import UserService +from org.gluu.oxauth.util import ServerUtil +from org.gluu.util import StringHelper, ArrayHelper +from javax.faces.application import FacesMessage +from org.gluu.jsf2.message import FacesMessages + +from org.jsmpp import InvalidResponseException, PDUException +from org.jsmpp.bean import Alphabet, BindType, ESMClass, GeneralDataCoding, MessageClass, NumberingPlanIndicator, RegisteredDelivery, SMSCDeliveryReceipt, TypeOfNumber +from org.jsmpp.extra import NegativeResponseException, ResponseTimeoutException +from org.jsmpp.session import BindParameter, SMPPSession +from org.jsmpp.util import AbsoluteTimeFormatter, TimeFormatter +import random + + +class SmppAttributeError(Exception): + pass + + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + self.identity = CdiUtil.bean(Identity) + + def get_and_parse_smpp_config(self, config, attribute, _type = None, convert = False, optional = False, default_desc = None): + try: + value = config.get(attribute).getValue2() + except: + if default_desc: + default_desc = " using default '{}'".format(default_desc) + else: + default_desc = "" + + if optional: + raise SmppAttributeError("SMPP missing optional configuration attribute '{}'{}".format(attribute, default_desc)) + else: + raise SmppAttributeError("SMPP missing required configuration attribute '{}'".format(attribute)) + + if _type and issubclass(_type, Enum): + try: + return getattr(_type, value) + except AttributeError: + raise SmppAttributeError("SMPP could not find attribute '{}' in {}".format(attribute, _type)) + + if convert: + try: + value = int(value) + except AttributeError: + try: + value = int(value, 16) + except AttributeError: + raise SmppAttributeError("SMPP could not parse value '{}' of attribute '{}'".format(value, attribute)) + + return value + + def init(self, customScript, configurationAttributes): + print("SMPP Initialization") + + self.TIME_FORMATTER = AbsoluteTimeFormatter() + + self.SMPP_SERVER = None + self.SMPP_PORT = None + + self.SYSTEM_ID = None + self.PASSWORD = None + + # Setup some good defaults for TON, NPI and source (from) address + # TON (Type of Number), NPI (Number Plan Indicator) + self.SRC_ADDR_TON = TypeOfNumber.ALPHANUMERIC # Alphanumeric + self.SRC_ADDR_NPI = NumberingPlanIndicator.ISDN # ISDN (E163/E164) + self.SRC_ADDR = "Gluu OTP" + + # Don't touch these unless you know what your doing, we don't handle number reformatting for + # any other type than international. + self.DST_ADDR_TON = TypeOfNumber.INTERNATIONAL # International + self.DST_ADDR_NPI = NumberingPlanIndicator.ISDN # ISDN (E163/E164) + + # Priority flag and data_coding bits + self.PRIORITY_FLAG = 3 # Very Urgent (ANSI-136), Emergency (IS-95) + self.DATA_CODING_ALPHABET = Alphabet.ALPHA_DEFAULT # SMS default alphabet + self.DATA_CODING_MESSAGE_CLASS = MessageClass.CLASS1 # EM (Mobile Equipment (mobile memory), normal message + + # Required server settings + try: + self.SMPP_SERVER = self.get_and_parse_smpp_config(configurationAttributes, "smpp_server") + except SmppAttributeError as e: + print(e) + + try: + self.SMPP_PORT = self.get_and_parse_smpp_config(configurationAttributes, "smpp_port", convert = True) + except SmppAttributeError as e: + print(e) + + if None in (self.SMPP_SERVER, self.SMPP_PORT): + print("SMPP smpp_server and smpp_port is empty, will not enable SMPP service") + return False + + # Optional system_id and password for bind auth + try: + self.SYSTEM_ID = self.get_and_parse_smpp_config(configurationAttributes, "system_id", optional = True) + except SmppAttributeError as e: + print(e) + + try: + self.PASSWORD = self.get_and_parse_smpp_config(configurationAttributes, "password", optional = True) + except SmppAttributeError as e: + print(e) + + if None in (self.SYSTEM_ID, self.PASSWORD): + print("SMPP Authentication disabled") + + # From number and to number settings + try: + self.SRC_ADDR_TON = self.get_and_parse_smpp_config( + configurationAttributes, + "source_addr_ton", + _type = TypeOfNumber, + optional = True, + default_desc = self.SRC_ADDR_TON + ) + except SmppAttributeError as e: + print(e) + + try: + self.SRC_ADDR_NPI = self.get_and_parse_smpp_config( + configurationAttributes, + "source_addr_npi", + _type = NumberingPlanIndicator, + optional = True, + default_desc = self.SRC_ADDR_NPI + ) + except SmppAttributeError as e: + print(e) + + try: + self.SRC_ADDR = self.get_and_parse_smpp_config( + configurationAttributes, + "source_addr", + optional = True, + default_desc = self.SRC_ADDR + ) + except SmppAttributeError as e: + print(e) + + try: + self.DST_ADDR_TON = self.get_and_parse_smpp_config( + configurationAttributes, + "dest_addr_ton", + _type = TypeOfNumber, + optional = True, + default_desc = self.DST_ADDR_TON + ) + except SmppAttributeError as e: + print(e) + + try: + self.DST_ADDR_NPI = self.get_and_parse_smpp_config( + configurationAttributes, + "dest_addr_npi", + _type = NumberingPlanIndicator, + optional = True, + default_desc = self.DST_ADDR_NPI + ) + except SmppAttributeError as e: + print(e) + + # Priority flag and data coding, don't touch these unless you know what your doing... + try: + self.PRIORITY_FLAG = self.get_and_parse_smpp_config( + configurationAttributes, + "priority_flag", + convert = True, + optional = True, + default_desc = "3 (Very Urgent, Emergency)" + ) + except SmppAttributeError as e: + print(e) + + try: + self.DATA_CODING_ALPHABET = self.get_and_parse_smpp_config( + configurationAttributes, + "data_coding_alphabet", + _type = Alphabet, + optional = True, + default_desc = self.DATA_CODING_ALPHABET + ) + except SmppAttributeError as e: + print(e) + + try: + self.DATA_CODING_MESSAGE_CLASS = self.get_and_parse_smpp_config( + configurationAttributes, + "data_coding_alphabet", + _type = MessageClass, + optional = True, + default_desc = self.DATA_CODING_MESSAGE_CLASS + ) + except SmppAttributeError as e: + print(e) + + print("SMPP Initialized successfully") + return True + + def destroy(self, configurationAttributes): + print("SMPP Destroy") + print("SMPP Destroyed successfully") + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + userService = CdiUtil.bean(UserService) + authenticationService = CdiUtil.bean(AuthenticationService) + + facesMessages = CdiUtil.bean(FacesMessages) + facesMessages.setKeepMessages() + + session_attributes = self.identity.getSessionId().getSessionAttributes() + form_passcode = ServerUtil.getFirstValue(requestParameters, "passcode") + + print("SMPP form_response_passcode: {}".format(str(form_passcode))) + + if step == 1: + print("SMPP Step 1 Password Authentication") + credentials = self.identity.getCredentials() + + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + logged_in = False + if StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password): + logged_in = authenticationService.authenticate(user_name, user_password) + + if not logged_in: + return False + + # Get the Person's number and generate a code + foundUser = None + try: + foundUser = authenticationService.getAuthenticatedUser() + except: + print("SMPP Error retrieving user {} from LDAP".format(user_name)) + return False + + mobile_number = None + try: + isVerified = foundUser.getAttribute("phoneNumberVerified") + if isVerified: + mobile_number = foundUser.getAttribute("employeeNumber") + if not mobile_number: + mobile_number = foundUser.getAttribute("mobile") + if not mobile_number: + mobile_number = foundUser.getAttribute("telephoneNumber") + if not mobile_number: + facesMessages.add(FacesMessage.SEVERITY_ERROR, "Failed to determine mobile phone number") + print("SMPP Error finding mobile number for user '{}'".format(user_name)) + return False + except Exception as e: + facesMessages.add(FacesMessage.SEVERITY_ERROR, "Failed to determine mobile phone number") + print("SMPP Error finding mobile number for {}: {}".format(user_name, e)) + return False + + # Generate Random six digit code + code = random.randint(100000, 999999) + + # Get code and save it in LDAP temporarily with special session entry + self.identity.setWorkingParameter("code", code) + + self.identity.setWorkingParameter("mobile_number", mobile_number) + self.identity.getSessionId().getSessionAttributes().put("mobile_number", mobile_number) + if not self.sendMessage(mobile_number, str(code)): + facesMessages.add(FacesMessage.SEVERITY_ERROR, "Failed to send message to mobile phone") + return False + + return True + elif step == 2: + # Retrieve the session attribute + print("SMPP Step 2 SMS/OTP Authentication") + code = session_attributes.get("code") + print("SMPP Code: {}".format(str(code))) + + if code is None: + print("SMPP Failed to find previously sent code") + return False + + if form_passcode is None: + print("SMPP Passcode is empty") + return False + + if len(form_passcode) != 6: + print("SMPP Passcode from response is not 6 digits: {}".format(form_passcode)) + return False + + if form_passcode == code: + print("SMPP SUCCESS! User entered the same code!") + return True + + print("SMPP failed, user entered the wrong code! {} != {}".format(form_passcode, code)) + facesMessages.add(facesMessage.SEVERITY_ERROR, "Incorrect SMS code, please try again.") + return False + + print("SMPP ERROR: step param not found or != (1|2)") + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + if step == 1: + print("SMPP Prepare for Step 1") + return True + elif step == 2: + print("SMPP Prepare for Step 2") + return True + + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + if step == 2: + return Arrays.asList("code") + + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + return 2 + + def getPageForStep(self, configurationAttributes, step): + if step == 2: + return "/auth/otp_sms/otp_sms.xhtml" + + return "" + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True + + def sendMessage(self, number, code): + status = False + session = SMPPSession() + session.setTransactionTimer(10000) + + # We only handle international destination number reformatting. + # All others may vary by configuration decisions taken on SMPP + # server side which we have no clue about. + if self.DST_ADDR_TON == TypeOfNumber.INTERNATIONAL and number.startswith("+"): + number = number[1:] + + try: + print("SMPP Connecting") + reference_id = session.connectAndBind( + self.SMPP_SERVER, + self.SMPP_PORT, + BindParameter( + BindType.BIND_TX, + self.SYSTEM_ID, + self.PASSWORD, + None, + self.SRC_ADDR_TON, + self.SRC_ADDR_NPI, + None + ) + ) + print("SMPP Connected to server with system id {}".format(reference_id)) + + try: + message_id = session.submitShortMessage( + "CMT", + self.SRC_ADDR_TON, + self.SRC_ADDR_NPI, + self.SRC_ADDR, + self.DST_ADDR_TON, + self.DST_ADDR_NPI, + number, + ESMClass(), + 0, + self.PRIORITY_FLAG, + self.TIME_FORMATTER.format(Date()), + None, + RegisteredDelivery(SMSCDeliveryReceipt.DEFAULT), + 0, + GeneralDataCoding( + self.DATA_CODING_ALPHABET, + self.DATA_CODING_MESSAGE_CLASS, + False + ), + 0, + code + ) + print("SMPP Message '{}' sent to #{} with message id {}".format(code, number, message_id)) + status = True + except PDUException as e: + print("SMPP Invalid PDU parameter: {}".format(e)) + except ResponseTimeoutException as e: + print("SMPP Response timeout: {}".format(e)) + except InvalidResponseException as e: + print("SMPP Receive invalid response: {}".format(e)) + except NegativeResponseException as e: + print("SMPP Receive negative response: {}".format(e)) + except IOException as e: + print("SMPP IO error occured: {}".format(e)) + finally: + session.unbindAndClose() + except IOException as e: + print("SMPP Failed connect and bind to host: {}".format(e)) + + return status diff --git a/oxAuth/Server/integrations/stytch/README.md b/oxAuth/Server/integrations/stytch/README.md new file mode 100644 index 00000000..a58024d5 --- /dev/null +++ b/oxAuth/Server/integrations/stytch/README.md @@ -0,0 +1,104 @@ +# Stytch one time passcodes over SMS + +## Overview +Integrate Stytch one-time passcodes as your multi-factor authentication solution. +This document explains how to configure the Gluu Server for two-step, two-factor authentication (2FA) with username / password as the first step, and an OTP sent via text message as the second step. + + +## Prerequisites + +- A Gluu Server (installation instructions [here](../installation-guide/index.md)); +- The [Stytch SMS OTP script](https://github.com/GluuFederation/oxAuth/blob/master/Server/integrations/stytch/stytchExternalAuthenticator.py) + +- A mobile device and phone number that can receive SMS text messages + + +## Properties + +The custom script has the following properties: +| Property | Description | Example | +|-----------------------|-------------------------------|---------------| +|SMS_ENDPOINT |https://stytch.com/docs/api/send-otp-by-sms |`https://test.stytch.com/v1/otps/sms/send`| +|AUTH_ENDPOINT |https://stytch.com/docs/api/authenticate-otp |`https://test.stytch.com/v1/otps/authenticate`| +|ENROLL_ENDPOINT |https://stytch.com/docs/api/log-in-or-create-user-by-sms |`https://test.stytch.com/v1/otps/sms/login_or_create`| +|PROJECT_ID |Project id provided by Stytch. |`project-test-dd1403b3-dd92-33c6-91dd-ddcde970a61e`| +|SECRET |secret provided by Stytch. |`secret-test-dd1403b3-dd92-33c6-91dd-ddcde970a61e`| + + +## Enable stytch acr + +### Add the custom script + +1. Log into oxTrust with admin credentials + +1. Visit `Configuration` > `Person Authentication Scripts`. At the bottom click on `Add custom script configuration` and fill values as follows: + + - For `name` use a meaningful identifier, like `stytch` + + - In the `script` field use the contents of this [file](https://github.com/GluuFederation/oxAuth/raw/version_4.5.1/Server/integrations/stytch/stytchExternalAuthenticator.py) + + - Tick the `enabled` checkbox + + - For the rest of fields, you can accept the defaults + +1. Click on `Add new property`. On the left enter `SMS_ENDPOINT` on the right copy the corresponding value you will find in your stytch account https://stytch.com/docs/api/send-otp-by-sms + +1. Repeat the process for `AUTH_ENDPOINT`, `ENROLL_ENDPOINT`, `PROJECT_ID` and `SECRET` + +1. Scroll down and click on the `Update` button at the bottom of the page +Now Stytch is an available authentication mechanism for your Gluu Server. This means that, using OpenID Connect `acr_values`, applications can now request OTP SMS authentication for users. + +!!! Note + To make sure Stytch has been enabled successfully, you can check your Gluu Server's OpenID Connect configuration by navigating to the following URL: `https:///.well-known/openid-configuration`. Find `"acr_values_supported":` and you should see `"stytch"`. + +## Make Stytch the Default +If Stytch should be the default authentication mechanism, follow these instructions: + +1. Navigate to `Configuration` > `Manage Authentication`. + +1. Select the `Default Authentication Method` tab. + +1. In the Default Authentication Method window you will see two options: `Default acr` and `oxTrust acr`. + + - `oxTrust acr` sets the authentication mechanism for accessing the oxTrust dashboard GUI (only managers should have acccess to oxTrust). + + - `Default acr` sets the default authentication mechanism for accessing all applications that leverage your Gluu Server for authentication (unless otherwise specified). + +If Stytch should be the default authentication mechanism for all access, change both fields to stytch. + +## SMS OTP Login Pages + +The Gluu Server includes one page for SMS OTP: + +1. A **login** page that is displayed for all SMS OTP authentications. +![sms](../img/user-authn/sms.png) + +The designs are being rendered from the [SMS xhtml page](https://github.com/GluuFederation/oxAuth/blob/master/Server/src/main/webapp/auth/otp_sms/otp_sms.xhtml). To customize the look and feel of the pages, follow the [customization guide](../operation/custom-design.md). + + +## Using SMS OTP + +### Phone Number Enrollment + +The script assumes the user phone number is already stored in his corresponding LDAP entry (attribute `phoneNumberVerified`). You can change the attribute by altering the script directly (see authenticate routine). + +### Subsequent Logins +All authentications will trigger an SMS with an OTP to the registered phone number. Enter the OTP to pass authentication. + +### Credential Management + +A user's registered phone number can be removed by a Gluu administrator either via the oxTrust UI in `Users` > `Manage People`, or in LDAP under the user entry. Once the phone number has been removed from the user's account, the user can re-enroll a new phone number following the [phone number enrollment](#phone-number-enrollment) instructions above. + +## Troubleshooting +If problems are encountered, take a look at the logs, specifically `/opt/gluu/jetty/oxauth/logs/oxauth_script.log`. Inspect all messages related to Stytch. For instance, the following messages show an example of correct script initialization: + +``` +Stytch Initialization +Stytch Initialized successfully +``` + +Also make sure you are using the latest version of the script that can be found [here](https://github.com/GluuFederation/oxAuth/blob/master/Server/integrations/stytch/stytchExternalAuthenticator.py). + +## Self-service account security + +To offer end-users a portal where they can manage their own account security preferences, including two-factor authentication credentials like phone numbers for SMS OTP, check out our new app, [Gluu Casa](https://casa.gluu.org). diff --git a/oxAuth/Server/integrations/stytch/stytchExternalAuthenticator.py b/oxAuth/Server/integrations/stytch/stytchExternalAuthenticator.py new file mode 100644 index 00000000..b2072b70 --- /dev/null +++ b/oxAuth/Server/integrations/stytch/stytchExternalAuthenticator.py @@ -0,0 +1,378 @@ +# Author: Madhumita Subramaniam +from java.util import Arrays, Date +from java.io import IOException +from java.lang import Enum +from org.gluu.oxauth.service.net import HttpService +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.service import AuthenticationService +from org.gluu.oxauth.service.common import UserService +from org.gluu.oxauth.util import ServerUtil +from org.gluu.util import StringHelper, ArrayHelper +from javax.faces.application import FacesMessage +from org.gluu.jsf2.message import FacesMessages +import base64 +try: + import json +except ImportError: + import simplejson as json +import random + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + self.identity = CdiUtil.bean(Identity) + + def init(self, customScript, configurationAttributes): + print("Stytch. Initialization") + + if not configurationAttributes.containsKey("SMS_ENDPOINT"): + print "Stytch. Initialization. Property SMS_ENDPOINT is mandatory" + return False + self.SMS_ENDPOINT = configurationAttributes.get("SMS_ENDPOINT").getValue2() + + if not configurationAttributes.containsKey("AUTH_ENDPOINT"): + print "Stytch. Initialization. Property AUTH_ENDPOINT is mandatory" + return False + self.AUTH_ENDPOINT = configurationAttributes.get("AUTH_ENDPOINT").getValue2() + + if not configurationAttributes.containsKey("ENROLL_ENDPOINT"): + print "Stytch. Initialization. Property ENROLL_ENDPOINT is mandatory" + return False + self.ENROLL_ENDPOINT = configurationAttributes.get("ENROLL_ENDPOINT").getValue2() + + if not configurationAttributes.containsKey("PROJECT_ID"): + print "Stytch. Initialization. Property PROJECT_ID is mandatory" + return False + self.PROJECT_ID = configurationAttributes.get("PROJECT_ID").getValue2() + + + if not configurationAttributes.containsKey("SECRET"): + print "Stytch. Initialization. Property SECRET is mandatory" + return False + self.SECRET = configurationAttributes.get("SECRET").getValue2() + + print("Stytch Initialized successfully") + return True + + def destroy(self, configurationAttributes): + print("Stytch Destroy") + print("Stytch Destroyed successfully") + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + userService = CdiUtil.bean(UserService) + authenticationService = CdiUtil.bean(AuthenticationService) + + facesMessages = CdiUtil.bean(FacesMessages) + facesMessages.setKeepMessages() + + session_attributes = self.identity.getSessionId().getSessionAttributes() + if step == 1: + print("Stytch Step 1 Password Authentication") + credentials = self.identity.getCredentials() + + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + logged_in = False + if StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password): + logged_in = authenticationService.authenticate(user_name, user_password) + + if not logged_in: + return False + + foundUser = None + try: + foundUser = authenticationService.getAuthenticatedUser() + except: + print("Stytch Error retrieving user {} from LDAP".format(user_name)) + return False + + mobile_number = None + try: + isVerified = foundUser.getAttribute("phoneNumberVerified") + if isVerified: + mobile_number = foundUser.getAttribute("employeeNumber") + if not mobile_number: + mobile_number = foundUser.getAttribute("mobile") + if not mobile_number: + mobile_number = foundUser.getAttribute("telephoneNumber") + if not mobile_number: + facesMessages.add(FacesMessage.SEVERITY_ERROR, "Failed to determine mobile phone number") + print("Stytch Error finding mobile number for user '{}'".format(user_name)) + return False + except Exception as e: + facesMessages.add(FacesMessage.SEVERITY_ERROR, "Failed to determine mobile phone number") + print("Stytch Error finding mobile number for {}: {}".format(user_name, e)) + return False + + self.identity.setWorkingParameter("mobile_number", mobile_number) + self.identity.getSessionId().getSessionAttributes().put("mobile_number", mobile_number) + + mobileDevices = self.getUserAttributeValue(user_name, "oxMobileDevices") + if mobileDevices is None: + # enrollment + print "No phones registered. Adding %s " % mobile_number + phone_id = self.addUser(mobile_number, user_name) + if phone_id is not None: + self.identity.setWorkingParameter("phone_id", phone_id) + print "phone_id to which SMS has been sent: %s" % phone_id + return True + # if enroll is success, send sms and move on to step 2 + else: + print "Failed to send sms to user. In the next login attempt, user will be prompted for passcode anyway, so it is safe to return true" + return True + ### end of enrollment + + # already contains registered mobiles + print "mobileDevices: %s" % mobileDevices + data = json.loads(mobileDevices) + for phone in data['phones']: + print "phone number : %s " % phone['number'] + print "mobile_number : %s" % mobile_number + if StringHelper.equals(mobile_number.strip('+'), phone['number'].strip('+')): + phone_id = phone['stytch_phone_id'] + print "phone_id stored in oxMobileDevices: %s " % phone_id + if StringHelper.isNotEmptyString(phone_id) : + ### authentication + self.identity.setWorkingParameter("phone_id", phone_id) + phone_id = self.sendPasscodeSMSToUser(mobile_number) + print "SendPasscodeSMSToUser: %s " % phone_id + if self.sendPasscodeSMSToUser(mobile_number) is None: + facesMessages.add(FacesMessage.SEVERITY_ERROR, "Failed to send message to mobile phone") + return False + else: + print "SMS sent successfully" + return True + ### end of authentication + else: + # enrollment. + phone_id = self.addUser(mobile_number, user_name) + if phone_id is not None: + self.identity.setWorkingParameter("phone_id", phone_id) + print "phone_id to which SMS has been sent: %s" % phone_id + return True + # if enroll is success, send sms and move on to step 2 + else: + print "Failed to send sms to user. In the next login attempt, user will be prompted for passcode anyway, so it is safe to return true" + return True + ### end of enrollment + + return False + elif step == 2: + form_passcode = ServerUtil.getFirstValue(requestParameters, "passcode") + print("Stytch form_response_passcode: {}".format(str(form_passcode))) + phone_id = session_attributes.get("phone_id") + print("Stytch phone_id: {}".format(str(phone_id))) + + if phone_id is None: + print("Stytch Failed to find phone_id in session") + return False + + if form_passcode is None: + print("Stytch Passcode is empty") + return False + + if len(form_passcode) != 6: + print("Stytch Passcode from response is not 6 digits: {}".format(form_passcode)) + return False + + #use the phone_id to send the request for authentication + result = self.verifyPasscode(phone_id, form_passcode) + if result is False: + print("Stytch failed, user entered the wrong code! {} ".format(form_passcode)) + facesMessages.add(FacesMessage.SEVERITY_ERROR, "Incorrect SMS code, please try again.") + else: + return True + + print("Stytch ERROR: step param not found or != (1|2)") + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + if step == 1: + print("Stytch Prepare for Step 1") + return True + elif step == 2: + print("Stytch Prepare for Step 2") + return True + + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + if step == 2: + return Arrays.asList("phone_id") + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + return 2 + + def getPageForStep(self, configurationAttributes, step): + if step == 2: + return "/auth/otp_sms/otp_sms.xhtml" + return "" + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True + + + def sendPasscodeSMSToUser(self, phoneNumber): + httpService = CdiUtil.bean(HttpService) + + http_client = httpService.getHttpsClient() + http_client_params = http_client.getParams() + + data = {"phone_number": phoneNumber } + payload = json.dumps(data) + encodedString = base64.b64encode((self.PROJECT_ID +":"+self.SECRET).encode('utf-8')) + headers = { "Accept" : "application/json" } + try: + http_service_response = httpService.executePost(http_client, self.SMS_ENDPOINT, encodedString, headers, payload) + http_response = http_service_response.getHttpResponse() + print "http_response sendPasscodeSMSToUser%s" % http_response + except: + print "Stytch. Exception: sendPasscodeSMSToUser", sys.exc_info()[1] + return None + + try: + if not httpService.isResponseStastusCodeOk(http_response): + print "Stytch. sendPasscodeSMSToUser: %s" % str(http_response.getStatusLine().getStatusCode()) + httpService.consume(http_response) + return None + else : + response_bytes = httpService.getResponseContent(http_response) + response_string = httpService.convertEntityToString(response_bytes) + httpService.consume(http_response) + response = json.loads(response_string) + phone_id = response["phone_id"] + return phone_id + finally: + http_service_response.closeConnection() + + return None + + def verifyPasscode(self, method_id, code): + httpService = CdiUtil.bean(HttpService) + + http_client = httpService.getHttpsClient() + http_client_params = http_client.getParams() + + data = {"method_id": method_id, "code": code } + payload = json.dumps(data) + encodedString = base64.b64encode((self.PROJECT_ID +":"+self.SECRET).encode('utf-8')) + headers = { "Accept" : "application/json" } + try: + + http_service_response = httpService.executePost(http_client, self.AUTH_ENDPOINT, encodedString, headers, payload) + http_response = http_service_response.getHttpResponse() + print "http_response verifyPasscode - %s" % http_response + except: + print "Stytch. Exception: verifyPasscode", sys.exc_info()[1] + return False + + try: + if not httpService.isResponseStastusCodeOk(http_response): + print "Stytch. Verify passcode: ", str(http_response.getStatusLine().getStatusCode()) + httpService.consume(http_response) + return False + else : + print "Stytch. User verified" + return True + finally: + http_service_response.closeConnection() + + return False + + def hasEnrollments(self, configurationAttributes, user): + return len(self.getNumbers(user)) > 0 + + def getNumbers(self, user): + numbers = set() + + tmp = user.getAttributeValues("mobile") + if tmp: + for t in tmp: + numbers.add(t) + + return list(numbers) + + def getUserAttributeValue(self, user_name, attribute_name): + if StringHelper.isEmpty(user_name): + return None + userService = CdiUtil.bean(UserService) + find_user_by_uid = userService.getUser(user_name, attribute_name) + if find_user_by_uid == None: + return None + custom_attribute_value = userService.getCustomAttribute(find_user_by_uid, attribute_name) + if custom_attribute_value == None: + return None + attribute_value = custom_attribute_value.getValue() + print "Stytch. Get user attribute. User's %s attribute %s value is %s" % (user_name, attribute_name, attribute_value) + return attribute_value + + def addUser(self, phoneNumber, gluu_user_name): + httpService = CdiUtil.bean(HttpService) + http_client = httpService.getHttpsClient() + userService = CdiUtil.bean(UserService) + data = {"phone_number": phoneNumber } + payload = json.dumps(data) + encodedString = base64.b64encode((self.PROJECT_ID +":"+self.SECRET).encode('utf-8')) + headers = { "Accept" : "application/json" } + try: + http_service_response = httpService.executePost(http_client, self.ENROLL_ENDPOINT, encodedString, headers, payload) + http_response = http_service_response.getHttpResponse() + print "http_response %s addUser" % http_response + except: + print "Stytch. Exception: addUser ", sys.exc_info()[1] + return None + try: + responseStatusCode = http_response.getStatusLine().getStatusCode(); + print "Stytch. response: %s " % str(http_response.getStatusLine().getStatusCode()) + if responseStatusCode == 200 or responseStatusCode == 201: + response_bytes = httpService.getResponseContent(http_response) + response_string = httpService.convertEntityToString(response_bytes) + httpService.consume(http_response) + response = json.loads(response_string) + phone_id = response["phone_id"] + user_id = response["user_id"] + print "phone id %s " % phone_id + print "user id %s " % user_id + find_user_by_uid = userService.getUser(gluu_user_name) + + oxMobileDevices = json.dumps({'phones': [{'nickname': "Stych Credential", 'number': phoneNumber, 'stytch_phone_id': phone_id, 'stytch_user_id':user_id, 'addedOn': Date().getTime()}]}) + + userService.setCustomAttribute(find_user_by_uid, "oxMobileDevices", oxMobileDevices) + updated_user = userService.updateUser(find_user_by_uid) + if updated_user is not None: + return phone_id + else: + print "Stytch. Failed to update user - addUser" + else: + print "Stytch. Add user response: ", str(http_response.getStatusLine().getStatusCode()) + httpService.consume(http_response) + return None + + finally: + http_service_response.closeConnection() + + return None \ No newline at end of file diff --git a/oxAuth/Server/integrations/super_gluu/SuperGluuExternalAuthenticator.py b/oxAuth/Server/integrations/super_gluu/SuperGluuExternalAuthenticator.py new file mode 100644 index 00000000..0df43b93 --- /dev/null +++ b/oxAuth/Server/integrations/super_gluu/SuperGluuExternalAuthenticator.py @@ -0,0 +1,1164 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2016, Gluu +# +# Author: Yuriy Movchan +# + +from com.google.android.gcm.server import Sender, Message +from java.util import Arrays +from org.apache.http.params import CoreConnectionPNames +from org.apache.http.entity import ContentType +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.model.config import ConfigurationFactory +from org.gluu.oxauth.service import AuthenticationService, SessionIdService +from org.gluu.oxauth.service.fido.u2f import DeviceRegistrationService +from org.gluu.oxauth.service.net import HttpService, HttpService2 +from org.gluu.oxauth.util import ServerUtil +from org.gluu.util import StringHelper +from org.gluu.oxauth.service.common import EncryptionService, UserService +from org.gluu.service import MailService +from org.gluu.oxauth.service.push.sns import PushPlatform, PushSnsService +from org.gluu.oxnotify.client import NotifyClientFactory +from java.util import Arrays, HashMap, Collections, IdentityHashMap, Date +from java.time import ZonedDateTime +from java.time.format import DateTimeFormatter + +from org.gluu.oxauth.service.custom import CustomScriptService + +import sys +import json +import base64 +import datetime +import urllib +import token + +try: + from com.notnoop.apns import APNS + has_apns = True +except ImportError: + print "Super-Gluu. Load. Native APNS will be disabled. There are missing libs needed to enable it" + has_apns = False + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Super-Gluu. Initialization" + + self.debugMode = False + + if not configurationAttributes.containsKey("authentication_mode"): + print "Super-Gluu. Initialization. Property authentication_mode is mandatory" + return False + + self.applicationId = None + if configurationAttributes.containsKey("application_id"): + self.applicationId = configurationAttributes.get("application_id").getValue2() + + self.registrationUri = None + if configurationAttributes.containsKey("registration_uri"): + self.registrationUri = configurationAttributes.get("registration_uri").getValue2() + + authentication_mode = configurationAttributes.get("authentication_mode").getValue2() + if StringHelper.isEmpty(authentication_mode): + print "Super-Gluu. Initialization. Failed to determine authentication_mode. authentication_mode configuration parameter is empty" + return False + + self.oneStep = StringHelper.equalsIgnoreCase(authentication_mode, "one_step") + self.twoStep = StringHelper.equalsIgnoreCase(authentication_mode, "two_step") + + if not (self.oneStep or self.twoStep): + print "Super-Gluu. Initialization. Valid authentication_mode values are one_step and two_step" + return False + + self.androidUrl = None + if configurationAttributes.containsKey("supergluu_android_download_url"): + self.androidUrl = configurationAttributes.get("supergluu_android_download_url").getValue2() + + self.IOSUrl = None + if configurationAttributes.containsKey("supergluu_ios_download_url"): + self.IOSUrl = configurationAttributes.get("supergluu_ios_download_url").getValue2() + + self.customLabel = None + if configurationAttributes.containsKey("label"): + self.customLabel = configurationAttributes.get("label").getValue2() + + self.customQrOptions = {} + if configurationAttributes.containsKey("qr_options"): + self.customQrOptions = configurationAttributes.get("qr_options").getValue2() + + self.use_super_gluu_group = False + if configurationAttributes.containsKey("super_gluu_group"): + self.super_gluu_group = configurationAttributes.get("super_gluu_group").getValue2() + self.use_super_gluu_group = True + print "Super-Gluu. Initialization. Using super_gluu only if user belong to group: %s" % self.super_gluu_group + + self.use_audit_group = False + if configurationAttributes.containsKey("audit_group"): + self.audit_group = configurationAttributes.get("audit_group").getValue2() + + if (not configurationAttributes.containsKey("audit_group_email")): + print "Super-Gluu. Initialization. Property audit_group_email is not specified" + return False + + self.audit_email = configurationAttributes.get("audit_group_email").getValue2() + self.use_audit_group = True + + print "Super-Gluu. Initialization. Using audit group: %s" % self.audit_group + + if self.use_super_gluu_group or self.use_audit_group: + if not configurationAttributes.containsKey("audit_attribute"): + print "Super-Gluu. Initialization. Property audit_attribute is not specified" + return False + else: + self.audit_attribute = configurationAttributes.get("audit_attribute").getValue2() + + # SSA section + if not configurationAttributes.containsKey("AS_CLIENT_ID"): + print "Super-Gluu. Scan. Initialization. Property AS_CLIENT_ID is mandatory" + return False + self.AS_CLIENT_ID = configurationAttributes.get("AS_CLIENT_ID").getValue2() + + if not configurationAttributes.containsKey("AS_CLIENT_SECRET"): + print "Super-Gluu. Scan. Initialization. Property AS_CLIENT_SECRET is mandatory" + return False + self.AS_CLIENT_SECRET = configurationAttributes.get("AS_CLIENT_SECRET").getValue2() + # SSA section + if not configurationAttributes.containsKey("AS_ENDPOINT"): + print "Super-Gluu. Scan. Initialization. Property AS_ENDPOINT is mandatory" + return False + self.AS_ENDPOINT = configurationAttributes.get("AS_ENDPOINT").getValue2() + + if not configurationAttributes.containsKey("AS_SSA"): + print "Super-Gluu. Scan. Initialization. Property AS_SSA is mandatory" + return False + self.AS_SSA = configurationAttributes.get("AS_SSA").getValue2() + + # Upon client creation, this value is populated, after that this call will not go through in subsequent script restart + if StringHelper.isEmptyString(self.AS_CLIENT_ID): + clientRegistrationResponse = self.registerScanClient(self.AS_ENDPOINT, self.AS_ENDPOINT, self.AS_SSA, customScript) + if clientRegistrationResponse == None: + print "Super-Gluu. Failed to register Scan client!!!" + else: + self.AS_CLIENT_ID = clientRegistrationResponse['client_id'] + self.AS_CLIENT_SECRET = clientRegistrationResponse['client_secret'] + + if StringHelper.isNotEmptyString(self.AS_CLIENT_ID) and StringHelper.isNotEmptyString(self.AS_CLIENT_SECRET): + self.enabledPushNotifications = self.initPushNotificationService(configurationAttributes) + else: + self.enabledPushNotifications = False + + print "Super-Gluu. Initialized successfully. oneStep: '%s', twoStep: '%s', pushNotifications: '%s', customLabel: '%s'" % (self.oneStep, self.twoStep, self.enabledPushNotifications, self.customLabel) + + return True + + def destroy(self, configurationAttributes): + print "Super-Gluu. Destroy" + + self.pushAndroidService = None + self.pushAppleService = None + + print "Super-Gluu. Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + authenticationService = CdiUtil.bean(AuthenticationService) + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + session_attributes = identity.getSessionId().getSessionAttributes() + + client_redirect_uri = self.getApplicationUri(session_attributes) + if client_redirect_uri == None: + print "Super-Gluu. Authenticate. redirect_uri is not set" + return False + + self.setRequestScopedParameters(identity, step) + + # Validate form result code and initialize QR code regeneration if needed (retry_current_step = True) + identity.setWorkingParameter("retry_current_step", False) + form_auth_result = ServerUtil.getFirstValue(requestParameters, "auth_result") + if StringHelper.isNotEmpty(form_auth_result): + print "Super-Gluu. Authenticate for step %s. Get auth_result: '%s'" % (step, form_auth_result) + if form_auth_result in ['error']: + return False + + if form_auth_result in ['timeout']: + if ((step == 1) and self.oneStep) or ((step == 2) and self.twoStep): + print "Super-Gluu. Authenticate for step %s. Reinitializing current step" % step + identity.setWorkingParameter("retry_current_step", True) + return False + + userService = CdiUtil.bean(UserService) + deviceRegistrationService = CdiUtil.bean(DeviceRegistrationService) + if step == 1: + print "Super-Gluu. Authenticate for step 1" + + user_name = credentials.getUsername() + if self.oneStep: + session_device_status = self.getSessionDeviceStatus(session_attributes, user_name) + if session_device_status == None: + return False + + u2f_device_id = session_device_status['device_id'] + + validation_result = self.validateSessionDeviceStatus(client_redirect_uri, session_device_status) + if validation_result: + print "Super-Gluu. Authenticate for step 1. User successfully authenticated with u2f_device '%s'" % u2f_device_id + else: + return False + + if not session_device_status['one_step']: + print "Super-Gluu. Authenticate for step 1. u2f_device '%s' is not one step device" % u2f_device_id + return False + + # There are two steps only in enrollment mode + if session_device_status['enroll']: + return validation_result + + identity.setWorkingParameter("super_gluu_count_login_steps", 1) + + user_inum = session_device_status['user_inum'] + + u2f_device = deviceRegistrationService.findUserDeviceRegistration(user_inum, u2f_device_id, "oxId") + if u2f_device == None: + print "Super-Gluu. Authenticate for step 1. Failed to load u2f_device '%s'" % u2f_device_id + return False + + logged_in = authenticationService.authenticate(user_name) + if not logged_in: + print "Super-Gluu. Authenticate for step 1. Failed to authenticate user '%s'" % user_name + return False + + print "Super-Gluu. Authenticate for step 1. User '%s' successfully authenticated with u2f_device '%s'" % (user_name, u2f_device_id) + + return True + elif self.twoStep: + authenticated_user = self.processBasicAuthentication(credentials) + if authenticated_user == None: + return False + + if (self.use_super_gluu_group): + print "Super-Gluu. Authenticate for step 1. Checking if user belong to super_gluu group" + is_member_super_gluu_group = self.isUserMemberOfGroup(authenticated_user, self.audit_attribute, self.super_gluu_group) + if (is_member_super_gluu_group): + print "Super-Gluu. Authenticate for step 1. User '%s' member of super_gluu group" % authenticated_user.getUserId() + super_gluu_count_login_steps = 2 + else: + if self.use_audit_group: + self.processAuditGroup(authenticated_user, self.audit_attribute, self.audit_group) + super_gluu_count_login_steps = 1 + + identity.setWorkingParameter("super_gluu_count_login_steps", super_gluu_count_login_steps) + + if super_gluu_count_login_steps == 1: + return True + + auth_method = 'authenticate' + enrollment_mode = ServerUtil.getFirstValue(requestParameters, "loginForm:registerButton") + if StringHelper.isNotEmpty(enrollment_mode): + auth_method = 'enroll' + + if auth_method == 'authenticate': + user_inum = userService.getUserInum(authenticated_user) + u2f_devices_list = deviceRegistrationService.findUserDeviceRegistrations(user_inum, client_redirect_uri, "oxId") + if u2f_devices_list.size() == 0: + auth_method = 'enroll' + print "Super-Gluu. Authenticate for step 1. There is no U2F '%s' user devices associated with application '%s'. Changing auth_method to '%s'" % (user_name, client_redirect_uri, auth_method) + + print "Super-Gluu. Authenticate for step 1. auth_method: '%s'" % auth_method + + identity.setWorkingParameter("super_gluu_auth_method", auth_method) + + return True + + return False + elif step == 2: + print "Super-Gluu. Authenticate for step 2" + + user = authenticationService.getAuthenticatedUser() + if (user == None): + print "Super-Gluu. Authenticate for step 2. Failed to determine user name" + return False + user_name = user.getUserId() + + session_attributes = identity.getSessionId().getSessionAttributes() + + session_device_status = self.getSessionDeviceStatus(session_attributes, user_name) + if session_device_status == None: + return False + + u2f_device_id = session_device_status['device_id'] + + # There are two steps only in enrollment mode + if self.oneStep and session_device_status['enroll']: + authenticated_user = self.processBasicAuthentication(credentials) + if authenticated_user == None: + return False + + user_inum = userService.getUserInum(authenticated_user) + + attach_result = deviceRegistrationService.attachUserDeviceRegistration(user_inum, u2f_device_id) + + print "Super-Gluu. Authenticate for step 2. Result after attaching u2f_device '%s' to user '%s': '%s'" % (u2f_device_id, user_name, attach_result) + + return attach_result + elif self.twoStep: + if user_name == None: + print "Super-Gluu. Authenticate for step 2. Failed to determine user name" + return False + + validation_result = self.validateSessionDeviceStatus(client_redirect_uri, session_device_status, user_name) + if validation_result: + print "Super-Gluu. Authenticate for step 2. User '%s' successfully authenticated with u2f_device '%s'" % (user_name, u2f_device_id) + else: + return False + + super_gluu_request = json.loads(session_device_status['super_gluu_request']) + auth_method = super_gluu_request['method'] + if auth_method in ['enroll', 'authenticate']: + if validation_result and self.use_audit_group: + user = authenticationService.getAuthenticatedUser() + self.processAuditGroup(user, self.audit_attribute, self.audit_group) + + return validation_result + + print "Super-Gluu. Authenticate for step 2. U2F auth_method is invalid" + + return False + else: + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + identity = CdiUtil.bean(Identity) + session_attributes = identity.getSessionId().getSessionAttributes() + + client_redirect_uri = self.getApplicationUri(session_attributes) + if client_redirect_uri == None: + print "Super-Gluu. Prepare for step. redirect_uri is not set" + return False + + self.setRequestScopedParameters(identity, step) + + if step == 1: + print "Super-Gluu. Prepare for step 1" + if self.oneStep: + session = CdiUtil.bean(SessionIdService).getSessionId() + if session == None: + print "Super-Gluu. Prepare for step 2. Failed to determine session_id" + return False + + issuer = CdiUtil.bean(ConfigurationFactory).getConfiguration().getIssuer() + super_gluu_request_dictionary = {'app': client_redirect_uri, + 'issuer': issuer, + 'state': session.getId(), + 'created': DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(ZonedDateTime.now().withNano(0))} + + self.addGeolocationData(session_attributes, super_gluu_request_dictionary) + + super_gluu_request = json.dumps(super_gluu_request_dictionary, separators=(',',':')) + print "Super-Gluu. Prepare for step 1. Prepared super_gluu_request:", super_gluu_request + + identity.setWorkingParameter("super_gluu_request", super_gluu_request) + elif self.twoStep: + identity.setWorkingParameter("display_register_action", True) + + return True + elif step == 2: + print "Super-Gluu. Prepare for step 2" + if self.oneStep: + return True + + authenticationService = CdiUtil.bean(AuthenticationService) + user = authenticationService.getAuthenticatedUser() + if user == None: + print "Super-Gluu. Prepare for step 2. Failed to determine user name" + return False + + if session_attributes.containsKey("super_gluu_request"): + super_gluu_request = session_attributes.get("super_gluu_request") + if not StringHelper.equalsIgnoreCase(super_gluu_request, "timeout"): + print "Super-Gluu. Prepare for step 2. Request was generated already" + return True + + session = CdiUtil.bean(SessionIdService).getSessionId() + if session == None: + print "Super-Gluu. Prepare for step 2. Failed to determine session_id" + return False + + auth_method = session_attributes.get("super_gluu_auth_method") + if StringHelper.isEmpty(auth_method): + print "Super-Gluu. Prepare for step 2. Failed to determine auth_method" + return False + + print "Super-Gluu. Prepare for step 2. auth_method: '%s'" % auth_method + + issuer = CdiUtil.bean(ConfigurationFactory).getAppConfiguration().getIssuer() + super_gluu_request_dictionary = {'username': user.getUserId(), + 'app': client_redirect_uri, + 'issuer': issuer, + 'method': auth_method, + 'state': session.getId(), + 'created': DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(ZonedDateTime.now().withNano(0))} + + self.addGeolocationData(session_attributes, super_gluu_request_dictionary) + + super_gluu_request = json.dumps(super_gluu_request_dictionary, separators=(',',':')) + print "Super-Gluu. Prepare for step 2. Prepared super_gluu_request:", super_gluu_request + + identity.setWorkingParameter("super_gluu_request", super_gluu_request) + identity.setWorkingParameter("super_gluu_auth_method", auth_method) + + if auth_method in ['authenticate']: + self.sendPushNotification(client_redirect_uri, user, super_gluu_request) + + return True + else: + return False + + def getNextStep(self, configurationAttributes, requestParameters, step): + # If user not pass current step change step to previous + identity = CdiUtil.bean(Identity) + retry_current_step = identity.getWorkingParameter("retry_current_step") + if retry_current_step: + print "Super-Gluu. Get next step. Retrying current step" + + # Remove old QR code + identity.setWorkingParameter("super_gluu_request", "timeout") + + resultStep = step + return resultStep + + return -1 + + def getExtraParametersForStep(self, configurationAttributes, step): + if step == 1: + if self.oneStep: + return Arrays.asList("super_gluu_request") + elif self.twoStep: + return Arrays.asList("display_register_action") + elif step == 2: + return Arrays.asList("super_gluu_auth_method", "super_gluu_request") + + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + identity = CdiUtil.bean(Identity) + if identity.isSetWorkingParameter("super_gluu_count_login_steps"): + return identity.getWorkingParameter("super_gluu_count_login_steps") + else: + return 2 + + def getPageForStep(self, configurationAttributes, step): + if step == 1: + if self.oneStep: + return "/auth/super-gluu/login.xhtml" + elif step == 2: + if self.oneStep: + return "/login.xhtml" + else: + identity = CdiUtil.bean(Identity) + authmethod = identity.getWorkingParameter("super_gluu_auth_method") + print "Super-Gluu. authmethod '%s'" % authmethod + if authmethod == "enroll": + return "/auth/super-gluu/login.xhtml" + else: + return "/auth/super-gluu/login.xhtml" + + return "" + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True + + def processBasicAuthentication(self, credentials): + authenticationService = CdiUtil.bean(AuthenticationService) + + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + logged_in = False + if StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password): + logged_in = authenticationService.authenticate(user_name, user_password) + + if not logged_in: + return None + + find_user_by_uid = authenticationService.getAuthenticatedUser() + if find_user_by_uid == None: + print "Super-Gluu. Process basic authentication. Failed to find user '%s'" % user_name + return None + + return find_user_by_uid + + def validateSessionDeviceStatus(self, client_redirect_uri, session_device_status, user_name = None): + userService = CdiUtil.bean(UserService) + deviceRegistrationService = CdiUtil.bean(DeviceRegistrationService) + + u2f_device_id = session_device_status['device_id'] + + u2f_device = None + if session_device_status['enroll'] and session_device_status['one_step']: + u2f_device = deviceRegistrationService.findOneStepUserDeviceRegistration(u2f_device_id) + if u2f_device == None: + print "Super-Gluu. Validate session device status. There is no one step u2f_device '%s'" % u2f_device_id + return False + else: + # Validate if user has specified device_id enrollment + user_inum = userService.getUserInum(user_name) + + if session_device_status['one_step']: + user_inum = session_device_status['user_inum'] + + u2f_device = deviceRegistrationService.findUserDeviceRegistration(user_inum, u2f_device_id) + if u2f_device == None: + print "Super-Gluu. Validate session device status. There is no u2f_device '%s' associated with user '%s'" % (u2f_device_id, user_inum) + return False + + if not StringHelper.equalsIgnoreCase(client_redirect_uri, u2f_device.application): + print "Super-Gluu. Validate session device status. u2f_device '%s' associated with other application '%s'" % (u2f_device_id, u2f_device.application) + return False + + return True + + def getSessionDeviceStatus(self, session_attributes, user_name): + print "Super-Gluu. Get session device status" + + if not session_attributes.containsKey("super_gluu_request"): + print "Super-Gluu. Get session device status. There is no Super-Gluu request in session attributes" + return None + + # Check session state extended + if not session_attributes.containsKey("session_custom_state"): + print "Super-Gluu. Get session device status. There is no session_custom_state in session attributes" + return None + + session_custom_state = session_attributes.get("session_custom_state") + if not StringHelper.equalsIgnoreCase("approved", session_custom_state): + print "Super-Gluu. Get session device status. User '%s' not approve or not pass U2F authentication. session_custom_state: '%s'" % (user_name, session_custom_state) + return None + + # Try to find device_id in session attribute + if not session_attributes.containsKey("oxpush2_u2f_device_id"): + print "Super-Gluu. Get session device status. There is no u2f_device associated with this request" + return None + + # Try to find user_inum in session attribute + if not session_attributes.containsKey("oxpush2_u2f_device_user_inum"): + print "Super-Gluu. Get session device status. There is no user_inum associated with this request" + return None + + enroll = False + if session_attributes.containsKey("oxpush2_u2f_device_enroll"): + enroll = StringHelper.equalsIgnoreCase("true", session_attributes.get("oxpush2_u2f_device_enroll")) + + one_step = False + if session_attributes.containsKey("oxpush2_u2f_device_one_step"): + one_step = StringHelper.equalsIgnoreCase("true", session_attributes.get("oxpush2_u2f_device_one_step")) + + super_gluu_request = session_attributes.get("super_gluu_request") + u2f_device_id = session_attributes.get("oxpush2_u2f_device_id") + user_inum = session_attributes.get("oxpush2_u2f_device_user_inum") + + session_device_status = {"super_gluu_request": super_gluu_request, "device_id": u2f_device_id, "user_inum" : user_inum, "enroll" : enroll, "one_step" : one_step} + print "Super-Gluu. Get session device status. session_device_status: '%s'" % (session_device_status) + + return session_device_status + + def initPushNotificationService(self, configurationAttributes): + print "Super-Gluu. Initialize Native/SNS/Gluu notification services" + + self.pushSnsMode = False + self.pushGluuMode = False + if configurationAttributes.containsKey("notification_service_mode"): + notificationServiceMode = configurationAttributes.get("notification_service_mode").getValue2() + if StringHelper.equalsIgnoreCase(notificationServiceMode, "sns"): + return self.initSnsPushNotificationService(configurationAttributes) + elif StringHelper.equalsIgnoreCase(notificationServiceMode, "gluu"): + return self.initGluuPushNotificationService(configurationAttributes) + + return self.initNativePushNotificationService(configurationAttributes) + + def initNativePushNotificationService(self, configurationAttributes): + print "Super-Gluu. Initialize native notification services" + + creds = self.loadPushNotificationCreds(configurationAttributes) + if creds == None: + return False + + try: + android_creds = creds["android"]["gcm"] + ios_creds = creds["ios"]["apns"] + except: + print "Super-Gluu. Initialize native notification services. Invalid credentials file format" + return False + + self.pushAndroidService = None + self.pushAppleService = None + if android_creds["enabled"]: + self.pushAndroidService = Sender(android_creds["api_key"]) + print "Super-Gluu. Initialize native notification services. Created Android notification service" + + if ios_creds["enabled"]: + p12_file_path = ios_creds["p12_file_path"] + p12_password = ios_creds["p12_password"] + + try: + encryptionService = CdiUtil.bean(EncryptionService) + p12_password = encryptionService.decrypt(p12_password) + except: + # Ignore exception. Password is not encrypted + print "Super-Gluu. Initialize native notification services. Assuming that 'p12_password' password in not encrypted" + + apnsServiceBuilder = APNS.newService().withCert(p12_file_path, p12_password) + if ios_creds["production"]: + self.pushAppleService = apnsServiceBuilder.withProductionDestination().build() + else: + self.pushAppleService = apnsServiceBuilder.withSandboxDestination().build() + + self.pushAppleServiceProduction = ios_creds["production"] + + print "Super-Gluu. Initialize native notification services. Created iOS notification service" + + enabled = self.pushAndroidService != None or self.pushAppleService != None + + return enabled + + def initSnsPushNotificationService(self, configurationAttributes): + print "Super-Gluu. Initialize SNS notification services" + self.pushSnsMode = True + + creds = self.loadPushNotificationCreds(configurationAttributes) + if creds == None: + return False + + try: + sns_creds = creds["sns"] + android_creds = creds["android"]["sns"] + ios_creds = creds["ios"]["sns"] + except: + print "Super-Gluu. Initialize SNS notification services. Invalid credentials file format" + return False + + self.pushAndroidService = None + self.pushAppleService = None + if not (android_creds["enabled"] or ios_creds["enabled"]): + print "Super-Gluu. Initialize SNS notification services. SNS disabled for all platforms" + return False + + sns_access_key = sns_creds["access_key"] + sns_secret_access_key = sns_creds["secret_access_key"] + sns_region = sns_creds["region"] + + encryptionService = CdiUtil.bean(EncryptionService) + + try: + sns_secret_access_key = encryptionService.decrypt(sns_secret_access_key) + except: + # Ignore exception. Password is not encrypted + print "Super-Gluu. Initialize SNS notification services. Assuming that 'sns_secret_access_key' in not encrypted" + + pushSnsService = CdiUtil.bean(PushSnsService) + pushClient = pushSnsService.createSnsClient(sns_access_key, sns_secret_access_key, sns_region) + + if android_creds["enabled"]: + self.pushAndroidService = pushClient + self.pushAndroidPlatformArn = android_creds["platform_arn"] + print "Super-Gluu. Initialize SNS notification services. Created Android notification service" + + if ios_creds["enabled"]: + self.pushAppleService = pushClient + self.pushApplePlatformArn = ios_creds["platform_arn"] + self.pushAppleServiceProduction = ios_creds["production"] + print "Super-Gluu. Initialize SNS notification services. Created iOS notification service" + + enabled = self.pushAndroidService != None or self.pushAppleService != None + + return enabled + + def initGluuPushNotificationService(self, configurationAttributes): + print "Super-Gluu. Initialize Gluu notification services" + + self.pushGluuMode = True + + creds = self.loadPushNotificationCreds(configurationAttributes) + if creds == None: + return False + + try: + gluu_conf = creds["gluu"] + android_creds = creds["android"]["gluu"] + ios_creds = creds["ios"]["gluu"] + except: + print "Super-Gluu. Initialize Gluu notification services. Invalid credentials file format" + return False + + self.pushAndroidService = None + self.pushAppleService = None + if not (android_creds["enabled"] or ios_creds["enabled"]): + print "Super-Gluu. Initialize Gluu notification services. Gluu disabled for all platforms" + return False + + gluu_server_uri = gluu_conf["server_uri"] + notifyClientFactory = NotifyClientFactory.instance() + metadataConfiguration = None + try: + metadataConfiguration = notifyClientFactory.createMetaDataConfigurationService(gluu_server_uri).getMetadataConfiguration() + except: + print "Super-Gluu. Initialize Gluu notification services. Failed to load metadata. Exception: ", sys.exc_info()[1] + return False + + gluuClient = notifyClientFactory.createNotifyService(metadataConfiguration) + encryptionService = CdiUtil.bean(EncryptionService) + + if android_creds["enabled"]: + self.pushAndroidService = gluuClient + self.gluu_android_platform_id = android_creds["platform_id"] + print "Super-Gluu. Initialize Gluu notification services. Created Android notification service" + + if ios_creds["enabled"]: + self.pushAppleService = gluuClient + self.gluu_ios_platform_id = ios_creds["platform_id"] + print "Super-Gluu. Initialize Gluu notification services. Created iOS notification service" + + enabled = self.pushAndroidService != None or self.pushAppleService != None + + return enabled + + def loadPushNotificationCreds(self, configurationAttributes): + print "Super-Gluu. Initialize notification services" + if not configurationAttributes.containsKey("credentials_file"): + return None + + super_gluu_creds_file = configurationAttributes.get("credentials_file").getValue2() + + # Load credentials from file + f = open(super_gluu_creds_file, 'r') + try: + creds = json.loads(f.read()) + except: + print "Super-Gluu. Initialize notification services. Failed to load credentials from file:", super_gluu_creds_file + return None + finally: + f.close() + + return creds + + def sendPushNotification(self, client_redirect_uri, user, super_gluu_request): + try: + self.sendPushNotificationImpl(client_redirect_uri, user, super_gluu_request) + except: + print "Super-Gluu. Send push notification. Failed to send push notification: ", sys.exc_info()[1] + + def sendPushNotificationImpl(self, client_redirect_uri, user, super_gluu_request): + if not self.enabledPushNotifications: + return + + user_name = user.getUserId() + print "Super-Gluu. Send push notification. Loading user '%s' devices" % user_name + + send_notification = False + send_notification_result = True + + userService = CdiUtil.bean(UserService) + deviceRegistrationService = CdiUtil.bean(DeviceRegistrationService) + + user_inum = userService.getUserInum(user_name) + + send_android = 0 + send_ios = 0 + u2f_devices_list = deviceRegistrationService.findUserDeviceRegistrations(user_inum, client_redirect_uri, "oxId", "oxDeviceData", "oxDeviceNotificationConf") + if u2f_devices_list.size() > 0: + for u2f_device in u2f_devices_list: + device_data = u2f_device.getDeviceData() + + # Device data which Super-Gluu gets during enrollment + if device_data == None: + continue + + platform = device_data.getPlatform() + push_token = device_data.getPushToken() + + if StringHelper.equalsIgnoreCase(platform, "ios") and StringHelper.isNotEmpty(push_token): + # Sending notification to iOS user's device + if self.pushAppleService == None: + print "Super-Gluu. Send push notification. Apple push notification service is not enabled" + else: + send_notification = True + + title = "Super Gluu" + message = "Confirm your sign in request to: %s" % client_redirect_uri + + if self.pushSnsMode or self.pushGluuMode: + pushSnsService = CdiUtil.bean(PushSnsService) + targetEndpointArn = self.getTargetEndpointArn(deviceRegistrationService, pushSnsService, PushPlatform.APNS, user, u2f_device) + if targetEndpointArn == None: + return + + send_notification = True + + sns_push_request_dictionary = { "aps": + { "badge": 0, + "alert" : {"body": message, "title" : title}, + "category": "ACTIONABLE", + "content-available": "1", + "sound": 'default' + }, + "request" : super_gluu_request + } + push_message = json.dumps(sns_push_request_dictionary, separators=(',',':')) + + if self.pushSnsMode: + apple_push_platform = PushPlatform.APNS + if not self.pushAppleServiceProduction: + apple_push_platform = PushPlatform.APNS_SANDBOX + + send_notification_result = pushSnsService.sendPushMessage(self.pushAppleService, apple_push_platform, targetEndpointArn, push_message, None) + if self.debugMode: + print "Super-Gluu. Send iOS SNS push notification. token: '%s', message: '%s', send_notification_result: '%s', apple_push_platform: '%s'" % (push_token, push_message, send_notification_result, apple_push_platform) + elif self.pushGluuMode: + send_notification_result = self.pushAppleService.sendNotification(self.buildNotifyAuthorizationHeader(), targetEndpointArn, push_message, self.gluu_ios_platform_id) + if self.debugMode: + print "Super-Gluu. Send iOS Gluu push notification. token: '%s', message: '%s', send_notification_result: '%s'" % (push_token, push_message, send_notification_result) + else: + additional_fields = { "request" : super_gluu_request } + + msgBuilder = APNS.newPayload().alertBody(message).alertTitle(title).sound("default") + msgBuilder.category('ACTIONABLE').badge(0) + msgBuilder.forNewsstand() + msgBuilder.customFields(additional_fields) + push_message = msgBuilder.build() + + send_notification_result = self.pushAppleService.push(push_token, push_message) + if self.debugMode: + print "Super-Gluu. Send iOS Native push notification. token: '%s', message: '%s', send_notification_result: '%s'" % (push_token, push_message, send_notification_result) + send_ios = send_ios + 1 + + if StringHelper.equalsIgnoreCase(platform, "android") and StringHelper.isNotEmpty(push_token): + # Sending notification to Android user's device + if self.pushAndroidService == None: + print "Super-Gluu. Send native push notification. Android push notification service is not enabled" + else: + send_notification = True + + title = "Super-Gluu" + if self.pushSnsMode or self.pushGluuMode: + pushSnsService = CdiUtil.bean(PushSnsService) + targetEndpointArn = self.getTargetEndpointArn(deviceRegistrationService, pushSnsService, PushPlatform.GCM, user, u2f_device) + if targetEndpointArn == None: + return + + send_notification = True + + sns_push_request_dictionary = { "collapse_key": "single", + "content_available": True, + "time_to_live": 60, + "data": + { "message" : super_gluu_request, + "title" : title } + } + push_message = json.dumps(sns_push_request_dictionary, separators=(',',':')) + + if self.pushSnsMode: + send_notification_result = pushSnsService.sendPushMessage(self.pushAndroidService, PushPlatform.GCM, targetEndpointArn, push_message, None) + if self.debugMode: + print "Super-Gluu. Send Android SNS push notification. token: '%s', message: '%s', send_notification_result: '%s'" % (push_token, push_message, send_notification_result) + elif self.pushGluuMode: + send_notification_result = self.pushAndroidService.sendNotification(self.buildNotifyAuthorizationHeader(), targetEndpointArn, push_message, self.gluu_android_platform_id) + if self.debugMode: + print "Super-Gluu. Send Android Gluu push notification. token: '%s', message: '%s', send_notification_result: '%s'" % (push_token, push_message, send_notification_result) + else: + msgBuilder = Message.Builder().addData("message", super_gluu_request).addData("title", title).collapseKey("single").contentAvailable(True) + push_message = msgBuilder.build() + + send_notification_result = self.pushAndroidService.send(push_message, push_token, 3) + if self.debugMode: + print "Super-Gluu. Send Android Native push notification. token: '%s', message: '%s', send_notification_result: '%s'" % (push_token, push_message, send_notification_result) + send_android = send_android + 1 + + print "Super-Gluu. Send push notification. send_android: '%s', send_ios: '%s'" % (send_android, send_ios) + + def getTargetEndpointArn(self, deviceRegistrationService, pushSnsService, platform, user, u2fDevice): + print "Super-Gluu. Get target endpoint ARN. Preparing to build register device request with user='%s', platform='%s'" % (user.getUserId(), platform) + targetEndpointArn = None + + # Return endpoint ARN if it created already + notificationConf = u2fDevice.getDeviceNotificationConf() + notificationConfJson = {} + if StringHelper.isNotEmpty(notificationConf): + notificationConfJson = json.loads(notificationConf) + if 'sns_endpoint_arn' in notificationConfJson: + print "Super-Gluu. Get target endpoint ARN. There is already created target endpoint ARN" + return notificationConfJson['sns_endpoint_arn'] + + # Create endpoint ARN + pushClient = None + pushClientAuth = None + platformApplicationArn = None + platformId = None + if platform == PushPlatform.GCM: + pushClient = self.pushAndroidService + platformId = self.gluu_android_platform_id + if self.pushSnsMode: + platformApplicationArn = self.pushAndroidPlatformArn + elif platform == PushPlatform.APNS: + pushClient = self.pushAppleService + platformId = self.gluu_ios_platform_id + if self.pushSnsMode: + platformApplicationArn = self.pushApplePlatformArn + else: + return None + + deviceData = u2fDevice.getDeviceData() + pushToken = deviceData.getPushToken() + + print "Super-Gluu. Get target endpoint ARN. Attempting to create target endpoint ARN for user: '%s'" % user.getUserId() + if self.pushSnsMode: + targetEndpointArn = pushSnsService.createPlatformArn(pushClient, platformApplicationArn, pushToken, user) + else: + customUserData = pushSnsService.getCustomUserData(user) + if self.debugMode: + print "Super-Gluu. Get target endpoint ARN. Attempting to send register device request with user='%s', pushToken='%s', platformId='%s', customUserData='%s'" % (user.getUserId(), pushToken, platformId, customUserData) + registerDeviceResponse = pushClient.registerDevice(self.buildNotifyAuthorizationHeader(), pushToken, customUserData, platformId); + if registerDeviceResponse != None and registerDeviceResponse.getStatusCode() == 200: + targetEndpointArn = registerDeviceResponse.getEndpointArn() + + if StringHelper.isEmpty(targetEndpointArn): + print "Super-Gluu. Failed to get endpoint ARN for user: '%s'" % user.getUserId() + return None + + print "Super-Gluu. Get target endpoint ARN. Create target endpoint ARN '%s' for user: '%s'" % (targetEndpointArn, user.getUserId()) + + # Store created endpoint ARN in device entry + notificationConfJson['sns_endpoint_arn'] = targetEndpointArn + userInum = user.getAttribute("inum") + u2fDeviceUpdate = deviceRegistrationService.findUserDeviceRegistration(userInum, u2fDevice.getId()) + u2fDeviceUpdate.setDeviceNotificationConf(json.dumps(notificationConfJson)) + deviceRegistrationService.updateDeviceRegistration(userInum, u2fDeviceUpdate) + + return targetEndpointArn + + def getApplicationUri(self, session_attributes): + if self.applicationId != None: + return self.applicationId + + if not session_attributes.containsKey("redirect_uri"): + return None + + return session_attributes.get("redirect_uri") + + def setRequestScopedParameters(self, identity, step): + downloadMap = HashMap() + if self.registrationUri != None: + identity.setWorkingParameter("external_registration_uri", self.registrationUri) + + if self.androidUrl!= None and step == 1: + downloadMap.put("android", self.androidUrl) + + if self.IOSUrl != None and step == 1: + downloadMap.put("ios", self.IOSUrl) + + if self.customLabel != None: + identity.setWorkingParameter("super_gluu_label", self.customLabel) + + identity.setWorkingParameter("download_url", downloadMap) + identity.setWorkingParameter("super_gluu_qr_options", self.customQrOptions) + + def addGeolocationData(self, session_attributes, super_gluu_request_dictionary): + if session_attributes.containsKey("remote_ip"): + remote_ip = session_attributes.get("remote_ip") + if StringHelper.isNotEmpty(remote_ip): + print "Super-Gluu. Prepare for step 2. Adding req_ip and req_loc to super_gluu_request" + super_gluu_request_dictionary['req_ip'] = remote_ip + + remote_loc_dic = self.determineGeolocationData(remote_ip) + if remote_loc_dic == None: + print "Super-Gluu. Prepare for step 2. Failed to determine remote location by remote IP '%s'" % remote_ip + return + + remote_loc = "%s, %s, %s" % ( remote_loc_dic['country'], remote_loc_dic['regionName'], remote_loc_dic['city'] ) + remote_loc_encoded = urllib.quote(remote_loc.encode('utf-8')) + super_gluu_request_dictionary['req_loc'] = remote_loc_encoded + + def determineGeolocationData(self, remote_ip): + print "Super-Gluu. Determine remote location. remote_ip: '%s'" % remote_ip + httpService = CdiUtil.bean(HttpService) + + http_client = httpService.getHttpsClient() + http_client_params = http_client.getParams() + http_client_params.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 15 * 1000) + + geolocation_service_url = "http://ip-api.com/json/%s?fields=49177" % remote_ip + geolocation_service_headers = { "Accept" : "application/json" } + + try: + http_service_response = httpService.executeGet(http_client, geolocation_service_url, geolocation_service_headers) + http_response = http_service_response.getHttpResponse() + except: + print "Super-Gluu. Determine remote location. Exception: ", sys.exc_info()[1] + return None + + try: + if not httpService.isResponseStastusCodeOk(http_response): + print "Super-Gluu. Determine remote location. Get invalid response from validation server: ", str(http_response.getStatusLine().getStatusCode()) + httpService.consume(http_response) + return None + + response_bytes = httpService.getResponseContent(http_response) + response_string = httpService.convertEntityToString(response_bytes) + httpService.consume(http_response) + finally: + http_service_response.closeConnection() + + if response_string == None: + print "Super-Gluu. Determine remote location. Get empty response from location server" + return None + + response = json.loads(response_string) + + if not StringHelper.equalsIgnoreCase(response['status'], "success"): + print "Super-Gluu. Determine remote location. Get response with status: '%s'" % response['status'] + return None + + return response + + def isUserMemberOfGroup(self, user, attribute, group): + is_member = False + member_of_list = user.getAttributeValues(attribute) + if (member_of_list != None): + for member_of in member_of_list: + if StringHelper.equalsIgnoreCase(group, member_of) or member_of.endswith(group): + is_member = True + break + + return is_member + + def processAuditGroup(self, user, attribute, group): + is_member = self.isUserMemberOfGroup(user, attribute, group) + if (is_member): + print "Super-Gluu. Authenticate for processAuditGroup. User '%s' member of audit group" % user.getUserId() + print "Super-Gluu. Authenticate for processAuditGroup. Sending e-mail about user '%s' login to %s" % (user.getUserId(), self.audit_email) + + # Send e-mail to administrator + user_id = user.getUserId() + mailService = CdiUtil.bean(MailService) + subject = "User log in: %s" % user_id + body = "User log in: %s" % user_id + mailService.sendMail(self.audit_email, subject, body) + + def buildNotifyAuthorizationHeader(self): + token = self.getAccessTokenJansServer(self.AS_ENDPOINT, self.AS_CLIENT_ID, self.AS_CLIENT_SECRET) + authorizationHeader = "Bearer %s" % token + + return authorizationHeader + + def getAccessTokenJansServer(self, asBaseUrl, asClientId, asClientSecret): + endpointUrl = asBaseUrl + "/jans-auth/restv1/token" + + body = "grant_type=client_credentials&scope=https://api.gluu.org/auth/scopes/scan.supergluu" + + authData = base64.b64encode(("%s:%s" % (asClientId, asClientSecret)).encode('utf-8')) + headers = {"Accept" : "application/json"} + + try: + httpService = CdiUtil.bean(HttpService2) + httpClient = httpService.getHttpsClient() + resultResponse = httpService.executePost(httpClient, endpointUrl, authData, headers, body, ContentType.APPLICATION_FORM_URLENCODED) + httpResponse = resultResponse.getHttpResponse() + httpResponseStatusCode = httpResponse.getStatusLine().getStatusCode() + print "Super-Gluu. Scan. Get token response status code: %s" % httpResponseStatusCode + + if not httpService.isResponseStastusCodeOk(httpResponse): + print "Super-Gluu. Scan. Get invalid token response" + httpService.consume(httpResponse) + return False + + bytes = httpService.getResponseContent(httpResponse) + + response = httpService.convertEntityToString(bytes) + except: + print "Super-Gluu. Scan. Failed to send token request: ", sys.exc_info()[1] + return False + + response_data = json.loads(response) + + access_token = response_data["access_token"]; + if StringHelper.isEmpty(access_token): + print "Super-Gluu. Scan. Faield to get access token" + return None + + return access_token + + def registerScanClient(self, asBaseUrl, asRedirectUri, asSSA, customScript): + print "Super-Gluu. Scan. Attempting to register client" + + redirect_str = "[\"%s\"]" % asRedirectUri + data_org = {'redirect_uris': json.loads(redirect_str), + 'software_statement': asSSA} + body = json.dumps(data_org) + + endpointUrl = asBaseUrl + "/jans-auth/restv1/register" + headers = {"Accept" : "application/json"} + + try: + httpService = CdiUtil.bean(HttpService2) + httpClient = httpService.getHttpsClient() + resultResponse = httpService.executePost(httpClient, endpointUrl, None, headers, body, ContentType.APPLICATION_JSON) + httpResponse = resultResponse.getHttpResponse() + httpResponseStatusCode = httpResponse.getStatusLine().getStatusCode() + print "Super-Gluu. Scan. Get client registration response status code: %s" % httpResponseStatusCode + + if not httpService.isResponseStastusCodeOk(httpResponse): + print "Super-Gluu. Scan. Get invalid registration" + httpService.consume(httpResponse) + return None + + bytes = httpService.getResponseContent(httpResponse) + + response = httpService.convertEntityToString(bytes) + except: + print "Super-Gluu. Scan. Failed to send client registration request: ", sys.exc_info()[1] + return None + + response_data = json.loads(response) + client_id = response_data["client_id"] + client_secret = response_data["client_secret"] + + print "Super-Gluu. Scan. Registered client: %s" % client_id + + print "Super-Gluu. Scan. Attempting to store client credentials in script parameters" + try: + custScriptService = CdiUtil.bean(CustomScriptService) + customScript = custScriptService.getScriptByDisplayName(customScript.getName()) + for conf in customScript.getConfigurationProperties(): + if (StringHelper.equalsIgnoreCase(conf.getValue1(), "AS_CLIENT_ID")): + conf.setValue2(client_id) + elif (StringHelper.equalsIgnoreCase(conf.getValue1(), "AS_CLIENT_SECRET")): + conf.setValue2(client_secret) + custScriptService.update(customScript) + + print "Super-Gluu. Scan. Stored client credentials in script parameters" + except: + print "Super-Gluu. Scan. Failed to store client credentials.", sys.exc_info()[1] + return None + + return {'client_id' : client_id, 'client_secret' : client_secret} diff --git a/oxAuth/Server/integrations/super_gluu/repository/com/google/gcm/gcm-server/1.0.1.gluu/gcm-server-1.0.1.gluu.jar b/oxAuth/Server/integrations/super_gluu/repository/com/google/gcm/gcm-server/1.0.1.gluu/gcm-server-1.0.1.gluu.jar new file mode 100644 index 0000000000000000000000000000000000000000..0af9d618a8ee7bbf1341e46e944e6f74e03a25d0 GIT binary patch literal 28229 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 sLir>5&z=%*(esV}k4gVPzkOy@2Hd| + + 4.0.0 + com.google.gcm + gcm-server + 1.0.1.gluu + diff --git a/oxAuth/Server/integrations/super_gluu/repository/com/google/gcm/gcm-server/1.0.1.gluu/gcm-server-1.0.1.gluu.pom.md5 b/oxAuth/Server/integrations/super_gluu/repository/com/google/gcm/gcm-server/1.0.1.gluu/gcm-server-1.0.1.gluu.pom.md5 new file mode 100644 index 00000000..6126b5f4 --- /dev/null +++ b/oxAuth/Server/integrations/super_gluu/repository/com/google/gcm/gcm-server/1.0.1.gluu/gcm-server-1.0.1.gluu.pom.md5 @@ -0,0 +1 @@ +3e2f5fe7273ec568da5d8daab6cb99a3 \ No newline at end of file diff --git a/oxAuth/Server/integrations/super_gluu/repository/com/google/gcm/gcm-server/1.0.1.gluu/gcm-server-1.0.1.gluu.pom.sha1 b/oxAuth/Server/integrations/super_gluu/repository/com/google/gcm/gcm-server/1.0.1.gluu/gcm-server-1.0.1.gluu.pom.sha1 new file mode 100644 index 00000000..eb0541fd --- /dev/null +++ b/oxAuth/Server/integrations/super_gluu/repository/com/google/gcm/gcm-server/1.0.1.gluu/gcm-server-1.0.1.gluu.pom.sha1 @@ -0,0 +1 @@ +82419a7d510b6809d6e34dcf84b12fccd3d63ab1 \ No newline at end of file diff --git a/oxAuth/Server/integrations/super_gluu/repository/com/google/gcm/gcm-server/maven-metadata.xml b/oxAuth/Server/integrations/super_gluu/repository/com/google/gcm/gcm-server/maven-metadata.xml new file mode 100644 index 00000000..3fe315a7 --- /dev/null +++ b/oxAuth/Server/integrations/super_gluu/repository/com/google/gcm/gcm-server/maven-metadata.xml @@ -0,0 +1,12 @@ + + + com.google.gcm + gcm-server + + 1.0.1.gluu + + 1.0.1.gluu + + 20160713194346 + + diff --git a/oxAuth/Server/integrations/super_gluu/repository/com/google/gcm/gcm-server/maven-metadata.xml.md5 b/oxAuth/Server/integrations/super_gluu/repository/com/google/gcm/gcm-server/maven-metadata.xml.md5 new file mode 100644 index 00000000..bd2630a9 --- /dev/null +++ b/oxAuth/Server/integrations/super_gluu/repository/com/google/gcm/gcm-server/maven-metadata.xml.md5 @@ -0,0 +1 @@ +6ed9b8a162275e13f176b718271fabac \ No newline at end of file diff --git a/oxAuth/Server/integrations/super_gluu/repository/com/google/gcm/gcm-server/maven-metadata.xml.sha1 b/oxAuth/Server/integrations/super_gluu/repository/com/google/gcm/gcm-server/maven-metadata.xml.sha1 new file mode 100644 index 00000000..4083cf36 --- /dev/null +++ b/oxAuth/Server/integrations/super_gluu/repository/com/google/gcm/gcm-server/maven-metadata.xml.sha1 @@ -0,0 +1 @@ +2c9a252aa4d788e7702cac5dafb31e0dd6393acf \ No newline at end of file diff --git a/oxAuth/Server/integrations/super_gluu/sample/super_gluu_creds.json b/oxAuth/Server/integrations/super_gluu/sample/super_gluu_creds.json new file mode 100644 index 00000000..4f19782c --- /dev/null +++ b/oxAuth/Server/integrations/super_gluu/sample/super_gluu_creds.json @@ -0,0 +1,45 @@ +{ + "android":{ + "gcm":{ + "enabled":false, + "api_key":"" + }, + "sns":{ + "enabled":false, + "platform_arn":"arn:aws:sns:..." + }, + "gluu":{ + "enabled":true, + "access_key":"36WH2JiexBOoAIBP", + "secret_access_key":"ueqsU2Dc7m3r4HmLz4M79DpzzCNqTfek" + "platform_id":"ce_gcm" + } + }, + "ios":{ + "apns":{ + "enabled":false, + "p12_file_path":"/etc/certs/SuperGluu-NotificationCertificate.p12", + "p12_password":"password", + "production":false + }, + "sns":{ + "enabled":false, + "platform_arn":"arn:aws:sns:...", + "production":false + }, + "gluu":{ + "enabled":true, + "access_key":"auONAdePWoYFBX6V", + "secret_access_key":"f050aW0nnihym0GwktWd7O15jGSQcoei" + "platform_id":"ce_apns" + } + }, + "sns":{ + "access_key":"", + "secret_key":"", + "region":"" + }, + "gluu":{ + "server_uri":"https://cloud.gluu.org/scan/push-api-server" + } +} diff --git a/oxAuth/Server/integrations/twilio_sms/README.txt b/oxAuth/Server/integrations/twilio_sms/README.txt new file mode 100644 index 00000000..3ca0409d --- /dev/null +++ b/oxAuth/Server/integrations/twilio_sms/README.txt @@ -0,0 +1,7 @@ +Twilio SMS Authentication Script + +This is a two step authentication workflow. The first step is standard username password authentication +against the local Gluu Server LDAP. The second step requires the person to enter a code that is sent via +SMS to the person's mobile number. + +See all details at https://gluu.org/docs/ce/authn-guide/sms-otp/ diff --git a/oxAuth/Server/integrations/twilio_sms/twilio2FA.py b/oxAuth/Server/integrations/twilio_sms/twilio2FA.py new file mode 100644 index 00000000..0e1ac16d --- /dev/null +++ b/oxAuth/Server/integrations/twilio_sms/twilio2FA.py @@ -0,0 +1,254 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2018, Gluu +# +# Author: Jose Gonzalez +# Author: Gasmyr Mougang + +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.service import AuthenticationService +from org.gluu.oxauth.service.common import UserService +from org.gluu.oxauth.service import SessionIdService +from org.gluu.oxauth.util import ServerUtil +from org.gluu.util import StringHelper, ArrayHelper +from java.util import Arrays +from javax.faces.application import FacesMessage +from org.gluu.jsf2.message import FacesMessages + +import com.twilio.Twilio as Twilio +import com.twilio.rest.api.v2010.account.Message as Message +import com.twilio.type.PhoneNumber as PhoneNumber +import org.codehaus.jettison.json.JSONArray as JSONArray + +import java +import random +import jarray +import sys + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + self.mobile_number = None + self.identity = CdiUtil.bean(Identity) + + def init(self, customScript, configurationAttributes): + print "==============================================" + print "===TWILIO SMS INITIALIZATION==================" + print "==============================================" + self.ACCOUNT_SID = None + self.AUTH_TOKEN = None + self.FROM_NUMBER = None + + # Get Custom Properties + try: + self.ACCOUNT_SID = configurationAttributes.get("twilio_sid").getValue2() + except: + print 'TwilioSMS, Missing required configuration attribute "twilio_sid"' + + try: + self.AUTH_TOKEN = configurationAttributes.get("twilio_token").getValue2() + except: + print'TwilioSMS, Missing required configuration attribute "twilio_token"' + try: + self.FROM_NUMBER = configurationAttributes.get("from_number").getValue2() + except: + print'TwilioSMS, Missing required configuration attribute "from_number"' + + if None in (self.ACCOUNT_SID, self.AUTH_TOKEN, self.FROM_NUMBER): + print "twilio_sid, twilio_token, from_number is empty ... returning False" + return False + + print "===TWILIO SMS INITIALIZATION DONE PROPERLY=====" + return True + + def destroy(self, configurationAttributes): + print "Twilio SMS. Destroy" + print "Twilio SMS. Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + print "==============================================" + print "====TWILIO SMS AUTHENCATION===================" + print "==============================================" + userService = CdiUtil.bean(UserService) + authenticationService = CdiUtil.bean(AuthenticationService) + sessionIdService = CdiUtil.bean(SessionIdService) + facesMessages = CdiUtil.bean(FacesMessages) + facesMessages.setKeepMessages() + + session_attributes = self.identity.getSessionId().getSessionAttributes() + form_passcode = ServerUtil.getFirstValue(requestParameters, "passcode") + form_name = ServerUtil.getFirstValue(requestParameters, "TwilioSmsloginForm") + + print "TwilioSMS. form_response_passcode: %s" % str(form_passcode) + + if step == 1: + print "==============================================" + print "=TWILIO SMS STEP 1 | Password Authentication==" + print "==============================================" + credentials = self.identity.getCredentials() + user_name = credentials.getUsername() + user_password = credentials.getPassword() + logged_in = False + if StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password): + logged_in = authenticationService.authenticate(user_name, user_password) + + if not logged_in: + return False + + # Get the Person's number and generate a code + foundUser = None + try: + foundUser = authenticationService.getAuthenticatedUser() + except: + print 'TwilioSMS, Error retrieving user %s from LDAP' % (user_name) + return False + + try: + isVerified = foundUser.getAttribute("phoneNumberVerified") + if isVerified: + self.mobile_number = foundUser.getAttribute("employeeNumber") + if self.mobile_number is None: + mobile_numbers_json = foundUser.getAttribute("mobile", True, True) + if mobile_numbers_json is not None: + if mobile_numbers_json is not None: + self.mobile_number = mobile_numbers_json.get(0) + if self.mobile_number is None: + self.mobile_number = foundUser.getAttribute("telephoneNumber") + if self.mobile_number is None: + print "TwilioSMS, Error finding mobile number for user '%s'" % user_name + + except: + facesMessages.add(FacesMessage.SEVERITY_ERROR, "Failed to determine mobile phone number") + print "TwilioSMS, Error finding mobile number for '%s'. Exception: '%s'" % (user_name, sys.exc_info()[1]) + return False + + # Generate Random six digit code and store it in array + code = random.randint(100000, 999999) + + # Get code and save it in LDAP temporarily with special session entry + self.identity.setWorkingParameter("code", code) + sessionId = sessionIdService.getSessionId() # fetch from persistence + sessionId.getSessionAttributes().put("code", code) + + try: + Twilio.init(self.ACCOUNT_SID, self.AUTH_TOKEN); + message = Message.creator(PhoneNumber(self.mobile_number), PhoneNumber(self.FROM_NUMBER), str(code)).create(); + print "++++++++++++++++++++++++++++++++++++++++++++++" + print 'TwilioSMs, Message Sid: %s' % (message.getSid()) + print 'TwilioSMs, User phone: %s' % (self.mobile_number) + print "++++++++++++++++++++++++++++++++++++++++++++++" + sessionId.getSessionAttributes().put("mobile_number", self.mobile_number) + sessionId.getSessionAttributes().put("mobile", self.mobile_number) + sessionIdService.updateSessionId(sessionId) + self.identity.setWorkingParameter("mobile_number", self.mobile_number) + self.identity.getSessionId().getSessionAttributes().put("mobile_number",self.mobile_number) + self.identity.setWorkingParameter("mobile", self.mobile_number) + self.identity.getSessionId().getSessionAttributes().put("mobile",self.mobile_number) + print "++++++++++++++++++++++++++++++++++++++++++++++" + print "Number: %s" % (self.identity.getWorkingParameter("mobile_number")) + print "Mobile: %s" % (self.identity.getWorkingParameter("mobile")) + print "++++++++++++++++++++++++++++++++++++++++++++++" + print "========================================" + print "===TWILIO SMS FIRST STEP DONE PROPERLY==" + print "========================================" + return True + except Exception, ex: + facesMessages.add(FacesMessage.SEVERITY_ERROR, "Failed to send message to mobile phone") + print "TwilioSMS. Error sending message to Twilio" + print "TwilioSMS. Unexpected error:", ex + + return False + elif step == 2: + # Retrieve the session attribute + print "==============================================" + print "=TWILIO SMS STEP 2 | Password Authentication==" + print "==============================================" + code = session_attributes.get("code") + print '=======> Session code is "%s"' % str(code) + sessionIdService = CdiUtil.bean(SessionIdService) + sessionId = sessionIdService.getSessionId() # fetch from persistence + code = sessionId.getSessionAttributes().get("code") + print '=======> Database code is "%s"' % str(code) + self.identity.setSessionId(sessionId) + print "==============================================" + print "TwilioSMS. Code: %s" % str(code) + print "==============================================" + if code is None: + print "TwilioSMS. Failed to find previously sent code" + return False + + if form_passcode is None: + print "TwilioSMS. Passcode is empty" + return False + + if len(form_passcode) != 6: + print "TwilioSMS. Passcode from response is not 6 digits: %s" % form_passcode + return False + + if form_passcode == code: + print "TiwlioSMS, SUCCESS! User entered the same code!" + print "========================================" + print "===TWILIO SMS SECOND STEP DONE PROPERLY" + print "========================================" + return True + + print "+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++" + print "TwilioSMS. FAIL! User entered the wrong code! %s != %s" % (form_passcode, code) + print "+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++" + facesMessages.add(FacesMessage.SEVERITY_ERROR, "Incorrect Twilio code, please try again.") + print "================================================" + print "===TWILIO SMS SECOND STEP FAILED: INCORRECT CODE" + print "================================================" + return False + + print "TwilioSMS. ERROR: step param not found or != (1|2)" + + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + if step == 1: + print "TwilioSMS. Prepare for Step 1" + return True + elif step == 2: + print "TwilioSMS. Prepare for Step 2" + return True + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + if step == 2: + return Arrays.asList("code") + + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + return 2 + + def getPageForStep(self, configurationAttributes, step): + if step == 2: + return "/auth/otp_sms/otp_sms.xhtml" + + return "" + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True diff --git a/oxAuth/Server/integrations/u2f/U2fExternalAuthenticator.py b/oxAuth/Server/integrations/u2f/U2fExternalAuthenticator.py new file mode 100644 index 00000000..95079b88 --- /dev/null +++ b/oxAuth/Server/integrations/u2f/U2fExternalAuthenticator.py @@ -0,0 +1,214 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2016, Gluu +# +# Author: Yuriy Movchan +# + +import java +import sys +from javax.ws.rs.core import Response +from javax.ws.rs import WebApplicationException +from javax.ws.rs import ClientErrorException +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.client.fido.u2f import FidoU2fClientFactory +from org.gluu.oxauth.model.config import Constants +from org.gluu.oxauth.security import Identity +from org.gluu.oxauth.service import AuthenticationService, SessionIdService +from org.gluu.oxauth.service.common import UserService +from org.gluu.oxauth.service.fido.u2f import DeviceRegistrationService +from org.gluu.oxauth.util import ServerUtil +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.util import StringHelper + + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "U2F. Initialization" + + print "U2F. Initialization. Downloading U2F metadata" + u2f_server_uri = configurationAttributes.get("u2f_server_uri").getValue2() + u2f_server_metadata_uri = u2f_server_uri + "/.well-known/fido-u2f-configuration" + + metaDataConfigurationService = FidoU2fClientFactory.instance().createMetaDataConfigurationService(u2f_server_metadata_uri) + + max_attempts = 20 + for attempt in range(1, max_attempts + 1): + try: + self.metaDataConfiguration = metaDataConfigurationService.getMetadataConfiguration() + break + except WebApplicationException, ex: + # Detect if last try or we still get Service Unavailable HTTP error + if (attempt == max_attempts) or (ex.getResponse().getStatus() != Response.Status.SERVICE_UNAVAILABLE.getStatusCode()): + raise ex + + java.lang.Thread.sleep(3000) + print "Attempting to load metadata: %d" % attempt + + print "U2F. Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "U2F. Destroy" + print "U2F. Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + authenticationService = CdiUtil.bean(AuthenticationService) + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + user_name = credentials.getUsername() + + if (step == 1): + print "U2F. Authenticate for step 1" + + user_password = credentials.getPassword() + logged_in = False + if (StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password)): + userService = CdiUtil.bean(UserService) + logged_in = authenticationService.authenticate(user_name, user_password) + + if (not logged_in): + return False + + return True + elif (step == 2): + print "U2F. Authenticate for step 2" + + token_response = ServerUtil.getFirstValue(requestParameters, "tokenResponse") + if token_response == None: + print "U2F. Authenticate for step 2. tokenResponse is empty" + return False + + auth_method = ServerUtil.getFirstValue(requestParameters, "authMethod") + if auth_method == None: + print "U2F. Authenticate for step 2. authMethod is empty" + return False + + authenticationService = CdiUtil.bean(AuthenticationService) + user = authenticationService.getAuthenticatedUser() + if (user == None): + print "U2F. Prepare for step 2. Failed to determine user name" + return False + + if (auth_method == 'authenticate'): + print "U2F. Prepare for step 2. Call FIDO U2F in order to finish authentication workflow" + authenticationRequestService = FidoU2fClientFactory.instance().createAuthenticationRequestService(self.metaDataConfiguration) + authenticationStatus = authenticationRequestService.finishAuthentication(user.getUserId(), token_response) + + if (authenticationStatus.getStatus() != Constants.RESULT_SUCCESS): + print "U2F. Authenticate for step 2. Get invalid authentication status from FIDO U2F server" + return False + + return True + elif (auth_method == 'enroll'): + print "U2F. Prepare for step 2. Call FIDO U2F in order to finish registration workflow" + registrationRequestService = FidoU2fClientFactory.instance().createRegistrationRequestService(self.metaDataConfiguration) + registrationStatus = registrationRequestService.finishRegistration(user.getUserId(), token_response) + + if (registrationStatus.getStatus() != Constants.RESULT_SUCCESS): + print "U2F. Authenticate for step 2. Get invalid registration status from FIDO U2F server" + return False + + return True + else: + print "U2F. Prepare for step 2. Authenticatiod method is invalid" + return False + + return False + else: + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + identity = CdiUtil.bean(Identity) + + if (step == 1): + return True + elif (step == 2): + print "U2F. Prepare for step 2" + + session = CdiUtil.bean(SessionIdService).getSessionId() + if session == None: + print "U2F. Prepare for step 2. Failed to determine session_id" + return False + + authenticationService = CdiUtil.bean(AuthenticationService) + user = authenticationService.getAuthenticatedUser() + if (user == None): + print "U2F. Prepare for step 2. Failed to determine user name" + return False + + u2f_application_id = configurationAttributes.get("u2f_application_id").getValue2() + + # Check if user have registered devices + deviceRegistrationService = CdiUtil.bean(DeviceRegistrationService) + + userInum = user.getAttribute("inum") + + registrationRequest = None + authenticationRequest = None + + deviceRegistrations = deviceRegistrationService.findUserDeviceRegistrations(userInum, u2f_application_id) + if (deviceRegistrations.size() > 0): + print "U2F. Prepare for step 2. Call FIDO U2F in order to start authentication workflow" + + try: + authenticationRequestService = FidoU2fClientFactory.instance().createAuthenticationRequestService(self.metaDataConfiguration) + authenticationRequest = authenticationRequestService.startAuthentication(user.getUserId(), None, u2f_application_id, session.getId()) + except ClientErrorException, ex: + if (ex.getResponse().getResponseStatus() != Response.Status.NOT_FOUND): + print "U2F. Prepare for step 2. Failed to start authentication workflow. Exception:", sys.exc_info()[1] + return False + else: + print "U2F. Prepare for step 2. Call FIDO U2F in order to start registration workflow" + registrationRequestService = FidoU2fClientFactory.instance().createRegistrationRequestService(self.metaDataConfiguration) + registrationRequest = registrationRequestService.startRegistration(user.getUserId(), u2f_application_id, session.getId()) + + identity.setWorkingParameter("fido_u2f_authentication_request", ServerUtil.asJson(authenticationRequest)) + identity.setWorkingParameter("fido_u2f_registration_request", ServerUtil.asJson(registrationRequest)) + + return True + elif (step == 3): + print "U2F. Prepare for step 3" + + return True + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + return 2 + + def getPageForStep(self, configurationAttributes, step): + if (step == 2): + return "/auth/u2f/login.xhtml" + + return "" + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True diff --git a/oxAuth/Server/integrations/uaf/Installation.md b/oxAuth/Server/integrations/uaf/Installation.md new file mode 100644 index 00000000..d2cf46ca --- /dev/null +++ b/oxAuth/Server/integrations/uaf/Installation.md @@ -0,0 +1,48 @@ +This list of steps needed to do to enable SAML person authentication module. + +1. Configure apache HTTP proxy to access UAF server: + - Make sure that enabled next apache2 plugins: proxy, proxy_http, ssl + To enable them use next commands: + * a2enmod proxy + * a2enmod proxy_http + * a2enmod ssl + - Add to Apache2 https_gluu.conf file next lines: +``` + SSLProxyEngine on + SSLProxyCheckPeerCN on + SSLProxyCheckPeerExpire on + + + ProxyPass https://evaluation4.noknoktest.com:8443/nnl retry=5 disablereuse=On + ProxyPassReverse https://evaluation4.noknoktest.com:8443/nnl + Order allow,deny + Allow from all + +``` + Proxy between UAF and oxAuth sever is needed because UAF server not supports CORS operations. In this configuration we uses evaluation server. + +2. Confire new custom module in oxTrust: + - Log into oxTrust with administrative permissions. + - Open "Configuration→Manage Custom Scripts" page. + - Select "Person Authentication" tab. + - Click on "Add custom script configuration" link. + - Enter name = uaf + - Enter level = 0-100 (priority of this method). + - Select usage type "Interactive". + - Add custom required and optional properties which specified in "Properties description.md". + - Copy/paste script from UafExternalAuthenticator.py. + - Activate it via "Enabled" checkbox. + - Click "Update" button at the bottom of this page. + +3. Configure oxAuth to use UAF authentication by default: + - Log into oxTrust with administrative permissions. + - Open "Configuration→Manage Authentication" page. + - Scroll to "Default Authentication Method" panel. Select "uaf" authentication mode. + - Click "Update" button at the bottom of this page. + +4. Try to log in using UAF authentication method: + - Wait 30 seconds and try to log in again. During this time oxAuth reload list of available person authentication modules. + - Open second browser or second browsing session and try to log in again. It's better to try to do that from another browser session because we can return back to previous authentication method if something will go wrong. + +There are log messages in this custom authentication script. In order to debug this module we can use command like this: +tail -f /opt/tomcat/logs/wrapper.log | grep "UAF" diff --git a/oxAuth/Server/integrations/uaf/Properties description.md b/oxAuth/Server/integrations/uaf/Properties description.md new file mode 100644 index 00000000..5c7e6385 --- /dev/null +++ b/oxAuth/Server/integrations/uaf/Properties description.md @@ -0,0 +1,20 @@ +This is a person authentication module for oxAuth that enables [UAF](https://www.noknok.com) for user authentication. + +The module has a few properties: + +1) uaf_server_uri - It's mandatory property. It's URL to UAF server. + Example: https://ce-dev.gluu.org + +2) uaf_policy_name - Specify UAF policy name. It's optional property. + Example: default + +3) send_push_notifaction - Specify if UAF server should send push notifications to person mobile phone. + It's optional property. + Allowed values: true/false + Example: false + +4) registration_uri - It's URL to page where user can register new account. It's optional property. + Example: https://ce-dev.gluu.org/identity/register + +5) qr_options - Specify width and height of QR image. It's optional property. + Example: qr_options: { width: 400, height: 400 } diff --git a/oxAuth/Server/integrations/uaf/Readme.md b/oxAuth/Server/integrations/uaf/Readme.md new file mode 100644 index 00000000..8073ff5b --- /dev/null +++ b/oxAuth/Server/integrations/uaf/Readme.md @@ -0,0 +1,51 @@ +# FIDO UAF Authenticator + +UAF allows applications to take advantage of the security capabilities of modern devices such as fingerprint, iris, and voice biometrics. It provides a unified infrastructure that allows you to integrate these capabilities in a simple manner to enable authentication that is both more user friendly and more secure than passwords. + +## Typical UAF architecture + +The following diagram provides an overview of the UAF infrastructure, which contains (1) components that resides on the user’s device, and (2) server side components that connect to a UAF server. The typical gateway between these two parts is a mobile browser with a UAF plugin. The RP should provide proxy capabilities to deliver messages from the mobile browser plugin to the UAF server. + +![Typical UAF design](./img/typical_uaf_architecture.png) + +It's not very convenient when the RP is a second device. Non Nok offers an Out-of-band (OOB) API which simplifies UAF integration in this case. + +## Out-of-Band Authentication +Out-of-Band authentication allows mobile device authenticatation with UAF even on devices that +do not have any UAF components installed. Using this workflow, the user of a laptop authenticates +to a web application using their mobile device. The user binds the browser session to his or her +mobile device by scanning a QR code, or by triggering a push notification. The user then performs +the UAF authentication on an out-of-band channel between the mobile device and the Nok Nok +Authentication Server. Once the user authenticates successfully, the Authentication Server +notifies your application server. OOB is a proprietary feature developed by Nok Nok Labs on top +of the FIDO UAF protocol. + +![OOB with QR codes](./img/oob_qr_code.png) + +Also it allows push notification messages to be sent by the platform. + +## Device integration models + +This is not a part of the UAF authentication script, but it shows the modular +architecture of the UAF mobile authentication stack. + +Some devices will feature a preloaded UAF Client and one or more UAF ASMs. In some cases, +devices may only feature a preloaded UAF ASM, rather than both a UAF Client and ASM. Older +legacy devices may feature neither a UAF Client nor a UAF ASM. Nevertheless the App SDK +can support each of these three scenarios illustrated below from the same mobile application. + +![Typical UAF design](./img/uaf_device_integration_models.png) + + +## Integration with oxAuth + +The oxAuth UAF integration leverages the Person Authentication module. This workflow shows +the communication process between the components. + +![Typical UAF design](./img/gluu_uaf_integration_authentication_workflow.png) + +## Person authentication module activation + +This module is part of CE. It has only one mandatory property "uaf_server_uri". There are more information about module configuration in "Installation.md" and "Properties description.md" + +Nok Nok SDK contains sample application which allows to test script. In the SDK there are binaries and source code of this application. diff --git a/oxAuth/Server/integrations/uaf/UafExternalAuthenticator.py b/oxAuth/Server/integrations/uaf/UafExternalAuthenticator.py new file mode 100644 index 00000000..73319124 --- /dev/null +++ b/oxAuth/Server/integrations/uaf/UafExternalAuthenticator.py @@ -0,0 +1,398 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2016, Gluu +# +# Author: Yuriy Movchan +# + +# Requires the following custom properties and values: +# uaf_server_uri: +# +# These are non mandatory custom properties and values: +# uaf_policy_name: default +# send_push_notifaction: false +# registration_uri: https:///identity/register +# qr_options: { width: 400, height: 400 } + +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.oxauth.service import AuthenticationService, SessionIdService +from org.gluu.oxauth.service.common import UserService +from org.gluu.util import StringHelper, ArrayHelper +from org.gluu.oxauth.util import ServerUtil +from org.gluu.oxauth.model.config import Constants +from javax.ws.rs.core import Response +from java.util import Arrays +from org.gluu.oxauth.service.net import HttpService +from org.apache.http.params import CoreConnectionPNames + +import sys +import java +import json + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "UAF. Initialization" + + if not configurationAttributes.containsKey("uaf_server_uri"): + print "UAF. Initialization. Property uaf_server_uri is mandatory" + return False + + self.uaf_server_uri = configurationAttributes.get("uaf_server_uri").getValue2() + + self.uaf_policy_name = "default" + if configurationAttributes.containsKey("uaf_policy_name"): + self.uaf_policy_name = configurationAttributes.get("uaf_policy_name").getValue2() + + self.send_push_notifaction = False + if configurationAttributes.containsKey("send_push_notifaction"): + self.send_push_notifaction = StringHelper.toBoolean(configurationAttributes.get("send_push_notifaction").getValue2(), False) + + self.registration_uri = None + if configurationAttributes.containsKey("registration_uri"): + self.registration_uri = configurationAttributes.get("registration_uri").getValue2() + + self.customQrOptions = {} + if configurationAttributes.containsKey("qr_options"): + self.customQrOptions = configurationAttributes.get("qr_options").getValue2() + + print "UAF. Initializing HTTP client" + httpService = CdiUtil.bean(HttpService) + self.http_client = httpService.getHttpsClient() + http_client_params = self.http_client.getParams() + http_client_params.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 15 * 1000) + + print "UAF. Initialized successfully. uaf_server_uri: '%s', uaf_policy_name: '%s', send_push_notifaction: '%s', registration_uri: '%s', qr_options: '%s'" % (self.uaf_server_uri, self.uaf_policy_name, self.send_push_notifaction, self.registration_uri, self.customQrOptions) + + print "UAF. Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "UAF. Destroy" + print "UAF. Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + session_attributes = identity.getSessionId().getSessionAttributes() + + self.setRequestScopedParameters(identity) + + if (step == 1): + print "UAF. Authenticate for step 1" + + user_name = credentials.getUsername() + + authenticated_user = self.processBasicAuthentication(credentials) + if authenticated_user == None: + return False + + uaf_auth_method = "authenticate" + # Uncomment this block if you need to allow user second device registration + #enrollment_mode = ServerUtil.getFirstValue(requestParameters, "loginForm:registerButton") + #if StringHelper.isNotEmpty(enrollment_mode): + # uaf_auth_method = "enroll" + + if uaf_auth_method == "authenticate": + user_enrollments = self.findEnrollments(credentials) + if len(user_enrollments) == 0: + uaf_auth_method = "enroll" + print "UAF. Authenticate for step 1. There is no UAF enrollment for user '%s'. Changing uaf_auth_method to '%s'" % (user_name, uaf_auth_method) + + print "UAF. Authenticate for step 1. uaf_auth_method: '%s'" % uaf_auth_method + + identity.setWorkingParameter("uaf_auth_method", uaf_auth_method) + + return True + elif (step == 2): + print "UAF. Authenticate for step 2" + + session = CdiUtil.bean(SessionIdService).getSessionId() + if session == None: + print "UAF. Prepare for step 2. Failed to determine session_id" + return False + + user = authenticationService.getAuthenticatedUser() + if (user == None): + print "UAF. Authenticate for step 2. Failed to determine user name" + return False + user_name = user.getUserId() + + uaf_auth_result = ServerUtil.getFirstValue(requestParameters, "auth_result") + if uaf_auth_result != "success": + print "UAF. Authenticate for step 2. auth_result is '%s'" % uaf_auth_result + return False + + # Restore state from session + uaf_auth_method = session_attributes.get("uaf_auth_method") + + if not uaf_auth_method in ['enroll', 'authenticate']: + print "UAF. Authenticate for step 2. Failed to authenticate user. uaf_auth_method: '%s'" % uaf_auth_method + return False + + # Request STATUS_OBB + if True: + #TODO: Remove this condition + # It's workaround becuase it's not possible to call STATUS_OBB 2 times. First time on browser and second ime on server + uaf_user_device_handle = ServerUtil.getFirstValue(requestParameters, "auth_handle") + else: + uaf_obb_auth_method = session_attributes.get("uaf_obb_auth_method") + uaf_obb_server_uri = session_attributes.get("uaf_obb_server_uri") + uaf_obb_start_response = session_attributes.get("uaf_obb_start_response") + + # Prepare STATUS_OBB + uaf_obb_start_response_json = json.loads(uaf_obb_start_response) + uaf_obb_status_request_dictionary = { "operation": "STATUS_%s" % uaf_obb_auth_method, + "userName": user_name, + "needDetails": 1, + "oobStatusHandle": uaf_obb_start_response_json["oobStatusHandle"], + } + + uaf_obb_status_request = json.dumps(uaf_obb_status_request_dictionary, separators=(',',':')) + print "UAF. Authenticate for step 2. Prepared STATUS request: '%s' to send to '%s'" % (uaf_obb_status_request, uaf_obb_server_uri) + + uaf_status_obb_response = self.executePost(uaf_obb_server_uri, uaf_obb_status_request) + if uaf_status_obb_response == None: + return False + + print "UAF. Authenticate for step 2. Get STATUS response: '%s'" % uaf_status_obb_response + uaf_status_obb_response_json = json.loads(uaf_status_obb_response) + + if uaf_status_obb_response_json["statusCode"] != 4000: + print "UAF. Authenticate for step 2. UAF operation status is invalid. statusCode: '%s'" % uaf_status_obb_response_json["statusCode"] + return False + + uaf_user_device_handle = uaf_status_obb_response_json["additionalInfo"]["authenticatorsResult"]["handle"] + + if StringHelper.isEmpty(uaf_user_device_handle): + print "UAF. Prepare for step 2. Failed to get UAF handle" + return False + + uaf_user_external_uid = "uaf:%s" % uaf_user_device_handle + print "UAF. Authenticate for step 2. UAF handle: '%s'" % uaf_user_external_uid + + if uaf_auth_method == "authenticate": + # Validate if user used device with same keYHandle + user_enrollments = self.findEnrollments(credentials) + if len(user_enrollments) == 0: + uaf_auth_method = "enroll" + print "UAF. Authenticate for step 2. There is no UAF enrollment for user '%s'." % user_name + return False + + for user_enrollment in user_enrollments: + if StringHelper.equalsIgnoreCase(user_enrollment, uaf_user_device_handle): + print "UAF. Authenticate for step 2. There is UAF enrollment for user '%s'. User authenticated successfully" % user_name + return True + else: + userService = CdiUtil.bean(UserService) + + # Double check just to make sure. We did checking in previous step + # Check if there is user which has uaf_user_external_uid + # Avoid mapping user cert to more than one IDP account + find_user_by_external_uid = userService.getUserByAttribute("oxExternalUid", uaf_user_external_uid) + if find_user_by_external_uid == None: + # Add uaf_user_external_uid to user's external GUID list + find_user_by_external_uid = userService.addUserAttribute(user_name, "oxExternalUid", uaf_user_external_uid) + if find_user_by_external_uid == None: + print "UAF. Authenticate for step 2. Failed to update current user" + return False + + return True + + return False + else: + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + authenticationService = CdiUtil.bean(AuthenticationService) + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + session_attributes = identity.getSessionId().getSessionAttributes() + + self.setRequestScopedParameters(identity) + + if (step == 1): + return True + elif (step == 2): + print "UAF. Prepare for step 2" + + session = CdiUtil.bean(SessionIdService).getSessionId() + if session == None: + print "UAF. Prepare for step 2. Failed to determine session_id" + return False + + user = authenticationService.getAuthenticatedUser() + if (user == None): + print "UAF. Prepare for step 2. Failed to determine user name" + return False + + uaf_auth_method = session_attributes.get("uaf_auth_method") + if StringHelper.isEmpty(uaf_auth_method): + print "UAF. Prepare for step 2. Failed to determine auth_method" + return False + + print "UAF. Prepare for step 2. uaf_auth_method: '%s'" % uaf_auth_method + + uaf_obb_auth_method = "OOB_REG" + uaf_obb_server_uri = self.uaf_server_uri + "/nnl/v2/reg" + if StringHelper.equalsIgnoreCase(uaf_auth_method, "authenticate"): + uaf_obb_auth_method = "OOB_AUTH" + uaf_obb_server_uri = self.uaf_server_uri + "/nnl/v2/auth" + + # Prepare START_OBB + uaf_obb_start_request_dictionary = { "operation": "START_%s" % uaf_obb_auth_method, + "userName": user.getUserId(), + "policyName": "default", + "oobMode": + { "qr": "true", "rawData": "false", "push": "false" } + } + + uaf_obb_start_request = json.dumps(uaf_obb_start_request_dictionary, separators=(',',':')) + print "UAF. Prepare for step 2. Prepared START request: '%s' to send to '%s'" % (uaf_obb_start_request, uaf_obb_server_uri) + + # Request START_OBB + uaf_obb_start_response = self.executePost(uaf_obb_server_uri, uaf_obb_start_request) + if uaf_obb_start_response == None: + return False + + print "UAF. Prepare for step 2. Get START response: '%s'" % uaf_obb_start_response + uaf_obb_start_response_json = json.loads(uaf_obb_start_response) + + # Prepare STATUS_OBB + #TODO: Remove needDetails parameter + uaf_obb_status_request_dictionary = { "operation": "STATUS_%s" % uaf_obb_auth_method, + "userName": user.getUserId(), + "needDetails": 1, + "oobStatusHandle": uaf_obb_start_response_json["oobStatusHandle"], + } + + uaf_obb_status_request = json.dumps(uaf_obb_status_request_dictionary, separators=(',',':')) + print "UAF. Prepare for step 2. Prepared STATUS request: '%s' to send to '%s'" % (uaf_obb_status_request, uaf_obb_server_uri) + + identity.setWorkingParameter("uaf_obb_auth_method", uaf_obb_auth_method) + identity.setWorkingParameter("uaf_obb_server_uri", uaf_obb_server_uri) + identity.setWorkingParameter("uaf_obb_start_response", uaf_obb_start_response) + identity.setWorkingParameter("qr_image", uaf_obb_start_response_json["modeResult"]["qrCode"]["qrImage"]) + identity.setWorkingParameter("uaf_obb_status_request", uaf_obb_status_request) + + return True + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + return Arrays.asList("uaf_auth_method", "uaf_obb_auth_method", "uaf_obb_server_uri", "uaf_obb_start_response") + + def getCountAuthenticationSteps(self, configurationAttributes): + return 2 + + def getPageForStep(self, configurationAttributes, step): + if (step == 2): + return "/auth/uaf/login.xhtml" + + return "" + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True + + def setRequestScopedParameters(self, identity): + if self.registration_uri != None: + identity.setWorkingParameter("external_registration_uri", self.registration_uri) + identity.setWorkingParameter("qr_options", self.customQrOptions) + + def processBasicAuthentication(self, credentials): + userService = CdiUtil.bean(UserService) + authenticationService = CdiUtil.bean(AuthenticationService) + + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + logged_in = False + if StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password): + logged_in = authenticationService.authenticate(user_name, user_password) + + if not logged_in: + return None + + find_user_by_uid = authenticationService.getAuthenticatedUser() + if find_user_by_uid == None: + print "UAF. Process basic authentication. Failed to find user '%s'" % user_name + return None + + return find_user_by_uid + + def findEnrollments(self, credentials): + result = [] + + userService = CdiUtil.bean(UserService) + user_name = credentials.getUsername() + user = userService.getUser(user_name, "oxExternalUid") + if user == None: + print "UAF. Find enrollments. Failed to find user" + return result + + user_custom_ext_attribute = userService.getCustomAttribute(user, "oxExternalUid") + if user_custom_ext_attribute == None: + return result + + uaf_prefix = "uaf:" + uaf_prefix_length = len(uaf_prefix) + for user_external_uid in user_custom_ext_attribute.getValues(): + index = user_external_uid.find(uaf_prefix) + if index != -1: + enrollment_uid = user_external_uid[uaf_prefix_length:] + result.append(enrollment_uid) + + return result + + def executePost(self, request_uri, request_data): + httpService = CdiUtil.bean(HttpService) + + request_headers = { "Content-type" : "application/json; charset=UTF-8", "Accept" : "application/json" } + + try: + http_service_response = httpService.executePost(self.http_client, request_uri, None, request_headers, request_data) + http_response = http_service_response.getHttpResponse() + except: + print "UAF. Validate POST response. Exception: ", sys.exc_info()[1] + return None + + try: + if not httpService.isResponseStastusCodeOk(http_response): + print "UAF. Validate POST response. Get invalid response from server: %s" % str(http_response.getStatusLine().getStatusCode()) + httpService.consume(http_response) + return None + + response_bytes = httpService.getResponseContent(http_response) + response_string = httpService.convertEntityToString(response_bytes) + httpService.consume(http_response) + + return response_string + finally: + http_service_response.closeConnection() + return None diff --git a/oxAuth/Server/integrations/uaf/img/gluu_uaf_integration_authentication_workflow.png b/oxAuth/Server/integrations/uaf/img/gluu_uaf_integration_authentication_workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..c247c7641e708987fad8ae7c2e958ce90ec629b9 GIT binary patch literal 59443 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�*(c*zNP6= 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 v+49zPcLJ0bi3!*azy5yC<)z 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

+ + + + + + diff --git a/oxAuth/Server/integrations/wwpass/pages/auth/wwpass/wwpass.xhtml b/oxAuth/Server/integrations/wwpass/pages/auth/wwpass/wwpass.xhtml new file mode 100644 index 00000000..8330f86c --- /dev/null +++ b/oxAuth/Server/integrations/wwpass/pages/auth/wwpass/wwpass.xhtml @@ -0,0 +1,285 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/oxAuth/Server/integrations/wwpass/pages/auth/wwpass/wwpassbind.xhtml b/oxAuth/Server/integrations/wwpass/pages/auth/wwpass/wwpassbind.xhtml new file mode 100644 index 00000000..2b93995a --- /dev/null +++ b/oxAuth/Server/integrations/wwpass/pages/auth/wwpass/wwpassbind.xhtml @@ -0,0 +1,329 @@ + + + + + + + + + + + + + + + + + + + +
+
+
+

#{identity.getWorkingParameter('errors')}

+
+ +
+ +
+ + +
+ +
+ +
+ + +
+ + +
+ +
+ + +
+ + +
+
+
+
+ +
+
+ + + +
+
+ +
+

You presented a new WWPass Key, not associated with any account. If you already have an account, and need to replace your WWPass Key, please follow this link.

+
+
+
+
+ + + +
+ +
+ diff --git a/oxAuth/Server/integrations/wwpass/static/js/wwpass-frontend.js b/oxAuth/Server/integrations/wwpass/static/js/wwpass-frontend.js new file mode 100644 index 00000000..aeb1d82d --- /dev/null +++ b/oxAuth/Server/integrations/wwpass/static/js/wwpass-frontend.js @@ -0,0 +1,6097 @@ +(function () { + 'use strict'; + + function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } + } + + var classCallCheck = _classCallCheck; + + function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } + } + + function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + return Constructor; + } + + var createClass = _createClass; + + function _defineProperty(obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, { + value: value, + enumerable: true, + configurable: true, + writable: true + }); + } else { + obj[key] = value; + } + + return obj; + } + + var defineProperty = _defineProperty; + + function _objectSpread(target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i] != null ? arguments[i] : {}; + var ownKeys = Object.keys(Object(source)); + + if (typeof Object.getOwnPropertySymbols === 'function') { + ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { + return Object.getOwnPropertyDescriptor(source, sym).enumerable; + })); + } + + ownKeys.forEach(function (key) { + defineProperty(target, key, source[key]); + }); + } + + return target; + } + + var objectSpread = _objectSpread; + + /* Constants */ + + /* Status codes */ + var WWPASS_OK_MSG = 'OK'; + var WWPASS_STATUS = { + CONTINUE: 100, + OK: 200, + INTERNAL_ERROR: 400, + ALREADY_PERSONALIZED: 401, + PASSWORD_MISMATCH: 402, + PASSWORD_LOCKOUT: 403, + WRONG_KEY: 404, + WRONG_KEY_SECOND: 405, + NOT_A_KEY: 406, + NOT_A_KEY_SECOND: 407, + KEY_DISABLED: 408, + NOT_ALLOWED: 409, + BLANK_TOKEN: 410, + BLANK_SECOND_TOKEN: 411, + ACTIVITY_PROFILE_LOCKED: 412, + SSL_REQUIRED: 413, + BLANK_NORMAL_TOKEN: 414, + BLANK_SECOND_NORMAL_TOKEN: 415, + BLANK_MASTER_TOKEN: 416, + BLANK_SECOND_MASTER_TOKEN: 417, + NOT_ACTIVATED_TOKEN: 418, + NOT_ACTIVATED_SECOND_TOKEN: 419, + WRONG_KEY_SET: 420, + NO_VERIFIER: 421, + INCOMPLETE_KEYSET: 422, + INVALID_TICKET: 423, + SAME_TOKEN: 424, + NO_RECOVERY_INFO: 425, + BAD_RECOVERY_REQUEST: 426, + RECOVERY_FAILED: 427, + TERMINAL_ERROR: 500, + TERMINAL_NOT_FOUND: 501, + TERMINAL_BAD_REQUEST: 502, + NO_CONNECTION: 503, + NETWORK_ERROR: 504, + PROTOCOL_ERROR: 505, + UNKNOWN_HANDLER: 506, + TERMINAL_CANCELED: 590, + TIMEOUT: 600, + TICKET_TIMEOUT: 601, + USER_REJECT: 603, + NO_AUTH_INTERFACES_FOUND: 604, + TERMINAL_TIMEOUT: 605, + UNSUPPORTED_PLATFORM: 606 + }; + var WWPASS_NO_AUTH_INTERFACES_FOUND_MSG = 'No WWPass SecurityPack is found on your computer or WWPass Browser Plugin is disabled'; + var WWPASS_UNSUPPORTED_PLATFORM_MSG_TMPL = 'WWPass authentication is not supported on'; + var WWPASS_KEY_TYPE_PASSKEY = 'passkey'; + var WWPASS_KEY_TYPE_DEFAULT = WWPASS_KEY_TYPE_PASSKEY; + + var getCallbackURL = function getCallbackURL() { + var initialOptions = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + var defaultOptions = { + ppx: 'wwp_', + version: 2, + status: 200, + reason: 'OK', + ticket: undefined, + callbackURL: undefined, + hw: false // hardware legacy + + }; + + var options = objectSpread({}, defaultOptions, initialOptions); + + var url = ''; + + if (typeof options.callbackURL === 'string') { + url = options.callbackURL; + } + + var firstDelimiter = url.indexOf('?') === -1 ? '?' : '&'; + url += "".concat(firstDelimiter + encodeURIComponent(options.ppx), "version=").concat(options.version); + url += "&".concat(encodeURIComponent(options.ppx), "ticket=").concat(encodeURIComponent(options.ticket)); + url += "&".concat(encodeURIComponent(options.ppx), "status=").concat(encodeURIComponent(options.status)); + url += "&".concat(encodeURIComponent(options.ppx), "reason=").concat(encodeURIComponent(options.reason)); + + if (options.hw) { + url += "&".concat(encodeURIComponent(options.ppx), "hw=1"); + } + + return url; + }; + + var getUniversalURL = function getUniversalURL() { + var initialOptions = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + var allowCallbackURL = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; + var defaultOptions = { + universal: false, + operation: 'auth', + ppx: 'wwp_', + version: 2, + ticket: undefined, + callbackURL: undefined, + clientKey: undefined + }; + + var options = objectSpread({}, defaultOptions, initialOptions); + + var url = options.universal ? 'https://get.wwpass.com/' : 'wwpass://'; + + if (options.operation === 'auth') { + url += 'auth'; + url += "?v=".concat(options.version); + url += "&t=".concat(encodeURIComponent(options.ticket)); + url += "&ppx=".concat(encodeURIComponent(options.ppx)); + + if (options.clientKey) { + url += "&ck=".concat(options.clientKey); + } + + if (options.callbackURL && allowCallbackURL) { + url += "&c=".concat(encodeURIComponent(options.callbackURL)); + } + } else { + url += "".concat(encodeURIComponent(options.operation), "?t=").concat(encodeURIComponent(options.ticket)); + } + + return url; + }; + + var navigateToCallback = function navigateToCallback(options) { + if (typeof options.callbackURL === 'function') { + options.callbackURL(getCallbackURL(options)); + } else { + // URL string + window.location.href = getCallbackURL(options); + } + }; + + var connectionPool = []; + + var closeConnectionPool = function closeConnectionPool() { + while (connectionPool.length) { + var connection = connectionPool.shift(); + + if (connection.readyState === WebSocket.OPEN) { + connection.close(); + } + } + }; + + var applyDefaults = function applyDefaults(initialOptions) { + var defaultOptions = { + ppx: 'wwp_', + version: 2, + ticket: undefined, + callbackURL: undefined, + returnErrors: false, + log: function log() {}, + development: false, + spfewsAddress: 'wss://spfews.wwpass.com', + echo: undefined, + clientKeyOnly: false + }; + return objectSpread({}, defaultOptions, initialOptions); + }; + /** + * WWPass SPFE WebSocket connection + * @param {object} options + * + * options = { + * 'ticket': undefined, // stirng + * 'callbackURL': undefined, //string + * 'development': false || 'string' , // work with another spfews.wwpass.* server + * 'log': function (message) || console.log, // another log handler + * 'echo': undefined + * } + */ + + + var getWebSocketResult = function getWebSocketResult(initialOptions) { + return new Promise(function (resolve, reject) { + var options = applyDefaults(initialOptions); + var clientKey = null; + var originalTicket = options.ticket; + var ttl = null; + + var settle = function settle(status, reason) { + if (status === 200) { + var result = { + ppx: options.ppx, + version: options.version, + status: status, + reason: WWPASS_OK_MSG, + ticket: options.ticket, + callbackURL: options.callbackURL, + clientKey: clientKey, + originalTicket: originalTicket, + ttl: ttl + }; + + if (!options.clientKeyOnly) { + navigateToCallback(result); + } + + resolve(result); + } else { + var err = { + ppx: options.ppx, + version: options.version, + status: status, + reason: reason, + ticket: options.ticket, + callbackURL: options.callbackURL + }; + + if ((status === WWPASS_STATUS.INTERNAL_ERROR || options.returnErrors) && !options.clientKeyOnly) { + navigateToCallback(err); + } + + reject(err); + } + }; + + if (!('WebSocket' in window)) { + settle(WWPASS_STATUS.INTERNAL_ERROR, 'WebSocket is not supported.'); + return; + } + + var websocketurl = options.spfewsAddress; + var socket = new WebSocket(websocketurl); + connectionPool.push(socket); + var log = options.log; + + socket.onopen = function () { + try { + log("Connected: ".concat(websocketurl)); + var message = JSON.stringify({ + ticket: options.ticket + }); + log("Sent message to server: ".concat(message)); + socket.send(message); + } catch (error) { + log(error); + settle(WWPASS_STATUS.INTERNAL_ERROR, 'WebSocket error'); + } + }; + + socket.onclose = function () { + try { + var index = connectionPool.indexOf(socket); + + if (index !== -1) { + connectionPool.splice(index, 1); + } + + log('Disconnected'); + resolve({ + refresh: true + }); + } catch (error) { + log(error); + settle(WWPASS_STATUS.INTERNAL_ERROR, 'WebSocket error'); + } + }; + + socket.onmessage = function (message) { + try { + log("Message received from server: ".concat(message.data)); + var response = JSON.parse(message.data); + var status = response.code; + var reason = response.reason; + + if ('clientKey' in response && !clientKey) { + clientKey = response.clientKey; + + if (response.originalTicket !== undefined) { + originalTicket = response.originalTicket; + ttl = response.ttl; + } + } + + if (status === 100) { + return; + } + + settle(status, reason); + socket.close(); + } catch (error) { + log(error); + settle(WWPASS_STATUS.INTERNAL_ERROR, 'WebSocket error'); + } + }; + }); + }; + + var abToB64 = function abToB64(data) { + return btoa(String.fromCharCode.apply(null, new Uint8Array(data))); + }; + + var b64ToAb = function b64ToAb(base64) { + var s = atob(base64); + var bytes = new Uint8Array(s.length); + + for (var i = 0; i < s.length; i += 1) { + bytes[i] = s.charCodeAt(i); + } + + return bytes.buffer; + }; + + var ab2str = function ab2str(buf) { + return String.fromCharCode.apply(null, new Uint16Array(buf)); + }; + + var str2ab = function str2ab(str) { + var buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char + + var bufView = new Uint16Array(buf); + + for (var i = 0, strLen = str.length; i < strLen; i += 1) { + bufView[i] = str.charCodeAt(i); + } + + return buf; + }; + + var crypto = window.crypto || window.msCrypto; + var subtle = crypto ? crypto.webkitSubtle || crypto.subtle : null; + + var encodeClientKey = function encodeClientKey(key) { + return abToB64(key).replace(/\+/g, '-').replace(/[/]/g, '.').replace(/=/g, '_'); + }; + + var encrypt = function encrypt(options, key, data) { + return subtle.encrypt(options, key, data); + }; + + var decrypt = function decrypt(options, key, data) { + return subtle.decrypt(options, key, data); + }; + + var importKey = function importKey(format, key, algoritm, extractable, operations) { + return subtle.importKey(format, key, algoritm, extractable, operations); + }; // eslint-disable-line max-len + + + var getRandomData = function getRandomData(buffer) { + return crypto.getRandomValues(buffer); + }; + + var concatBuffers = function concatBuffers() { + for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + var totalLen = args.reduce(function (accumulator, curentAB) { + return accumulator + curentAB.byteLength; + }, 0); + var i = 0; + var result = new Uint8Array(totalLen); + + while (args.length > 0) { + result.set(new Uint8Array(args[0]), i); + i += args[0].byteLength; + args.shift(); + } + + return result.buffer; + }; + + function _arrayWithHoles(arr) { + if (Array.isArray(arr)) return arr; + } + + var arrayWithHoles = _arrayWithHoles; + + function _iterableToArrayLimit(arr, i) { + if (!(Symbol.iterator in Object(arr) || Object.prototype.toString.call(arr) === "[object Arguments]")) { + return; + } + + var _arr = []; + var _n = true; + var _d = false; + var _e = undefined; + + try { + for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { + _arr.push(_s.value); + + if (i && _arr.length === i) break; + } + } catch (err) { + _d = true; + _e = err; + } finally { + try { + if (!_n && _i["return"] != null) _i["return"](); + } finally { + if (_d) throw _e; + } + } + + return _arr; + } + + var iterableToArrayLimit = _iterableToArrayLimit; + + function _nonIterableRest() { + throw new TypeError("Invalid attempt to destructure non-iterable instance"); + } + + var nonIterableRest = _nonIterableRest; + + function _slicedToArray(arr, i) { + return arrayWithHoles(arr) || iterableToArrayLimit(arr, i) || nonIterableRest(); + } + + var slicedToArray = _slicedToArray; + + var isClientKeyTicket = function isClientKeyTicket(ticket) { + var _ticket$split = ticket.split('@'), + _ticket$split2 = slicedToArray(_ticket$split, 1), + info = _ticket$split2[0]; + + var spnameFlagsOTP = info.split(':'); + + if (spnameFlagsOTP.length < 3) { + return false; + } + + var FLAGS_INDEX = 1; // second element of ticket — flags + + var flags = spnameFlagsOTP[FLAGS_INDEX]; + return flags.split('').some(function (element) { + return element === 'c'; + }); + }; + + var ticketAdapter = function ticketAdapter(response) { + if (response && response.data) { + var ticket = { + ticket: response.data, + ttl: response.ttl || 120 + }; + delete ticket.data; + return ticket; + } + + return response; + }; + + function createCommonjsModule(fn, module) { + return module = { exports: {} }, fn(module, module.exports), module.exports; + } + + var _typeof_1 = createCommonjsModule(function (module) { + function _typeof(obj) { + if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { + module.exports = _typeof = function _typeof(obj) { + return typeof obj; + }; + } else { + module.exports = _typeof = function _typeof(obj) { + return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; + }; + } + + return _typeof(obj); + } + + module.exports = _typeof; + }); + + function _assertThisInitialized(self) { + if (self === void 0) { + throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + } + + return self; + } + + var assertThisInitialized = _assertThisInitialized; + + function _possibleConstructorReturn(self, call) { + if (call && (_typeof_1(call) === "object" || typeof call === "function")) { + return call; + } + + return assertThisInitialized(self); + } + + var possibleConstructorReturn = _possibleConstructorReturn; + + var getPrototypeOf = createCommonjsModule(function (module) { + function _getPrototypeOf(o) { + module.exports = _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { + return o.__proto__ || Object.getPrototypeOf(o); + }; + return _getPrototypeOf(o); + } + + module.exports = _getPrototypeOf; + }); + + var setPrototypeOf = createCommonjsModule(function (module) { + function _setPrototypeOf(o, p) { + module.exports = _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { + o.__proto__ = p; + return o; + }; + + return _setPrototypeOf(o, p); + } + + module.exports = _setPrototypeOf; + }); + + function _inherits(subClass, superClass) { + if (typeof superClass !== "function" && superClass !== null) { + throw new TypeError("Super expression must either be null or a function"); + } + + subClass.prototype = Object.create(superClass && superClass.prototype, { + constructor: { + value: subClass, + writable: true, + configurable: true + } + }); + if (superClass) setPrototypeOf(subClass, superClass); + } + + var inherits = _inherits; + + function _isNativeFunction(fn) { + return Function.toString.call(fn).indexOf("[native code]") !== -1; + } + + var isNativeFunction = _isNativeFunction; + + var construct = createCommonjsModule(function (module) { + function isNativeReflectConstruct() { + if (typeof Reflect === "undefined" || !Reflect.construct) return false; + if (Reflect.construct.sham) return false; + if (typeof Proxy === "function") return true; + + try { + Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); + return true; + } catch (e) { + return false; + } + } + + function _construct(Parent, args, Class) { + if (isNativeReflectConstruct()) { + module.exports = _construct = Reflect.construct; + } else { + module.exports = _construct = function _construct(Parent, args, Class) { + var a = [null]; + a.push.apply(a, args); + var Constructor = Function.bind.apply(Parent, a); + var instance = new Constructor(); + if (Class) setPrototypeOf(instance, Class.prototype); + return instance; + }; + } + + return _construct.apply(null, arguments); + } + + module.exports = _construct; + }); + + var wrapNativeSuper = createCommonjsModule(function (module) { + function _wrapNativeSuper(Class) { + var _cache = typeof Map === "function" ? new Map() : undefined; + + module.exports = _wrapNativeSuper = function _wrapNativeSuper(Class) { + if (Class === null || !isNativeFunction(Class)) return Class; + + if (typeof Class !== "function") { + throw new TypeError("Super expression must either be null or a function"); + } + + if (typeof _cache !== "undefined") { + if (_cache.has(Class)) return _cache.get(Class); + + _cache.set(Class, Wrapper); + } + + function Wrapper() { + return construct(Class, arguments, getPrototypeOf(this).constructor); + } + + Wrapper.prototype = Object.create(Class.prototype, { + constructor: { + value: Wrapper, + enumerable: false, + writable: true, + configurable: true + } + }); + return setPrototypeOf(Wrapper, Class); + }; + + return _wrapNativeSuper(Class); + } + + module.exports = _wrapNativeSuper; + }); + + var WWPassError = + /*#__PURE__*/ + function (_Error) { + inherits(WWPassError, _Error); + + function WWPassError(code) { + var _this; + + classCallCheck(this, WWPassError); + + for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + + _this = possibleConstructorReturn(this, getPrototypeOf(WWPassError).call(this, args, WWPassError)); + Error.captureStackTrace(assertThisInitialized(_this), WWPassError); + _this.code = code; + return _this; + } + + createClass(WWPassError, [{ + key: "toString", + value: function toString() { + return "".concat(this.name, "(").concat(this.code, "): ").concat(this.message); + } + }]); + + return WWPassError; + }(wrapNativeSuper(Error)); + + var exportKey = function exportKey(type, key) { + return subtle.exportKey(type, key); + }; // generate digest from string + + + var hex = function hex(buffer) { + var hexCodes = []; + var view = new DataView(buffer); + + for (var i = 0; i < view.byteLength; i += 4) { + // Using getUint32 reduces the number of iterations needed (we process 4 bytes each time) + var value = view.getUint32(i); // toString(16) will give the hex representation of the number without padding + + var stringValue = value.toString(16); // We use concatenation and slice for padding + + var padding = '00000000'; + var paddedValue = (padding + stringValue).slice(-padding.length); + hexCodes.push(paddedValue); + } // Join all the hex strings into one + + + return hexCodes.join(''); + }; // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest + + + var sha256 = function sha256(str) { + // We transform the string into an arraybuffer. + var buffer = str2ab(str); + return subtle.digest({ + name: 'SHA-256' + }, buffer).then(function (hash) { + return hex(hash); + }); + }; + + var clean = function clean(items) { + var currentDate = window.Date.now(); + return items.filter(function (item) { + return item.deadline > currentDate; + }); + }; + + var loadNonces = function loadNonces() { + var wwpassNonce = window.localStorage.getItem('wwpassNonce'); + + if (!wwpassNonce) { + return []; + } + + try { + return clean(JSON.parse(wwpassNonce)); + } catch (error) { + window.localStorage.removeItem('wwpassNonce'); + throw error; + } + }; + + var saveNonces = function saveNonces(nonces) { + window.localStorage.setItem('wwpassNonce', JSON.stringify(nonces)); + }; // get from localStorage Client Nonce + + + var getClientNonce = function getClientNonce(ticket) { + var newTTL = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; + + if (!subtle) { + throw new WWPassError(WWPASS_STATUS.SSL_REQUIRED, 'Client-side encryption requires https.'); + } + + var nonces = loadNonces(); + return sha256(ticket).then(function (hash) { + var nonce = nonces.find(function (it) { + return hash === it.hash; + }); + var key = nonce && nonce.key ? b64ToAb(nonce.key) : undefined; + + if (newTTL && key) { + nonce.deadline = window.Date.now() + newTTL * 1000; + saveNonces(nonces); + } + + return key; + }); + }; // generate Client Nonce and set it to localStorage + + + var generateClientNonce = function generateClientNonce(ticket) { + var ttl = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 120; + + if (!subtle) { + throw new WWPassError(WWPASS_STATUS.SSL_REQUIRED, 'Client-side encryption requires https.'); + } + + return getClientNonce(ticket).then(function (loadedKey) { + if (loadedKey) { + return loadedKey; + } + + return subtle.generateKey({ + name: 'AES-CBC', + length: 256 + }, true, // is extractable + ['encrypt', 'decrypt']).then(function (key) { + return exportKey('raw', key); + }).then(function (rawKey) { + return sha256(ticket).then(function (digest) { + var nonce = { + hash: digest, + key: abToB64(rawKey), + deadline: window.Date.now() + ttl * 1000 + }; + var nonces = loadNonces(); + nonces.push(nonce); + saveNonces(nonces); // hack for return key + + return rawKey; + }); + }); + }); + }; + + var getClientNonceWrapper = function getClientNonceWrapper(ticket) { + var ttl = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 120; + + if (!isClientKeyTicket(ticket)) { + return new Promise(function (resolve) { + resolve(undefined); + }); + } + + return generateClientNonce(ticket, ttl); + }; + + var copyClientNonce = function copyClientNonce(oldTicket, newTicket, ttl) { + return getClientNonce(oldTicket).then(function (nonceKey) { + return sha256(newTicket) // eslint-disable-line max-len + .then(function (digest) { + var nonces = loadNonces(); + nonces.push({ + hash: digest, + key: abToB64(nonceKey), + deadline: window.Date.now() + ttl * 1000 + }); + saveNonces(nonces); + }); + }); + }; + + var clientKeyIV = new Uint8Array([176, 178, 97, 142, 156, 31, 45, 30, 81, 210, 85, 14, 202, 203, 86, 240]); + + var WWPassCryptoPromise = + /*#__PURE__*/ + function () { + createClass(WWPassCryptoPromise, [{ + key: "encryptArrayBuffer", + value: function encryptArrayBuffer(arrayBuffer) { + var iv = new Uint8Array(this.ivLen); + getRandomData(iv); + var algorithm = this.algorithm; + Object.assign(algorithm, { + iv: iv + }); + return encrypt(algorithm, this.clientKey, arrayBuffer).then(function (encryptedAB) { + return concatBuffers(iv.buffer, encryptedAB); + }); + } + }, { + key: "encryptString", + value: function encryptString(string) { + return this.encryptArrayBuffer(str2ab(string)).then(abToB64); + } + }, { + key: "decryptArrayBuffer", + value: function decryptArrayBuffer(encryptedArrayBuffer) { + var algorithm = this.algorithm; + Object.assign(algorithm, { + iv: encryptedArrayBuffer.slice(0, this.ivLen) + }); + return decrypt(algorithm, this.clientKey, encryptedArrayBuffer.slice(this.ivLen)); + } + }, { + key: "decryptString", + value: function decryptString(encryptedString) { + return this.decryptArrayBuffer(b64ToAb(encryptedString)).then(ab2str); + } // Private + + }], [{ + key: "getWWPassCrypto", + + /* Return Promise that will be resloved to catual crypto object + with encrypt/decrypt String/ArrayBuffer methods and cleintKey member. + Ticket must be authenticated with 'c' auth factor. + Only supported values for algorithm are 'AES-GCM' and 'AES-CBC'. + */ + value: function getWWPassCrypto(ticket) { + var algorithmName = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'AES-GCM'; + var encryptedClientKey = null; + var algorithm = { + name: algorithmName, + length: 256 + }; + return getWebSocketResult({ + ticket: ticket, + clientKeyOnly: true + }).then(function (result) { + if (!result.clientKey) { + throw Error("No client key associated with the ticket ".concat(ticket)); + } + + encryptedClientKey = result.clientKey; + return getClientNonce(result.originalTicket ? result.originalTicket : ticket, result.ttl); + }).then(function (key) { + if (!key) { + throw new Error('No client key nonce associated with the ticket in this browser'); + } + + return importKey('raw', key, { + name: 'AES-CBC' + }, false, ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']); + }).then(function (clientKeyNonce) { + return decrypt({ + name: 'AES-CBC', + iv: clientKeyIV + }, clientKeyNonce, b64ToAb(encryptedClientKey)); + }).then(function (arrayBuffer) { + return importKey('raw', arrayBuffer, algorithm, false, ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']); + }).then(function (key) { + return new WWPassCryptoPromise(key, algorithm); + }).catch(function (error) { + if (error.reason !== undefined) { + throw new Error(error.reason); + } + + throw error; + }); + } + }]); + + function WWPassCryptoPromise(key, algorithm) { + classCallCheck(this, WWPassCryptoPromise); + + this.ivLen = algorithm.name === 'AES-GCM' ? 12 : 16; + this.algorithm = algorithm; + + if (algorithm.name === 'AES-GCM') { + Object.assign(this.algorithm, { + tagLength: 128 + }); + } + + this.clientKey = key; + } + + return WWPassCryptoPromise; + }(); + + var runtime_1 = createCommonjsModule(function (module) { + /** + * Copyright (c) 2014-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + + var runtime = (function (exports) { + + var Op = Object.prototype; + var hasOwn = Op.hasOwnProperty; + var undefined$1; // More compressible than void 0. + var $Symbol = typeof Symbol === "function" ? Symbol : {}; + var iteratorSymbol = $Symbol.iterator || "@@iterator"; + var asyncIteratorSymbol = $Symbol.asyncIterator || "@@asyncIterator"; + var toStringTagSymbol = $Symbol.toStringTag || "@@toStringTag"; + + function wrap(innerFn, outerFn, self, tryLocsList) { + // If outerFn provided and outerFn.prototype is a Generator, then outerFn.prototype instanceof Generator. + var protoGenerator = outerFn && outerFn.prototype instanceof Generator ? outerFn : Generator; + var generator = Object.create(protoGenerator.prototype); + var context = new Context(tryLocsList || []); + + // The ._invoke method unifies the implementations of the .next, + // .throw, and .return methods. + generator._invoke = makeInvokeMethod(innerFn, self, context); + + return generator; + } + exports.wrap = wrap; + + // Try/catch helper to minimize deoptimizations. Returns a completion + // record like context.tryEntries[i].completion. This interface could + // have been (and was previously) designed to take a closure to be + // invoked without arguments, but in all the cases we care about we + // already have an existing method we want to call, so there's no need + // to create a new function object. We can even get away with assuming + // the method takes exactly one argument, since that happens to be true + // in every case, so we don't have to touch the arguments object. The + // only additional allocation required is the completion record, which + // has a stable shape and so hopefully should be cheap to allocate. + function tryCatch(fn, obj, arg) { + try { + return { type: "normal", arg: fn.call(obj, arg) }; + } catch (err) { + return { type: "throw", arg: err }; + } + } + + var GenStateSuspendedStart = "suspendedStart"; + var GenStateSuspendedYield = "suspendedYield"; + var GenStateExecuting = "executing"; + var GenStateCompleted = "completed"; + + // Returning this object from the innerFn has the same effect as + // breaking out of the dispatch switch statement. + var ContinueSentinel = {}; + + // Dummy constructor functions that we use as the .constructor and + // .constructor.prototype properties for functions that return Generator + // objects. For full spec compliance, you may wish to configure your + // minifier not to mangle the names of these two functions. + function Generator() {} + function GeneratorFunction() {} + function GeneratorFunctionPrototype() {} + + // This is a polyfill for %IteratorPrototype% for environments that + // don't natively support it. + var IteratorPrototype = {}; + IteratorPrototype[iteratorSymbol] = function () { + return this; + }; + + var getProto = Object.getPrototypeOf; + var NativeIteratorPrototype = getProto && getProto(getProto(values([]))); + if (NativeIteratorPrototype && + NativeIteratorPrototype !== Op && + hasOwn.call(NativeIteratorPrototype, iteratorSymbol)) { + // This environment has a native %IteratorPrototype%; use it instead + // of the polyfill. + IteratorPrototype = NativeIteratorPrototype; + } + + var Gp = GeneratorFunctionPrototype.prototype = + Generator.prototype = Object.create(IteratorPrototype); + GeneratorFunction.prototype = Gp.constructor = GeneratorFunctionPrototype; + GeneratorFunctionPrototype.constructor = GeneratorFunction; + GeneratorFunctionPrototype[toStringTagSymbol] = + GeneratorFunction.displayName = "GeneratorFunction"; + + // Helper for defining the .next, .throw, and .return methods of the + // Iterator interface in terms of a single ._invoke method. + function defineIteratorMethods(prototype) { + ["next", "throw", "return"].forEach(function(method) { + prototype[method] = function(arg) { + return this._invoke(method, arg); + }; + }); + } + + exports.isGeneratorFunction = function(genFun) { + var ctor = typeof genFun === "function" && genFun.constructor; + return ctor + ? ctor === GeneratorFunction || + // For the native GeneratorFunction constructor, the best we can + // do is to check its .name property. + (ctor.displayName || ctor.name) === "GeneratorFunction" + : false; + }; + + exports.mark = function(genFun) { + if (Object.setPrototypeOf) { + Object.setPrototypeOf(genFun, GeneratorFunctionPrototype); + } else { + genFun.__proto__ = GeneratorFunctionPrototype; + if (!(toStringTagSymbol in genFun)) { + genFun[toStringTagSymbol] = "GeneratorFunction"; + } + } + genFun.prototype = Object.create(Gp); + return genFun; + }; + + // Within the body of any async function, `await x` is transformed to + // `yield regeneratorRuntime.awrap(x)`, so that the runtime can test + // `hasOwn.call(value, "__await")` to determine if the yielded value is + // meant to be awaited. + exports.awrap = function(arg) { + return { __await: arg }; + }; + + function AsyncIterator(generator) { + function invoke(method, arg, resolve, reject) { + var record = tryCatch(generator[method], generator, arg); + if (record.type === "throw") { + reject(record.arg); + } else { + var result = record.arg; + var value = result.value; + if (value && + typeof value === "object" && + hasOwn.call(value, "__await")) { + return Promise.resolve(value.__await).then(function(value) { + invoke("next", value, resolve, reject); + }, function(err) { + invoke("throw", err, resolve, reject); + }); + } + + return Promise.resolve(value).then(function(unwrapped) { + // When a yielded Promise is resolved, its final value becomes + // the .value of the Promise<{value,done}> result for the + // current iteration. + result.value = unwrapped; + resolve(result); + }, function(error) { + // If a rejected Promise was yielded, throw the rejection back + // into the async generator function so it can be handled there. + return invoke("throw", error, resolve, reject); + }); + } + } + + var previousPromise; + + function enqueue(method, arg) { + function callInvokeWithMethodAndArg() { + return new Promise(function(resolve, reject) { + invoke(method, arg, resolve, reject); + }); + } + + return previousPromise = + // If enqueue has been called before, then we want to wait until + // all previous Promises have been resolved before calling invoke, + // so that results are always delivered in the correct order. If + // enqueue has not been called before, then it is important to + // call invoke immediately, without waiting on a callback to fire, + // so that the async generator function has the opportunity to do + // any necessary setup in a predictable way. This predictability + // is why the Promise constructor synchronously invokes its + // executor callback, and why async functions synchronously + // execute code before the first await. Since we implement simple + // async functions in terms of async generators, it is especially + // important to get this right, even though it requires care. + previousPromise ? previousPromise.then( + callInvokeWithMethodAndArg, + // Avoid propagating failures to Promises returned by later + // invocations of the iterator. + callInvokeWithMethodAndArg + ) : callInvokeWithMethodAndArg(); + } + + // Define the unified helper method that is used to implement .next, + // .throw, and .return (see defineIteratorMethods). + this._invoke = enqueue; + } + + defineIteratorMethods(AsyncIterator.prototype); + AsyncIterator.prototype[asyncIteratorSymbol] = function () { + return this; + }; + exports.AsyncIterator = AsyncIterator; + + // Note that simple async functions are implemented on top of + // AsyncIterator objects; they just return a Promise for the value of + // the final result produced by the iterator. + exports.async = function(innerFn, outerFn, self, tryLocsList) { + var iter = new AsyncIterator( + wrap(innerFn, outerFn, self, tryLocsList) + ); + + return exports.isGeneratorFunction(outerFn) + ? iter // If outerFn is a generator, return the full iterator. + : iter.next().then(function(result) { + return result.done ? result.value : iter.next(); + }); + }; + + function makeInvokeMethod(innerFn, self, context) { + var state = GenStateSuspendedStart; + + return function invoke(method, arg) { + if (state === GenStateExecuting) { + throw new Error("Generator is already running"); + } + + if (state === GenStateCompleted) { + if (method === "throw") { + throw arg; + } + + // Be forgiving, per 25.3.3.3.3 of the spec: + // https://people.mozilla.org/~jorendorff/es6-draft.html#sec-generatorresume + return doneResult(); + } + + context.method = method; + context.arg = arg; + + while (true) { + var delegate = context.delegate; + if (delegate) { + var delegateResult = maybeInvokeDelegate(delegate, context); + if (delegateResult) { + if (delegateResult === ContinueSentinel) continue; + return delegateResult; + } + } + + if (context.method === "next") { + // Setting context._sent for legacy support of Babel's + // function.sent implementation. + context.sent = context._sent = context.arg; + + } else if (context.method === "throw") { + if (state === GenStateSuspendedStart) { + state = GenStateCompleted; + throw context.arg; + } + + context.dispatchException(context.arg); + + } else if (context.method === "return") { + context.abrupt("return", context.arg); + } + + state = GenStateExecuting; + + var record = tryCatch(innerFn, self, context); + if (record.type === "normal") { + // If an exception is thrown from innerFn, we leave state === + // GenStateExecuting and loop back for another invocation. + state = context.done + ? GenStateCompleted + : GenStateSuspendedYield; + + if (record.arg === ContinueSentinel) { + continue; + } + + return { + value: record.arg, + done: context.done + }; + + } else if (record.type === "throw") { + state = GenStateCompleted; + // Dispatch the exception by looping back around to the + // context.dispatchException(context.arg) call above. + context.method = "throw"; + context.arg = record.arg; + } + } + }; + } + + // Call delegate.iterator[context.method](context.arg) and handle the + // result, either by returning a { value, done } result from the + // delegate iterator, or by modifying context.method and context.arg, + // setting context.delegate to null, and returning the ContinueSentinel. + function maybeInvokeDelegate(delegate, context) { + var method = delegate.iterator[context.method]; + if (method === undefined$1) { + // A .throw or .return when the delegate iterator has no .throw + // method always terminates the yield* loop. + context.delegate = null; + + if (context.method === "throw") { + // Note: ["return"] must be used for ES3 parsing compatibility. + if (delegate.iterator["return"]) { + // If the delegate iterator has a return method, give it a + // chance to clean up. + context.method = "return"; + context.arg = undefined$1; + maybeInvokeDelegate(delegate, context); + + if (context.method === "throw") { + // If maybeInvokeDelegate(context) changed context.method from + // "return" to "throw", let that override the TypeError below. + return ContinueSentinel; + } + } + + context.method = "throw"; + context.arg = new TypeError( + "The iterator does not provide a 'throw' method"); + } + + return ContinueSentinel; + } + + var record = tryCatch(method, delegate.iterator, context.arg); + + if (record.type === "throw") { + context.method = "throw"; + context.arg = record.arg; + context.delegate = null; + return ContinueSentinel; + } + + var info = record.arg; + + if (! info) { + context.method = "throw"; + context.arg = new TypeError("iterator result is not an object"); + context.delegate = null; + return ContinueSentinel; + } + + if (info.done) { + // Assign the result of the finished delegate to the temporary + // variable specified by delegate.resultName (see delegateYield). + context[delegate.resultName] = info.value; + + // Resume execution at the desired location (see delegateYield). + context.next = delegate.nextLoc; + + // If context.method was "throw" but the delegate handled the + // exception, let the outer generator proceed normally. If + // context.method was "next", forget context.arg since it has been + // "consumed" by the delegate iterator. If context.method was + // "return", allow the original .return call to continue in the + // outer generator. + if (context.method !== "return") { + context.method = "next"; + context.arg = undefined$1; + } + + } else { + // Re-yield the result returned by the delegate method. + return info; + } + + // The delegate iterator is finished, so forget it and continue with + // the outer generator. + context.delegate = null; + return ContinueSentinel; + } + + // Define Generator.prototype.{next,throw,return} in terms of the + // unified ._invoke helper method. + defineIteratorMethods(Gp); + + Gp[toStringTagSymbol] = "Generator"; + + // A Generator should always return itself as the iterator object when the + // @@iterator function is called on it. Some browsers' implementations of the + // iterator prototype chain incorrectly implement this, causing the Generator + // object to not be returned from this call. This ensures that doesn't happen. + // See https://github.com/facebook/regenerator/issues/274 for more details. + Gp[iteratorSymbol] = function() { + return this; + }; + + Gp.toString = function() { + return "[object Generator]"; + }; + + function pushTryEntry(locs) { + var entry = { tryLoc: locs[0] }; + + if (1 in locs) { + entry.catchLoc = locs[1]; + } + + if (2 in locs) { + entry.finallyLoc = locs[2]; + entry.afterLoc = locs[3]; + } + + this.tryEntries.push(entry); + } + + function resetTryEntry(entry) { + var record = entry.completion || {}; + record.type = "normal"; + delete record.arg; + entry.completion = record; + } + + function Context(tryLocsList) { + // The root entry object (effectively a try statement without a catch + // or a finally block) gives us a place to store values thrown from + // locations where there is no enclosing try statement. + this.tryEntries = [{ tryLoc: "root" }]; + tryLocsList.forEach(pushTryEntry, this); + this.reset(true); + } + + exports.keys = function(object) { + var keys = []; + for (var key in object) { + keys.push(key); + } + keys.reverse(); + + // Rather than returning an object with a next method, we keep + // things simple and return the next function itself. + return function next() { + while (keys.length) { + var key = keys.pop(); + if (key in object) { + next.value = key; + next.done = false; + return next; + } + } + + // To avoid creating an additional object, we just hang the .value + // and .done properties off the next function object itself. This + // also ensures that the minifier will not anonymize the function. + next.done = true; + return next; + }; + }; + + function values(iterable) { + if (iterable) { + var iteratorMethod = iterable[iteratorSymbol]; + if (iteratorMethod) { + return iteratorMethod.call(iterable); + } + + if (typeof iterable.next === "function") { + return iterable; + } + + if (!isNaN(iterable.length)) { + var i = -1, next = function next() { + while (++i < iterable.length) { + if (hasOwn.call(iterable, i)) { + next.value = iterable[i]; + next.done = false; + return next; + } + } + + next.value = undefined$1; + next.done = true; + + return next; + }; + + return next.next = next; + } + } + + // Return an iterator with no values. + return { next: doneResult }; + } + exports.values = values; + + function doneResult() { + return { value: undefined$1, done: true }; + } + + Context.prototype = { + constructor: Context, + + reset: function(skipTempReset) { + this.prev = 0; + this.next = 0; + // Resetting context._sent for legacy support of Babel's + // function.sent implementation. + this.sent = this._sent = undefined$1; + this.done = false; + this.delegate = null; + + this.method = "next"; + this.arg = undefined$1; + + this.tryEntries.forEach(resetTryEntry); + + if (!skipTempReset) { + for (var name in this) { + // Not sure about the optimal order of these conditions: + if (name.charAt(0) === "t" && + hasOwn.call(this, name) && + !isNaN(+name.slice(1))) { + this[name] = undefined$1; + } + } + } + }, + + stop: function() { + this.done = true; + + var rootEntry = this.tryEntries[0]; + var rootRecord = rootEntry.completion; + if (rootRecord.type === "throw") { + throw rootRecord.arg; + } + + return this.rval; + }, + + dispatchException: function(exception) { + if (this.done) { + throw exception; + } + + var context = this; + function handle(loc, caught) { + record.type = "throw"; + record.arg = exception; + context.next = loc; + + if (caught) { + // If the dispatched exception was caught by a catch block, + // then let that catch block handle the exception normally. + context.method = "next"; + context.arg = undefined$1; + } + + return !! caught; + } + + for (var i = this.tryEntries.length - 1; i >= 0; --i) { + var entry = this.tryEntries[i]; + var record = entry.completion; + + if (entry.tryLoc === "root") { + // Exception thrown outside of any try block that could handle + // it, so set the completion value of the entire function to + // throw the exception. + return handle("end"); + } + + if (entry.tryLoc <= this.prev) { + var hasCatch = hasOwn.call(entry, "catchLoc"); + var hasFinally = hasOwn.call(entry, "finallyLoc"); + + if (hasCatch && hasFinally) { + if (this.prev < entry.catchLoc) { + return handle(entry.catchLoc, true); + } else if (this.prev < entry.finallyLoc) { + return handle(entry.finallyLoc); + } + + } else if (hasCatch) { + if (this.prev < entry.catchLoc) { + return handle(entry.catchLoc, true); + } + + } else if (hasFinally) { + if (this.prev < entry.finallyLoc) { + return handle(entry.finallyLoc); + } + + } else { + throw new Error("try statement without catch or finally"); + } + } + } + }, + + abrupt: function(type, arg) { + for (var i = this.tryEntries.length - 1; i >= 0; --i) { + var entry = this.tryEntries[i]; + if (entry.tryLoc <= this.prev && + hasOwn.call(entry, "finallyLoc") && + this.prev < entry.finallyLoc) { + var finallyEntry = entry; + break; + } + } + + if (finallyEntry && + (type === "break" || + type === "continue") && + finallyEntry.tryLoc <= arg && + arg <= finallyEntry.finallyLoc) { + // Ignore the finally entry if control is not jumping to a + // location outside the try/catch block. + finallyEntry = null; + } + + var record = finallyEntry ? finallyEntry.completion : {}; + record.type = type; + record.arg = arg; + + if (finallyEntry) { + this.method = "next"; + this.next = finallyEntry.finallyLoc; + return ContinueSentinel; + } + + return this.complete(record); + }, + + complete: function(record, afterLoc) { + if (record.type === "throw") { + throw record.arg; + } + + if (record.type === "break" || + record.type === "continue") { + this.next = record.arg; + } else if (record.type === "return") { + this.rval = this.arg = record.arg; + this.method = "return"; + this.next = "end"; + } else if (record.type === "normal" && afterLoc) { + this.next = afterLoc; + } + + return ContinueSentinel; + }, + + finish: function(finallyLoc) { + for (var i = this.tryEntries.length - 1; i >= 0; --i) { + var entry = this.tryEntries[i]; + if (entry.finallyLoc === finallyLoc) { + this.complete(entry.completion, entry.afterLoc); + resetTryEntry(entry); + return ContinueSentinel; + } + } + }, + + "catch": function(tryLoc) { + for (var i = this.tryEntries.length - 1; i >= 0; --i) { + var entry = this.tryEntries[i]; + if (entry.tryLoc === tryLoc) { + var record = entry.completion; + if (record.type === "throw") { + var thrown = record.arg; + resetTryEntry(entry); + } + return thrown; + } + } + + // The context.catch method must only be called with a location + // argument that corresponds to a known catch block. + throw new Error("illegal catch attempt"); + }, + + delegateYield: function(iterable, resultName, nextLoc) { + this.delegate = { + iterator: values(iterable), + resultName: resultName, + nextLoc: nextLoc + }; + + if (this.method === "next") { + // Deliberately forget the last sent value so that we don't + // accidentally pass it on to the delegate. + this.arg = undefined$1; + } + + return ContinueSentinel; + } + }; + + // Regardless of whether this script is executing as a CommonJS module + // or not, return the runtime object so that we can declare the variable + // regeneratorRuntime in the outer scope, which allows this module to be + // injected easily by `bin/regenerator --include-runtime script.js`. + return exports; + + }( + // If this script is executing as a CommonJS module, use module.exports + // as the regeneratorRuntime namespace. Otherwise create a new empty + // object. Either way, the resulting object will be used to initialize + // the regeneratorRuntime variable at the top of this file. + module.exports + )); + + try { + regeneratorRuntime = runtime; + } catch (accidentalStrictMode) { + // This module should not be running in strict mode, so the above + // assignment should always work unless something is misconfigured. Just + // in case runtime.js accidentally runs in strict mode, we can escape + // strict mode using a global Function call. This could conceivably fail + // if a Content Security Policy forbids using Function, but in that case + // the proper solution is to fix the accidental strict mode problem. If + // you've misconfigured your bundler to force strict mode and applied a + // CSP to forbid Function, and you're not willing to fix either of those + // problems, please detail your unique predicament in a GitHub issue. + Function("r", "regeneratorRuntime = r")(runtime); + } + }); + + var regenerator = runtime_1; + + function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { + try { + var info = gen[key](arg); + var value = info.value; + } catch (error) { + reject(error); + return; + } + + if (info.done) { + resolve(value); + } else { + Promise.resolve(value).then(_next, _throw); + } + } + + function _asyncToGenerator(fn) { + return function () { + var self = this, + args = arguments; + return new Promise(function (resolve, reject) { + var gen = fn.apply(self, args); + + function _next(value) { + asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); + } + + function _throw(err) { + asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); + } + + _next(undefined); + }); + }; + } + + var asyncToGenerator = _asyncToGenerator; + + var noCacheHeaders = { + pragma: 'no-cache', + 'cache-control': 'no-cache' + }; + + var getTicket = function getTicket(url) { + return fetch(url, { + cache: 'no-store', + headers: noCacheHeaders + }).then(function (response) { + if (!response.ok) { + throw Error("Error fetching ticket from \"".concat(url, "\": ").concat(response.statusText)); + } + + return response.json(); + }); + }; + /* updateTicket should be called when the client wants to extend the session beyond + ticket's TTL. The URL handler on the server should use putTicket to get new ticket + whith the same credentials as the old one. The URL should return JSON object: + {"oldTicket": "", "newTicket": "", "ttl": } + The functions ultimately resolves to: + {"ticket": "", "ttl": } + */ + + + var updateTicket = function updateTicket(url) { + return fetch(url, { + cache: 'no-store', + headers: noCacheHeaders + }).then(function (response) { + if (!response.ok) { + throw Error("Error updating ticket from \"".concat(url, "\": ").concat(response.statusText)); + } + + return response.json(); + }).then(function (response) { + if (!response.newTicket || !response.oldTicket || !response.ttl) { + throw Error("Invalid response ot updateTicket: ".concat(response)); + } + + var result = { + ticket: response.newTicket, + ttl: response.ttl + }; + + if (!isClientKeyTicket(response.newTicket)) { + return result; + } // We have to call getWebSocketResult and getClientNonce to check for Nonce and update + // TTL on original ticket + + + return getWebSocketResult({ + ticket: response.newTicket, + clientKeyOnly: true + }).then(function (wsResult) { + if (!wsResult.clientKey) { + throw Error("No client key associated with the ticket ".concat(response.newTicket)); + } + + return getClientNonce(wsResult.originalTicket ? wsResult.originalTicket : response.newTicket, wsResult.ttl); + }).then(function () { + return result; + }); + }); + }; + + var toString = {}.toString; + + var isarray = Array.isArray || function (arr) { + return toString.call(arr) == '[object Array]'; + }; + + var K_MAX_LENGTH = 0x7fffffff; + + function Buffer (arg, offset, length) { + if (typeof arg === 'number') { + return allocUnsafe(arg) + } + + return from(arg, offset, length) + } + + Buffer.prototype.__proto__ = Uint8Array.prototype; + Buffer.__proto__ = Uint8Array; + + // Fix subarray() in ES2016. See: https://github.com/feross/buffer/pull/97 + if (typeof Symbol !== 'undefined' && Symbol.species && + Buffer[Symbol.species] === Buffer) { + Object.defineProperty(Buffer, Symbol.species, { + value: null, + configurable: true, + enumerable: false, + writable: false + }); + } + + function checked (length) { + // Note: cannot use `length < K_MAX_LENGTH` here because that fails when + // length is NaN (which is otherwise coerced to zero.) + if (length >= K_MAX_LENGTH) { + throw new RangeError('Attempt to allocate Buffer larger than maximum ' + + 'size: 0x' + K_MAX_LENGTH.toString(16) + ' bytes') + } + return length | 0 + } + + function isnan (val) { + return val !== val // eslint-disable-line no-self-compare + } + + function createBuffer (length) { + var buf = new Uint8Array(length); + buf.__proto__ = Buffer.prototype; + return buf + } + + function allocUnsafe (size) { + return createBuffer(size < 0 ? 0 : checked(size) | 0) + } + + function fromString (string) { + var length = byteLength(string) | 0; + var buf = createBuffer(length); + + var actual = buf.write(string); + + if (actual !== length) { + // Writing a hex string, for example, that contains invalid characters will + // cause everything after the first invalid character to be ignored. (e.g. + // 'abxxcd' will be treated as 'ab') + buf = buf.slice(0, actual); + } + + return buf + } + + function fromArrayLike (array) { + var length = array.length < 0 ? 0 : checked(array.length) | 0; + var buf = createBuffer(length); + for (var i = 0; i < length; i += 1) { + buf[i] = array[i] & 255; + } + return buf + } + + function fromArrayBuffer (array, byteOffset, length) { + if (byteOffset < 0 || array.byteLength < byteOffset) { + throw new RangeError('\'offset\' is out of bounds') + } + + if (array.byteLength < byteOffset + (length || 0)) { + throw new RangeError('\'length\' is out of bounds') + } + + var buf; + if (byteOffset === undefined && length === undefined) { + buf = new Uint8Array(array); + } else if (length === undefined) { + buf = new Uint8Array(array, byteOffset); + } else { + buf = new Uint8Array(array, byteOffset, length); + } + + // Return an augmented `Uint8Array` instance + buf.__proto__ = Buffer.prototype; + return buf + } + + function fromObject (obj) { + if (Buffer.isBuffer(obj)) { + var len = checked(obj.length) | 0; + var buf = createBuffer(len); + + if (buf.length === 0) { + return buf + } + + obj.copy(buf, 0, 0, len); + return buf + } + + if (obj) { + if ((typeof ArrayBuffer !== 'undefined' && + obj.buffer instanceof ArrayBuffer) || 'length' in obj) { + if (typeof obj.length !== 'number' || isnan(obj.length)) { + return createBuffer(0) + } + return fromArrayLike(obj) + } + + if (obj.type === 'Buffer' && Array.isArray(obj.data)) { + return fromArrayLike(obj.data) + } + } + + throw new TypeError('First argument must be a string, Buffer, ArrayBuffer, Array, or array-like object.') + } + + function utf8ToBytes (string, units) { + units = units || Infinity; + var codePoint; + var length = string.length; + var leadSurrogate = null; + var bytes = []; + + for (var i = 0; i < length; ++i) { + codePoint = string.charCodeAt(i); + + // is surrogate component + if (codePoint > 0xD7FF && codePoint < 0xE000) { + // last char was a lead + if (!leadSurrogate) { + // no lead yet + if (codePoint > 0xDBFF) { + // unexpected trail + if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD); + continue + } else if (i + 1 === length) { + // unpaired lead + if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD); + continue + } + + // valid lead + leadSurrogate = codePoint; + + continue + } + + // 2 leads in a row + if (codePoint < 0xDC00) { + if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD); + leadSurrogate = codePoint; + continue + } + + // valid surrogate pair + codePoint = (leadSurrogate - 0xD800 << 10 | codePoint - 0xDC00) + 0x10000; + } else if (leadSurrogate) { + // valid bmp char, but last char was a lead + if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD); + } + + leadSurrogate = null; + + // encode utf8 + if (codePoint < 0x80) { + if ((units -= 1) < 0) break + bytes.push(codePoint); + } else if (codePoint < 0x800) { + if ((units -= 2) < 0) break + bytes.push( + codePoint >> 0x6 | 0xC0, + codePoint & 0x3F | 0x80 + ); + } else if (codePoint < 0x10000) { + if ((units -= 3) < 0) break + bytes.push( + codePoint >> 0xC | 0xE0, + codePoint >> 0x6 & 0x3F | 0x80, + codePoint & 0x3F | 0x80 + ); + } else if (codePoint < 0x110000) { + if ((units -= 4) < 0) break + bytes.push( + codePoint >> 0x12 | 0xF0, + codePoint >> 0xC & 0x3F | 0x80, + codePoint >> 0x6 & 0x3F | 0x80, + codePoint & 0x3F | 0x80 + ); + } else { + throw new Error('Invalid code point') + } + } + + return bytes + } + + function byteLength (string) { + if (Buffer.isBuffer(string)) { + return string.length + } + if (typeof ArrayBuffer !== 'undefined' && typeof ArrayBuffer.isView === 'function' && + (ArrayBuffer.isView(string) || string instanceof ArrayBuffer)) { + return string.byteLength + } + if (typeof string !== 'string') { + string = '' + string; + } + + var len = string.length; + if (len === 0) return 0 + + return utf8ToBytes(string).length + } + + function blitBuffer (src, dst, offset, length) { + for (var i = 0; i < length; ++i) { + if ((i + offset >= dst.length) || (i >= src.length)) break + dst[i + offset] = src[i]; + } + return i + } + + function utf8Write (buf, string, offset, length) { + return blitBuffer(utf8ToBytes(string, buf.length - offset), buf, offset, length) + } + + function from (value, offset, length) { + if (typeof value === 'number') { + throw new TypeError('"value" argument must not be a number') + } + + if (typeof ArrayBuffer !== 'undefined' && value instanceof ArrayBuffer) { + return fromArrayBuffer(value, offset, length) + } + + if (typeof value === 'string') { + return fromString(value, offset) + } + + return fromObject(value) + } + + Buffer.prototype.write = function write (string, offset, length) { + // Buffer#write(string) + if (offset === undefined) { + length = this.length; + offset = 0; + // Buffer#write(string, encoding) + } else if (length === undefined && typeof offset === 'string') { + length = this.length; + offset = 0; + // Buffer#write(string, offset[, length]) + } else if (isFinite(offset)) { + offset = offset | 0; + if (isFinite(length)) { + length = length | 0; + } else { + length = undefined; + } + } + + var remaining = this.length - offset; + if (length === undefined || length > remaining) length = remaining; + + if ((string.length > 0 && (length < 0 || offset < 0)) || offset > this.length) { + throw new RangeError('Attempt to write outside buffer bounds') + } + + return utf8Write(this, string, offset, length) + }; + + Buffer.prototype.slice = function slice (start, end) { + var len = this.length; + start = ~~start; + end = end === undefined ? len : ~~end; + + if (start < 0) { + start += len; + if (start < 0) start = 0; + } else if (start > len) { + start = len; + } + + if (end < 0) { + end += len; + if (end < 0) end = 0; + } else if (end > len) { + end = len; + } + + if (end < start) end = start; + + var newBuf = this.subarray(start, end); + // Return an augmented `Uint8Array` instance + newBuf.__proto__ = Buffer.prototype; + return newBuf + }; + + Buffer.prototype.copy = function copy (target, targetStart, start, end) { + if (!start) start = 0; + if (!end && end !== 0) end = this.length; + if (targetStart >= target.length) targetStart = target.length; + if (!targetStart) targetStart = 0; + if (end > 0 && end < start) end = start; + + // Copy 0 bytes; we're done + if (end === start) return 0 + if (target.length === 0 || this.length === 0) return 0 + + // Fatal error conditions + if (targetStart < 0) { + throw new RangeError('targetStart out of bounds') + } + if (start < 0 || start >= this.length) throw new RangeError('sourceStart out of bounds') + if (end < 0) throw new RangeError('sourceEnd out of bounds') + + // Are we oob? + if (end > this.length) end = this.length; + if (target.length - targetStart < end - start) { + end = target.length - targetStart + start; + } + + var len = end - start; + var i; + + if (this === target && start < targetStart && targetStart < end) { + // descending copy from end + for (i = len - 1; i >= 0; --i) { + target[i + targetStart] = this[i + start]; + } + } else if (len < 1000) { + // ascending copy from start + for (i = 0; i < len; ++i) { + target[i + targetStart] = this[i + start]; + } + } else { + Uint8Array.prototype.set.call( + target, + this.subarray(start, start + len), + targetStart + ); + } + + return len + }; + + Buffer.prototype.fill = function fill (val, start, end) { + // Handle string cases: + if (typeof val === 'string') { + if (typeof start === 'string') { + start = 0; + end = this.length; + } else if (typeof end === 'string') { + end = this.length; + } + if (val.length === 1) { + var code = val.charCodeAt(0); + if (code < 256) { + val = code; + } + } + } else if (typeof val === 'number') { + val = val & 255; + } + + // Invalid ranges are not set to a default, so can range check early. + if (start < 0 || this.length < start || this.length < end) { + throw new RangeError('Out of range index') + } + + if (end <= start) { + return this + } + + start = start >>> 0; + end = end === undefined ? this.length : end >>> 0; + + if (!val) val = 0; + + var i; + if (typeof val === 'number') { + for (i = start; i < end; ++i) { + this[i] = val; + } + } else { + var bytes = Buffer.isBuffer(val) + ? val + : new Buffer(val); + var len = bytes.length; + for (i = 0; i < end - start; ++i) { + this[i + start] = bytes[i % len]; + } + } + + return this + }; + + Buffer.concat = function concat (list, length) { + if (!isarray(list)) { + throw new TypeError('"list" argument must be an Array of Buffers') + } + + if (list.length === 0) { + return createBuffer(null, 0) + } + + var i; + if (length === undefined) { + length = 0; + for (i = 0; i < list.length; ++i) { + length += list[i].length; + } + } + + var buffer = allocUnsafe(length); + var pos = 0; + for (i = 0; i < list.length; ++i) { + var buf = list[i]; + if (!Buffer.isBuffer(buf)) { + throw new TypeError('"list" argument must be an Array of Buffers') + } + buf.copy(buffer, pos); + pos += buf.length; + } + return buffer + }; + + Buffer.byteLength = byteLength; + + Buffer.prototype._isBuffer = true; + Buffer.isBuffer = function isBuffer (b) { + return !!(b != null && b._isBuffer) + }; + + var typedarrayBuffer = Buffer; + + var toSJISFunction; + var CODEWORDS_COUNT = [ + 0, // Not used + 26, 44, 70, 100, 134, 172, 196, 242, 292, 346, + 404, 466, 532, 581, 655, 733, 815, 901, 991, 1085, + 1156, 1258, 1364, 1474, 1588, 1706, 1828, 1921, 2051, 2185, + 2323, 2465, 2611, 2761, 2876, 3034, 3196, 3362, 3532, 3706 + ]; + + /** + * Returns the QR Code size for the specified version + * + * @param {Number} version QR Code version + * @return {Number} size of QR code + */ + var getSymbolSize = function getSymbolSize (version) { + if (!version) throw new Error('"version" cannot be null or undefined') + if (version < 1 || version > 40) throw new Error('"version" should be in range from 1 to 40') + return version * 4 + 17 + }; + + /** + * Returns the total number of codewords used to store data and EC information. + * + * @param {Number} version QR Code version + * @return {Number} Data length in bits + */ + var getSymbolTotalCodewords = function getSymbolTotalCodewords (version) { + return CODEWORDS_COUNT[version] + }; + + /** + * Encode data with Bose-Chaudhuri-Hocquenghem + * + * @param {Number} data Value to encode + * @return {Number} Encoded value + */ + var getBCHDigit = function (data) { + var digit = 0; + + while (data !== 0) { + digit++; + data >>>= 1; + } + + return digit + }; + + var setToSJISFunction = function setToSJISFunction (f) { + if (typeof f !== 'function') { + throw new Error('"toSJISFunc" is not a valid function.') + } + + toSJISFunction = f; + }; + + var isKanjiModeEnabled = function () { + return typeof toSJISFunction !== 'undefined' + }; + + var toSJIS = function toSJIS (kanji) { + return toSJISFunction(kanji) + }; + + var utils = { + getSymbolSize: getSymbolSize, + getSymbolTotalCodewords: getSymbolTotalCodewords, + getBCHDigit: getBCHDigit, + setToSJISFunction: setToSJISFunction, + isKanjiModeEnabled: isKanjiModeEnabled, + toSJIS: toSJIS + }; + + var errorCorrectionLevel = createCommonjsModule(function (module, exports) { + exports.L = { bit: 1 }; + exports.M = { bit: 0 }; + exports.Q = { bit: 3 }; + exports.H = { bit: 2 }; + + function fromString (string) { + if (typeof string !== 'string') { + throw new Error('Param is not a string') + } + + var lcStr = string.toLowerCase(); + + switch (lcStr) { + case 'l': + case 'low': + return exports.L + + case 'm': + case 'medium': + return exports.M + + case 'q': + case 'quartile': + return exports.Q + + case 'h': + case 'high': + return exports.H + + default: + throw new Error('Unknown EC Level: ' + string) + } + } + + exports.isValid = function isValid (level) { + return level && typeof level.bit !== 'undefined' && + level.bit >= 0 && level.bit < 4 + }; + + exports.from = function from (value, defaultValue) { + if (exports.isValid(value)) { + return value + } + + try { + return fromString(value) + } catch (e) { + return defaultValue + } + }; + }); + var errorCorrectionLevel_1 = errorCorrectionLevel.L; + var errorCorrectionLevel_2 = errorCorrectionLevel.M; + var errorCorrectionLevel_3 = errorCorrectionLevel.Q; + var errorCorrectionLevel_4 = errorCorrectionLevel.H; + var errorCorrectionLevel_5 = errorCorrectionLevel.isValid; + + function BitBuffer () { + this.buffer = []; + this.length = 0; + } + + BitBuffer.prototype = { + + get: function (index) { + var bufIndex = Math.floor(index / 8); + return ((this.buffer[bufIndex] >>> (7 - index % 8)) & 1) === 1 + }, + + put: function (num, length) { + for (var i = 0; i < length; i++) { + this.putBit(((num >>> (length - i - 1)) & 1) === 1); + } + }, + + getLengthInBits: function () { + return this.length + }, + + putBit: function (bit) { + var bufIndex = Math.floor(this.length / 8); + if (this.buffer.length <= bufIndex) { + this.buffer.push(0); + } + + if (bit) { + this.buffer[bufIndex] |= (0x80 >>> (this.length % 8)); + } + + this.length++; + } + }; + + var bitBuffer = BitBuffer; + + /** + * Helper class to handle QR Code symbol modules + * + * @param {Number} size Symbol size + */ + function BitMatrix (size) { + if (!size || size < 1) { + throw new Error('BitMatrix size must be defined and greater than 0') + } + + this.size = size; + this.data = new typedarrayBuffer(size * size); + this.data.fill(0); + this.reservedBit = new typedarrayBuffer(size * size); + this.reservedBit.fill(0); + } + + /** + * Set bit value at specified location + * If reserved flag is set, this bit will be ignored during masking process + * + * @param {Number} row + * @param {Number} col + * @param {Boolean} value + * @param {Boolean} reserved + */ + BitMatrix.prototype.set = function (row, col, value, reserved) { + var index = row * this.size + col; + this.data[index] = value; + if (reserved) this.reservedBit[index] = true; + }; + + /** + * Returns bit value at specified location + * + * @param {Number} row + * @param {Number} col + * @return {Boolean} + */ + BitMatrix.prototype.get = function (row, col) { + return this.data[row * this.size + col] + }; + + /** + * Applies xor operator at specified location + * (used during masking process) + * + * @param {Number} row + * @param {Number} col + * @param {Boolean} value + */ + BitMatrix.prototype.xor = function (row, col, value) { + this.data[row * this.size + col] ^= value; + }; + + /** + * Check if bit at specified location is reserved + * + * @param {Number} row + * @param {Number} col + * @return {Boolean} + */ + BitMatrix.prototype.isReserved = function (row, col) { + return this.reservedBit[row * this.size + col] + }; + + var bitMatrix = BitMatrix; + + var alignmentPattern = createCommonjsModule(function (module, exports) { + /** + * Alignment pattern are fixed reference pattern in defined positions + * in a matrix symbology, which enables the decode software to re-synchronise + * the coordinate mapping of the image modules in the event of moderate amounts + * of distortion of the image. + * + * Alignment patterns are present only in QR Code symbols of version 2 or larger + * and their number depends on the symbol version. + */ + + var getSymbolSize = utils.getSymbolSize; + + /** + * Calculate the row/column coordinates of the center module of each alignment pattern + * for the specified QR Code version. + * + * The alignment patterns are positioned symmetrically on either side of the diagonal + * running from the top left corner of the symbol to the bottom right corner. + * + * Since positions are simmetrical only half of the coordinates are returned. + * Each item of the array will represent in turn the x and y coordinate. + * @see {@link getPositions} + * + * @param {Number} version QR Code version + * @return {Array} Array of coordinate + */ + exports.getRowColCoords = function getRowColCoords (version) { + if (version === 1) return [] + + var posCount = Math.floor(version / 7) + 2; + var size = getSymbolSize(version); + var intervals = size === 145 ? 26 : Math.ceil((size - 13) / (2 * posCount - 2)) * 2; + var positions = [size - 7]; // Last coord is always (size - 7) + + for (var i = 1; i < posCount - 1; i++) { + positions[i] = positions[i - 1] - intervals; + } + + positions.push(6); // First coord is always 6 + + return positions.reverse() + }; + + /** + * Returns an array containing the positions of each alignment pattern. + * Each array's element represent the center point of the pattern as (x, y) coordinates + * + * Coordinates are calculated expanding the row/column coordinates returned by {@link getRowColCoords} + * and filtering out the items that overlaps with finder pattern + * + * @example + * For a Version 7 symbol {@link getRowColCoords} returns values 6, 22 and 38. + * The alignment patterns, therefore, are to be centered on (row, column) + * positions (6,22), (22,6), (22,22), (22,38), (38,22), (38,38). + * Note that the coordinates (6,6), (6,38), (38,6) are occupied by finder patterns + * and are not therefore used for alignment patterns. + * + * var pos = getPositions(7) + * // [[6,22], [22,6], [22,22], [22,38], [38,22], [38,38]] + * + * @param {Number} version QR Code version + * @return {Array} Array of coordinates + */ + exports.getPositions = function getPositions (version) { + var coords = []; + var pos = exports.getRowColCoords(version); + var posLength = pos.length; + + for (var i = 0; i < posLength; i++) { + for (var j = 0; j < posLength; j++) { + // Skip if position is occupied by finder patterns + if ((i === 0 && j === 0) || // top-left + (i === 0 && j === posLength - 1) || // bottom-left + (i === posLength - 1 && j === 0)) { // top-right + continue + } + + coords.push([pos[i], pos[j]]); + } + } + + return coords + }; + }); + var alignmentPattern_1 = alignmentPattern.getRowColCoords; + var alignmentPattern_2 = alignmentPattern.getPositions; + + var getSymbolSize$1 = utils.getSymbolSize; + var FINDER_PATTERN_SIZE = 7; + + /** + * Returns an array containing the positions of each finder pattern. + * Each array's element represent the top-left point of the pattern as (x, y) coordinates + * + * @param {Number} version QR Code version + * @return {Array} Array of coordinates + */ + var getPositions = function getPositions (version) { + var size = getSymbolSize$1(version); + + return [ + // top-left + [0, 0], + // top-right + [size - FINDER_PATTERN_SIZE, 0], + // bottom-left + [0, size - FINDER_PATTERN_SIZE] + ] + }; + + var finderPattern = { + getPositions: getPositions + }; + + var maskPattern = createCommonjsModule(function (module, exports) { + /** + * Data mask pattern reference + * @type {Object} + */ + exports.Patterns = { + PATTERN000: 0, + PATTERN001: 1, + PATTERN010: 2, + PATTERN011: 3, + PATTERN100: 4, + PATTERN101: 5, + PATTERN110: 6, + PATTERN111: 7 + }; + + /** + * Weighted penalty scores for the undesirable features + * @type {Object} + */ + var PenaltyScores = { + N1: 3, + N2: 3, + N3: 40, + N4: 10 + }; + + /** + * Find adjacent modules in row/column with the same color + * and assign a penalty value. + * + * Points: N1 + i + * i is the amount by which the number of adjacent modules of the same color exceeds 5 + */ + exports.getPenaltyN1 = function getPenaltyN1 (data) { + var size = data.size; + var points = 0; + var sameCountCol = 0; + var sameCountRow = 0; + var lastCol = null; + var lastRow = null; + + for (var row = 0; row < size; row++) { + sameCountCol = sameCountRow = 0; + lastCol = lastRow = null; + + for (var col = 0; col < size; col++) { + var module = data.get(row, col); + if (module === lastCol) { + sameCountCol++; + } else { + if (sameCountCol >= 5) points += PenaltyScores.N1 + (sameCountCol - 5); + lastCol = module; + sameCountCol = 1; + } + + module = data.get(col, row); + if (module === lastRow) { + sameCountRow++; + } else { + if (sameCountRow >= 5) points += PenaltyScores.N1 + (sameCountRow - 5); + lastRow = module; + sameCountRow = 1; + } + } + + if (sameCountCol >= 5) points += PenaltyScores.N1 + (sameCountCol - 5); + if (sameCountRow >= 5) points += PenaltyScores.N1 + (sameCountRow - 5); + } + + return points + }; + + /** + * Find 2x2 blocks with the same color and assign a penalty value + * + * Points: N2 * (m - 1) * (n - 1) + */ + exports.getPenaltyN2 = function getPenaltyN2 (data) { + var size = data.size; + var points = 0; + + for (var row = 0; row < size - 1; row++) { + for (var col = 0; col < size - 1; col++) { + var last = data.get(row, col) + + data.get(row, col + 1) + + data.get(row + 1, col) + + data.get(row + 1, col + 1); + + if (last === 4 || last === 0) points++; + } + } + + return points * PenaltyScores.N2 + }; + + /** + * Find 1:1:3:1:1 ratio (dark:light:dark:light:dark) pattern in row/column, + * preceded or followed by light area 4 modules wide + * + * Points: N3 * number of pattern found + */ + exports.getPenaltyN3 = function getPenaltyN3 (data) { + var size = data.size; + var points = 0; + var bitsCol = 0; + var bitsRow = 0; + + for (var row = 0; row < size; row++) { + bitsCol = bitsRow = 0; + for (var col = 0; col < size; col++) { + bitsCol = ((bitsCol << 1) & 0x7FF) | data.get(row, col); + if (col >= 10 && (bitsCol === 0x5D0 || bitsCol === 0x05D)) points++; + + bitsRow = ((bitsRow << 1) & 0x7FF) | data.get(col, row); + if (col >= 10 && (bitsRow === 0x5D0 || bitsRow === 0x05D)) points++; + } + } + + return points * PenaltyScores.N3 + }; + + /** + * Calculate proportion of dark modules in entire symbol + * + * Points: N4 * k + * + * k is the rating of the deviation of the proportion of dark modules + * in the symbol from 50% in steps of 5% + */ + exports.getPenaltyN4 = function getPenaltyN4 (data) { + var darkCount = 0; + var modulesCount = data.data.length; + + for (var i = 0; i < modulesCount; i++) darkCount += data.data[i]; + + var k = Math.abs(Math.ceil((darkCount * 100 / modulesCount) / 5) - 10); + + return k * PenaltyScores.N4 + }; + + /** + * Return mask value at given position + * + * @param {Number} maskPattern Pattern reference value + * @param {Number} i Row + * @param {Number} j Column + * @return {Boolean} Mask value + */ + function getMaskAt (maskPattern, i, j) { + switch (maskPattern) { + case exports.Patterns.PATTERN000: return (i + j) % 2 === 0 + case exports.Patterns.PATTERN001: return i % 2 === 0 + case exports.Patterns.PATTERN010: return j % 3 === 0 + case exports.Patterns.PATTERN011: return (i + j) % 3 === 0 + case exports.Patterns.PATTERN100: return (Math.floor(i / 2) + Math.floor(j / 3)) % 2 === 0 + case exports.Patterns.PATTERN101: return (i * j) % 2 + (i * j) % 3 === 0 + case exports.Patterns.PATTERN110: return ((i * j) % 2 + (i * j) % 3) % 2 === 0 + case exports.Patterns.PATTERN111: return ((i * j) % 3 + (i + j) % 2) % 2 === 0 + + default: throw new Error('bad maskPattern:' + maskPattern) + } + } + + /** + * Apply a mask pattern to a BitMatrix + * + * @param {Number} pattern Pattern reference number + * @param {BitMatrix} data BitMatrix data + */ + exports.applyMask = function applyMask (pattern, data) { + var size = data.size; + + for (var col = 0; col < size; col++) { + for (var row = 0; row < size; row++) { + if (data.isReserved(row, col)) continue + data.xor(row, col, getMaskAt(pattern, row, col)); + } + } + }; + + /** + * Returns the best mask pattern for data + * + * @param {BitMatrix} data + * @return {Number} Mask pattern reference number + */ + exports.getBestMask = function getBestMask (data, setupFormatFunc) { + var numPatterns = Object.keys(exports.Patterns).length; + var bestPattern = 0; + var lowerPenalty = Infinity; + + for (var p = 0; p < numPatterns; p++) { + setupFormatFunc(p); + exports.applyMask(p, data); + + // Calculate penalty + var penalty = + exports.getPenaltyN1(data) + + exports.getPenaltyN2(data) + + exports.getPenaltyN3(data) + + exports.getPenaltyN4(data); + + // Undo previously applied mask + exports.applyMask(p, data); + + if (penalty < lowerPenalty) { + lowerPenalty = penalty; + bestPattern = p; + } + } + + return bestPattern + }; + }); + var maskPattern_1 = maskPattern.Patterns; + var maskPattern_2 = maskPattern.getPenaltyN1; + var maskPattern_3 = maskPattern.getPenaltyN2; + var maskPattern_4 = maskPattern.getPenaltyN3; + var maskPattern_5 = maskPattern.getPenaltyN4; + var maskPattern_6 = maskPattern.applyMask; + var maskPattern_7 = maskPattern.getBestMask; + + var EC_BLOCKS_TABLE = [ + // L M Q H + 1, 1, 1, 1, + 1, 1, 1, 1, + 1, 1, 2, 2, + 1, 2, 2, 4, + 1, 2, 4, 4, + 2, 4, 4, 4, + 2, 4, 6, 5, + 2, 4, 6, 6, + 2, 5, 8, 8, + 4, 5, 8, 8, + 4, 5, 8, 11, + 4, 8, 10, 11, + 4, 9, 12, 16, + 4, 9, 16, 16, + 6, 10, 12, 18, + 6, 10, 17, 16, + 6, 11, 16, 19, + 6, 13, 18, 21, + 7, 14, 21, 25, + 8, 16, 20, 25, + 8, 17, 23, 25, + 9, 17, 23, 34, + 9, 18, 25, 30, + 10, 20, 27, 32, + 12, 21, 29, 35, + 12, 23, 34, 37, + 12, 25, 34, 40, + 13, 26, 35, 42, + 14, 28, 38, 45, + 15, 29, 40, 48, + 16, 31, 43, 51, + 17, 33, 45, 54, + 18, 35, 48, 57, + 19, 37, 51, 60, + 19, 38, 53, 63, + 20, 40, 56, 66, + 21, 43, 59, 70, + 22, 45, 62, 74, + 24, 47, 65, 77, + 25, 49, 68, 81 + ]; + + var EC_CODEWORDS_TABLE = [ + // L M Q H + 7, 10, 13, 17, + 10, 16, 22, 28, + 15, 26, 36, 44, + 20, 36, 52, 64, + 26, 48, 72, 88, + 36, 64, 96, 112, + 40, 72, 108, 130, + 48, 88, 132, 156, + 60, 110, 160, 192, + 72, 130, 192, 224, + 80, 150, 224, 264, + 96, 176, 260, 308, + 104, 198, 288, 352, + 120, 216, 320, 384, + 132, 240, 360, 432, + 144, 280, 408, 480, + 168, 308, 448, 532, + 180, 338, 504, 588, + 196, 364, 546, 650, + 224, 416, 600, 700, + 224, 442, 644, 750, + 252, 476, 690, 816, + 270, 504, 750, 900, + 300, 560, 810, 960, + 312, 588, 870, 1050, + 336, 644, 952, 1110, + 360, 700, 1020, 1200, + 390, 728, 1050, 1260, + 420, 784, 1140, 1350, + 450, 812, 1200, 1440, + 480, 868, 1290, 1530, + 510, 924, 1350, 1620, + 540, 980, 1440, 1710, + 570, 1036, 1530, 1800, + 570, 1064, 1590, 1890, + 600, 1120, 1680, 1980, + 630, 1204, 1770, 2100, + 660, 1260, 1860, 2220, + 720, 1316, 1950, 2310, + 750, 1372, 2040, 2430 + ]; + + /** + * Returns the number of error correction block that the QR Code should contain + * for the specified version and error correction level. + * + * @param {Number} version QR Code version + * @param {Number} errorCorrectionLevel Error correction level + * @return {Number} Number of error correction blocks + */ + var getBlocksCount = function getBlocksCount (version, errorCorrectionLevel$1) { + switch (errorCorrectionLevel$1) { + case errorCorrectionLevel.L: + return EC_BLOCKS_TABLE[(version - 1) * 4 + 0] + case errorCorrectionLevel.M: + return EC_BLOCKS_TABLE[(version - 1) * 4 + 1] + case errorCorrectionLevel.Q: + return EC_BLOCKS_TABLE[(version - 1) * 4 + 2] + case errorCorrectionLevel.H: + return EC_BLOCKS_TABLE[(version - 1) * 4 + 3] + default: + return undefined + } + }; + + /** + * Returns the number of error correction codewords to use for the specified + * version and error correction level. + * + * @param {Number} version QR Code version + * @param {Number} errorCorrectionLevel Error correction level + * @return {Number} Number of error correction codewords + */ + var getTotalCodewordsCount = function getTotalCodewordsCount (version, errorCorrectionLevel$1) { + switch (errorCorrectionLevel$1) { + case errorCorrectionLevel.L: + return EC_CODEWORDS_TABLE[(version - 1) * 4 + 0] + case errorCorrectionLevel.M: + return EC_CODEWORDS_TABLE[(version - 1) * 4 + 1] + case errorCorrectionLevel.Q: + return EC_CODEWORDS_TABLE[(version - 1) * 4 + 2] + case errorCorrectionLevel.H: + return EC_CODEWORDS_TABLE[(version - 1) * 4 + 3] + default: + return undefined + } + }; + + var errorCorrectionCode = { + getBlocksCount: getBlocksCount, + getTotalCodewordsCount: getTotalCodewordsCount + }; + + var EXP_TABLE = new typedarrayBuffer(512); + var LOG_TABLE = new typedarrayBuffer(256) + + /** + * Precompute the log and anti-log tables for faster computation later + * + * For each possible value in the galois field 2^8, we will pre-compute + * the logarithm and anti-logarithm (exponential) of this value + * + * ref {@link https://en.wikiversity.org/wiki/Reed%E2%80%93Solomon_codes_for_coders#Introduction_to_mathematical_fields} + */ + ;(function initTables () { + var x = 1; + for (var i = 0; i < 255; i++) { + EXP_TABLE[i] = x; + LOG_TABLE[x] = i; + + x <<= 1; // multiply by 2 + + // The QR code specification says to use byte-wise modulo 100011101 arithmetic. + // This means that when a number is 256 or larger, it should be XORed with 0x11D. + if (x & 0x100) { // similar to x >= 256, but a lot faster (because 0x100 == 256) + x ^= 0x11D; + } + } + + // Optimization: double the size of the anti-log table so that we don't need to mod 255 to + // stay inside the bounds (because we will mainly use this table for the multiplication of + // two GF numbers, no more). + // @see {@link mul} + for (i = 255; i < 512; i++) { + EXP_TABLE[i] = EXP_TABLE[i - 255]; + } + }()); + + /** + * Returns log value of n inside Galois Field + * + * @param {Number} n + * @return {Number} + */ + var log = function log (n) { + if (n < 1) throw new Error('log(' + n + ')') + return LOG_TABLE[n] + }; + + /** + * Returns anti-log value of n inside Galois Field + * + * @param {Number} n + * @return {Number} + */ + var exp = function exp (n) { + return EXP_TABLE[n] + }; + + /** + * Multiplies two number inside Galois Field + * + * @param {Number} x + * @param {Number} y + * @return {Number} + */ + var mul = function mul (x, y) { + if (x === 0 || y === 0) return 0 + + // should be EXP_TABLE[(LOG_TABLE[x] + LOG_TABLE[y]) % 255] if EXP_TABLE wasn't oversized + // @see {@link initTables} + return EXP_TABLE[LOG_TABLE[x] + LOG_TABLE[y]] + }; + + var galoisField = { + log: log, + exp: exp, + mul: mul + }; + + var polynomial = createCommonjsModule(function (module, exports) { + /** + * Multiplies two polynomials inside Galois Field + * + * @param {Buffer} p1 Polynomial + * @param {Buffer} p2 Polynomial + * @return {Buffer} Product of p1 and p2 + */ + exports.mul = function mul (p1, p2) { + var coeff = new typedarrayBuffer(p1.length + p2.length - 1); + coeff.fill(0); + + for (var i = 0; i < p1.length; i++) { + for (var j = 0; j < p2.length; j++) { + coeff[i + j] ^= galoisField.mul(p1[i], p2[j]); + } + } + + return coeff + }; + + /** + * Calculate the remainder of polynomials division + * + * @param {Buffer} divident Polynomial + * @param {Buffer} divisor Polynomial + * @return {Buffer} Remainder + */ + exports.mod = function mod (divident, divisor) { + var result = new typedarrayBuffer(divident); + + while ((result.length - divisor.length) >= 0) { + var coeff = result[0]; + + for (var i = 0; i < divisor.length; i++) { + result[i] ^= galoisField.mul(divisor[i], coeff); + } + + // remove all zeros from buffer head + var offset = 0; + while (offset < result.length && result[offset] === 0) offset++; + result = result.slice(offset); + } + + return result + }; + + /** + * Generate an irreducible generator polynomial of specified degree + * (used by Reed-Solomon encoder) + * + * @param {Number} degree Degree of the generator polynomial + * @return {Buffer} Buffer containing polynomial coefficients + */ + exports.generateECPolynomial = function generateECPolynomial (degree) { + var poly = new typedarrayBuffer([1]); + for (var i = 0; i < degree; i++) { + poly = exports.mul(poly, [1, galoisField.exp(i)]); + } + + return poly + }; + }); + var polynomial_1 = polynomial.mul; + var polynomial_2 = polynomial.mod; + var polynomial_3 = polynomial.generateECPolynomial; + + function ReedSolomonEncoder (degree) { + this.genPoly = undefined; + this.degree = degree; + + if (this.degree) this.initialize(this.degree); + } + + /** + * Initialize the encoder. + * The input param should correspond to the number of error correction codewords. + * + * @param {Number} degree + */ + ReedSolomonEncoder.prototype.initialize = function initialize (degree) { + // create an irreducible generator polynomial + this.degree = degree; + this.genPoly = polynomial.generateECPolynomial(this.degree); + }; + + /** + * Encodes a chunk of data + * + * @param {Buffer} data Buffer containing input data + * @return {Buffer} Buffer containing encoded data + */ + ReedSolomonEncoder.prototype.encode = function encode (data) { + if (!this.genPoly) { + throw new Error('Encoder not initialized') + } + + // Calculate EC for this data block + // extends data size to data+genPoly size + var pad = new typedarrayBuffer(this.degree); + pad.fill(0); + var paddedData = typedarrayBuffer.concat([data, pad], data.length + this.degree); + + // The error correction codewords are the remainder after dividing the data codewords + // by a generator polynomial + var remainder = polynomial.mod(paddedData, this.genPoly); + + // return EC data blocks (last n byte, where n is the degree of genPoly) + // If coefficients number in remainder are less than genPoly degree, + // pad with 0s to the left to reach the needed number of coefficients + var start = this.degree - remainder.length; + if (start > 0) { + var buff = new typedarrayBuffer(this.degree); + buff.fill(0); + remainder.copy(buff, start); + + return buff + } + + return remainder + }; + + var reedSolomonEncoder = ReedSolomonEncoder; + + var numeric = '[0-9]+'; + var alphanumeric = '[A-Z $%*+-./:]+'; + var kanji = '(?:[\u3000-\u303F]|[\u3040-\u309F]|[\u30A0-\u30FF]|' + + '[\uFF00-\uFFEF]|[\u4E00-\u9FAF]|[\u2605-\u2606]|[\u2190-\u2195]|\u203B|' + + '[\u2010\u2015\u2018\u2019\u2025\u2026\u201C\u201D\u2225\u2260]|' + + '[\u0391-\u0451]|[\u00A7\u00A8\u00B1\u00B4\u00D7\u00F7])+'; + var byte = '(?:(?![A-Z0-9 $%*+-./:]|' + kanji + ').)+'; + + var KANJI = new RegExp(kanji, 'g'); + var BYTE_KANJI = new RegExp('[^A-Z0-9 $%*+-./:]+', 'g'); + var BYTE = new RegExp(byte, 'g'); + var NUMERIC = new RegExp(numeric, 'g'); + var ALPHANUMERIC = new RegExp(alphanumeric, 'g'); + + var TEST_KANJI = new RegExp('^' + kanji + '$'); + var TEST_NUMERIC = new RegExp('^' + numeric + '$'); + var TEST_ALPHANUMERIC = new RegExp('^[A-Z0-9 $%*+-./:]+$'); + + var testKanji = function testKanji (str) { + return TEST_KANJI.test(str) + }; + + var testNumeric = function testNumeric (str) { + return TEST_NUMERIC.test(str) + }; + + var testAlphanumeric = function testAlphanumeric (str) { + return TEST_ALPHANUMERIC.test(str) + }; + + var regex = { + KANJI: KANJI, + BYTE_KANJI: BYTE_KANJI, + BYTE: BYTE, + NUMERIC: NUMERIC, + ALPHANUMERIC: ALPHANUMERIC, + testKanji: testKanji, + testNumeric: testNumeric, + testAlphanumeric: testAlphanumeric + }; + + var mode = createCommonjsModule(function (module, exports) { + /** + * Numeric mode encodes data from the decimal digit set (0 - 9) + * (byte values 30HEX to 39HEX). + * Normally, 3 data characters are represented by 10 bits. + * + * @type {Object} + */ + exports.NUMERIC = { + id: 'Numeric', + bit: 1 << 0, + ccBits: [10, 12, 14] + }; + + /** + * Alphanumeric mode encodes data from a set of 45 characters, + * i.e. 10 numeric digits (0 - 9), + * 26 alphabetic characters (A - Z), + * and 9 symbols (SP, $, %, *, +, -, ., /, :). + * Normally, two input characters are represented by 11 bits. + * + * @type {Object} + */ + exports.ALPHANUMERIC = { + id: 'Alphanumeric', + bit: 1 << 1, + ccBits: [9, 11, 13] + }; + + /** + * In byte mode, data is encoded at 8 bits per character. + * + * @type {Object} + */ + exports.BYTE = { + id: 'Byte', + bit: 1 << 2, + ccBits: [8, 16, 16] + }; + + /** + * The Kanji mode efficiently encodes Kanji characters in accordance with + * the Shift JIS system based on JIS X 0208. + * The Shift JIS values are shifted from the JIS X 0208 values. + * JIS X 0208 gives details of the shift coded representation. + * Each two-byte character value is compacted to a 13-bit binary codeword. + * + * @type {Object} + */ + exports.KANJI = { + id: 'Kanji', + bit: 1 << 3, + ccBits: [8, 10, 12] + }; + + /** + * Mixed mode will contain a sequences of data in a combination of any of + * the modes described above + * + * @type {Object} + */ + exports.MIXED = { + bit: -1 + }; + + /** + * Returns the number of bits needed to store the data length + * according to QR Code specifications. + * + * @param {Mode} mode Data mode + * @param {Number} version QR Code version + * @return {Number} Number of bits + */ + exports.getCharCountIndicator = function getCharCountIndicator (mode, version$1) { + if (!mode.ccBits) throw new Error('Invalid mode: ' + mode) + + if (!version.isValid(version$1)) { + throw new Error('Invalid version: ' + version$1) + } + + if (version$1 >= 1 && version$1 < 10) return mode.ccBits[0] + else if (version$1 < 27) return mode.ccBits[1] + return mode.ccBits[2] + }; + + /** + * Returns the most efficient mode to store the specified data + * + * @param {String} dataStr Input data string + * @return {Mode} Best mode + */ + exports.getBestModeForData = function getBestModeForData (dataStr) { + if (regex.testNumeric(dataStr)) return exports.NUMERIC + else if (regex.testAlphanumeric(dataStr)) return exports.ALPHANUMERIC + else if (regex.testKanji(dataStr)) return exports.KANJI + else return exports.BYTE + }; + + /** + * Return mode name as string + * + * @param {Mode} mode Mode object + * @returns {String} Mode name + */ + exports.toString = function toString (mode) { + if (mode && mode.id) return mode.id + throw new Error('Invalid mode') + }; + + /** + * Check if input param is a valid mode object + * + * @param {Mode} mode Mode object + * @returns {Boolean} True if valid mode, false otherwise + */ + exports.isValid = function isValid (mode) { + return mode && mode.bit && mode.ccBits + }; + + /** + * Get mode object from its name + * + * @param {String} string Mode name + * @returns {Mode} Mode object + */ + function fromString (string) { + if (typeof string !== 'string') { + throw new Error('Param is not a string') + } + + var lcStr = string.toLowerCase(); + + switch (lcStr) { + case 'numeric': + return exports.NUMERIC + case 'alphanumeric': + return exports.ALPHANUMERIC + case 'kanji': + return exports.KANJI + case 'byte': + return exports.BYTE + default: + throw new Error('Unknown mode: ' + string) + } + } + + /** + * Returns mode from a value. + * If value is not a valid mode, returns defaultValue + * + * @param {Mode|String} value Encoding mode + * @param {Mode} defaultValue Fallback value + * @return {Mode} Encoding mode + */ + exports.from = function from (value, defaultValue) { + if (exports.isValid(value)) { + return value + } + + try { + return fromString(value) + } catch (e) { + return defaultValue + } + }; + }); + var mode_1 = mode.NUMERIC; + var mode_2 = mode.ALPHANUMERIC; + var mode_3 = mode.BYTE; + var mode_4 = mode.KANJI; + var mode_5 = mode.MIXED; + var mode_6 = mode.getCharCountIndicator; + var mode_7 = mode.getBestModeForData; + var mode_8 = mode.isValid; + + var version = createCommonjsModule(function (module, exports) { + // Generator polynomial used to encode version information + var G18 = (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0); + var G18_BCH = utils.getBCHDigit(G18); + + function getBestVersionForDataLength (mode, length, errorCorrectionLevel) { + for (var currentVersion = 1; currentVersion <= 40; currentVersion++) { + if (length <= exports.getCapacity(currentVersion, errorCorrectionLevel, mode)) { + return currentVersion + } + } + + return undefined + } + + function getReservedBitsCount (mode$1, version) { + // Character count indicator + mode indicator bits + return mode.getCharCountIndicator(mode$1, version) + 4 + } + + function getTotalBitsFromDataArray (segments, version) { + var totalBits = 0; + + segments.forEach(function (data) { + var reservedBits = getReservedBitsCount(data.mode, version); + totalBits += reservedBits + data.getBitsLength(); + }); + + return totalBits + } + + function getBestVersionForMixedData (segments, errorCorrectionLevel) { + for (var currentVersion = 1; currentVersion <= 40; currentVersion++) { + var length = getTotalBitsFromDataArray(segments, currentVersion); + if (length <= exports.getCapacity(currentVersion, errorCorrectionLevel, mode.MIXED)) { + return currentVersion + } + } + + return undefined + } + + /** + * Check if QR Code version is valid + * + * @param {Number} version QR Code version + * @return {Boolean} true if valid version, false otherwise + */ + exports.isValid = function isValid (version) { + return !isNaN(version) && version >= 1 && version <= 40 + }; + + /** + * Returns version number from a value. + * If value is not a valid version, returns defaultValue + * + * @param {Number|String} value QR Code version + * @param {Number} defaultValue Fallback value + * @return {Number} QR Code version number + */ + exports.from = function from (value, defaultValue) { + if (exports.isValid(value)) { + return parseInt(value, 10) + } + + return defaultValue + }; + + /** + * Returns how much data can be stored with the specified QR code version + * and error correction level + * + * @param {Number} version QR Code version (1-40) + * @param {Number} errorCorrectionLevel Error correction level + * @param {Mode} mode Data mode + * @return {Number} Quantity of storable data + */ + exports.getCapacity = function getCapacity (version, errorCorrectionLevel, mode$1) { + if (!exports.isValid(version)) { + throw new Error('Invalid QR Code version') + } + + // Use Byte mode as default + if (typeof mode$1 === 'undefined') mode$1 = mode.BYTE; + + // Total codewords for this QR code version (Data + Error correction) + var totalCodewords = utils.getSymbolTotalCodewords(version); + + // Total number of error correction codewords + var ecTotalCodewords = errorCorrectionCode.getTotalCodewordsCount(version, errorCorrectionLevel); + + // Total number of data codewords + var dataTotalCodewordsBits = (totalCodewords - ecTotalCodewords) * 8; + + if (mode$1 === mode.MIXED) return dataTotalCodewordsBits + + var usableBits = dataTotalCodewordsBits - getReservedBitsCount(mode$1, version); + + // Return max number of storable codewords + switch (mode$1) { + case mode.NUMERIC: + return Math.floor((usableBits / 10) * 3) + + case mode.ALPHANUMERIC: + return Math.floor((usableBits / 11) * 2) + + case mode.KANJI: + return Math.floor(usableBits / 13) + + case mode.BYTE: + default: + return Math.floor(usableBits / 8) + } + }; + + /** + * Returns the minimum version needed to contain the amount of data + * + * @param {Segment} data Segment of data + * @param {Number} [errorCorrectionLevel=H] Error correction level + * @param {Mode} mode Data mode + * @return {Number} QR Code version + */ + exports.getBestVersionForData = function getBestVersionForData (data, errorCorrectionLevel$1) { + var seg; + + var ecl = errorCorrectionLevel.from(errorCorrectionLevel$1, errorCorrectionLevel.M); + + if (isarray(data)) { + if (data.length > 1) { + return getBestVersionForMixedData(data, ecl) + } + + if (data.length === 0) { + return 1 + } + + seg = data[0]; + } else { + seg = data; + } + + return getBestVersionForDataLength(seg.mode, seg.getLength(), ecl) + }; + + /** + * Returns version information with relative error correction bits + * + * The version information is included in QR Code symbols of version 7 or larger. + * It consists of an 18-bit sequence containing 6 data bits, + * with 12 error correction bits calculated using the (18, 6) Golay code. + * + * @param {Number} version QR Code version + * @return {Number} Encoded version info bits + */ + exports.getEncodedBits = function getEncodedBits (version) { + if (!exports.isValid(version) || version < 7) { + throw new Error('Invalid QR Code version') + } + + var d = version << 12; + + while (utils.getBCHDigit(d) - G18_BCH >= 0) { + d ^= (G18 << (utils.getBCHDigit(d) - G18_BCH)); + } + + return (version << 12) | d + }; + }); + var version_1 = version.isValid; + var version_2 = version.getCapacity; + var version_3 = version.getBestVersionForData; + var version_4 = version.getEncodedBits; + + var G15 = (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0); + var G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1); + var G15_BCH = utils.getBCHDigit(G15); + + /** + * Returns format information with relative error correction bits + * + * The format information is a 15-bit sequence containing 5 data bits, + * with 10 error correction bits calculated using the (15, 5) BCH code. + * + * @param {Number} errorCorrectionLevel Error correction level + * @param {Number} mask Mask pattern + * @return {Number} Encoded format information bits + */ + var getEncodedBits = function getEncodedBits (errorCorrectionLevel, mask) { + var data = ((errorCorrectionLevel.bit << 3) | mask); + var d = data << 10; + + while (utils.getBCHDigit(d) - G15_BCH >= 0) { + d ^= (G15 << (utils.getBCHDigit(d) - G15_BCH)); + } + + // xor final data with mask pattern in order to ensure that + // no combination of Error Correction Level and data mask pattern + // will result in an all-zero data string + return ((data << 10) | d) ^ G15_MASK + }; + + var formatInfo = { + getEncodedBits: getEncodedBits + }; + + function NumericData (data) { + this.mode = mode.NUMERIC; + this.data = data.toString(); + } + + NumericData.getBitsLength = function getBitsLength (length) { + return 10 * Math.floor(length / 3) + ((length % 3) ? ((length % 3) * 3 + 1) : 0) + }; + + NumericData.prototype.getLength = function getLength () { + return this.data.length + }; + + NumericData.prototype.getBitsLength = function getBitsLength () { + return NumericData.getBitsLength(this.data.length) + }; + + NumericData.prototype.write = function write (bitBuffer) { + var i, group, value; + + // The input data string is divided into groups of three digits, + // and each group is converted to its 10-bit binary equivalent. + for (i = 0; i + 3 <= this.data.length; i += 3) { + group = this.data.substr(i, 3); + value = parseInt(group, 10); + + bitBuffer.put(value, 10); + } + + // If the number of input digits is not an exact multiple of three, + // the final one or two digits are converted to 4 or 7 bits respectively. + var remainingNum = this.data.length - i; + if (remainingNum > 0) { + group = this.data.substr(i); + value = parseInt(group, 10); + + bitBuffer.put(value, remainingNum * 3 + 1); + } + }; + + var numericData = NumericData; + + /** + * Array of characters available in alphanumeric mode + * + * As per QR Code specification, to each character + * is assigned a value from 0 to 44 which in this case coincides + * with the array index + * + * @type {Array} + */ + var ALPHA_NUM_CHARS = [ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + ' ', '$', '%', '*', '+', '-', '.', '/', ':' + ]; + + function AlphanumericData (data) { + this.mode = mode.ALPHANUMERIC; + this.data = data; + } + + AlphanumericData.getBitsLength = function getBitsLength (length) { + return 11 * Math.floor(length / 2) + 6 * (length % 2) + }; + + AlphanumericData.prototype.getLength = function getLength () { + return this.data.length + }; + + AlphanumericData.prototype.getBitsLength = function getBitsLength () { + return AlphanumericData.getBitsLength(this.data.length) + }; + + AlphanumericData.prototype.write = function write (bitBuffer) { + var i; + + // Input data characters are divided into groups of two characters + // and encoded as 11-bit binary codes. + for (i = 0; i + 2 <= this.data.length; i += 2) { + // The character value of the first character is multiplied by 45 + var value = ALPHA_NUM_CHARS.indexOf(this.data[i]) * 45; + + // The character value of the second digit is added to the product + value += ALPHA_NUM_CHARS.indexOf(this.data[i + 1]); + + // The sum is then stored as 11-bit binary number + bitBuffer.put(value, 11); + } + + // If the number of input data characters is not a multiple of two, + // the character value of the final character is encoded as a 6-bit binary number. + if (this.data.length % 2) { + bitBuffer.put(ALPHA_NUM_CHARS.indexOf(this.data[i]), 6); + } + }; + + var alphanumericData = AlphanumericData; + + function ByteData (data) { + this.mode = mode.BYTE; + this.data = new typedarrayBuffer(data); + } + + ByteData.getBitsLength = function getBitsLength (length) { + return length * 8 + }; + + ByteData.prototype.getLength = function getLength () { + return this.data.length + }; + + ByteData.prototype.getBitsLength = function getBitsLength () { + return ByteData.getBitsLength(this.data.length) + }; + + ByteData.prototype.write = function (bitBuffer) { + for (var i = 0, l = this.data.length; i < l; i++) { + bitBuffer.put(this.data[i], 8); + } + }; + + var byteData = ByteData; + + function KanjiData (data) { + this.mode = mode.KANJI; + this.data = data; + } + + KanjiData.getBitsLength = function getBitsLength (length) { + return length * 13 + }; + + KanjiData.prototype.getLength = function getLength () { + return this.data.length + }; + + KanjiData.prototype.getBitsLength = function getBitsLength () { + return KanjiData.getBitsLength(this.data.length) + }; + + KanjiData.prototype.write = function (bitBuffer) { + var i; + + // In the Shift JIS system, Kanji characters are represented by a two byte combination. + // These byte values are shifted from the JIS X 0208 values. + // JIS X 0208 gives details of the shift coded representation. + for (i = 0; i < this.data.length; i++) { + var value = utils.toSJIS(this.data[i]); + + // For characters with Shift JIS values from 0x8140 to 0x9FFC: + if (value >= 0x8140 && value <= 0x9FFC) { + // Subtract 0x8140 from Shift JIS value + value -= 0x8140; + + // For characters with Shift JIS values from 0xE040 to 0xEBBF + } else if (value >= 0xE040 && value <= 0xEBBF) { + // Subtract 0xC140 from Shift JIS value + value -= 0xC140; + } else { + throw new Error( + 'Invalid SJIS character: ' + this.data[i] + '\n' + + 'Make sure your charset is UTF-8') + } + + // Multiply most significant byte of result by 0xC0 + // and add least significant byte to product + value = (((value >>> 8) & 0xff) * 0xC0) + (value & 0xff); + + // Convert result to a 13-bit binary string + bitBuffer.put(value, 13); + } + }; + + var kanjiData = KanjiData; + + var dijkstra_1 = createCommonjsModule(function (module) { + + /****************************************************************************** + * Created 2008-08-19. + * + * Dijkstra path-finding functions. Adapted from the Dijkstar Python project. + * + * Copyright (C) 2008 + * Wyatt Baldwin + * All rights reserved + * + * Licensed under the MIT license. + * + * http://www.opensource.org/licenses/mit-license.php + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + *****************************************************************************/ + var dijkstra = { + single_source_shortest_paths: function(graph, s, d) { + // Predecessor map for each node that has been encountered. + // node ID => predecessor node ID + var predecessors = {}; + + // Costs of shortest paths from s to all nodes encountered. + // node ID => cost + var costs = {}; + costs[s] = 0; + + // Costs of shortest paths from s to all nodes encountered; differs from + // `costs` in that it provides easy access to the node that currently has + // the known shortest path from s. + // XXX: Do we actually need both `costs` and `open`? + var open = dijkstra.PriorityQueue.make(); + open.push(s, 0); + + var closest, + u, v, + cost_of_s_to_u, + adjacent_nodes, + cost_of_e, + cost_of_s_to_u_plus_cost_of_e, + cost_of_s_to_v, + first_visit; + while (!open.empty()) { + // In the nodes remaining in graph that have a known cost from s, + // find the node, u, that currently has the shortest path from s. + closest = open.pop(); + u = closest.value; + cost_of_s_to_u = closest.cost; + + // Get nodes adjacent to u... + adjacent_nodes = graph[u] || {}; + + // ...and explore the edges that connect u to those nodes, updating + // the cost of the shortest paths to any or all of those nodes as + // necessary. v is the node across the current edge from u. + for (v in adjacent_nodes) { + if (adjacent_nodes.hasOwnProperty(v)) { + // Get the cost of the edge running from u to v. + cost_of_e = adjacent_nodes[v]; + + // Cost of s to u plus the cost of u to v across e--this is *a* + // cost from s to v that may or may not be less than the current + // known cost to v. + cost_of_s_to_u_plus_cost_of_e = cost_of_s_to_u + cost_of_e; + + // If we haven't visited v yet OR if the current known cost from s to + // v is greater than the new cost we just found (cost of s to u plus + // cost of u to v across e), update v's cost in the cost list and + // update v's predecessor in the predecessor list (it's now u). + cost_of_s_to_v = costs[v]; + first_visit = (typeof costs[v] === 'undefined'); + if (first_visit || cost_of_s_to_v > cost_of_s_to_u_plus_cost_of_e) { + costs[v] = cost_of_s_to_u_plus_cost_of_e; + open.push(v, cost_of_s_to_u_plus_cost_of_e); + predecessors[v] = u; + } + } + } + } + + if (typeof d !== 'undefined' && typeof costs[d] === 'undefined') { + var msg = ['Could not find a path from ', s, ' to ', d, '.'].join(''); + throw new Error(msg); + } + + return predecessors; + }, + + extract_shortest_path_from_predecessor_list: function(predecessors, d) { + var nodes = []; + var u = d; + var predecessor; + while (u) { + nodes.push(u); + predecessor = predecessors[u]; + u = predecessors[u]; + } + nodes.reverse(); + return nodes; + }, + + find_path: function(graph, s, d) { + var predecessors = dijkstra.single_source_shortest_paths(graph, s, d); + return dijkstra.extract_shortest_path_from_predecessor_list( + predecessors, d); + }, + + /** + * A very naive priority queue implementation. + */ + PriorityQueue: { + make: function (opts) { + var T = dijkstra.PriorityQueue, + t = {}, + key; + opts = opts || {}; + for (key in T) { + if (T.hasOwnProperty(key)) { + t[key] = T[key]; + } + } + t.queue = []; + t.sorter = opts.sorter || T.default_sorter; + return t; + }, + + default_sorter: function (a, b) { + return a.cost - b.cost; + }, + + /** + * Add a new item to the queue and ensure the highest priority element + * is at the front of the queue. + */ + push: function (value, cost) { + var item = {value: value, cost: cost}; + this.queue.push(item); + this.queue.sort(this.sorter); + }, + + /** + * Return the highest priority element in the queue. + */ + pop: function () { + return this.queue.shift(); + }, + + empty: function () { + return this.queue.length === 0; + } + } + }; + + + // node.js module exports + { + module.exports = dijkstra; + } + }); + + var segments = createCommonjsModule(function (module, exports) { + /** + * Returns UTF8 byte length + * + * @param {String} str Input string + * @return {Number} Number of byte + */ + function getStringByteLength (str) { + return unescape(encodeURIComponent(str)).length + } + + /** + * Get a list of segments of the specified mode + * from a string + * + * @param {Mode} mode Segment mode + * @param {String} str String to process + * @return {Array} Array of object with segments data + */ + function getSegments (regex, mode, str) { + var segments = []; + var result; + + while ((result = regex.exec(str)) !== null) { + segments.push({ + data: result[0], + index: result.index, + mode: mode, + length: result[0].length + }); + } + + return segments + } + + /** + * Extracts a series of segments with the appropriate + * modes from a string + * + * @param {String} dataStr Input string + * @return {Array} Array of object with segments data + */ + function getSegmentsFromString (dataStr) { + var numSegs = getSegments(regex.NUMERIC, mode.NUMERIC, dataStr); + var alphaNumSegs = getSegments(regex.ALPHANUMERIC, mode.ALPHANUMERIC, dataStr); + var byteSegs; + var kanjiSegs; + + if (utils.isKanjiModeEnabled()) { + byteSegs = getSegments(regex.BYTE, mode.BYTE, dataStr); + kanjiSegs = getSegments(regex.KANJI, mode.KANJI, dataStr); + } else { + byteSegs = getSegments(regex.BYTE_KANJI, mode.BYTE, dataStr); + kanjiSegs = []; + } + + var segs = numSegs.concat(alphaNumSegs, byteSegs, kanjiSegs); + + return segs + .sort(function (s1, s2) { + return s1.index - s2.index + }) + .map(function (obj) { + return { + data: obj.data, + mode: obj.mode, + length: obj.length + } + }) + } + + /** + * Returns how many bits are needed to encode a string of + * specified length with the specified mode + * + * @param {Number} length String length + * @param {Mode} mode Segment mode + * @return {Number} Bit length + */ + function getSegmentBitsLength (length, mode$1) { + switch (mode$1) { + case mode.NUMERIC: + return numericData.getBitsLength(length) + case mode.ALPHANUMERIC: + return alphanumericData.getBitsLength(length) + case mode.KANJI: + return kanjiData.getBitsLength(length) + case mode.BYTE: + return byteData.getBitsLength(length) + } + } + + /** + * Merges adjacent segments which have the same mode + * + * @param {Array} segs Array of object with segments data + * @return {Array} Array of object with segments data + */ + function mergeSegments (segs) { + return segs.reduce(function (acc, curr) { + var prevSeg = acc.length - 1 >= 0 ? acc[acc.length - 1] : null; + if (prevSeg && prevSeg.mode === curr.mode) { + acc[acc.length - 1].data += curr.data; + return acc + } + + acc.push(curr); + return acc + }, []) + } + + /** + * Generates a list of all possible nodes combination which + * will be used to build a segments graph. + * + * Nodes are divided by groups. Each group will contain a list of all the modes + * in which is possible to encode the given text. + * + * For example the text '12345' can be encoded as Numeric, Alphanumeric or Byte. + * The group for '12345' will contain then 3 objects, one for each + * possible encoding mode. + * + * Each node represents a possible segment. + * + * @param {Array} segs Array of object with segments data + * @return {Array} Array of object with segments data + */ + function buildNodes (segs) { + var nodes = []; + for (var i = 0; i < segs.length; i++) { + var seg = segs[i]; + + switch (seg.mode) { + case mode.NUMERIC: + nodes.push([seg, + { data: seg.data, mode: mode.ALPHANUMERIC, length: seg.length }, + { data: seg.data, mode: mode.BYTE, length: seg.length } + ]); + break + case mode.ALPHANUMERIC: + nodes.push([seg, + { data: seg.data, mode: mode.BYTE, length: seg.length } + ]); + break + case mode.KANJI: + nodes.push([seg, + { data: seg.data, mode: mode.BYTE, length: getStringByteLength(seg.data) } + ]); + break + case mode.BYTE: + nodes.push([ + { data: seg.data, mode: mode.BYTE, length: getStringByteLength(seg.data) } + ]); + } + } + + return nodes + } + + /** + * Builds a graph from a list of nodes. + * All segments in each node group will be connected with all the segments of + * the next group and so on. + * + * At each connection will be assigned a weight depending on the + * segment's byte length. + * + * @param {Array} nodes Array of object with segments data + * @param {Number} version QR Code version + * @return {Object} Graph of all possible segments + */ + function buildGraph (nodes, version) { + var table = {}; + var graph = {'start': {}}; + var prevNodeIds = ['start']; + + for (var i = 0; i < nodes.length; i++) { + var nodeGroup = nodes[i]; + var currentNodeIds = []; + + for (var j = 0; j < nodeGroup.length; j++) { + var node = nodeGroup[j]; + var key = '' + i + j; + + currentNodeIds.push(key); + table[key] = { node: node, lastCount: 0 }; + graph[key] = {}; + + for (var n = 0; n < prevNodeIds.length; n++) { + var prevNodeId = prevNodeIds[n]; + + if (table[prevNodeId] && table[prevNodeId].node.mode === node.mode) { + graph[prevNodeId][key] = + getSegmentBitsLength(table[prevNodeId].lastCount + node.length, node.mode) - + getSegmentBitsLength(table[prevNodeId].lastCount, node.mode); + + table[prevNodeId].lastCount += node.length; + } else { + if (table[prevNodeId]) table[prevNodeId].lastCount = node.length; + + graph[prevNodeId][key] = getSegmentBitsLength(node.length, node.mode) + + 4 + mode.getCharCountIndicator(node.mode, version); // switch cost + } + } + } + + prevNodeIds = currentNodeIds; + } + + for (n = 0; n < prevNodeIds.length; n++) { + graph[prevNodeIds[n]]['end'] = 0; + } + + return { map: graph, table: table } + } + + /** + * Builds a segment from a specified data and mode. + * If a mode is not specified, the more suitable will be used. + * + * @param {String} data Input data + * @param {Mode | String} modesHint Data mode + * @return {Segment} Segment + */ + function buildSingleSegment (data, modesHint) { + var mode$1; + var bestMode = mode.getBestModeForData(data); + + mode$1 = mode.from(modesHint, bestMode); + + // Make sure data can be encoded + if (mode$1 !== mode.BYTE && mode$1.bit < bestMode.bit) { + throw new Error('"' + data + '"' + + ' cannot be encoded with mode ' + mode.toString(mode$1) + + '.\n Suggested mode is: ' + mode.toString(bestMode)) + } + + // Use Mode.BYTE if Kanji support is disabled + if (mode$1 === mode.KANJI && !utils.isKanjiModeEnabled()) { + mode$1 = mode.BYTE; + } + + switch (mode$1) { + case mode.NUMERIC: + return new numericData(data) + + case mode.ALPHANUMERIC: + return new alphanumericData(data) + + case mode.KANJI: + return new kanjiData(data) + + case mode.BYTE: + return new byteData(data) + } + } + + /** + * Builds a list of segments from an array. + * Array can contain Strings or Objects with segment's info. + * + * For each item which is a string, will be generated a segment with the given + * string and the more appropriate encoding mode. + * + * For each item which is an object, will be generated a segment with the given + * data and mode. + * Objects must contain at least the property "data". + * If property "mode" is not present, the more suitable mode will be used. + * + * @param {Array} array Array of objects with segments data + * @return {Array} Array of Segments + */ + exports.fromArray = function fromArray (array) { + return array.reduce(function (acc, seg) { + if (typeof seg === 'string') { + acc.push(buildSingleSegment(seg, null)); + } else if (seg.data) { + acc.push(buildSingleSegment(seg.data, seg.mode)); + } + + return acc + }, []) + }; + + /** + * Builds an optimized sequence of segments from a string, + * which will produce the shortest possible bitstream. + * + * @param {String} data Input string + * @param {Number} version QR Code version + * @return {Array} Array of segments + */ + exports.fromString = function fromString (data, version) { + var segs = getSegmentsFromString(data, utils.isKanjiModeEnabled()); + + var nodes = buildNodes(segs); + var graph = buildGraph(nodes, version); + var path = dijkstra_1.find_path(graph.map, 'start', 'end'); + + var optimizedSegs = []; + for (var i = 1; i < path.length - 1; i++) { + optimizedSegs.push(graph.table[path[i]].node); + } + + return exports.fromArray(mergeSegments(optimizedSegs)) + }; + + /** + * Splits a string in various segments with the modes which + * best represent their content. + * The produced segments are far from being optimized. + * The output of this function is only used to estimate a QR Code version + * which may contain the data. + * + * @param {string} data Input string + * @return {Array} Array of segments + */ + exports.rawSplit = function rawSplit (data) { + return exports.fromArray( + getSegmentsFromString(data, utils.isKanjiModeEnabled()) + ) + }; + }); + var segments_1 = segments.fromArray; + var segments_2 = segments.fromString; + var segments_3 = segments.rawSplit; + + /** + * QRCode for JavaScript + * + * modified by Ryan Day for nodejs support + * Copyright (c) 2011 Ryan Day + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + * + //--------------------------------------------------------------------- + // QRCode for JavaScript + // + // Copyright (c) 2009 Kazuhiko Arase + // + // URL: http://www.d-project.com/ + // + // Licensed under the MIT license: + // http://www.opensource.org/licenses/mit-license.php + // + // The word "QR Code" is registered trademark of + // DENSO WAVE INCORPORATED + // http://www.denso-wave.com/qrcode/faqpatent-e.html + // + //--------------------------------------------------------------------- + */ + + /** + * Add finder patterns bits to matrix + * + * @param {BitMatrix} matrix Modules matrix + * @param {Number} version QR Code version + */ + function setupFinderPattern (matrix, version) { + var size = matrix.size; + var pos = finderPattern.getPositions(version); + + for (var i = 0; i < pos.length; i++) { + var row = pos[i][0]; + var col = pos[i][1]; + + for (var r = -1; r <= 7; r++) { + if (row + r <= -1 || size <= row + r) continue + + for (var c = -1; c <= 7; c++) { + if (col + c <= -1 || size <= col + c) continue + + if ((r >= 0 && r <= 6 && (c === 0 || c === 6)) || + (c >= 0 && c <= 6 && (r === 0 || r === 6)) || + (r >= 2 && r <= 4 && c >= 2 && c <= 4)) { + matrix.set(row + r, col + c, true, true); + } else { + matrix.set(row + r, col + c, false, true); + } + } + } + } + } + + /** + * Add timing pattern bits to matrix + * + * Note: this function must be called before {@link setupAlignmentPattern} + * + * @param {BitMatrix} matrix Modules matrix + */ + function setupTimingPattern (matrix) { + var size = matrix.size; + + for (var r = 8; r < size - 8; r++) { + var value = r % 2 === 0; + matrix.set(r, 6, value, true); + matrix.set(6, r, value, true); + } + } + + /** + * Add alignment patterns bits to matrix + * + * Note: this function must be called after {@link setupTimingPattern} + * + * @param {BitMatrix} matrix Modules matrix + * @param {Number} version QR Code version + */ + function setupAlignmentPattern (matrix, version) { + var pos = alignmentPattern.getPositions(version); + + for (var i = 0; i < pos.length; i++) { + var row = pos[i][0]; + var col = pos[i][1]; + + for (var r = -2; r <= 2; r++) { + for (var c = -2; c <= 2; c++) { + if (r === -2 || r === 2 || c === -2 || c === 2 || + (r === 0 && c === 0)) { + matrix.set(row + r, col + c, true, true); + } else { + matrix.set(row + r, col + c, false, true); + } + } + } + } + } + + /** + * Add version info bits to matrix + * + * @param {BitMatrix} matrix Modules matrix + * @param {Number} version QR Code version + */ + function setupVersionInfo (matrix, version$1) { + var size = matrix.size; + var bits = version.getEncodedBits(version$1); + var row, col, mod; + + for (var i = 0; i < 18; i++) { + row = Math.floor(i / 3); + col = i % 3 + size - 8 - 3; + mod = ((bits >> i) & 1) === 1; + + matrix.set(row, col, mod, true); + matrix.set(col, row, mod, true); + } + } + + /** + * Add format info bits to matrix + * + * @param {BitMatrix} matrix Modules matrix + * @param {ErrorCorrectionLevel} errorCorrectionLevel Error correction level + * @param {Number} maskPattern Mask pattern reference value + */ + function setupFormatInfo (matrix, errorCorrectionLevel, maskPattern) { + var size = matrix.size; + var bits = formatInfo.getEncodedBits(errorCorrectionLevel, maskPattern); + var i, mod; + + for (i = 0; i < 15; i++) { + mod = ((bits >> i) & 1) === 1; + + // vertical + if (i < 6) { + matrix.set(i, 8, mod, true); + } else if (i < 8) { + matrix.set(i + 1, 8, mod, true); + } else { + matrix.set(size - 15 + i, 8, mod, true); + } + + // horizontal + if (i < 8) { + matrix.set(8, size - i - 1, mod, true); + } else if (i < 9) { + matrix.set(8, 15 - i - 1 + 1, mod, true); + } else { + matrix.set(8, 15 - i - 1, mod, true); + } + } + + // fixed module + matrix.set(size - 8, 8, 1, true); + } + + /** + * Add encoded data bits to matrix + * + * @param {BitMatrix} matrix Modules matrix + * @param {Buffer} data Data codewords + */ + function setupData (matrix, data) { + var size = matrix.size; + var inc = -1; + var row = size - 1; + var bitIndex = 7; + var byteIndex = 0; + + for (var col = size - 1; col > 0; col -= 2) { + if (col === 6) col--; + + while (true) { + for (var c = 0; c < 2; c++) { + if (!matrix.isReserved(row, col - c)) { + var dark = false; + + if (byteIndex < data.length) { + dark = (((data[byteIndex] >>> bitIndex) & 1) === 1); + } + + matrix.set(row, col - c, dark); + bitIndex--; + + if (bitIndex === -1) { + byteIndex++; + bitIndex = 7; + } + } + } + + row += inc; + + if (row < 0 || size <= row) { + row -= inc; + inc = -inc; + break + } + } + } + } + + /** + * Create encoded codewords from data input + * + * @param {Number} version QR Code version + * @param {ErrorCorrectionLevel} errorCorrectionLevel Error correction level + * @param {ByteData} data Data input + * @return {Buffer} Buffer containing encoded codewords + */ + function createData (version, errorCorrectionLevel, segments) { + // Prepare data buffer + var buffer = new bitBuffer(); + + segments.forEach(function (data) { + // prefix data with mode indicator (4 bits) + buffer.put(data.mode.bit, 4); + + // Prefix data with character count indicator. + // The character count indicator is a string of bits that represents the + // number of characters that are being encoded. + // The character count indicator must be placed after the mode indicator + // and must be a certain number of bits long, depending on the QR version + // and data mode + // @see {@link Mode.getCharCountIndicator}. + buffer.put(data.getLength(), mode.getCharCountIndicator(data.mode, version)); + + // add binary data sequence to buffer + data.write(buffer); + }); + + // Calculate required number of bits + var totalCodewords = utils.getSymbolTotalCodewords(version); + var ecTotalCodewords = errorCorrectionCode.getTotalCodewordsCount(version, errorCorrectionLevel); + var dataTotalCodewordsBits = (totalCodewords - ecTotalCodewords) * 8; + + // Add a terminator. + // If the bit string is shorter than the total number of required bits, + // a terminator of up to four 0s must be added to the right side of the string. + // If the bit string is more than four bits shorter than the required number of bits, + // add four 0s to the end. + if (buffer.getLengthInBits() + 4 <= dataTotalCodewordsBits) { + buffer.put(0, 4); + } + + // If the bit string is fewer than four bits shorter, add only the number of 0s that + // are needed to reach the required number of bits. + + // After adding the terminator, if the number of bits in the string is not a multiple of 8, + // pad the string on the right with 0s to make the string's length a multiple of 8. + while (buffer.getLengthInBits() % 8 !== 0) { + buffer.putBit(0); + } + + // Add pad bytes if the string is still shorter than the total number of required bits. + // Extend the buffer to fill the data capacity of the symbol corresponding to + // the Version and Error Correction Level by adding the Pad Codewords 11101100 (0xEC) + // and 00010001 (0x11) alternately. + var remainingByte = (dataTotalCodewordsBits - buffer.getLengthInBits()) / 8; + for (var i = 0; i < remainingByte; i++) { + buffer.put(i % 2 ? 0x11 : 0xEC, 8); + } + + return createCodewords(buffer, version, errorCorrectionLevel) + } + + /** + * Encode input data with Reed-Solomon and return codewords with + * relative error correction bits + * + * @param {BitBuffer} bitBuffer Data to encode + * @param {Number} version QR Code version + * @param {ErrorCorrectionLevel} errorCorrectionLevel Error correction level + * @return {Buffer} Buffer containing encoded codewords + */ + function createCodewords (bitBuffer, version, errorCorrectionLevel) { + // Total codewords for this QR code version (Data + Error correction) + var totalCodewords = utils.getSymbolTotalCodewords(version); + + // Total number of error correction codewords + var ecTotalCodewords = errorCorrectionCode.getTotalCodewordsCount(version, errorCorrectionLevel); + + // Total number of data codewords + var dataTotalCodewords = totalCodewords - ecTotalCodewords; + + // Total number of blocks + var ecTotalBlocks = errorCorrectionCode.getBlocksCount(version, errorCorrectionLevel); + + // Calculate how many blocks each group should contain + var blocksInGroup2 = totalCodewords % ecTotalBlocks; + var blocksInGroup1 = ecTotalBlocks - blocksInGroup2; + + var totalCodewordsInGroup1 = Math.floor(totalCodewords / ecTotalBlocks); + + var dataCodewordsInGroup1 = Math.floor(dataTotalCodewords / ecTotalBlocks); + var dataCodewordsInGroup2 = dataCodewordsInGroup1 + 1; + + // Number of EC codewords is the same for both groups + var ecCount = totalCodewordsInGroup1 - dataCodewordsInGroup1; + + // Initialize a Reed-Solomon encoder with a generator polynomial of degree ecCount + var rs = new reedSolomonEncoder(ecCount); + + var offset = 0; + var dcData = new Array(ecTotalBlocks); + var ecData = new Array(ecTotalBlocks); + var maxDataSize = 0; + var buffer = new typedarrayBuffer(bitBuffer.buffer); + + // Divide the buffer into the required number of blocks + for (var b = 0; b < ecTotalBlocks; b++) { + var dataSize = b < blocksInGroup1 ? dataCodewordsInGroup1 : dataCodewordsInGroup2; + + // extract a block of data from buffer + dcData[b] = buffer.slice(offset, offset + dataSize); + + // Calculate EC codewords for this data block + ecData[b] = rs.encode(dcData[b]); + + offset += dataSize; + maxDataSize = Math.max(maxDataSize, dataSize); + } + + // Create final data + // Interleave the data and error correction codewords from each block + var data = new typedarrayBuffer(totalCodewords); + var index = 0; + var i, r; + + // Add data codewords + for (i = 0; i < maxDataSize; i++) { + for (r = 0; r < ecTotalBlocks; r++) { + if (i < dcData[r].length) { + data[index++] = dcData[r][i]; + } + } + } + + // Apped EC codewords + for (i = 0; i < ecCount; i++) { + for (r = 0; r < ecTotalBlocks; r++) { + data[index++] = ecData[r][i]; + } + } + + return data + } + + /** + * Build QR Code symbol + * + * @param {String} data Input string + * @param {Number} version QR Code version + * @param {ErrorCorretionLevel} errorCorrectionLevel Error level + * @return {Object} Object containing symbol data + */ + function createSymbol (data, version$1, errorCorrectionLevel) { + var segments$1; + + if (isarray(data)) { + segments$1 = segments.fromArray(data); + } else if (typeof data === 'string') { + var estimatedVersion = version$1; + + if (!estimatedVersion) { + var rawSegments = segments.rawSplit(data); + + // Estimate best version that can contain raw splitted segments + estimatedVersion = version.getBestVersionForData(rawSegments, + errorCorrectionLevel); + } + + // Build optimized segments + // If estimated version is undefined, try with the highest version + segments$1 = segments.fromString(data, estimatedVersion); + } else { + throw new Error('Invalid data') + } + + // Get the min version that can contain data + var bestVersion = version.getBestVersionForData(segments$1, + errorCorrectionLevel); + + // If no version is found, data cannot be stored + if (!bestVersion) { + throw new Error('The amount of data is too big to be stored in a QR Code') + } + + // If not specified, use min version as default + if (!version$1) { + version$1 = bestVersion; + + // Check if the specified version can contain the data + } else if (version$1 < bestVersion) { + throw new Error('\n' + + 'The chosen QR Code version cannot contain this amount of data.\n' + + 'Minimum version required to store current data is: ' + bestVersion + '.\n' + ) + } + + var dataBits = createData(version$1, errorCorrectionLevel, segments$1); + + // Allocate matrix buffer + var moduleCount = utils.getSymbolSize(version$1); + var modules = new bitMatrix(moduleCount); + + // Add function modules + setupFinderPattern(modules, version$1); + setupTimingPattern(modules); + setupAlignmentPattern(modules, version$1); + + // Add temporary dummy bits for format info just to set them as reserved. + // This is needed to prevent these bits from being masked by {@link MaskPattern.applyMask} + // since the masking operation must be performed only on the encoding region. + // These blocks will be replaced with correct values later in code. + setupFormatInfo(modules, errorCorrectionLevel, 0); + + if (version$1 >= 7) { + setupVersionInfo(modules, version$1); + } + + // Add data codewords + setupData(modules, dataBits); + + // Find best mask pattern + var maskPattern$1 = maskPattern.getBestMask(modules, + setupFormatInfo.bind(null, modules, errorCorrectionLevel)); + + // Apply mask pattern + maskPattern.applyMask(maskPattern$1, modules); + + // Replace format info bits with correct values + setupFormatInfo(modules, errorCorrectionLevel, maskPattern$1); + + return { + modules: modules, + version: version$1, + errorCorrectionLevel: errorCorrectionLevel, + maskPattern: maskPattern$1, + segments: segments$1 + } + } + + /** + * QR Code + * + * @param {String | Array} data Input data + * @param {Object} options Optional configurations + * @param {Number} options.version QR Code version + * @param {String} options.errorCorrectionLevel Error correction level + * @param {Function} options.toSJISFunc Helper func to convert utf8 to sjis + */ + var create = function create (data, options) { + if (typeof data === 'undefined' || data === '') { + throw new Error('No input text') + } + + var errorCorrectionLevel$1 = errorCorrectionLevel.M; + var version$1; + + if (typeof options !== 'undefined') { + // Use higher error correction level as default + errorCorrectionLevel$1 = errorCorrectionLevel.from(options.errorCorrectionLevel, errorCorrectionLevel.M); + version$1 = version.from(options.version); + + if (options.toSJISFunc) { + utils.setToSJISFunction(options.toSJISFunc); + } + } + + return createSymbol(data, version$1, errorCorrectionLevel$1) + }; + + var qrcode = { + create: create + }; + + function hex2rgba (hex) { + if (typeof hex !== 'string') { + throw new Error('Color should be defined as hex string') + } + + var hexCode = hex.slice().replace('#', '').split(''); + if (hexCode.length < 3 || hexCode.length === 5 || hexCode.length > 8) { + throw new Error('Invalid hex color: ' + hex) + } + + // Convert from short to long form (fff -> ffffff) + if (hexCode.length === 3 || hexCode.length === 4) { + hexCode = Array.prototype.concat.apply([], hexCode.map(function (c) { + return [c, c] + })); + } + + // Add default alpha value + if (hexCode.length === 6) hexCode.push('F', 'F'); + + var hexValue = parseInt(hexCode.join(''), 16); + + return { + r: (hexValue >> 24) & 255, + g: (hexValue >> 16) & 255, + b: (hexValue >> 8) & 255, + a: hexValue & 255 + } + } + + var getOptions = function getOptions (options) { + if (!options) options = {}; + if (!options.color) options.color = {}; + + var margin = typeof options.margin === 'undefined' || + options.margin === null || + options.margin < 0 ? 4 : options.margin; + + return { + scale: options.scale || 4, + margin: margin, + color: { + dark: hex2rgba(options.color.dark || '#000000ff'), + light: hex2rgba(options.color.light || '#ffffffff') + }, + type: options.type, + rendererOpts: options.rendererOpts || {} + } + }; + + var qrToImageData = function qrToImageData (imgData, qr, margin, scale, color) { + var size = qr.modules.size; + var data = qr.modules.data; + var scaledMargin = margin * scale; + var symbolSize = size * scale + scaledMargin * 2; + var palette = [color.light, color.dark]; + + for (var i = 0; i < symbolSize; i++) { + for (var j = 0; j < symbolSize; j++) { + var posDst = (i * symbolSize + j) * 4; + var pxColor = color.light; + + if (i >= scaledMargin && j >= scaledMargin && + i < symbolSize - scaledMargin && j < symbolSize - scaledMargin) { + var iSrc = Math.floor((i - scaledMargin) / scale); + var jSrc = Math.floor((j - scaledMargin) / scale); + pxColor = palette[data[iSrc * size + jSrc]]; + } + + imgData[posDst++] = pxColor.r; + imgData[posDst++] = pxColor.g; + imgData[posDst++] = pxColor.b; + imgData[posDst] = pxColor.a; + } + } + }; + + var utils$1 = { + getOptions: getOptions, + qrToImageData: qrToImageData + }; + + var canvas = createCommonjsModule(function (module, exports) { + function clearCanvas (ctx, canvas, size) { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + if (!canvas.style) canvas.style = {}; + canvas.height = size; + canvas.width = size; + canvas.style.height = size + 'px'; + canvas.style.width = size + 'px'; + } + + function getCanvasElement () { + try { + return document.createElement('canvas') + } catch (e) { + throw new Error('You need to specify a canvas element') + } + } + + exports.render = function render (qrData, canvas, options) { + var opts = options; + var canvasEl = canvas; + + if (typeof opts === 'undefined' && (!canvas || !canvas.getContext)) { + opts = canvas; + canvas = undefined; + } + + if (!canvas) { + canvasEl = getCanvasElement(); + } + + opts = utils$1.getOptions(opts); + var size = (qrData.modules.size + opts.margin * 2) * opts.scale; + + var ctx = canvasEl.getContext('2d'); + var image = ctx.createImageData(size, size); + utils$1.qrToImageData(image.data, qrData, opts.margin, opts.scale, opts.color); + + clearCanvas(ctx, canvasEl, size); + ctx.putImageData(image, 0, 0); + + return canvasEl + }; + + exports.renderToDataURL = function renderToDataURL (qrData, canvas, options) { + var opts = options; + + if (typeof opts === 'undefined' && (!canvas || !canvas.getContext)) { + opts = canvas; + canvas = undefined; + } + + if (!opts) opts = {}; + + var canvasEl = exports.render(qrData, canvas, opts); + + var type = opts.type || 'image/png'; + var rendererOpts = opts.rendererOpts || {}; + + return canvasEl.toDataURL(type, rendererOpts.quality) + }; + }); + var canvas_1 = canvas.render; + var canvas_2 = canvas.renderToDataURL; + + function getColorAttrib (color) { + return 'fill="rgb(' + [color.r, color.g, color.b].join(',') + ')" ' + + 'fill-opacity="' + (color.a / 255).toFixed(2) + '"' + } + + var render = function render (qrData, options) { + var opts = utils$1.getOptions(options); + var size = qrData.modules.size; + var data = qrData.modules.data; + var qrcodesize = (size + opts.margin * 2) * opts.scale; + + var xmlStr = '\n'; + xmlStr += '\n'; + + xmlStr += '\n'; + xmlStr += '\n'; + xmlStr += '\n'; + + for (var i = 0; i < size; i++) { + for (var j = 0; j < size; j++) { + if (!data[i * size + j]) continue + + var x = (opts.margin + j) * opts.scale; + var y = (opts.margin + i) * opts.scale; + xmlStr += '\n'; + } + } + + xmlStr += '\n'; + xmlStr += ''; + + return xmlStr + }; + + var svgRender = { + render: render + }; + + var browser = createCommonjsModule(function (module, exports) { + function renderCanvas (renderFunc, canvas, text, opts, cb) { + var argsNum = arguments.length - 1; + if (argsNum < 2) { + throw new Error('Too few arguments provided') + } + + if (argsNum === 2) { + cb = text; + text = canvas; + canvas = opts = undefined; + } else if (argsNum === 3) { + if (canvas.getContext && typeof cb === 'undefined') { + cb = opts; + opts = undefined; + } else { + cb = opts; + opts = text; + text = canvas; + canvas = undefined; + } + } + + if (typeof cb !== 'function') { + throw new Error('Callback required as last argument') + } + + try { + var data = qrcode.create(text, opts); + cb(null, renderFunc(data, canvas, opts)); + } catch (e) { + cb(e); + } + } + + exports.create = qrcode.create; + exports.toCanvas = renderCanvas.bind(null, canvas.render); + exports.toDataURL = renderCanvas.bind(null, canvas.renderToDataURL); + + // only svg for now. + exports.toString = renderCanvas.bind(null, function (data, _, opts) { + return svgRender.render(data, opts) + }); + + /** + * Legacy API + */ + exports.qrcodedraw = function () { + return { + draw: exports.toCanvas + } + }; + }); + var browser_1 = browser.create; + var browser_2 = browser.toCanvas; + var browser_3 = browser.toDataURL; + var browser_4 = browser.qrcodedraw; + + var isMobile = function isMobile() { + return navigator && ('userAgent' in navigator && navigator.userAgent.match(/iPhone|iPod|iPad|Android/i) || navigator.maxTouchPoints > 1 && navigator.platform === 'MacIntel'); + }; + + var removeLoader = function removeLoader(element) { + while (element.firstChild) { + element.removeChild(element.firstChild); + } + }; + + var haveStyleSheet = false; + + var setLoader = function setLoader(element, styles) { + var loaderClass = "".concat(styles.prefix || 'wwp_', "qrcode_loader"); + var loader = document.createElement('div'); + loader.className = loaderClass; + loader.innerHTML = "
\n
\n
\n
"); + + if (!haveStyleSheet) { + var style = document.createElement('style'); + style.innerHTML = "@keyframes ".concat(styles.prefix || 'wwp_', "pulse {\n 0% { opacity: 1; }\n 100% { opacity: 0; }\n }\n .").concat(loaderClass, " {\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n justify-content: space-around;\n align-items: center;\n width: 30%;\n height: 30%;\n margin-left: 35%;\n padding-top: 35%;\n }\n .").concat(loaderClass, "_blk {\n height: 35%;\n width: 35%;\n animation: ").concat(styles.prefix || 'wwp_', "pulse 0.75s ease-in infinite alternate;\n background-color: #cccccc;\n }\n .").concat(loaderClass, "_delay {\n animation-delay: 0.75s;\n }"); + document.getElementsByTagName('head')[0].appendChild(style); + haveStyleSheet = true; + } + + removeLoader(element); + element.appendChild(loader); + }; + + var setRefersh = function setRefersh(element, error) { + var httpsRequired = error instanceof WWPassError && error.code === WWPASS_STATUS.SSL_REQUIRED; + var offline = window.navigator.onLine !== undefined && !window.navigator.onLine; + var wrapper = document.createElement('div'); + wrapper.style.display = 'flex'; + wrapper.style.alignItems = 'center'; + wrapper.style.height = '100%'; + wrapper.style.width = '100%'; + var refreshNote = document.createElement('div'); + refreshNote.style.margin = '0 10%'; + refreshNote.style.width = '80%'; + refreshNote.style.textAlign = 'center'; + refreshNote.style.overflow = 'hidden'; + var text = 'Error occured'; + + if (httpsRequired) { + text = 'Please use HTTPS'; + } else if (offline) { + text = 'No internet connection'; + } + + refreshNote.innerHTML = "

".concat(text, "

"); + var refreshButton = null; + + if (!httpsRequired) { + refreshButton = document.createElement('a'); + refreshButton.textContent = 'Retry'; + refreshButton.style.fontWeight = '400'; + refreshButton.style.fontFamily = '"Arial", sans-serif'; + refreshButton.style.fontSize = '1.2em'; + refreshButton.style.lineHeight = '1.7em'; + refreshButton.style.cursor = 'pointer'; + refreshButton.href = '#'; + refreshNote.appendChild(refreshButton); + } + + wrapper.appendChild(refreshNote); // eslint-disable-next-line no-console + + console.error("Error in WWPass Library: ".concat(error)); + removeLoader(element); + element.appendChild(wrapper); + return httpsRequired ? Promise.reject(error.message) : new Promise(function (resolve) { + // Refresh after 1 minute or on click + setTimeout(function () { + resolve({ + refresh: true + }); + }, 60000); + refreshButton.addEventListener('click', function (event) { + resolve({ + refresh: true + }); + event.preventDefault(); + }); + + if (offline) { + window.addEventListener('online', function () { + return resolve({ + refresh: true + }); + }); + } + }); + }; + + var debouncePageVisibilityFactory = function debouncePageVisibilityFactory() { + var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'visible'; + var debounce = null; + return function (fn) { + debounce = fn; + + var onDebounce = function onDebounce() { + if (document.visibilityState === state) { + debounce(); + document.removeEventListener('visibilitychange', onDebounce); + } + }; + + if (document.visibilityState === state) { + debounce(); + } else { + document.addEventListener('visibilitychange', onDebounce); + } + }; + }; + + var debouncePageVisible = debouncePageVisibilityFactory(); + + var QRCodePromise = function QRCodePromise(parentElement, wwpassURLoptions, ttl, qrcodeStyle) { + return new Promise(function (resolve) { + var QRCodeElement = document.createElement('canvas'); + browser.toCanvas(QRCodeElement, getUniversalURL(wwpassURLoptions, false), qrcodeStyle || {}, function (error) { + if (error) { + throw error; + } + }); + + if (qrcodeStyle) { + QRCodeElement.className = "".concat(qrcodeStyle.prefix, "qrcode_canvas"); + QRCodeElement.style.max_width = "".concat(qrcodeStyle.width, "px"); + QRCodeElement.style.max_height = "".concat(qrcodeStyle.width, "px"); + } + + QRCodeElement.style.height = '100%'; + QRCodeElement.style.width = '100%'; + + if (isMobile()) { + // Wrapping QRCode canvas in + var universalLinkElement = document.createElement('a'); + universalLinkElement.href = getUniversalURL(wwpassURLoptions); + universalLinkElement.appendChild(QRCodeElement); + universalLinkElement.addEventListener('click', function () { + resolve({ + away: true + }); + }); + QRCodeElement = universalLinkElement; + } + + removeLoader(parentElement); + parentElement.appendChild(QRCodeElement); + setTimeout(function () { + debouncePageVisible(function () { + resolve({ + refresh: true + }); + }); + }, ttl * 900); + }); + }; + + var clearQRCode = function clearQRCode(parentElement, style) { + return setLoader(parentElement, style); + }; + + // const DEFAULT_WAIT_CLASS = 'focused'; + // style.transition = 'all .4s ease-out'; + // style.opacity = '.3'; + + var PROTOCOL_VERSION = 2; + var WAIT_ON_CLICK = 2000; + var WAIT_ON_ERROR = 500; + + function wait(ms) { + if (ms) return new Promise(function (r) { + return setTimeout(r, ms); + }); + return null; + } + + var tryQRCodeAuth = + /*#__PURE__*/ + function () { + var _ref = asyncToGenerator( + /*#__PURE__*/ + regenerator.mark(function _callee(options) { + var log, json, response, ticket, ttl, key, wwpassURLoptions, result; + return regenerator.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + log = options.log; + _context.prev = 1; + log(options); + clearQRCode(options.qrcode, options.qrcodeStyle); + _context.next = 6; + return getTicket(options.ticketURL); + + case 6: + json = _context.sent; + response = ticketAdapter(json); + ticket = response.ticket; + ttl = response.ttl; + _context.next = 12; + return getClientNonceWrapper(ticket, ttl); + + case 12: + key = _context.sent; + wwpassURLoptions = { + universal: options.universal, + ticket: ticket, + callbackURL: options.callbackURL, + ppx: options.ppx, + version: PROTOCOL_VERSION, + clientKey: key ? encodeClientKey(key) : undefined + }; + _context.next = 16; + return Promise.race([QRCodePromise(options.qrcode, wwpassURLoptions, ttl, options.qrcodeStyle), getWebSocketResult({ + callbackURL: options.callbackURL, + ticket: ticket, + log: log, + development: options.development, + version: options.version, + ppx: options.ppx, + spfewsAddress: options.spfewsAddress, + returnErrors: options.returnErrors + })]); + + case 16: + result = _context.sent; + clearQRCode(options.qrcode, options.qrcodeStyle); + + if (!result.refresh) { + _context.next = 20; + break; + } + + return _context.abrupt("return", { + status: WWPASS_STATUS.CONTINUE, + reason: 'Need to refresh QRCode' + }); + + case 20: + if (result.clientKey && options.catchClientKey) { + options.catchClientKey(result.clientKey); + } + + if (!result.away) { + _context.next = 24; + break; + } + + closeConnectionPool(); + return _context.abrupt("return", { + status: WWPASS_STATUS.OK, + reason: 'User clicked on QRCode' + }); + + case 24: + return _context.abrupt("return", result); + + case 27: + _context.prev = 27; + _context.t0 = _context["catch"](1); + + if (_context.t0.status) { + _context.next = 35; + break; + } + + log('QRCode auth error', _context.t0); + _context.next = 33; + return setRefersh(options.qrcode, _context.t0); + + case 33: + clearQRCode(options.qrcode, options.qrcodeStyle); + return _context.abrupt("return", { + status: WWPASS_STATUS.NETWORK_ERROR, + reason: _context.t0 + }); + + case 35: + clearQRCode(options.qrcode, options.qrcodeStyle); + + if (!(_context.t0.status === WWPASS_STATUS.INTERNAL_ERROR || options.returnErrors)) { + _context.next = 39; + break; + } + + navigateToCallback(_context.t0); + return _context.abrupt("return", _context.t0); + + case 39: + if (_context.t0.status === WWPASS_STATUS.TICKET_TIMEOUT) { + log('ticket timed out'); + } + + return _context.abrupt("return", _context.t0); + + case 41: + case "end": + return _context.stop(); + } + } + }, _callee, null, [[1, 27]]); + })); + + return function tryQRCodeAuth(_x) { + return _ref.apply(this, arguments); + }; + }(); + + var getDelay = function getDelay(status) { + switch (status) { + case WWPASS_STATUS.OK: + return WAIT_ON_CLICK; + + case WWPASS_STATUS.CONTINUE: + return 0; + + default: + return WAIT_ON_ERROR; + } + }; + /* + * WWPass QR code auth function + * + options = { + 'ticketURL': undefined, // string + 'callbackURL': undefined, // string + 'development': false, // work with dev server + 'log': function (message) || console.log, // another log handler + } + */ + + + var wwpassQRCodeAuth = + /*#__PURE__*/ + function () { + var _ref2 = asyncToGenerator( + /*#__PURE__*/ + regenerator.mark(function _callee2(initialOptions) { + var defaultOptions, options, result; + return regenerator.wrap(function _callee2$(_context2) { + while (1) { + switch (_context2.prev = _context2.next) { + case 0: + defaultOptions = { + universal: false, + ticketURL: undefined, + callbackURL: undefined, + development: false, + once: false, + // Repeat authentication while possible + version: 2, + ppx: 'wwp_', + spfewsAddress: 'wss://spfews.wwpass.com', + qrcodeStyle: { + width: 256, + prefix: 'wwp_' + }, + log: function log() {} + }; + options = objectSpread({}, defaultOptions, initialOptions); + options.qrcodeStyle = objectSpread({}, defaultOptions.qrcodeStyle, initialOptions.qrcodeStyle); + + if (options.ticketURL) { + _context2.next = 5; + break; + } + + throw Error('ticketURL not found'); + + case 5: + if (options.callbackURL) { + _context2.next = 7; + break; + } + + throw Error('callbackURL not found'); + + case 7: + if (options.qrcode) { + _context2.next = 9; + break; + } + + throw Error('Element not found'); + + case 9: + _context2.next = 11; + return tryQRCodeAuth(options); + + case 11: + result = _context2.sent; + + if (!(options.once && result.status !== WWPASS_STATUS.CONTINUE)) { + _context2.next = 14; + break; + } + + return _context2.abrupt("return", result); + + case 14: + _context2.next = 16; + return wait(getDelay(result.status)); + + case 16: + if (document.documentElement.contains(options.qrcode)) { + _context2.next = 9; + break; + } + + case 17: + return _context2.abrupt("return", { + status: WWPASS_STATUS.TERMINAL_ERROR, + reason: 'QRCode element is not in DOM' + }); + + case 18: + case "end": + return _context2.stop(); + } + } + }, _callee2); + })); + + return function wwpassQRCodeAuth(_x2) { + return _ref2.apply(this, arguments); + }; + }(); + + var openWithTicket = function openWithTicket(initialOptions) { + return new Promise(function (resolve) { + var defaultOptions = { + ticket: '', + ttl: 120, + callbackURL: '', + ppx: 'wwp_', + away: true + }; + + var options = objectSpread({}, defaultOptions, initialOptions); + + if (isClientKeyTicket(options.ticket)) { + generateClientNonce(options.ticket, options.ttl).then(function (key) { + options = objectSpread({}, options, { + clientKey: encodeClientKey(key) + }); + var url = getUniversalURL(options); + + if (options.away) { + window.location.href = url; + } else { + resolve(url); + } + }); + } else { + var url = getUniversalURL(options); + + if (options.away) { + window.location.href = url; + } else { + resolve(url); + } + } + }); + }; + + var prefix = window.location.protocol === 'https:' ? 'https:' : 'http:'; + var CSS = "".concat(prefix, "//cdn.wwpass.com/packages/wwpass.js/2.4/wwpass.js.css"); + + var isNativeMessaging = function isNativeMessaging() { + var _navigator = navigator, + userAgent = _navigator.userAgent; + var re = /Firefox\/([0-9]+)\./; + var match = userAgent.match(re); + + if (match && match.length > 1) { + var version = match[1]; + + if (Number(version) >= 51) { + return 'Firefox'; + } + } + + re = /Chrome\/([0-9]+)\./; + match = userAgent.match(re); + + if (match && match.length > 1) { + var _version = match[1]; + + if (Number(_version) >= 45) { + return 'Chrome'; + } + } + + return false; + }; + + var wwpassPlatformName = function wwpassPlatformName() { + var _navigator2 = navigator, + userAgent = _navigator2.userAgent; + var knownPlatforms = ['Android', 'iPhone', 'iPad']; + + for (var i = 0; i < knownPlatforms.length; i += 1) { + if (userAgent.search(new RegExp(knownPlatforms[i], 'i')) !== -1) { + return knownPlatforms[i]; + } + } + + return null; + }; + + var wwpassMessageForPlatform = function wwpassMessageForPlatform(platformName) { + return "".concat(WWPASS_UNSUPPORTED_PLATFORM_MSG_TMPL, " ").concat(platformName); + }; + + var wwpassShowError = function wwpassShowError(message, title, onCloseCallback) { + if (!document.getElementById('_wwpass_css')) { + var l = document.createElement('link'); + l.id = '_wwpass_css'; + l.rel = 'stylesheet'; + l.href = CSS; + document.head.appendChild(l); + } + + var dlg = document.createElement('div'); + dlg.id = '_wwpass_err_dlg'; + var dlgClose = document.createElement('span'); + dlgClose.innerHTML = 'Close'; + dlgClose.id = '_wwpass_err_close'; + var header = document.createElement('h1'); + header.innerHTML = title; + var text = document.createElement('div'); + text.innerHTML = message; + dlg.appendChild(header); + dlg.appendChild(text); + dlg.appendChild(dlgClose); + document.body.appendChild(dlg); + document.getElementById('_wwpass_err_close').addEventListener('click', function () { + var elem = document.getElementById('_wwpass_err_dlg'); + elem.parentNode.removeChild(elem); + onCloseCallback(); + return false; + }); + return true; + }; + + var wwpassNoSoftware = function wwpassNoSoftware(code, onclose) { + if (code === WWPASS_STATUS.NO_AUTH_INTERFACES_FOUND) { + var client = isNativeMessaging(); + var message = ''; + + if (client) { + if (client === 'Chrome') { + var returnURL = encodeURIComponent(window.location.href); + message = '

The WWPass Authentication extension for Chrome is not installed or is disabled in browser settings.'; + message += '

Click the link below to install and enable the WWPass Authentication extension.'; + message += "

Install WWPass Authentication Extension"); + } else if (client === 'Firefox') { + // Firefox + var _returnURL = encodeURIComponent(window.location.href); + + message = '

The WWPass Authentication extension for Firefox is not installed or is disabled in browser settings.'; + message += '

Click the link below to install and enable the WWPass Authentication extension.'; + message += "

Install WWPass Authentication Extension"); + } + } else { + message = '

No Security Pack is found on your computer or WWPass Browser Plugin is disabled.

To install Security Pack visit Key Services or check plugin settings of your browser to activate WWPass Browser Plugin.

Learn more...

'; + } + + wwpassShowError(message, 'WWPass — No Software Found', onclose); + } else if (code === WWPASS_STATUS.UNSUPPORTED_PLATFORM) { + wwpassShowError(wwpassMessageForPlatform(wwpassPlatformName()), 'WWPass — Unsupported Platform', onclose); + } + }; + + var renderPassKeyButton = function renderPassKeyButton() { + var button = document.createElement('button'); + button.innerHTML = ' Log in with PassKey'; + button.setAttribute('style', 'color: white; background-color: #2277E6; font-weight: 400; font-size: 18px; line-height: 36px; font-family: "Arial", sans-serif; padding-right: 15px; cursor: pointer; height: 40px; width: 255px; border-radius: 3px; border: 1px solid #2277E6; padding-left: 60px; text-decoration: none; position: relative;'); + return button; + }; + + var PLUGIN_OBJECT_ID = '_wwpass_plugin'; + var PLUGIN_MIME_TYPE = 'application/x-wwauth'; + var PLUGIN_TIMEOUT = 10000; + var REDUCED_PLUGIN_TIMEOUT = 1000; + var PLUGIN_AUTH_KEYTYPE_REVISION = 9701; + var PluginInfo = {}; + var savedPluginInstance; + var pendingReqests = []; + + var havePlugin = function havePlugin() { + return navigator.mimeTypes[PLUGIN_MIME_TYPE] !== undefined; + }; + + var wwpassPluginShowsErrors = function wwpassPluginShowsErrors(pluginVersionString) { + if (typeof pluginVersionString === 'string') { + var pluginVersion = pluginVersionString.split('.'); + + for (var i = 0; i < pluginVersion.length; i += 1) { + pluginVersion[i] = parseInt(pluginVersion[i], 10); + } + + if (pluginVersion.length === 3) { + if (pluginVersion[0] > 2 || pluginVersion[0] === 2 && pluginVersion[1] > 4 || pluginVersion[0] === 2 && pluginVersion[1] === 4 && pluginVersion[2] >= 1305) { + return true; + } + } + } + + return false; + }; + + var getPluginInstance = function getPluginInstance(log) { + return new Promise(function (resolve, reject) { + if (savedPluginInstance) { + if (window._wwpass_plugin_loaded !== undefined) { + // eslint-disable-line no-underscore-dangle + pendingReqests.push([resolve, reject]); + } else { + log('%s: plugin is already initialized', 'getPluginInstance'); + resolve(savedPluginInstance); + } + } else { + var junkBrowser = navigator.mimeTypes.length === 0; + var pluginInstalled = havePlugin(); + var timeout = junkBrowser ? REDUCED_PLUGIN_TIMEOUT : PLUGIN_TIMEOUT; + + if (pluginInstalled || junkBrowser) { + log('%s: trying to create plugin instance(junkBrowser=%s, timeout=%d)', 'getPluginInstance', junkBrowser, timeout); + var pluginHtml = ""); + var pluginDiv = document.createElement('div'); + pluginDiv.setAttribute('style', 'position: fixed; left: 0; top:0; width: 1px; height: 1px; z-index: -1; opacity: 0.01'); + document.body.appendChild(pluginDiv); + pluginDiv.innerHTML += pluginHtml; + savedPluginInstance = document.getElementById(PLUGIN_OBJECT_ID); + var timer = setTimeout(function () { + delete window._wwpass_plugin_loaded; // eslint-disable-line no-underscore-dangle + + savedPluginInstance = null; + log('%s: WWPass plugin loading timeout', 'getPluginInstance'); + reject({ + code: WWPASS_STATUS.NO_AUTH_INTERFACES_FOUND, + message: WWPASS_NO_AUTH_INTERFACES_FOUND_MSG + }); + + for (var i = 0; i < pendingReqests.length; i += 1) { + var pendingReject = pendingReqests[i][1]; + pendingReject({ + code: WWPASS_STATUS.NO_AUTH_INTERFACES_FOUND, + message: WWPASS_NO_AUTH_INTERFACES_FOUND_MSG + }); + } + }, PLUGIN_TIMEOUT); + + window._wwpass_plugin_loaded = function () { + // eslint-disable-line no-underscore-dangle + log('%s: plugin loaded', 'getPluginInstance'); + delete window._wwpass_plugin_loaded; // eslint-disable-line no-underscore-dangle + + clearTimeout(timer); + + try { + PluginInfo.versionString = savedPluginInstance.version; + PluginInfo.revision = parseInt(savedPluginInstance.version.split('.')[2], 10); + PluginInfo.showsErrors = wwpassPluginShowsErrors(PluginInfo.versionString); + } catch (err) { + log('%s: error parsing plugin version: %s', 'getPluginInstance', err); + } + + resolve(savedPluginInstance); + + for (var i = 0; i < pendingReqests.length; i += 1) { + var pendingResolve = pendingReqests[i][0]; + pendingResolve(savedPluginInstance); + } + }; + } else { + log('%s: no suitable plugins installed', 'getPluginInstance'); + reject({ + code: WWPASS_STATUS.NO_AUTH_INTERFACES_FOUND, + message: WWPASS_NO_AUTH_INTERFACES_FOUND_MSG + }); + } + } + }); + }; + + var wrapCallback = function wrapCallback(callback) { + if (!PluginInfo.showsErrors) { + return function (code, ticketOrMessage) { + if (code !== WWPASS_STATUS.OK && code !== WWPASS_STATUS.USER_REJECT) { + var message = "

A error has occured: ".concat(ticketOrMessage, "

") + "

Learn more

"); + wwpassShowError(message, 'WWPass Error', function () { + callback(code, ticketOrMessage); + }); + } else { + callback(code, ticketOrMessage); + } + }; + } + + return callback; + }; + + var wwpassPluginExecute = function wwpassPluginExecute(inputRequest) { + return new Promise(function (resolve, reject) { + var defaultOptions = { + log: function log() {} + }; + + var request = objectSpread({}, defaultOptions, inputRequest); + + request.log('%s: called, operation name is "%s"', 'wwpassPluginExecute', request.operation || null); + getPluginInstance(request.log).then(function (plugin) { + var wrappedCallback = wrapCallback(function (code, ticketOrMessage) { + if (code === WWPASS_STATUS.OK) { + resolve(ticketOrMessage); + } else { + reject({ + code: code, + message: ticketOrMessage + }); + } + }); + + if (plugin.execute !== undefined) { + request.callback = wrappedCallback; + plugin.execute(request); + } else if (request.operation === 'auth') { + if (PluginInfo.revision < PLUGIN_AUTH_KEYTYPE_REVISION) { + plugin.authenticate(request.ticket, wrappedCallback); + } else { + plugin.authenticate(request.ticket, wrappedCallback, request.firstKeyType || WWPASS_KEY_TYPE_DEFAULT); + } + } else { + plugin.do_operation(request.operation, wrappedCallback); + } + }).catch(reject); + }); + }; + + var pluginWaitForRemoval = function pluginWaitForRemoval() { + var log = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : function () {}; + return new Promise(function (resolve, reject) { + getPluginInstance(log).then(function (plugin) { + plugin.on_key_removed(resolve); + }).catch(reject); + }); + }; + + var EXTENSION_POLL_TIMEOUT = 200; + var EXTENSION_POLL_ATTEMPTS = 15; + var extensionNotInstalled = false; + + var timedPoll = function timedPoll(args) { + var condition = args.condition; + + if (typeof condition === 'function') { + condition = condition(); + } + + if (condition) { + args.onCondition(); + } else { + var attempts = args.attempts || 0; + + if (attempts--) { + // eslint-disable-line no-plusplus + var timeout = args.timeout || 100; + setTimeout(function (p) { + return function () { + timedPoll(p); + }; + }({ + timeout: timeout, + attempts: attempts, + condition: args.condition, + onCondition: args.onCondition, + onTimeout: args.onTimeout + }), timeout); + } else { + args.onTimeout(); + } + } + }; + + var isNativeMessagingExtensionReady = function isNativeMessagingExtensionReady() { + return (document.querySelector('meta[property="wwpass:extension:version"]') || document.getElementById('_WWAuth_Chrome_Installed_')) !== null; + }; + + var randomID = function randomID() { + return ((1 + Math.random()) * 0x100000000 | 0).toString(16).substring(1); + }; // eslint-disable-line no-bitwise,max-len + + + var wwpassNMCall = function wwpassNMCall(func, args) { + var log = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : function () {}; + return new Promise(function (resolve, reject) { + if (extensionNotInstalled) { + log('%s: chrome native messaging extension is not installed', 'wwpassNMExecute'); + reject({ + code: WWPASS_STATUS.NO_AUTH_INTERFACES_FOUND, + message: WWPASS_NO_AUTH_INTERFACES_FOUND_MSG + }); + return; + } + + timedPoll({ + timeout: EXTENSION_POLL_TIMEOUT, + attempts: EXTENSION_POLL_ATTEMPTS, + condition: isNativeMessagingExtensionReady, + onCondition: function onCondition() { + var id = randomID(); + window.postMessage({ + type: '_WWAuth_Message', + src: 'client', + id: id, + func: func, + args: args ? JSON.parse(JSON.stringify(args)) : args + }, '*'); + window.addEventListener('message', function onMessageCallee(event) { + if (event.data.type === '_WWAuth_Message' && event.data.src === 'plugin' && event.data.id === id) { + window.removeEventListener('message', onMessageCallee, false); + + if (event.data.code === WWPASS_STATUS.NO_AUTH_INTERFACES_FOUND) { + var message = '

No Security Pack is found on your computer or WWPass native host is not responding.

To install Security Pack visit Key Services

Learn more...

'; + wwpassShowError(message, 'WWPass Error', function () { + reject({ + code: event.data.code, + message: event.data.ticketOrMessage + }); + }); + } else if (event.data.code === WWPASS_STATUS.OK) { + resolve(event.data.ticketOrMessage); + } else { + reject({ + code: event.data.code, + message: event.data.ticketOrMessage + }); + } + } + }, false); + }, + onTimeout: function onTimeout() { + extensionNotInstalled = true; + log('%s: chrome native messaging extension is not installed', 'wwpassNMExecute'); + reject({ + code: WWPASS_STATUS.NO_AUTH_INTERFACES_FOUND, + message: WWPASS_NO_AUTH_INTERFACES_FOUND_MSG + }); + } + }); + }); + }; + + var wwpassNMExecute = function wwpassNMExecute(inputRequest) { + var defaultOptions = { + log: function log() {} + }; + + var request = objectSpread({}, defaultOptions, inputRequest); + + var log = request.log; + delete request.log; + log('%s: called', 'wwpassNMExecute'); + request.uri = { + domain: window.location.hostname, + protocol: window.location.protocol + }; + return wwpassNMCall('exec', [request], log); + }; + + var nmWaitForRemoval = function nmWaitForRemoval() { + var log = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : function () {}; + return wwpassNMCall('on_key_rm', undefined, log); + }; + + var pluginPresent = function pluginPresent() { + return havePlugin() || isNativeMessagingExtensionReady(); + }; + + var wwpassPlatformName$1 = function wwpassPlatformName() { + var _navigator = navigator, + userAgent = _navigator.userAgent; + var knownPlatforms = ['Android', 'iPhone', 'iPad']; + + for (var i = 0; i < knownPlatforms.length; i += 1) { + if (userAgent.search(new RegExp(knownPlatforms[i], 'i')) !== -1) { + return knownPlatforms[i]; + } + } + + return null; + }; // N.B. it call functions in REVERSE order + + + var chainedCall = function chainedCall(functions, request, resolve, reject) { + functions.pop()(request).then(resolve, function (e) { + if (e.code === WWPASS_STATUS.NO_AUTH_INTERFACES_FOUND) { + if (functions.length > 0) { + chainedCall(functions, request, resolve, reject); + } else { + wwpassNoSoftware(e.code, function () {}); + reject(e); + } + } else { + reject(e); + } + }); + }; + + var wwpassCall = function wwpassCall(nmFunc, pluginFunc, request) { + return new Promise(function (resolve, reject) { + var platformName = wwpassPlatformName$1(); + + if (platformName !== null) { + wwpassNoSoftware(WWPASS_STATUS.UNSUPPORTED_PLATFORM, function () { + reject({ + code: WWPASS_STATUS.UNSUPPORTED_PLATFORM, + message: wwpassMessageForPlatform(platformName) + }); + }); + return; + } + + if (havePlugin()) { + chainedCall([nmFunc, pluginFunc], request, resolve, reject); + } else { + chainedCall([pluginFunc, nmFunc], request, resolve, reject); + } + }); + }; + + var wwpassAuth = function wwpassAuth(request) { + return wwpassCall(wwpassNMExecute, wwpassPluginExecute, objectSpread({}, request, { + operation: 'auth' + })); + }; + + var waitForRemoval = function waitForRemoval() { + return wwpassCall(nmWaitForRemoval, pluginWaitForRemoval); + }; + + var doWWPassPasskeyAuth = function doWWPassPasskeyAuth(options) { + return getTicket(options.ticketURL).then(function (json) { + var response = ticketAdapter(json); + var ticket = response.ticket; + return getClientNonceWrapper(ticket, response.ttl).then(function (key) { + return wwpassAuth({ + ticket: ticket, + clientKeyNonce: key !== undefined ? abToB64(key) : undefined, + log: options.log + }); + }).then(function () { + return ticket; + }); + /* We may receive new ticket here but we need + * to keep the original one to find nonce */ + }); + }; + + var initPasskeyButton = function initPasskeyButton(options, resolve, reject) { + if (options.passkeyButton.innerHTML.length === 0) { + options.passkeyButton.appendChild(renderPassKeyButton()); + } + + var authUnderway = false; + options.passkeyButton.addEventListener('click', function (e) { + if (!authUnderway) { + authUnderway = true; + doWWPassPasskeyAuth(options).then(function (newTicket) { + authUnderway = false; + resolve({ + ppx: options.ppx, + version: options.version, + code: WWPASS_STATUS.OK, + message: WWPASS_OK_MSG, + ticket: newTicket, + callbackURL: options.callbackURL, + hw: true + }); + }, function (err) { + authUnderway = false; + + if (!err.code) { + options.log('passKey error', err); + } else if (err.code === WWPASS_STATUS.INTERNAL_ERROR || options.returnErrors) { + reject({ + ppx: options.ppx, + version: options.version, + code: err.code, + message: err.message, + callbackURL: options.callbackURL + }); + } + }); + } + + e.preventDefault(); + }, false); + }; + + var wwpassPasskeyAuth = function wwpassPasskeyAuth(initialOptions) { + return new Promise(function (resolve, reject) { + var defaultOptions = { + ticketURL: '', + callbackURL: '', + ppx: 'wwp_', + forcePasskeyButton: true, + log: function log() {} + }; + + var options = objectSpread({}, defaultOptions, initialOptions); + + if (!options.passkeyButton) { + reject({ + ppx: options.ppx, + version: options.version, + code: WWPASS_STATUS.INTERNAL_ERROR, + message: 'Cannot find passkey element', + callbackURL: options.callbackURL + }); + } + + if (options.forcePasskeyButton || pluginPresent()) { + if (options.passkeyButton.style.display === 'none') { + options.passkeyButton.style.display = null; + } + + initPasskeyButton(options, resolve, reject); + } else { + var displayBackup = options.passkeyButton.style.display; + options.passkeyButton.style.display = 'none'; + var observer = new MutationObserver(function (_mutationsList, _observer) { + if (pluginPresent()) { + _observer.disconnect(); + + options.passkeyButton.style.display = displayBackup === 'none' ? null : displayBackup; + initPasskeyButton(options, resolve, reject); + } + }); + observer.observe(document.head, { + childList: true + }); + } + }).then(navigateToCallback, navigateToCallback); + }; + + var absolutePath = function absolutePath(href) { + var link = document.createElement('a'); + link.href = href; + return link.href; + }; + + var authInit = function authInit(initialOptions) { + var defaultOptions = { + ticketURL: '', + callbackURL: '', + hw: false, + ppx: 'wwp_', + version: 2, + log: function log() {} + }; + + var options = objectSpread({}, defaultOptions, initialOptions); + + if (typeof options.callbackURL === 'string') { + options.callbackURL = absolutePath(options.callbackURL); + } + + options.passkeyButton = typeof options.passkey === 'string' ? document.querySelector(options.passkey) : options.passkey; + options.qrcode = typeof options.qrcode === 'string' ? document.querySelector(options.qrcode) : options.qrcode; + var promises = []; + + if (options.passkeyButton) { + promises.push(wwpassPasskeyAuth(options)); + } + + promises.push(wwpassQRCodeAuth(options)); + return Promise.race(promises); + }; + + var version$1 = "2.1.6"; + + if ('console' in window && window.console.log) { + window.console.log("WWPass frontend library version ".concat(version$1)); + } + + window.WWPass = { + authInit: authInit, + openWithTicket: openWithTicket, + isClientKeyTicket: isClientKeyTicket, + cryptoPromise: WWPassCryptoPromise, + copyClientNonce: copyClientNonce, + updateTicket: updateTicket, + pluginPresent: pluginPresent, + waitForRemoval: waitForRemoval, + STATUS: WWPASS_STATUS + }; + +}()); +//# sourceMappingURL=wwpass-frontend.js.map diff --git a/oxAuth/Server/integrations/wwpass/ticket.json b/oxAuth/Server/integrations/wwpass/ticket.json new file mode 100644 index 00000000..bdff117e --- /dev/null +++ b/oxAuth/Server/integrations/wwpass/ticket.json @@ -0,0 +1,9 @@ +#!/bin/bash +if [[ "$QUERY_STRING" =~ ^p=.* ]] +then + auth_type="p" +fi + +echo "Content-type: application/json" +echo "" +curl "https://spfe.wwpass.com/get.json?auth_type=$auth_type" --cert /opt/wwpass_gluu/gluu_client.crt --key /opt/wwpass_gluu/gluu_client.key --cacert /opt/wwpass_gluu/wwpass.ca.crt -s diff --git a/oxAuth/Server/integrations/wwpass/wwpass.ca.crt b/oxAuth/Server/integrations/wwpass/wwpass.ca.crt new file mode 100644 index 00000000..19c91f55 --- /dev/null +++ b/oxAuth/Server/integrations/wwpass/wwpass.ca.crt @@ -0,0 +1,35 @@ +-----BEGIN CERTIFICATE----- +MIIGATCCA+mgAwIBAgIJAN7JZUlglGn4MA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNV +BAYTAlVTMRswGQYDVQQKExJXV1Bhc3MgQ29ycG9yYXRpb24xKzApBgNVBAMTIldX +UGFzcyBDb3Jwb3JhdGlvbiBQcmltYXJ5IFJvb3QgQ0EwIhgPMjAxMjExMjgwOTAw +MDBaGA8yMDUyMTEyODA4NTk1OVowVzELMAkGA1UEBhMCVVMxGzAZBgNVBAoTEldX +UGFzcyBDb3Jwb3JhdGlvbjErMCkGA1UEAxMiV1dQYXNzIENvcnBvcmF0aW9uIFBy +aW1hcnkgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMmF +pl1WX80osygWx4ZX8xGyYfHx8cpz29l5s/7mgQIYCrmUSLK9KtSryA0pmzrOFkyN +BuT0OU5ucCuv2WNgUriJZ78b8sekW1oXy2QXndZSs+CA+UoHFw0YqTEDO659/Tjk +NqlE5HMXdYvIb7jhcOAxC8gwAJFgAkQboaMIkuWsAnpOtKzrnkWHGz45qoyICjqz +feDcN0dh3ITMHXrYiwkVq5fGXHPbuJPbuBN+unnakbL3Ogk3yPnEcm6YV+HrxQ7S +Ky83q60Abdy8ft0RpSJeUkBjJVwiHu4y4j5iKC1tNgtV8qE9Zf2g5vAHzL3obqnu +IMr8JpmWp0MrrUa9jYOtKXk2LnZnfxurJ74NVk2RmuN5I/H0a/tUrHWtCE5pcVNk +b3vmoqeFsbTs2KDCMq/gzUhHU31l4Zrlz+9DfBUxlb5fNYB5lF4FnR+5/hKgo75+ +OaNjiSfp9gTH6YfFCpS0OlHmKhsRJlR2aIKpTUEG9hjSg3Oh7XlpJHhWolQQ2BeL +++3UOyRMTDSTZ1bGa92oz5nS+UUsE5noUZSjLM+KbaJjZGCxzO9y2wiFBbRSbhL2 +zXpUD2dMB1G30jZwytjn15VAMEOYizBoHEp2Nf9PNhsDGa32AcpJ2a0n89pbSOlu +yr/vEzYjJ2DZ/TWQQb7upi0G2kRX17UIZ5ZfhjmBAgMBAAGjgcswgcgwHQYDVR0O +BBYEFGu/H4b/gn8RzL7XKHBT6K4BQcl7MIGIBgNVHSMEgYAwfoAUa78fhv+CfxHM +vtcocFPorgFByXuhW6RZMFcxCzAJBgNVBAYTAlVTMRswGQYDVQQKExJXV1Bhc3Mg +Q29ycG9yYXRpb24xKzApBgNVBAMTIldXUGFzcyBDb3Jwb3JhdGlvbiBQcmltYXJ5 +IFJvb3QgQ0GCCQDeyWVJYJRp+DAPBgNVHRMBAf8EBTADAQH/MAsGA1UdDwQEAwIB +BjANBgkqhkiG9w0BAQsFAAOCAgEAE46CMikI7378mkC3qZyKcVxkNfLRe3eD4h04 +OO27rmfZj/cMrDDCt0Bn2t9LBUGBdXfZEn13gqn598F6lmLoObtN4QYqlyXrFcPz +FiwQarba+xq8togxjMkZ2y70MlV3/PbkKkwv4bBjOcLZQ1DsYehPdsr57C6Id4Ee +kEQs/aMtKcMzZaSipkTuXFxfxW4uBifkH++tUASD44OD2r7m1UlSQ5viiv3l0qvA +B89dPifVnIeAvPcd7+GY2RXTZCw36ZipnFiOWT9TkyTDpB/wjWQNFrgmmQvxQLeW +BWIUSaXJwlVzMztdtThnt/bNZNGPMRfaZ76OljYB9BKC7WUmss2f8toHiys+ERHz +0xfCTVhowlz8XtwWfb3A17jzJBm+KAlQsHPgeBEqtocxvBJcqhOiKDOpsKHHz+ng +exIO3elr1TCVutPTE+UczYTBRsL+jIdoIxm6aA9rrN3qDVwMnuHThSrsiwyqOXCz +zjCaCf4l5+KG5VNiYPytiGicv8PCBjwFkzIr+LRSyUiYzAZuiyRchpdT+yRAfL7q +qHBuIHYhG3E47a3GguwUwUGcXR+NjrSmteHRDONOUYUCH41hw6240Mo1lL4F+rpr +LEBB84k3+v+AtbXePEwvp+o1nu/+1sRkhqlNFHN67vakqC4xTxiuPxu6Pb/uDeNI +ip0+E9I= +-----END CERTIFICATE----- diff --git a/oxAuth/Server/integrations/wwpass/wwpass.py b/oxAuth/Server/integrations/wwpass/wwpass.py new file mode 100644 index 00000000..00139c78 --- /dev/null +++ b/oxAuth/Server/integrations/wwpass/wwpass.py @@ -0,0 +1,235 @@ +# -*- coding: utf-8 -*- +__author__="Rostislav Kondratenko " +__date__ ="$27.11.2014 18:05:15$" + +# Copyright 2009-2019 WWPASS Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pickle +from threading import Lock +import ssl +try: + # python3 + from urllib.request import urlopen + from urllib.parse import urlencode + from urllib.error import URLError +except ImportError: + # python2 + from urllib2 import urlopen, URLError + from urllib import urlencode + +DEFAULT_CADATA = u'''-----BEGIN CERTIFICATE----- +MIIGATCCA+mgAwIBAgIJAN7JZUlglGn4MA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNV +BAYTAlVTMRswGQYDVQQKExJXV1Bhc3MgQ29ycG9yYXRpb24xKzApBgNVBAMTIldX +UGFzcyBDb3Jwb3JhdGlvbiBQcmltYXJ5IFJvb3QgQ0EwIhgPMjAxMjExMjgwOTAw +MDBaGA8yMDUyMTEyODA4NTk1OVowVzELMAkGA1UEBhMCVVMxGzAZBgNVBAoTEldX +UGFzcyBDb3Jwb3JhdGlvbjErMCkGA1UEAxMiV1dQYXNzIENvcnBvcmF0aW9uIFBy +aW1hcnkgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMmF +pl1WX80osygWx4ZX8xGyYfHx8cpz29l5s/7mgQIYCrmUSLK9KtSryA0pmzrOFkyN +BuT0OU5ucCuv2WNgUriJZ78b8sekW1oXy2QXndZSs+CA+UoHFw0YqTEDO659/Tjk +NqlE5HMXdYvIb7jhcOAxC8gwAJFgAkQboaMIkuWsAnpOtKzrnkWHGz45qoyICjqz +feDcN0dh3ITMHXrYiwkVq5fGXHPbuJPbuBN+unnakbL3Ogk3yPnEcm6YV+HrxQ7S +Ky83q60Abdy8ft0RpSJeUkBjJVwiHu4y4j5iKC1tNgtV8qE9Zf2g5vAHzL3obqnu +IMr8JpmWp0MrrUa9jYOtKXk2LnZnfxurJ74NVk2RmuN5I/H0a/tUrHWtCE5pcVNk +b3vmoqeFsbTs2KDCMq/gzUhHU31l4Zrlz+9DfBUxlb5fNYB5lF4FnR+5/hKgo75+ +OaNjiSfp9gTH6YfFCpS0OlHmKhsRJlR2aIKpTUEG9hjSg3Oh7XlpJHhWolQQ2BeL +++3UOyRMTDSTZ1bGa92oz5nS+UUsE5noUZSjLM+KbaJjZGCxzO9y2wiFBbRSbhL2 +zXpUD2dMB1G30jZwytjn15VAMEOYizBoHEp2Nf9PNhsDGa32AcpJ2a0n89pbSOlu +yr/vEzYjJ2DZ/TWQQb7upi0G2kRX17UIZ5ZfhjmBAgMBAAGjgcswgcgwHQYDVR0O +BBYEFGu/H4b/gn8RzL7XKHBT6K4BQcl7MIGIBgNVHSMEgYAwfoAUa78fhv+CfxHM +vtcocFPorgFByXuhW6RZMFcxCzAJBgNVBAYTAlVTMRswGQYDVQQKExJXV1Bhc3Mg +Q29ycG9yYXRpb24xKzApBgNVBAMTIldXUGFzcyBDb3Jwb3JhdGlvbiBQcmltYXJ5 +IFJvb3QgQ0GCCQDeyWVJYJRp+DAPBgNVHRMBAf8EBTADAQH/MAsGA1UdDwQEAwIB +BjANBgkqhkiG9w0BAQsFAAOCAgEAE46CMikI7378mkC3qZyKcVxkNfLRe3eD4h04 +OO27rmfZj/cMrDDCt0Bn2t9LBUGBdXfZEn13gqn598F6lmLoObtN4QYqlyXrFcPz +FiwQarba+xq8togxjMkZ2y70MlV3/PbkKkwv4bBjOcLZQ1DsYehPdsr57C6Id4Ee +kEQs/aMtKcMzZaSipkTuXFxfxW4uBifkH++tUASD44OD2r7m1UlSQ5viiv3l0qvA +B89dPifVnIeAvPcd7+GY2RXTZCw36ZipnFiOWT9TkyTDpB/wjWQNFrgmmQvxQLeW +BWIUSaXJwlVzMztdtThnt/bNZNGPMRfaZ76OljYB9BKC7WUmss2f8toHiys+ERHz +0xfCTVhowlz8XtwWfb3A17jzJBm+KAlQsHPgeBEqtocxvBJcqhOiKDOpsKHHz+ng +exIO3elr1TCVutPTE+UczYTBRsL+jIdoIxm6aA9rrN3qDVwMnuHThSrsiwyqOXCz +zjCaCf4l5+KG5VNiYPytiGicv8PCBjwFkzIr+LRSyUiYzAZuiyRchpdT+yRAfL7q +qHBuIHYhG3E47a3GguwUwUGcXR+NjrSmteHRDONOUYUCH41hw6240Mo1lL4F+rpr +LEBB84k3+v+AtbXePEwvp+o1nu/+1sRkhqlNFHN67vakqC4xTxiuPxu6Pb/uDeNI +ip0+E9I= +-----END CERTIFICATE----- ''' + +PIN = 'p' +SESSION_KEY = 's' +CLIENT_KEY = 'c' + +class WWPassException(IOError): + pass + +class WWPassConnection(object): + def __init__(self, key_file, cert_file, timeout=10, spfe_addr='https://spfe.wwpass.com', cafile=None): + self.context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLSv1) + self.context.load_cert_chain(certfile=cert_file, keyfile=key_file) + if cafile is None: + self.context.load_verify_locations(cadata=DEFAULT_CADATA) + else: + self.context.load_verify_locations(cafile=cafile) + self.spfe_addr = 'https://%s' % spfe_addr if spfe_addr.find('://') == -1 else spfe_addr + self.timeout = timeout + + def makeRequest(self, method, command, attempts=3,**paramsDict): + params = {k:v.encode('UTF-8') if hasattr(v,"encode") else v for k, v in paramsDict.items() if v is not None} + try: + if method == 'GET': + res = urlopen(self.spfe_addr +'/'+command+'?'+urlencode(params), context=self.context, timeout=self.timeout) + else: + res = urlopen(self.spfe_addr +'/'+command, data=urlencode(params).encode('UTF-8'), context=self.context, timeout=self.timeout) + res = pickle.loads(res.read()) + if not res['result']: + if 'code'in res: + raise WWPassException('SPFE returned error: %s: %s' %(res['code'], res['data'])) + raise WWPassException('SPFE returned error: %s' % res['data']) + return res + except URLError as e: + if attempts>0: + attempts -= 1 + else: + raise + return self.makeRequest(method, command, attempts,**params) + + def makeAuthTypeString(self, auth_types): + valid_auth_types = (PIN, SESSION_KEY, CLIENT_KEY) + return ''.join(x for x in auth_types if x in valid_auth_types) + + def getName(self): + ticket = self.getTicket(ttl=0)['ticket'] + pos = ticket.find(':') + if pos == -1: + raise WWPassException('Cannot extract service provider name from ticket.') + return ticket[:pos] + + def getTicket(self, ttl=None, auth_types=()): + result = self.makeRequest('GET','get', ttl=ttl or None, auth_type=self.makeAuthTypeString(auth_types) or None) + return {'ticket' : result['data'], 'ttl' : result['ttl']} + + def getPUID(self, ticket, auth_types=(), finalize=None): + result = self.makeRequest('GET','puid', ticket=ticket, auth_type=self.makeAuthTypeString(auth_types) or None, finalize=finalize) + return {'puid' : result['data']} + + def putTicket(self, ticket, ttl=None, auth_types=(), finalize=None): + result = self.makeRequest('GET','put', ticket=ticket, ttl=ttl or None, auth_type=self.makeAuthTypeString(auth_types) or None, finalize=finalize) + return {'ticket' : result['data'], 'ttl' : result['ttl']} + + def readData(self, ticket, container=b'', finalize=None): + result = self.makeRequest('GET','read', ticket=ticket, container=container or None, finalize=finalize) + return {'data' : result['data']} + + def readDataAndLock(self, ticket, lockTimeout, container=b''): + result = self.makeRequest('GET','read', ticket=ticket, container=container or None, lock='1', to=lockTimeout) + return {'data' : result['data']} + + + def writeData(self, ticket, data, container=b'', finalize=None): + self.makeRequest('POST','write', ticket=ticket, data=data, container=container or None, finalize=finalize) + return True + + def writeDataAndUnlock(self, ticket, data, container=b'', finalize=None): + self.makeRequest('POST','write', ticket=ticket, data=data, container=container or None, unlock='1', finalize=finalize) + return True + + def lock(self, ticket, lockTimeout, lockid): + self.makeRequest('GET','lock',ticket=ticket, lockid=lockid, to=lockTimeout) + return True + + def unlock(self, ticket, lockid, finalize=None): + self.makeRequest('GET','unlock', ticket=ticket, lockid=lockid, finalize=finalize) + return True + + def getSessionKey(self, ticket, finalize=None): + result = self.makeRequest('GET','key', ticket=ticket, finalize=finalize) + return {'sessionKey' : result['data']} + + def createPFID(self, data=''): + if data: + result = self.makeRequest('POST','sp/create', data=data) + else: + result = self.makeRequest('GET','sp/create') + return {'pfid' : result['data']} + + def removePFID(self, pfid): + self.makeRequest('POST','sp/remove', pfid=pfid) + return True + + def readDataSP(self, pfid): + result = self.makeRequest('GET','sp/read', pfid=pfid) + return {'data' : result['data']} + + def readDataSPandLock(self, pfid, lockTimeout): + result = self.makeRequest('GET','sp/read', pfid=pfid, to=lockTimeout, lock=1) + return {'data' : result['data']} + + def writeDataSP(self, pfid, data): + self.makeRequest('POST','sp/write', pfid=pfid, data=data) + return True + + def writeDataSPandUnlock(self, pfid, data): + self.makeRequest('POST','sp/write', pfid=pfid, data=data, unlock=1) + return True + + def lockSP(self, lockid, lockTimeout): + self.makeRequest('GET','sp/lock',lockid=lockid, to=lockTimeout) + return True + + def unlockSP(self, lockid): + self.makeRequest('GET','sp/unlock',lockid=lockid) + return True + + def getClientKey(self, ticket): + result = self.makeRequest('GET','clientkey',ticket=ticket) + res_dict = {'clientKey' : result['data'], 'ttl' : result['ttl']} + if 'originalTicket' in result: + res_dict['originalTicket'] = result['originalTicket'] + return res_dict + +class WWPassConnectionMT(WWPassConnection): + def __init__(self, key_file, cert_file, timeout=10, spfe_addr='https://spfe.wwpass.com', ca_file=None, initial_connections=2): + self.Pool = [] + self.key_file = key_file + self.cert_file = cert_file + self.ca_file = ca_file + self.timeout = timeout + self.spfe_addr = spfe_addr + for _ in xrange(initial_connections): + self.addConnection() + + def addConnection(self, acquired = False): + c = WWPassConnection(self.key_file, self.cert_file, self.timeout, self.spfe_addr, self.ca_file) + c.lock = Lock() + if acquired: + c.lock.acquire() + self.Pool.append(c) + return c + + def getConnection(self): + for conn in (c for c in self.Pool if c.lock.acquire(False)): + return conn + conn=self.addConnection(True) + return conn + + def makeRequest(self, method, command, attempts=3,**paramsDict): + conn = None + try: + conn = self.getConnection() + return conn.makeRequest(method, command, attempts, **paramsDict) + finally: + if conn is not None: + conn.lock.release() + +WWPASSConnection = WWPassConnection +WWPASSConnectionMT = WWPassConnectionMT diff --git a/oxAuth/Server/integrations/wwpass/wwpassauth.py b/oxAuth/Server/integrations/wwpass/wwpassauth.py new file mode 100644 index 00000000..453e60ba --- /dev/null +++ b/oxAuth/Server/integrations/wwpass/wwpassauth.py @@ -0,0 +1,289 @@ +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.oxauth.model.configuration import AppConfiguration +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.service import AuthenticationService +from org.xdi.oxauth.service import UserService +from org.gluu.util import StringHelper +from org.gluu.service import MailService +from org.gluu.oxauth.model.configuration import AppConfiguration + +from javax.faces.context import FacesContext + +from com.google.common.io import BaseEncoding + +from urlparse import urlparse + +import jarray +from java.util import Arrays +from java.security import SecureRandom +import java + +from time import time + +from wwpass import WWPassConnection + + +class PersonAuthentication(PersonAuthenticationType): + EMAIL_NONCE_EXPIRATION = 600 # seconds + + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + self.user = None + + @staticmethod + def generateNonce(keyLength): + bytes = jarray.zeros(keyLength, "b") + secureRandom = SecureRandom() + secureRandom.nextBytes(bytes) + return BaseEncoding.base64().omitPadding().encode(bytes) + + def init(self, configurationAttributes): + print "WWPASS. Initialization" + self.allow_email_bind = configurationAttributes.get("allow_email_bind").getValue2() if configurationAttributes.containsKey("allow_email_bind") else '' + self.allow_password_bind = configurationAttributes.get("allow_password_bind").getValue2() if configurationAttributes.containsKey("allow_password_bind") else '' + self.allow_passkey_bind = configurationAttributes.get("allow_passkey_bind").getValue2() if configurationAttributes.containsKey("allow_passkey_bind") else '' + self.registration_url = configurationAttributes.get("registration_url").getValue2() if configurationAttributes.containsKey("registration_url") else '' + self.recovery_url = configurationAttributes.get("recovery_url").getValue2() if configurationAttributes.containsKey("recovery_url") else '' + self.wwc = WWPassConnection( + configurationAttributes.get("wwpass_key_file").getValue2(), + configurationAttributes.get("wwpass_crt_file").getValue2()) + self.use_pin = configurationAttributes.get("use_pin").getValue2() if configurationAttributes.containsKey("use_pin") else None + self.auth_type = ('p',) if self.use_pin else () + self.sso_cookie_domain = '.'.join(urlparse(CdiUtil.bean(AppConfiguration).getBaseEndpoint()).netloc.split('.')[-2:]) + sso_cookie_tags = configurationAttributes.get("sso_cookie_tags").getValue2() if configurationAttributes.containsKey("sso_cookie_tags") else None + if sso_cookie_tags: + self.sso_cookie_tags = sso_cookie_tags.split(' ') + else: + self.sso_cookie_tags = [] + print "WWPASS. Initialized successfully" + return True + + def destroy(self, configurationAttributes): + print "WWPASS. Destroy" + return True + + def getApiVersion(self): + return 2 + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def tryFirstLogin(self, puid, userService, authenticationService): # Login user that was just registered via external link + user = userService.getUserByAttribute("oxTrustExternalId", "wwpass:%s"%puid) + if user and authenticationService.authenticate(user.getUserId()): + userService.addUserAttribute(user.getUserId(), "oxExternalUid", "wwpass:%s"%puid) + userService.removeUserAttribute(user.getUserId(),"oxTrustExternalId", "wwpass:%s"%puid) + return True + return False + + def getPuid(self, ticket): + puid = self.wwc.getPUID(ticket, self.auth_type)['puid'] + assert puid #Just in case it's empty or None + return puid + + def authenticateWithWWPass(self, userService, authenticationService, identity, ticket): + puid = self.getPuid(ticket) + user = userService.getUserByAttribute("oxExternalUid", "wwpass:%s"%puid) + if user: + if authenticationService.authenticate(user.getUserId()): + return True + else: + if self.registration_url and self.tryFirstLogin(puid, userService, authenticationService): + return True + identity.setWorkingParameter("puid", puid) + identity.setWorkingParameter("ticket", ticket) + return True + return False + + def bindWWPass(self, requestParameters, userService, authenticationService, identity, ticket): + puid = identity.getWorkingParameter("puid") + email = requestParameters.get('email')[0] if 'email' in requestParameters else None + if not puid: + identity.setWorkingParameter("errors", "WWPass login failed") + return False + if ticket: + puid_new = self.getPuid(ticket) + # Always use the latest PUID when retrying step 2 + identity.setWorkingParameter("puid", puid_new) + if puid == puid_new: + # Registering via external web service + if not self.registration_url: + return False + if self.tryFirstLogin(puid, userService, authenticationService): + identity.setWorkingParameter("puid", None) + return True + else: + if not self.allow_passkey_bind: + return False + # Binding with existing PassKey + user = userService.getUserByAttribute("oxExternalUid", "wwpass:%s"%puid_new) + if user: + if authenticationService.authenticate(user.getUserId()): + userService.addUserAttribute(user.getUserId(), "oxExternalUid", "wwpass:%s"%puid) + identity.setWorkingParameter("puid", None) + return True + identity.setWorkingParameter("errors", "Invalid user") + return False + elif email: + # Binding via email + if not self.allow_email_bind: + return False + email = requestParameters.get('email')[0] if 'email' in requestParameters else None + identity.setWorkingParameter("email", email) + user = userService.getUserByAttribute('mail', email) + if not user: + print("User with email '%s' not found." % email) + return True + nonce = self.generateNonce(33) + mailService = CdiUtil.bean(MailService) + identity.setWorkingParameter("email_nonce", nonce) + identity.setWorkingParameter("email_nonce_exp", str(time() + self.EMAIL_NONCE_EXPIRATION)) + subject = "Bind your WWPass Key" + body = """ +To bind your WWPass Key to your account, copy and paste the following +code into "Email code" field in the login form: +%s +If you haven't requested this operation, you can safely disregard this email. + """ + mailService.sendMail(email, subject, body % nonce) + return True + else: + # Binding via username/password + if not self.allow_password_bind: + return False + puid = identity.getWorkingParameter("puid") + if not puid: + return False + credentials = identity.getCredentials() + user_name = credentials.getUsername() + user_password = credentials.getPassword() + logged_in = False + if (StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password)): + try: + logged_in = authenticationService.authenticate(user_name, user_password) + except Exception as e: + print(e) + if not logged_in: + identity.setWorkingParameter("errors", "Invalid username or password") + return False + user = authenticationService.getAuthenticatedUser() + if not user: + identity.setWorkingParameter("errors", "Invalid user") + return False + userService.addUserAttribute(user_name, "oxExternalUid", "wwpass:%s"%puid) + identity.setWorkingParameter("puid", None) + return True + return False + + def checkEmailNonce(self, requestParameters, userService, authenticationService, identity, ticket): + # Verify email nonce + if not self.allow_email_bind: + identity.setWorkingParameter("email", None) + return False + puid = identity.getWorkingParameter("puid") + if not puid: + return False + nonce = requestParameters.get('code')[0] if 'code' in requestParameters else None + email = identity.getWorkingParameter("email") + proper_nonce = identity.getWorkingParameter("email_nonce") + nonce_expiration = float(identity.getWorkingParameter("email_nonce_exp") or 0.0) + if not nonce or not email or not proper_nonce or not nonce_expiration or nonce_expiration < time() or nonce != proper_nonce: + print("WWPass. Wrong email verification code", nonce,email,proper_nonce,nonce_expiration, time()) + identity.setWorkingParameter("email", None) + identity.setWorkingParameter("errors", "Invalid email or verification code") + return False + user = userService.getUserByAttribute('mail', email) + identity.setWorkingParameter("email", None) + if user: + if authenticationService.authenticate(user.getUserId()): + userService.addUserAttribute(user.getUserId(), "oxExternalUid", "wwpass:%s"%identity.getWorkingParameter("puid")) + identity.setWorkingParameter("puid", None) + return True + print("No user") + return False + + + def doAuthenticate(self, step, requestParameters, userService, authenticationService, identity, ticket): + if step == 1: + return self.authenticateWithWWPass(userService, authenticationService, identity, ticket) + elif step == 2: + return self.bindWWPass(requestParameters, userService, authenticationService, identity, ticket) + elif step == 3: + return self.checkEmailNonce(requestParameters, userService, authenticationService, identity, ticket) + else: + return False + + + def authenticate(self, configurationAttributes, requestParameters, step): + print("WWPass. Authenticate for step %d" %step) + authenticationService = CdiUtil.bean(AuthenticationService) + userService = CdiUtil.bean(UserService) + ticket = requestParameters.get('wwp_ticket')[0] if 'wwp_ticket' in requestParameters else None + identity = CdiUtil.bean(Identity) + identity.setWorkingParameter("errors", "") + result = self.doAuthenticate(step, requestParameters, userService, authenticationService, identity, ticket) + if result and self.sso_cookie_tags: + externalContext = CdiUtil.bean(FacesContext).getExternalContext() + for tag in self.sso_cookie_tags: + externalContext.addResponseCookie("sso_magic_%s"%tag, "auth", {"path":"/", "domain":self.sso_cookie_domain, "maxAge": CdiUtil.bean(AppConfiguration).getSessionIdUnusedLifetime()}) + return result + + def prepareForStep(self, configurationAttributes, requestParameters, step): + identity = CdiUtil.bean(Identity) + identity.setWorkingParameter("sessionLifetime",CdiUtil.bean(AppConfiguration).getSessionIdUnauthenticatedUnusedLifetime()) + identity.setWorkingParameter("use_pin", bool(self.use_pin)) + print("PrepareForStep %s" % step) + if (step == 1): + return True + elif (step == 2): + identity.setWorkingParameter("registration_url", self.registration_url) + identity.setWorkingParameter("recovery_url", self.recovery_url) + identity.setWorkingParameter("allow_email_bind", self.allow_email_bind) + identity.setWorkingParameter("allow_password_bind", self.allow_password_bind) + identity.setWorkingParameter("allow_passkey_bind", self.allow_passkey_bind) + print("WWPASS. Errors:%s" % identity.getWorkingParameter("errors")) + return True + elif (step == 3): + return True + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + if step == 3: + return Arrays.asList("puid", "email", "email_nonce", "email_nonce_exp", "sessionLifetime") + return Arrays.asList("puid", "ticket", "use_pin", "errors", "email", "sessionLifetime") + + def getCountAuthenticationSteps(self, configurationAttributes): + identity = CdiUtil.bean(Identity) + if not identity.isSetWorkingParameter("puid"): + return 1 + if not identity.isSetWorkingParameter("email"): + return 2 + return 3 + + def getPageForStep(self, configurationAttributes, step): + if step == 1: + return "/auth/wwpass/wwpass.xhtml" + if step == 2: + return "/auth/wwpass/wwpassbind.xhtml" + return "/auth/wwpass/checkemail.xhtml" + + def logout(self, configurationAttributes, requestParameters): + print("WWPASS. Logout") + # This is not called. Probably bug in Gluu + # externalContext = CdiUtil.bean(FacesContext).getExternalContext() + # externalContext.addResponseCookie("sso_magic", "auth", {"path":"/", "domain":self.sso_cookie_domain, "maxAge": 0}) + return True + + def getNextStep(self, configurationAttributes, requestParameters, step): + # If user did not pass this step, change the step to previous one + identity = CdiUtil.bean(Identity) + puid = identity.getWorkingParameter("puid") + email = identity.getWorkingParameter("email") + print ("WWPass getNextStep for step %d, email: %s" % (step, email)) + if puid and not email and step != 1: + return 2 + return -1 diff --git a/oxAuth/Server/integrations/yubicloud/README.txt b/oxAuth/Server/integrations/yubicloud/README.txt new file mode 100644 index 00000000..eea4807b --- /dev/null +++ b/oxAuth/Server/integrations/yubicloud/README.txt @@ -0,0 +1,35 @@ +Yubicloud OTP Validataion +========================= + +This is a single step authentication workflow. Instead of a human entering a +password, Yubico's Yubikey OTP will be taken in as password. + +This script uses the Yubicloud Service by Yubico for validation of the OTP. + +Here are the steps required to setup the Yubico OTP as password. + + 1. Setup a custom attribute named the `yubikeyId`. Add this attribute the + users who have to be authenticated via this method. Store the public part + of the Yubikey Idendity (usually the first 12 chars of OTP) in this + attribute against each user. This matches the user against the key. + + Setting up Custom Attributes: https://www.gluu.org/docs/customize/attributes/ + Find the Yubikey Idendity: https://demo.yubico.com + + 2. Register for a Yubicloud API Key and get client ID and client Secret. + https://upgrade.yubico.com/getapikey/ + + 3. Configure the custom script: + i) Enter the value for `yubicloud_uri` as any one of the following: + api.yubico.com + api2.yubico.com + api3.yubico.com + api4.yubico.com + api5.yubico.com + ii) Enter the client secret as `yubicloud_api_key` + iii) Enter the client ID as `yubicloud_id` + +Now the method `yubicloud` can be set as the authentication mechanism and Yubikey +can be used in place of the password of the users for authentication. + + diff --git a/oxAuth/Server/integrations/yubicloud/YubicloudExternalAuthenticator.py b/oxAuth/Server/integrations/yubicloud/YubicloudExternalAuthenticator.py new file mode 100644 index 00000000..24fe0919 --- /dev/null +++ b/oxAuth/Server/integrations/yubicloud/YubicloudExternalAuthenticator.py @@ -0,0 +1,118 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2016, Gluu +# +# Author: Yuriy Movchan, Arunmozhi +# + +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.oxauth.security import Identity +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.service import UserService +from org.gluu.util import StringHelper + +import java + +import urllib2 +import urllib +import uuid + + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Yubicloud. Initialization" + + self.api_server = configurationAttributes.get("yubicloud_uri").getValue2() + self.api_key = configurationAttributes.get("yubicloud_api_key").getValue2() + self.client_id = configurationAttributes.get("yubicloud_id").getValue2() + + return True + + def destroy(self, configurationAttributes): + print "Yubicloud. Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + if (step == 1): + print "Yubicloud. Authenticate for step 1" + + identity = CdiUtil.bean(Identity) + credentials = identity.getCredentials() + + username = credentials.getUsername() + otp = credentials.getPassword() + + # Validate otp length + if len(otp) < 32 or len(otp) > 48: + print "Yubicloud. Invalid OTP length" + return False + + user_service = CdiUtil.bean(UserService) + user = user_service.getUser(username) + + public_key = user.getAttribute('yubikeyId') + + # Match the user with the yubikey + if public_key not in otp: + print "Yubicloud. Public Key not matching OTP" + return False + + data = "" + try: + nonce = str(uuid.uuid4()).replace("-", "") + params = urllib.urlencode({"id": self.client_id, "otp": otp, "nonce": nonce}) + url = "https://" + self.api_server + "/wsapi/2.0/verify/?" + params + f = urllib2.urlopen(url) + data = f.read() + except Exception as e: + print "Yubicloud. Exception ", e + + if 'status=OK' in data: + user_service.authenticate(username) + print "Yubicloud. Authentication Successful" + return True + + print "Yubicloud. End of Step 1. Returning False." + return False + else: + return False + + def prepareForStep(self, configurationAttributes, requestParameters, step): + if (step == 1): + print "Yubicloud. Prepare for Step 1" + return True + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + return 1 + + def getPageForStep(self, configurationAttributes, step): + return "" + + def getNextStep(self, configurationAttributes, requestParameters, step): + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True diff --git a/oxAuth/Server/pom.xml b/oxAuth/Server/pom.xml new file mode 100644 index 00000000..6b7f5e0b --- /dev/null +++ b/oxAuth/Server/pom.xml @@ -0,0 +1,1263 @@ + + + 4.0.0 + oxauth-server + oxAuth Server + war + + + org.gluu + oxauth + 4.5.6-SNAPSHOT + + + + ${maven.min-version} + + + + ${project.artifactId} + + + profiles/${cfg}/config-build.properties + profiles/${cfg}/config-oxauth-test-data.properties + + + + + src/main/resources + true + + **/*.xml + **/*.properties + META-INF/navigation/*.navigation.xml + META-INF/services/*.* + + + + + + + src/main/webapp + + WEB-INF/** + + true + + + src/test/resources + true + + + + + + org.apache.maven.plugins + maven-install-plugin + + + + install-jar + install + + install-file + + + jar + ${project.artifactId} + ${project.groupId} + ${project.version} + + ${project.build.directory}/${project.build.finalName}.jar + + + + + install-war + install + + install-file + + + war + ${project.artifactId} + ${project.groupId} + ${project.version} + + ${project.build.directory}/${project.build.finalName}.war + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + make-a-jar + compile + + jar + + + + + test-jar + + + + + + + org.apache.maven.plugins + maven-resources-plugin + + + prepare-configuration-files + process-test-resources + + copy-resources + + + ${project.build.directory}/conf + + + ${basedir}/conf + true + + + + ${basedir}/profiles/${cfg}/config-oxauth.properties + ${basedir}/profiles/${cfg}/config-oxauth-test.properties + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + -Xms1024m -Xmx2048m -XX:MaxMetaspaceSize=512m -XX:+DisableExplicitGC + -Dgluu.base=${project.build.directory} + -Dserver.base=${project.build.directory} + -Dlog.base=${project.build.directory} + -Dgluu.disable.scheduler=true + + + + + + + org.apache.maven.plugins + maven-war-plugin + + false + + + true + + + ${buildNumber} + + + + + + src/main/webapp + true + + **/*.xml + **/*.properties + + + + src/main/webapp + false + + META-INF/navigation/*.navigation.xml + **/*.xhtml + **/*.jsp + **/*.html + **/*.pdf + **/*.js + **/*.css + **/*.xcss + **/*.png + **/*.jpg + **/*.gif + **/*.ico + + + + + + + + + org.codehaus.mojo + buildnumber-maven-plugin + 1.1 + + + validate + + create + + + + + false + false + + + + + pl.project13.maven + git-commit-id-plugin + 2.2.6 + + + get-the-git-infos + + revision + + + + + true + false + + + + + + org.eclipse.jetty + jetty-maven-plugin + + + ${project.build.directory}/${project.build.finalName}/WEB-INF/web.xml + /oxauth + + + ${project.build.directory}/${project.build.finalName} + 3 + + + + + + + + + + org.owasp + dependency-check-maven + + + + aggregate + + + + + + + + + + io.prometheus + simpleclient_common + 0.9.0 + + + net.agkn + hll + 1.6.0 + + + + com.wywy + log4j-plugin-fluency + + + + org.gluu + oxauth-model + + + org.gluu + oxauth-common + + + org.bouncycastle + bcprov-jdk15on + + + + + + org.gluu + oxauth-persistence-model + + + + org.gluu + oxauth-client + + + org.gluu + oxauth-static + + + + + org.gluu + oxcore-model + + + org.gluu + oxcore-util + + + org.gluu + gluu-orm-annotation + + + org.gluu + gluu-orm-ldap + + + org.gluu + gluu-orm-couchbase + + + org.gluu + gluu-orm-sql + + + org.gluu + gluu-orm-spanner + + + org.gluu + gluu-orm-hybrid + + + org.gluu + gluu-orm-cdi + + + org.gluu + oxcore-service + + + org.gluu + oxcore-server + + + org.gluu + oxcore-jsf-util + + + org.gluu + oxcore-timer-weld + + + org.gluu + oxcore-script + + + + + org.gluu + oxeleven-model + + + org.gluu + oxeleven-client + + + + + org.gluu + oxnotify-client2 + + + + + org.gluu + fido2-client + + + + + javax.enterprise + cdi-api + provided + + + + javax.servlet + javax.servlet-api + provided + + + + net.bootsfaces + bootsfaces + 1.5.0 + compile + + + + + org.bouncycastle + bcprov-jdk18on + provided + + + org.bouncycastle + bcpkix-jdk18on + provided + + + org.bouncycastle + bcmail-jdk18on + provided + + + + + org.omnifaces + omnifaces + + + + + org.antlr + antlr-runtime + + + commons-beanutils + commons-beanutils + + + commons-codec + commons-codec + + + commons-collections + commons-collections + + + commons-configuration + commons-configuration + + + commons-io + commons-io + + + commons-lang + commons-lang + + + commons-net + commons-net + + + + + org.apache.logging.log4j + log4j-core + + + org.apache.logging.log4j + log4j-slf4j-impl + + + org.apache.logging.log4j + log4j-jul + + + + + org.apache.httpcomponents + httpclient + + + org.apache.httpcomponents + httpcore + + + com.sun.mail + jakarta.mail + + + + + io.dropwizard.metrics + metrics-core + + + + + com.google.guava + guava + + + com.google.code.findbugs + jsr305 + + + + + org.gluufederation + jython-standalone + + + + + javax.jms + javax.jms-api + + + org.apache.activemq + activemq-client + + + org.apache.activemq + activemq-pool + + + org.apache.commons + commons-pool2 + + + + + org.jboss.resteasy + resteasy-cdi + + + org.jboss.resteasy + resteasy-client + + + org.jboss.resteasy + resteasy-servlet-initializer + + + org.jboss.resteasy + resteasy-jaxb-provider + + + org.jboss.resteasy + resteasy-jackson2-provider + + + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-databind + + + + + com.amazonaws + aws-java-sdk-sns + + + + + org.codehaus.jettison + jettison + + + com.googlecode.json-simple + json-simple + + + org.mvel + mvel2 + + + org.jetbrains + annotations + + + + + joda-time + joda-time + + + + + org.gluu + oxauth-model + test-jar + test + + + org.gluu + oxauth-client + 4.5.6-SNAPSHOT + test-jar + test + + + + org.testng + testng + test + + + javax.websocket + javax.websocket-api + test + + + org.mockito + mockito-core + 3.11.2 + test + + + org.mockito + mockito-testng + 0.4.8 + test + + + + + org.jboss.arquillian.testng + arquillian-testng-container + test + + + org.jboss.arquillian.extension + arquillian-rest-client-impl-3x + test + + + org.eu.ingwar.tools + arquillian-suite-extension + test + + + org.reflections + reflections + 0.9.10 + test + + + + + + com.couchbase.client + java-client + test + + + + com.google.cloud + google-cloud-spanner + test + + + + + org.jboss.arquillian.container + arquillian-container-test-impl-base + 1.4.0.Final + test + + + org.jboss.arquillian.container + arquillian-container-spi + 1.4.0.Final + test + + + org.jboss.arquillian.container + arquillian-container-test-spi + 1.4.0.Final + test + + + org.jboss.arquillian.testenricher + arquillian-testenricher-cdi + 1.4.0.Final + test + + + + + org.jboss.shrinkwrap.resolver + shrinkwrap-resolver-depchain + pom + test + + + org.jboss.shrinkwrap.resolver + shrinkwrap-resolver-impl-maven + test + + + org.jboss.shrinkwrap.descriptors + shrinkwrap-descriptors-impl-javaee + test + + + + + + webapp + + + !webapp.disable + + + + + + javax.enterprise + cdi-api + + + org.glassfish + jakarta.faces + + + + org.glassfish.web + el-impl + + + javax.el + el-api + + + + + + javax.el + el-api + test + + + + org.jboss.weld.servlet + weld-servlet-core + + + org.jboss.weld + weld-core-impl + + + org.jboss.weld.module + weld-jsf + + + org.jboss.spec.javax.ejb + jboss-ejb-api_3.2_spec + provided + + + + + org.jboss.arquillian.container + arquillian-jetty-embedded-9 + test + + + org.eclipse.jetty + jetty-webapp + test + + + org.eclipse.jetty + jetty-deploy + test + + + org.eclipse.jetty + jetty-annotations + test + + + org.eclipse.jetty + jetty-plus + test + + + + org.bitbucket.b_c + jose4j + + + + + + + org.apache.maven.plugins + maven-war-plugin + + + + true + + + ${buildNumber} + + ${git.branch} + + + + + + src/main/webapp + true + + **/*.xml + **/*.properties + + + + src/main/webapp + false + + META-INF/navigation/*.navigation.xml + **/*.xhtml + **/*.jsp + **/*.html + **/*.pdf + **/*.js + **/*.css + **/*.xcss + **/*.png + **/*.jpg + **/*.gif + **/*.ico + + + + + + + + + + + webapp-jetty + + + !jetty.disable + + + + + + + org.apache.maven.plugins + maven-war-plugin + + + + + src/main/webapp + true + + **/*.xml + **/*.properties + + + + src/main/webapp + false + + META-INF/navigation/*.navigation.xml + **/*.xhtml + **/*.jsp + **/*.html + **/*.pdf + **/*.js + **/*.css + **/*.xcss + **/*.png + **/*.jpg + **/*.gif + **/*.ico + + + + + src/main/webapp-jetty + true + + + + + + + + + + webapp-tomcat + + + tomcat.enable + + + + + + + org.apache.maven.plugins + maven-war-plugin + + + + + src/main/webapp + true + + **/*.xml + **/*.properties + + + + src/main/webapp + false + + META-INF/navigation/*.navigation.xml + **/*.xhtml + **/*.jsp + **/*.html + **/*.pdf + **/*.js + **/*.css + **/*.xcss + **/*.png + **/*.jpg + **/*.gif + **/*.ico + + + + + src/main/webapp-tomcat + true + + + + + + + + + + jetty-embedded + + true + + jetty.embedded + + + + + + + org.jboss.resteasy + resteasy-jaxrs + 3.0.21.Final + test + + + org.jboss.resteasy + resteasy-cdi + 3.0.21.Final + test + + + org.jboss.resteasy + resteasy-client + 3.0.21.Final + test + + + org.jboss.resteasy + resteasy-servlet-initializer + 3.0.21.Final + test + + + + + + generic-provider-libs + + true + + !fips.enable + + + + + + org.bouncycastle + bcprov-jdk18on + + + org.bouncycastle + bcpkix-jdk18on + + + org.bouncycastle + bcmail-jdk18on + + + + + + fips-provider-libs + + + fips.enable + + + + + + + org.bouncycastle + bc-fips + + + org.bouncycastle + bcpkix-fips + + + org.bouncycastle + bcmail-fips + + + + + + glassfish + + + glassfish.enable + + + + + + + org.glassfish.main.extras + glassfish-embedded-all + 4.1.2 + test + + + org.jboss.arquillian.container + arquillian-glassfish-embedded-3.1 + 1.0.0.Final + test + + + + + + swagger + + + swagger-enabled + + + + + + + + com.github.kongchen + swagger-maven-plugin + 2.0 + + + + org.gluu.oxauth.model.uma; + + v1 + http://gluu.org + ${basedir}/src/main/docs/mustache/markdown.mustache + ${basedir}/generated/swagger.markdown + ${basedir}/generated/apidocs + + + ${basedir}/src/main/docs/mustache + + + + + + + compile + + generate + + + + + + javax.servlet + servlet-api + 2.3 + runtime + + + + + + + + com.wordnik + swagger-jaxrs_2.10 + 1.3.13 + + + com.wordnik + swagger-servlet_2.9.1 + 1.3.1 + + + + + + push_sender + + true + + !disable-push-messages + + + + + push_sender-local + file://${project.basedir}/integrations/super_gluu/repository + + + + + com.google.gcm + gcm-server + 1.0.1.gluu + compile + + + + + + otp_auth + + true + + !disable-otp_auth + + + + + otp_auth-local + file://${project.basedir}/integrations/otp/repository + + + + + com.lochbridge.oath + oath-otp + 0.0.1-SNAPSHOT + compile + + + com.lochbridge.oath + oath-otp-keyprovisioning + 0.0.1-SNAPSHOT + compile + + + + + + test-dependencies + + + maven.test.skip + false + + + + + org.gluu + oxauth-model + test-jar + test + + + org.gluu + oxauth-client + 4.5.6-SNAPSHOT + test-jar + test + + + + + + owasp-check + + + dependency.check + true + + + + + + + + org.owasp + dependency-check-maven + + true + ${cvss-score} + + + + + check + + + + + + + + + + \ No newline at end of file diff --git a/oxAuth/Server/profiles/.gitignore b/oxAuth/Server/profiles/.gitignore new file mode 100644 index 00000000..f02e853b --- /dev/null +++ b/oxAuth/Server/profiles/.gitignore @@ -0,0 +1,5 @@ +/hudson +/local +/ce-dev +/ce-dev2 +/ce-dev.gluu.org/ diff --git a/oxAuth/Server/profiles/default/client_keystore.pkcs12 b/oxAuth/Server/profiles/default/client_keystore.pkcs12 new file mode 100644 index 0000000000000000000000000000000000000000..83704fd6155d2c0e847771f04c8feb178f724cba GIT binary patch literal 23345 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 z + + + + + Your session has timed out, please try again + + + + + + The request is invalid, please try again + + + + + + + #{logout.missingParameters} + + + + + + Your session has timed out, please try again + + + + + + + + + + Required data not found to complete this action. Please try again + or report your issue to a BT administrator. + + + + + + Unexpected Error + + + + + + Unexpected error, please try again + + + \ No newline at end of file diff --git a/oxAuth/Server/src/main/docs/mustache/markdown.mustache b/oxAuth/Server/src/main/docs/mustache/markdown.mustache new file mode 100644 index 00000000..4e395468 --- /dev/null +++ b/oxAuth/Server/src/main/docs/mustache/markdown.mustache @@ -0,0 +1,112 @@ +## API Document + +{{#apiDocuments}} +### {{resourcePath}} + +#### Overview +{{{description}}} + +{{#apis}} +#### `{{path}}` +{{#operations}} +##### {{nickname}} +**{{httpMethod}}** `{{path}}` + +{{summary}} +{{{notes}}} + +###### URL + {{url}} +###### Parameters +{{#parameters}} +- {{paramType}} + + + + + + + + + {{#paras}} + + + + + + + {{/paras}} +
ParameterRequiredDescriptionData Type
{{name}}{{required}}{{description}}{{#linkType}}{{type}}{{/linkType}}{{^linkType}}{{type}}{{/linkType}}
+{{/parameters}} + +{{#responseClass}} +###### Response +[{{className}}](#{{classLinkName}}){{^genericClasses}}{{/genericClasses}}{{#genericClasses}}< [{{className}}](#{{classLinkName}}) >{{/genericClasses}} +{{/responseClass}} + + +###### Errors + + + + + + {{#errorResponses}} + + + + + {{/errorResponses}} +
Status CodeReason
{{code}}{{message}}
+ +{{#samples}} +###### Samples +{{/samples}} +{{#samples}} +{{sampleDescription}} + +- Sample Request + +``` +{{{sampleRequest}}} +``` + +- Sample Response + +``` +{{{sampleResponse}}} +``` + +{{/samples}} + +- - - +{{/operations}} +{{/apis}} +{{/apiDocuments}} + +## Data Types +{{#dataTypes}} + + +## {{name}} + + + + + + + + + + {{#items}} + + + + + + + + {{/items}} +
typerequiredaccessdescriptionnotes
{{#linkType}}{{type}}{{/linkType}}{{^linkType}}{{type}}{{/linkType}}{{#required}}required{{/required}}{{^required}}optional{{/required}}{{#access}}{{{access}}}{{/access}}{{^access}}-{{/access}}{{#description}}{{{description}}}{{/description}}{{^description}}-{{/description}}{{#notes}}{{{notes}}}{{/notes}}{{^notes}}-{{/notes}}
+ +{{/dataTypes}} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/audit/ApplicationAuditLogger.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/audit/ApplicationAuditLogger.java new file mode 100644 index 00000000..800c8827 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/audit/ApplicationAuditLogger.java @@ -0,0 +1,205 @@ +package org.gluu.oxauth.audit; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.concurrent.locks.ReentrantLock; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Observes; +import javax.inject.Inject; +import javax.inject.Named; +import javax.jms.JMSException; +import javax.jms.MessageProducer; +import javax.jms.QueueConnection; +import javax.jms.QueueSession; +import javax.jms.Session; +import javax.jms.TextMessage; + +import org.apache.activemq.ActiveMQConnectionFactory; +import org.apache.activemq.pool.PooledConnectionFactory; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang.BooleanUtils; +import org.gluu.oxauth.model.audit.OAuth2AuditLog; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.util.ServerUtil; +import org.gluu.service.cdi.async.Asynchronous; +import org.gluu.service.cdi.event.ConfigurationUpdate; +import org.gluu.util.StringHelper; +import org.slf4j.Logger; + +import com.google.common.base.Objects; + +@ApplicationScoped +public class ApplicationAuditLogger { + + @Inject + private Logger log; + + @Inject + private AppConfiguration appConfiguration; + + private final String BROKER_URL_PREFIX = "failover:("; + private final String BROKER_URL_SUFFIX = ")?timeout=5000&jms.useAsyncSend=true"; + private final int ACK_MODE = Session.AUTO_ACKNOWLEDGE; + private final String CLIENT_QUEUE_NAME = "oauth2.audit.logging"; + private final boolean transacted = false; + + private volatile PooledConnectionFactory pooledConnectionFactory; + + private Set jmsBrokerURISet; + private String jmsUserName; + private String jmsPassword; + + private final ReentrantLock lock = new ReentrantLock(); + + private boolean enabled; + private boolean sendAuditJms; + + @PostConstruct + public void init() { + updateConfiguration(appConfiguration); + } + + public void updateConfiguration(@Observes @ConfigurationUpdate AppConfiguration appConfiguration) { + this.enabled = BooleanUtils.isTrue(appConfiguration.getEnabledOAuthAuditLogging()); + this.sendAuditJms = StringHelper.isNotEmpty(appConfiguration.getJmsUserName()) + && StringHelper.isNotEmpty(appConfiguration.getJmsPassword()) + && CollectionUtils.isNotEmpty(appConfiguration.getJmsBrokerURISet()); + + boolean configChanged = !Objects.equal(this.jmsUserName, appConfiguration.getJmsUserName()) + || !Objects.equal(this.jmsPassword, appConfiguration.getJmsPassword()) + || !Objects.equal(this.jmsBrokerURISet, appConfiguration.getJmsBrokerURISet()); + + if (configChanged) { + destroy(); + } + } + + @Asynchronous + public void sendMessage(OAuth2AuditLog oAuth2AuditLog) { + if (!enabled) { + return; + } + + boolean messageDelivered = false; + if (sendAuditJms) { + if (tryToEstablishJMSConnection()) { + messageDelivered = loggingThroughJMS(oAuth2AuditLog); + } + } + + if (!messageDelivered) { + loggingThroughFile(oAuth2AuditLog); + } + } + + @PreDestroy + public void destroy() { + if (this.pooledConnectionFactory == null) { + return; + } + + this.pooledConnectionFactory.clear(); + this.pooledConnectionFactory = null; + } + + private boolean tryToEstablishJMSConnection() { + if (this.pooledConnectionFactory != null) { + return true; + } + + lock.lock(); + try { + // Check if another thread initialized JMS pool already + if (this.pooledConnectionFactory == null) { + return tryToEstablishJMSConnectionImpl(); + } + + return true; + } finally { + lock.unlock(); + } + } + + private boolean tryToEstablishJMSConnectionImpl() { + Set jmsBrokerURISet = appConfiguration.getJmsBrokerURISet(); + if (!enabled || CollectionUtils.isEmpty(jmsBrokerURISet)) { + return false; + } + + this.jmsBrokerURISet = new HashSet(jmsBrokerURISet); + this.jmsUserName = appConfiguration.getJmsUserName(); + this.jmsPassword = appConfiguration.getJmsPassword(); + + Iterator jmsBrokerURIIterator = jmsBrokerURISet.iterator(); + + StringBuilder uriBuilder = new StringBuilder(); + while (jmsBrokerURIIterator.hasNext()) { + String jmsBrokerURI = jmsBrokerURIIterator.next(); + uriBuilder.append("tcp://"); + uriBuilder.append(jmsBrokerURI); + if (jmsBrokerURIIterator.hasNext()) { + uriBuilder.append(","); + } + } + + String brokerUrl = BROKER_URL_PREFIX + uriBuilder + BROKER_URL_SUFFIX; + + ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(this.jmsUserName, this.jmsPassword, brokerUrl); + this.pooledConnectionFactory = new PooledConnectionFactory(connectionFactory); + + pooledConnectionFactory.setIdleTimeout(5000); + pooledConnectionFactory.setMaxConnections(10); + pooledConnectionFactory.start(); + + return true; + } + + private boolean loggingThroughJMS(OAuth2AuditLog oAuth2AuditLog) { + QueueConnection connection = null; + try { + connection = pooledConnectionFactory.createQueueConnection(); + connection.start(); + + QueueSession session = connection.createQueueSession(transacted, ACK_MODE); + MessageProducer producer = session.createProducer(session.createQueue(CLIENT_QUEUE_NAME)); + + TextMessage txtMessage = session.createTextMessage(); + txtMessage.setText(ServerUtil.asPrettyJson(oAuth2AuditLog)); + producer.send(txtMessage); + + return true; + } catch (JMSException e) { + log.error("Can't send message", e); + } catch (IOException e) { + log.error("Can't serialize the audit log", e); + } catch (Exception e) { + log.error("Can't send message, please check your activeMQ configuration.", e); + } finally { + if (connection == null) { + return false; + } + + try { + connection.close(); + } catch (JMSException e) { + log.error("Can't close connection."); + } + } + + return false; + } + + private void loggingThroughFile(OAuth2AuditLog oAuth2AuditLog) { + try { + log.info(ServerUtil.asPrettyJson(oAuth2AuditLog)); + } catch (IOException e) { + log.error("Can't serialize the audit log", e); + } + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/audit/debug/ServletLoggingFilter.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/audit/debug/ServletLoggingFilter.java new file mode 100644 index 00000000..4722ac9f --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/audit/debug/ServletLoggingFilter.java @@ -0,0 +1,143 @@ +package org.gluu.oxauth.audit.debug; + +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; +import java.util.Set; + +import javax.inject.Inject; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.annotation.WebFilter; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang.BooleanUtils; +import org.gluu.oxauth.audit.debug.entity.HttpRequest; +import org.gluu.oxauth.audit.debug.entity.HttpResponse; +import org.gluu.oxauth.audit.debug.wrapper.RequestWrapper; +import org.gluu.oxauth.audit.debug.wrapper.ResponseWrapper; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.slf4j.Logger; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Created by eugeniuparvan on 5/10/17. + * + * @author Yuriy Movchan Date: 06/09/2019 + */ +@WebFilter(urlPatterns = {"/*"}) +public class ServletLoggingFilter implements Filter { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + static { + OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); + } + + @Inject + private Logger log; + + @Inject + private AppConfiguration appConfiguration; + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + Instant start = now(); + + if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) { + throw new ServletException("LoggingFilter just supports HTTP requests"); + } + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + + if (!BooleanUtils.toBoolean(appConfiguration.getHttpLoggingEnabled())) { + chain.doFilter(httpRequest, httpResponse); + return; + } + Set excludedPaths = appConfiguration.getHttpLoggingExludePaths(); + if (!CollectionUtils.isEmpty(excludedPaths)) { + for (String excludedPath : excludedPaths) { + String requestURI = httpRequest.getRequestURI(); + if (requestURI.startsWith(excludedPath)) { + chain.doFilter(httpRequest, httpResponse); + return; + } + } + } + + RequestWrapper requestWrapper = new RequestWrapper(httpRequest); + ResponseWrapper responseWrapper = new ResponseWrapper(httpResponse); + + chain.doFilter(httpRequest, httpResponse); + + Duration duration = duration(start); + + // yuriyz: log request and response only after filter handling. + // #914 - we don't want to effect server functionality due to logging. Currently content can be messed if it is InputStream. + if (log.isDebugEnabled()) { + log.debug(getRequestDescription(requestWrapper, duration)); + log.debug(getResponseDescription(responseWrapper)); + } + } + + @Override + public void destroy() { + + } + + protected String getRequestDescription(RequestWrapper requestWrapper, Duration duration) { + try { + HttpRequest httpRequest = new HttpRequest(); + httpRequest.setSenderIP(requestWrapper.getLocalAddr()); + httpRequest.setMethod(requestWrapper.getMethod()); + httpRequest.setPath(requestWrapper.getRequestURI()); + httpRequest.setParams(requestWrapper.isFormPost() ? null : requestWrapper.getParameters()); + httpRequest.setHeaders(requestWrapper.getHeaders()); + httpRequest.setBody(requestWrapper.getContent()); + httpRequest.setDuration(duration.toString()); + return OBJECT_MAPPER.writeValueAsString(httpRequest); + } catch (Exception e) { + log.warn("Cannot serialize Request to JSON", e); + return null; + } + } + + protected String getResponseDescription(ResponseWrapper responseWrapper) { + HttpResponse httpResponse = new HttpResponse(); + httpResponse.setStatus(responseWrapper.getStatus()); + httpResponse.setHeaders(responseWrapper.getHeaders()); + try { + return OBJECT_MAPPER.writeValueAsString(httpResponse); + } catch (JsonProcessingException e) { + log.warn("Cannot serialize Response to JSON", e); + return null; + } + } + + public Instant now() { + return Instant.now(); + } + + public Duration duration(Instant start) { + Instant end = Instant.now(); + return Duration.between(start, end); + } + + public Duration duration(Instant start, Instant end) { + return Duration.between(start, end); + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/audit/debug/entity/HttpRequest.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/audit/debug/entity/HttpRequest.java new file mode 100644 index 00000000..d061171b --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/audit/debug/entity/HttpRequest.java @@ -0,0 +1,75 @@ +package org.gluu.oxauth.audit.debug.entity; + +import java.util.Map; + +/** + * Created by eugeniuparvan on 5/15/17. + * + * @author Yuriy Movchan Date: 06/09/2019 + */ +public class HttpRequest { + private String senderIP; + private String method; + private String path; + private Map params; + private Map headers; + private String body; + private String duration; + + public String getSenderIP() { + return senderIP; + } + + public void setSenderIP(String senderIP) { + this.senderIP = senderIP; + } + + public String getMethod() { + return method; + } + + public void setMethod(String method) { + this.method = method; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public Map getParams() { + return params; + } + + public void setParams(Map params) { + this.params = params; + } + + public Map getHeaders() { + return headers; + } + + public void setHeaders(Map headers) { + this.headers = headers; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + public String getDuration() { + return duration; + } + + public void setDuration(String duration) { + this.duration = duration; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/audit/debug/entity/HttpResponse.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/audit/debug/entity/HttpResponse.java new file mode 100644 index 00000000..742e76b7 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/audit/debug/entity/HttpResponse.java @@ -0,0 +1,27 @@ +package org.gluu.oxauth.audit.debug.entity; + +import java.util.Map; + +/** + * Created by eugeniuparvan on 5/15/17. + */ +public class HttpResponse { + private int status; + private Map headers; + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public Map getHeaders() { + return headers; + } + + public void setHeaders(Map headers) { + this.headers = headers; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/audit/debug/wrapper/RequestWrapper.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/audit/debug/wrapper/RequestWrapper.java new file mode 100644 index 00000000..e7034292 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/audit/debug/wrapper/RequestWrapper.java @@ -0,0 +1,148 @@ +package org.gluu.oxauth.audit.debug.wrapper; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.ws.rs.HttpMethod; +import javax.ws.rs.core.MediaType; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.lang.StringUtils; + +/** + * Created by eugeniuparvan on 5/10/17. + */ +public class RequestWrapper extends HttpServletRequestWrapper { + + private byte[] content; + + private final Map parameterMap; + + private final HttpServletRequest delegate; + + + /** + * Constructs a request object wrapping the given request. + * + * @param request + * @throws IllegalArgumentException if the request is null + */ + public RequestWrapper(HttpServletRequest request) { + super(request); + this.delegate = request; + if (isFormPost()) { + this.parameterMap = request.getParameterMap() != null ? new HashMap(request.getParameterMap()) : Collections.emptyMap(); + } else { + this.parameterMap = Collections.emptyMap(); + } + } + + public Map getParams() { + if (ArrayUtils.isEmpty(content) || this.parameterMap.isEmpty()) { + return delegate.getParameterMap(); + } + return this.parameterMap; + } + + public String getContent() { + try { + if (this.parameterMap.isEmpty()) { + if (ArrayUtils.isEmpty(content)) + content = IOUtils.toByteArray(delegate.getInputStream()); + else + content = IOUtils.toByteArray(new LoggingServletInputStream(content)); + } else { + content = getContentFromParameterMap(this.parameterMap); + } + String requestEncoding = delegate.getCharacterEncoding(); + String normalizedContent = StringUtils.normalizeSpace(new String(content, requestEncoding != null ? requestEncoding : StandardCharsets.UTF_8.name())); + return StringUtils.isBlank(normalizedContent) ? null : normalizedContent; + } catch (IOException e) { + throw new IllegalStateException(); + } + } + + + public Map getHeaders() { + Map headers = new HashMap(0); + Enumeration headerNames = getHeaderNames(); + while (headerNames.hasMoreElements()) { + String headerName = headerNames.nextElement(); + if (headerName != null) { + headers.put(headerName, getHeader(headerName)); + } + } + return headers; + } + + public Map getParameters() { + Map params = new HashMap(); + for (Map.Entry entry : getParams().entrySet()) { + String[] values = entry.getValue(); + params.put(entry.getKey(), values.length > 0 ? values[0] : null); + } + return params; + } + + public boolean isFormPost() { + String contentType = getContentType(); + return (contentType != null && contentType.contains(MediaType.APPLICATION_FORM_URLENCODED) && HttpMethod.POST.equalsIgnoreCase(getMethod())); + } + + private byte[] getContentFromParameterMap(Map parameterMap) { + StringBuilder sb = new StringBuilder(); + String ampersand = "&"; + for (Map.Entry entry : parameterMap.entrySet()) { + String[] value = entry.getValue(); + sb.append(entry.getKey() + "=" + (value.length == 1 ? value[0] : Arrays.toString(value)) + ampersand); + } + String params = sb.toString(); + return params.substring(0, params.length() - 1).getBytes(); + } + + private class LoggingServletInputStream extends ServletInputStream { + + private final InputStream is; + + private LoggingServletInputStream(byte[] content) { + this.is = new ByteArrayInputStream(content); + } + + @Override + public boolean isFinished() { + return true; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) { + } + + @Override + public int read() throws IOException { + return this.is.read(); + } + + @Override + public void close() throws IOException { + super.close(); + is.close(); + } + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/audit/debug/wrapper/ResponseWrapper.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/audit/debug/wrapper/ResponseWrapper.java new file mode 100644 index 00000000..7dc9f0cf --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/audit/debug/wrapper/ResponseWrapper.java @@ -0,0 +1,31 @@ +package org.gluu.oxauth.audit.debug.wrapper; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + +/** + * Created by eugeniuparvan on 5/10/17. + */ +public class ResponseWrapper extends HttpServletResponseWrapper { + + /** + * Constructs a response adaptor wrapping the given response. + * + * @param response + * @throws IllegalArgumentException if the response is null + */ + public ResponseWrapper(HttpServletResponse response) { + super(response); + } + + public Map getHeaders() { + Map headers = new HashMap(0); + for (String headerName : getHeaderNames()) { + headers.put(headerName, getHeader(headerName)); + } + return headers; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/auth/AuthenticationFilter.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/auth/AuthenticationFilter.java new file mode 100644 index 00000000..a3a5b8f4 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/auth/AuthenticationFilter.java @@ -0,0 +1,534 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.auth; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang.StringUtils; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpStatus; +import org.apache.http.entity.ContentType; +import org.gluu.model.security.Identity; +import org.gluu.oxauth.model.authorize.AuthorizeRequestParam; +import org.gluu.oxauth.model.common.*; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.crypto.AbstractCryptoProvider; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.exception.InvalidJwtException; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.model.session.SessionIdState; +import org.gluu.oxauth.model.token.ClientAssertion; +import org.gluu.oxauth.model.token.ClientAssertionType; +import org.gluu.oxauth.model.token.HttpAuthTokenType; +import org.gluu.oxauth.model.token.TokenErrorResponseType; +import org.gluu.oxauth.model.util.Util; +import org.gluu.oxauth.service.ClientFilterService; +import org.gluu.oxauth.service.ClientService; +import org.gluu.oxauth.service.CookieService; +import org.gluu.oxauth.service.SessionIdService; +import org.gluu.oxauth.service.token.TokenService; +import org.gluu.oxauth.util.ServerUtil; +import org.gluu.util.StringHelper; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.servlet.*; +import javax.servlet.annotation.WebFilter; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.WebApplicationException; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.gluu.oxauth.model.ciba.BackchannelAuthenticationErrorResponseType.INVALID_REQUEST; + +/** + * @author Javier Rojas Blum + * @version May 29, 2020 + */ +@WebFilter( + asyncSupported = true, + urlPatterns = { + "/restv1/authorize", + "/restv1/token", + "/restv1/userinfo", + "/restv1/revoke", + "/restv1/revoke_session", + "/restv1/bc-authorize", + "/restv1/device_authorization"}, + displayName = "oxAuth") +public class AuthenticationFilter implements Filter { + + private static final String REALM = "oxAuth"; + + @Inject + private Logger log; + + @Inject + private Authenticator authenticator; + + @Inject + private SessionIdService sessionIdService; + + @Inject + private CookieService cookieService; + + @Inject + private ClientService clientService; + + @Inject + private ClientFilterService clientFilterService; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private Identity identity; + + @Inject + private AuthorizationGrantList authorizationGrantList; + + @Inject + private AbstractCryptoProvider cryptoProvider; + + @Inject + private MTLSService mtlsService; + + @Inject + private TokenService tokenService; + + private String realm; + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException { + final HttpServletRequest httpRequest = (HttpServletRequest) servletRequest; + final HttpServletResponse httpResponse = (HttpServletResponse) servletResponse; + + try { + final String requestUrl = httpRequest.getRequestURL().toString(); + log.trace("Get request to: '{}'", requestUrl); + + final String method = httpRequest.getMethod(); + if (appConfiguration.isSkipAuthenticationFilterOptionsMethod() && "OPTIONS".equals(method)) { + log.trace("Ignoring '{}' request to to: '{}'", method, requestUrl); + filterChain.doFilter(httpRequest, httpResponse); + return; + } + + boolean tokenEndpoint = ServerUtil.isSameRequestPath(requestUrl, appConfiguration.getTokenEndpoint()); + boolean tokenRevocationEndpoint = ServerUtil.isSameRequestPath(requestUrl, appConfiguration.getTokenRevocationEndpoint()); + boolean backchannelAuthenticationEnpoint = ServerUtil.isSameRequestPath(requestUrl, appConfiguration.getBackchannelAuthenticationEndpoint()); + boolean deviceAuthorizationEndpoint = ServerUtil.isSameRequestPath(requestUrl, appConfiguration.getDeviceAuthzEndpoint()); + boolean umaTokenEndpoint = requestUrl.endsWith("/uma/token"); + boolean revokeSessionEndpoint = requestUrl.endsWith("/revoke_session"); + String authorizationHeader = httpRequest.getHeader("Authorization"); + + try { + if (processMTLS(httpRequest, httpResponse, filterChain)) { + return; + } + } catch (Throwable ex) { + // Catch exceptions like org.eclipse.jetty.http.BadMessageException when form is invalid + // https://github.com/GluuFederation/oxAuth/issues/1843 + log.error(ex.getMessage(), ex); + } + + if ((tokenRevocationEndpoint || deviceAuthorizationEndpoint) && clientService.isPublic(httpRequest.getParameter("client_id"))) { + log.trace("Skipped authentication for {} for public client.", tokenRevocationEndpoint ? "Token Revocation" : "Device Authorization"); + filterChain.doFilter(httpRequest, httpResponse); + return; + } + + if (tokenEndpoint || umaTokenEndpoint || revokeSessionEndpoint || tokenRevocationEndpoint || deviceAuthorizationEndpoint) { + log.debug("Starting endpoint authentication {}", requestUrl); + + // #686 : allow authenticated client via user access_token + final String accessToken = tokenService.getToken(authorizationHeader, + HttpAuthTokenType.Bearer,HttpAuthTokenType.AccessToken); + if (StringUtils.isNotBlank(accessToken)) { + processAuthByAccessToken(accessToken, httpRequest, httpResponse, filterChain); + return; + } + + if (httpRequest.getParameter("client_assertion") != null + && httpRequest.getParameter("client_assertion_type") != null) { + log.debug("Starting JWT token endpoint authentication"); + processJwtAuth(httpRequest, httpResponse, filterChain); + } else if (tokenService.isBasicAuthToken(authorizationHeader)) { + log.debug("Starting Basic Auth token endpoint authentication"); + processBasicAuth(httpRequest, httpResponse, filterChain); + } else { + log.debug("Starting POST Auth token endpoint authentication"); + processPostAuth(clientFilterService, httpRequest, httpResponse, filterChain, tokenEndpoint); + } + } else if (backchannelAuthenticationEnpoint) { + if (httpRequest.getParameter("client_assertion") != null + && httpRequest.getParameter("client_assertion_type") != null) { + log.debug("Starting JWT token endpoint authentication"); + processJwtAuth(httpRequest, httpResponse, filterChain); + } else if (tokenService.isBasicAuthToken(authorizationHeader)) { + processBasicAuth(httpRequest, httpResponse, filterChain); + } else { + String entity = errorResponseFactory.getErrorAsJson(INVALID_REQUEST); + httpResponse.setStatus(HttpStatus.SC_BAD_REQUEST); + httpResponse.addHeader("WWW-Authenticate", "Basic realm=\"" + getRealm() + "\""); + httpResponse.setContentType(ContentType.APPLICATION_JSON.toString()); + httpResponse.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(entity.length())); + PrintWriter out = httpResponse.getWriter(); + out.print(entity); + out.flush(); + } + } else if (authorizationHeader != null && !tokenService.isNegotiateAuthToken(authorizationHeader)) { + if (tokenService.isBearerAuthToken(authorizationHeader)) { + processBearerAuth(httpRequest, httpResponse, filterChain); + } else if (tokenService.isBasicAuthToken(authorizationHeader)) { + processBasicAuth(httpRequest, httpResponse, filterChain); + } else { + httpResponse.addHeader("WWW-Authenticate", "Basic realm=\"" + getRealm() + "\""); + httpResponse.sendError(401, "Not authorized"); + } + } else { + String sessionId = cookieService.getSessionIdFromCookie(httpRequest); + List prompts = Prompt.fromString(httpRequest.getParameter(AuthorizeRequestParam.PROMPT), " "); + + if (StringUtils.isBlank(sessionId) && appConfiguration.getSessionIdRequestParameterEnabled()) { + sessionId = httpRequest.getParameter(AuthorizeRequestParam.SESSION_ID); + } + + SessionId sessionIdObject = null; + if (StringUtils.isNotBlank(sessionId)) { + sessionIdObject = sessionIdService.getSessionId(sessionId); + } + if (sessionIdObject != null && SessionIdState.AUTHENTICATED == sessionIdObject.getState() + && !prompts.contains(Prompt.LOGIN)) { + processSessionAuth(sessionId, httpRequest, httpResponse, filterChain); + } else { + filterChain.doFilter(httpRequest, httpResponse); + } + } + } catch (WebApplicationException ex) { + if (ex.getResponse() != null) { + sendResponse(httpResponse, ex); + return; + } + log.error(ex.getMessage(), ex); + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + } + } + + /** + * @return whether successful or not + */ + private boolean processMTLS(HttpServletRequest httpRequest, HttpServletResponse httpResponse, FilterChain filterChain) throws Exception { + if (cryptoProvider == null) { + log.debug("Unable to create cryptoProvider."); + return false; + } + + final String clientId = httpRequest.getParameter("client_id"); + if (StringUtils.isNotBlank(clientId)) { + final Client client = clientService.getClient(clientId); + if (client != null && + (client.getAuthenticationMethod() == AuthenticationMethod.TLS_CLIENT_AUTH || + client.getAuthenticationMethod() == AuthenticationMethod.SELF_SIGNED_TLS_CLIENT_AUTH)) { + return mtlsService.processMTLS(httpRequest, httpResponse, filterChain, client); + } + } + return false; + } + + private void processAuthByAccessToken(String accessToken, HttpServletRequest httpRequest, HttpServletResponse httpResponse, FilterChain filterChain) { + try { + log.trace("Authenticating client by access token {} ...", accessToken); + if (StringUtils.isBlank(accessToken)) { + sendError(httpResponse); + return; + } + + AuthorizationGrant grant = authorizationGrantList.getAuthorizationGrantByAccessToken(accessToken); + if (grant == null) { + sendError(httpResponse); + return; + } + final AbstractToken accessTokenObj = grant.getAccessToken(accessToken); + if (accessTokenObj == null || !accessTokenObj.isValid()) { + sendError(httpResponse); + return; + } + + Client client = grant.getClient(); + authenticator.configureSessionClient(client); + filterChain.doFilter(httpRequest, httpResponse); + return; + } catch (Exception ex) { + log.error("Failed to authenticate client by access_token", ex); + } + + sendError(httpResponse); + } + + private void processSessionAuth(String p_sessionId, HttpServletRequest p_httpRequest, HttpServletResponse p_httpResponse, FilterChain p_filterChain) { + boolean requireAuth; + + requireAuth = !authenticator.authenticateBySessionId(p_sessionId); + log.trace("Process Session Auth, sessionId = {}, requireAuth = {}", p_sessionId, requireAuth); + + if (!requireAuth) { + try { + p_filterChain.doFilter(p_httpRequest, p_httpResponse); + } catch (Exception ex) { + log.error("Failed to process session authentication", ex); + requireAuth = true; + } + } + + if (requireAuth) { + sendError(p_httpResponse); + } + } + + private void processBasicAuth(HttpServletRequest servletRequest, HttpServletResponse servletResponse, FilterChain filterChain) { + boolean requireAuth = true; + + try { + String header = servletRequest.getHeader("Authorization"); + if (tokenService.isBasicAuthToken(header)) { + String base64Token = tokenService.getBasicToken(header); + String token = new String(Base64.decodeBase64(base64Token), StandardCharsets.UTF_8); + + String username = ""; + String password = ""; + int delim = token.indexOf(":"); + + if (delim != -1) { + // oxAuth #677 URL decode the username and password + username = URLDecoder.decode(token.substring(0, delim), Util.UTF8_STRING_ENCODING); + password = URLDecoder.decode(token.substring(delim + 1), Util.UTF8_STRING_ENCODING); + } + + requireAuth = !StringHelper.equals(username, identity.getCredentials().getUsername()) + || !identity.isLoggedIn(); + + // Only authenticate if username doesn't match Identity.username + // and user isn't authenticated + if (requireAuth) { + if (!username.equals(identity.getCredentials().getUsername()) || !identity.isLoggedIn()) { + identity.getCredentials().setUsername(username); + identity.getCredentials().setPassword(password); + + if (servletRequest.getRequestURI().endsWith("/token") + || servletRequest.getRequestURI().endsWith("/revoke") + || servletRequest.getRequestURI().endsWith("/revoke_session") + || servletRequest.getRequestURI().endsWith("/userinfo") + || servletRequest.getRequestURI().endsWith("/bc-authorize") + || servletRequest.getRequestURI().endsWith("/device_authorization")) { + Client client = clientService.getClient(username); + if (client == null + || AuthenticationMethod.CLIENT_SECRET_BASIC != client.getAuthenticationMethod()) { + throw new Exception("The Token Authentication Method is not valid."); + } + requireAuth = !authenticator.authenticateClient(servletRequest); + } else { + requireAuth = !authenticator.authenticateUser(servletRequest); + } + } + } + } + + if (!requireAuth) { + filterChain.doFilter(servletRequest, servletResponse); + return; + } + } catch (Exception ex) { + log.info("Basic authentication failed", ex); + } + + if (requireAuth && !identity.isLoggedIn()) { + sendError(servletResponse); + } + } + + private void processBearerAuth(HttpServletRequest servletRequest, HttpServletResponse servletResponse, + FilterChain filterChain) { + try { + String header = servletRequest.getHeader("Authorization"); + if (tokenService.isBearerAuthToken(header)) { + // Immutable object + // servletRequest.getParameterMap().put("access_token", new + // String[]{accessToken}); + filterChain.doFilter(servletRequest, servletResponse); + } + } catch (Exception ex) { + log.info("Bearer authorization failed: {}", ex); + } + } + + private void processPostAuth(ClientFilterService clientFilterService, HttpServletRequest servletRequest, + HttpServletResponse servletResponse, FilterChain filterChain, boolean tokenEndpoint) { + try { + String clientId = ""; + String clientSecret = ""; + boolean isExistUserPassword = false; + if (StringHelper.isNotEmpty(servletRequest.getParameter("client_id")) + && StringHelper.isNotEmpty(servletRequest.getParameter("client_secret"))) { + clientId = servletRequest.getParameter("client_id"); + clientSecret = servletRequest.getParameter("client_secret"); + isExistUserPassword = true; + } + log.trace("isExistUserPassword: {}", isExistUserPassword); + + boolean requireAuth = !StringHelper.equals(clientId, identity.getCredentials().getUsername()) + || !identity.isLoggedIn(); + log.debug("requireAuth: '{}'", requireAuth); + + if (requireAuth) { + if (isExistUserPassword) { + Client client = clientService.getClient(clientId); + if (client != null && AuthenticationMethod.CLIENT_SECRET_POST == client.getAuthenticationMethod()) { + // Only authenticate if username doesn't match + // Identity.username and user isn't authenticated + if (!clientId.equals(identity.getCredentials().getUsername()) || !identity.isLoggedIn()) { + identity.logout(); + + identity.getCredentials().setUsername(clientId); + identity.getCredentials().setPassword(clientSecret); + + requireAuth = !authenticator.authenticateClient(servletRequest); + } else { + authenticator.configureSessionClient(client); + } + } + } else if (Boolean.TRUE.equals(appConfiguration.getClientAuthenticationFiltersEnabled())) { + String clientDn = clientFilterService + .processAuthenticationFilters(servletRequest.getParameterMap()); + if (clientDn != null) { + Client client = clientService.getClientByDn(clientDn); + + identity.logout(); + + identity.getCredentials().setUsername(client.getClientId()); + identity.getCredentials().setPassword(null); + + requireAuth = !authenticator.authenticateClient(servletRequest, true); + } + } else if (tokenEndpoint) { + Client client = clientService.getClient(servletRequest.getParameter("client_id")); + if (client != null && client.getAuthenticationMethod() == AuthenticationMethod.NONE) { + identity.logout(); + + identity.getCredentials().setUsername(client.getClientId()); + identity.getCredentials().setPassword(null); + + requireAuth = !authenticator.authenticateClient(servletRequest, true); + } + } + } + + if (!requireAuth) { + filterChain.doFilter(servletRequest, servletResponse); + return; + } + + if (!identity.isLoggedIn()) { + sendError(servletResponse); + } + } catch (Exception ex) { + log.error("Post authentication failed: {}", ex); + } + } + + private void processJwtAuth(HttpServletRequest servletRequest, HttpServletResponse servletResponse, + FilterChain filterChain) { + boolean authorized = false; + + try { + if (servletRequest.getParameter("client_assertion") != null + && servletRequest.getParameter("client_assertion_type") != null) { + String clientId = servletRequest.getParameter("client_id"); + ClientAssertionType clientAssertionType = ClientAssertionType + .fromString(servletRequest.getParameter("client_assertion_type")); + String encodedAssertion = servletRequest.getParameter("client_assertion"); + + if (clientAssertionType == ClientAssertionType.JWT_BEARER) { + ClientAssertion clientAssertion = new ClientAssertion(appConfiguration, cryptoProvider, clientId, + clientAssertionType, encodedAssertion); + + String username = clientAssertion.getSubjectIdentifier(); + String password = clientAssertion.getClientSecret(); + + // Only authenticate if username doesn't match + // Identity.username and user isn't authenticated + if (!username.equals(identity.getCredentials().getUsername()) || !identity.isLoggedIn()) { + identity.getCredentials().setUsername(username); + identity.getCredentials().setPassword(password); + + authenticator.authenticateClient(servletRequest, true); + authorized = true; + } + } + } + + filterChain.doFilter(servletRequest, servletResponse); + } catch (ServletException | IOException | InvalidJwtException ex) { + log.info("JWT authentication failed: {}", ex); + } + + if (!authorized) { + sendError(servletResponse); + } + } + + private void sendError(HttpServletResponse servletResponse) { + try (PrintWriter out = servletResponse.getWriter()) { + servletResponse.setStatus(401); + servletResponse.addHeader("WWW-Authenticate", "Basic realm=\"" + getRealm() + "\""); + servletResponse.setContentType("application/json;charset=UTF-8"); + out.write(errorResponseFactory.errorAsJson(TokenErrorResponseType.INVALID_CLIENT, "Unable to authenticate client.")); + } catch (IOException ex) { + log.error(ex.getMessage(), ex); + } + } + + private void sendResponse(HttpServletResponse servletResponse, WebApplicationException e) { + try (PrintWriter out = servletResponse.getWriter()) { + servletResponse.setStatus(e.getResponse().getStatus()); + servletResponse.addHeader("WWW-Authenticate", "Basic realm=\"" + getRealm() + "\""); + servletResponse.setContentType("application/json;charset=UTF-8"); + out.write(e.getResponse().getEntity().toString()); + } catch (IOException ex) { + log.error(ex.getMessage(), ex); + } + } + + public String getRealm() { + if (realm != null) { + return realm; + } else { + return REALM; + } + } + + public void setRealm(String realm) { + this.realm = realm; + } + + @Override + public void destroy() { + } + +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/auth/Authenticator.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/auth/Authenticator.java new file mode 100644 index 00000000..6b864d66 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/auth/Authenticator.java @@ -0,0 +1,826 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.auth; + +import org.apache.commons.lang.StringUtils; +import org.gluu.jsf2.message.FacesMessages; +import org.gluu.jsf2.service.FacesService; +import org.gluu.model.AuthenticationScriptUsageType; +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.model.security.Credentials; +import org.gluu.oxauth.i18n.LanguageBean; +import org.gluu.oxauth.model.authorize.AuthorizeErrorResponseType; +import org.gluu.oxauth.model.common.User; +import org.gluu.oxauth.model.config.Constants; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.exception.InvalidSessionStateException; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.model.session.SessionIdState; +import org.gluu.oxauth.security.Identity; +import org.gluu.oxauth.service.*; +import org.gluu.oxauth.service.external.ExternalAuthenticationService; +import org.gluu.util.StringHelper; +import org.slf4j.Logger; + +import javax.enterprise.context.RequestScoped; +import javax.faces.application.FacesMessage; +import javax.faces.application.FacesMessage.Severity; +import javax.faces.context.ExternalContext; +import javax.faces.context.FacesContext; +import javax.inject.Inject; +import javax.inject.Named; +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +/** + * Authenticator component + * + * @author Javier Rojas Blum + * @author Yuriy Movchan + * @version August 20, 2019 + */ +@RequestScoped +@Named +public class Authenticator { + + public static final String INVALID_SESSION_MESSAGE = "login.errorSessionInvalidMessage"; + public static final String AUTHENTICATION_ERROR_MESSAGE = "login.failedToAuthenticate"; + + @Inject + private Logger logger; + + @Inject + private Identity identity; + + @Inject + private Credentials credentials; + + @Inject + private ClientService clientService; + + @Inject + private SessionIdService sessionIdService; + + @Inject + private AuthenticationService authenticationService; + + @Inject + private ExternalAuthenticationService externalAuthenticationService; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private FacesContext facesContext; + + @Inject + private ExternalContext externalContext; + + @Inject + private FacesService facesService; + + @Inject + private FacesMessages facesMessages; + + @Inject + private LanguageBean languageBean; + + @Inject + private RequestParameterService requestParameterService; + + @Inject + private ErrorHandlerService errorHandlerService; + + private String authAcr; + + private Integer authStep; + + private String lastResult; + private SessionId curentSessionId; + + /** + * Tries to authenticate an user, returns true if the + * authentication succeed + * + * @return Returns true if the authentication succeed + */ + public boolean authenticate() { + HttpServletRequest servletRequest = (HttpServletRequest) facesContext.getExternalContext().getRequest(); + + final SessionId sessionId = getSessionId(servletRequest); + if (sessionIdService.isSessionIdAuthenticated(sessionId)) { + // #1029 : session is already authenticated, we run into second authorization + // request + errorHandlerService.handleError("login.userAlreadyAuthenticated", AuthorizeErrorResponseType.RETRY, + "Session is already authenticated. Please re-send authorization request. If AS errorHandlingMethod=remote then RP can get redirect with error and re-send authorization request automatically."); + return false; + } + + lastResult = authenticateImpl(servletRequest, true, false, false); + logger.debug("authenticate resultCode: {}", lastResult); + + if (Constants.RESULT_SUCCESS.equals(lastResult)) { + return true; + } else if (Constants.RESULT_FAILURE.equals(lastResult)) { + authenticationFailed(); + } else if (Constants.RESULT_NO_PERMISSIONS.equals(lastResult)) { + handlePermissionsError(); + } else if (Constants.RESULT_EXPIRED.equals(lastResult)) { + handleSessionInvalid(); + } else if (Constants.RESULT_AUTHENTICATION_FAILED.equals(lastResult)) { + // Do nothing to keep compatibility with older versions + if (facesMessages.getMessages().size() == 0) { + addMessage(FacesMessage.SEVERITY_ERROR, "login.failedToAuthenticate"); + } + } + + return false; + } + + public String authenticateWithOutcome() { + HttpServletRequest servletRequest = (HttpServletRequest) facesContext.getExternalContext().getRequest(); + lastResult = authenticateImpl(servletRequest, true, false, false); + + if (Constants.RESULT_SUCCESS.equals(lastResult)) { + } else if (Constants.RESULT_FAILURE.equals(lastResult)) { + authenticationFailed(); + } else if (Constants.RESULT_NO_PERMISSIONS.equals(lastResult)) { + handlePermissionsError(); + } else if (Constants.RESULT_EXPIRED.equals(lastResult)) { + handleSessionInvalid(); + } else if (Constants.RESULT_AUTHENTICATION_FAILED.equals(lastResult)) { + // Do nothing to keep compatibility with older versions + if (facesMessages.getMessages().size() == 0) { + addMessage(FacesMessage.SEVERITY_ERROR, "login.failedToAuthenticate"); + } + handleLoginError(null); + } + + return lastResult; + } + + public boolean authenticateClient(HttpServletRequest servletRequest, boolean skipPassword) { + String result = authenticateImpl(servletRequest, false, skipPassword, true); + return Constants.RESULT_SUCCESS.equals(result); + } + + public boolean authenticateClient(HttpServletRequest servletRequest) { + String result = authenticateImpl(servletRequest, false, false, true); + return Constants.RESULT_SUCCESS.equals(result); + } + + public boolean authenticateUser(HttpServletRequest servletRequest) { + String result = authenticateImpl(servletRequest, false, false, false); + return Constants.RESULT_SUCCESS.equals(result); + } + + public String authenticateImpl(HttpServletRequest servletRequest, boolean interactive, boolean skipPassword, + boolean service) { + String result = Constants.RESULT_FAILURE; + try { + logger.trace("Authenticating ... (interactive: " + interactive + ", skipPassword: " + skipPassword + + ", credentials.username: " + credentials.getUsername() + ")"); + if (service && (StringHelper.isNotEmpty(credentials.getUsername()) + && (skipPassword || StringHelper.isNotEmpty(credentials.getPassword())) && servletRequest != null + && (servletRequest.getRequestURI().endsWith("/token") + || servletRequest.getRequestURI().endsWith("/revoke") + || servletRequest.getRequestURI().endsWith("/revoke_session") + || servletRequest.getRequestURI().endsWith("/userinfo") + || servletRequest.getRequestURI().endsWith("/bc-authorize") + || servletRequest.getRequestURI().endsWith("/device_authorization")))) { + boolean authenticated = clientAuthentication(credentials, interactive, skipPassword); + if (authenticated) { + result = Constants.RESULT_SUCCESS; + } + } else { + if (interactive) { + result = userAuthenticationInteractive(servletRequest); + } else { + boolean authenticated = userAuthenticationService(); + if (authenticated) { + result = Constants.RESULT_SUCCESS; + } + } + } + } catch (InvalidSessionStateException ex) { + // Allow to handle it via GlobalExceptionHandler + throw ex; + } catch (Exception ex) { + logger.error(ex.getMessage(), ex); + } + + if (Constants.RESULT_SUCCESS.equals(result)) { + logger.trace("Authentication successfully for '{}'", credentials.getUsername()); + return result; + } + + logger.info("Authentication failed for '{}'", credentials.getUsername()); + return result; + } + + public boolean clientAuthentication(Credentials credentials, boolean interactive, boolean skipPassword) { + boolean isServiceUsesExternalAuthenticator = !interactive + && externalAuthenticationService.isEnabled(AuthenticationScriptUsageType.SERVICE); + if (isServiceUsesExternalAuthenticator) { + CustomScriptConfiguration customScriptConfiguration = externalAuthenticationService + .determineCustomScriptConfiguration(AuthenticationScriptUsageType.SERVICE, 1, this.authAcr); + + if (customScriptConfiguration == null) { + logger.error("Failed to get CustomScriptConfiguration. acr: '{}'", this.authAcr); + } else { + this.authAcr = customScriptConfiguration.getCustomScript().getName(); + + boolean result = externalAuthenticationService.executeExternalAuthenticate(customScriptConfiguration, + null, 1); + logger.info("Authentication result for user '{}', result: '{}'", credentials.getUsername(), result); + + if (result) { + Client client = authenticationService.configureSessionClient(); + showClientAuthenticationLog(client); + return true; + } + } + } + + boolean loggedIn = skipPassword; + if (!loggedIn) { + loggedIn = clientService.authenticate(credentials.getUsername(), credentials.getPassword()); + } + if (loggedIn) { + Client client = authenticationService.configureSessionClient(); + showClientAuthenticationLog(client); + return true; + } + + return false; + } + + private void showClientAuthenticationLog(Client client) { + StringBuilder sb = new StringBuilder("Authentication success for Client"); + if (StringHelper.toBoolean(appConfiguration.getLogClientIdOnClientAuthentication(), false) + || StringHelper.toBoolean(appConfiguration.getLogClientNameOnClientAuthentication(), false)) { + sb.append(":"); + if (appConfiguration.getLogClientIdOnClientAuthentication()) { + sb.append(" ").append("'").append(client.getClientId()).append("'"); + } + if (appConfiguration.getLogClientNameOnClientAuthentication()) { + sb.append(" ").append("('").append(client.getClientName()).append("')"); + } + } + logger.info(sb.toString()); + } + + private String userAuthenticationInteractive(HttpServletRequest servletRequest) { + SessionId sessionId = getSessionId(servletRequest); + Map sessionIdAttributes = sessionIdService.getSessionAttributes(sessionId); + if (sessionIdAttributes == null) { + logger.debug("Unable to get session attributes. SessionId: " + (sessionId != null ? sessionId.getId() : null)); + return Constants.RESULT_EXPIRED; + } + + // Set current state into identity to allow use in login form and + // authentication scripts + identity.setSessionId(sessionId); + + initCustomAuthenticatorVariables(sessionIdAttributes); + boolean useExternalAuthenticator = externalAuthenticationService + .isEnabled(AuthenticationScriptUsageType.INTERACTIVE); + if (useExternalAuthenticator && !StringHelper.isEmpty(this.authAcr)) { + initCustomAuthenticatorVariables(sessionIdAttributes); + if ((this.authStep == null) || StringHelper.isEmpty(this.authAcr)) { + logger.error("Failed to determine authentication mode"); + return Constants.RESULT_EXPIRED; + } + + CustomScriptConfiguration customScriptConfiguration = externalAuthenticationService + .getCustomScriptConfiguration(AuthenticationScriptUsageType.INTERACTIVE, this.authAcr); + if (customScriptConfiguration == null) { + logger.error("Failed to get CustomScriptConfiguration for acr: '{}', auth_step: '{}'", this.authAcr, + this.authStep); + return Constants.RESULT_FAILURE; + } + + // Check if all previous steps had passed + boolean passedPreviousSteps = isPassedPreviousAuthSteps(sessionIdAttributes, this.authStep); + if (!passedPreviousSteps) { + logger.error("There are authentication steps not marked as passed. acr: '{}', auth_step: '{}'", + this.authAcr, this.authStep); + return Constants.RESULT_FAILURE; + } + + // Restore identity working parameters from session + setIdentityWorkingParameters(sessionIdAttributes); + + boolean result = externalAuthenticationService.executeExternalAuthenticate(customScriptConfiguration, + externalContext.getRequestParameterValuesMap(), this.authStep); + if (logger.isDebugEnabled()) { + String userId = credentials.getUsername(); + if (StringHelper.isEmpty(userId)) { + User user = identity.getUser(); + if (user != null) { + userId = user.getUserId(); + } + logger.debug( + "Authentication result for user '{}'. auth_step: '{}', result: '{}', credentials: '{}'", + userId, this.authStep, result, System.identityHashCode(credentials)); + } + } + + int overridenNextStep = -1; + logger.trace("#########################################################################"); + logger.trace("++++++++++++++++++++++++++++++++++++++++++CURRENT ACR:" + this.authAcr); + logger.trace("++++++++++++++++++++++++++++++++++++++++++CURRENT STEP:" + this.authStep); + int apiVersion = externalAuthenticationService.executeExternalGetApiVersion(customScriptConfiguration); + if (apiVersion > 1) { + logger.trace("According to API version script supports steps overriding"); + overridenNextStep = externalAuthenticationService.getNextStep(customScriptConfiguration, + externalContext.getRequestParameterValuesMap(), this.authStep); + logger.debug("Get next step from script: '{}'", overridenNextStep); + } + + if (!result && (overridenNextStep == -1)) { + // Force session lastUsedAt update if authentication attempt is failed + sessionIdService.updateSessionId(sessionId); + return Constants.RESULT_AUTHENTICATION_FAILED; + } + + boolean overrideCurrentStep = false; + if (overridenNextStep > -1) { + overrideCurrentStep = true; + // Reload session id +/* + * TODO: Remove after 6.0. Check if this will not led to external script problems. + sessionId = sessionIdService.getSessionId(); +*/ + + // Reset to specified step + sessionId = sessionIdService.resetToStep(sessionId, overridenNextStep); + if (sessionId == null) { + return Constants.RESULT_AUTHENTICATION_FAILED; + } + + this.authStep = overridenNextStep; + logger.info("Authentication reset to step : '{}'", this.authStep); + } + + // Update parameters map to allow access it from count + // authentication steps method + updateExtraParameters(customScriptConfiguration, this.authStep + 1, sessionIdAttributes); + + // Determine count authentication methods + int countAuthenticationSteps = externalAuthenticationService + .executeExternalGetCountAuthenticationSteps(customScriptConfiguration); +/* + * TODO: Remove after 6.0. Check if this will not led to external script problems. + // Reload from LDAP to make sure that we are updating latest session + // attributes + sessionId = sessionIdService.getSessionId(); +*/ + sessionIdAttributes = sessionIdService.getSessionAttributes(sessionId); + + // Prepare for next step + if ((this.authStep < countAuthenticationSteps) || overrideCurrentStep) { + int nextStep; + if (overrideCurrentStep) { + nextStep = overridenNextStep; + } else { + nextStep = this.authStep + 1; + } + + String redirectTo = externalAuthenticationService + .executeExternalGetPageForStep(customScriptConfiguration, nextStep); + if (redirectTo == null) { + return Constants.RESULT_FAILURE; + } else if (StringHelper.isEmpty(redirectTo)) { + redirectTo = "/login.xhtml"; + } + + // Store/Update extra parameters in session attributes map + updateExtraParameters(customScriptConfiguration, nextStep, sessionIdAttributes); + + if (!overrideCurrentStep) { + // Update auth_step + sessionIdAttributes.put("auth_step", Integer.toString(nextStep)); + + // Mark step as passed + markAuthStepAsPassed(sessionIdAttributes, this.authStep); + } + + if (sessionId != null) { + boolean updateResult = updateSession(sessionId, sessionIdAttributes); + if (!updateResult) { + return Constants.RESULT_EXPIRED; + } + } + + logger.trace("Redirect to page: '{}'", redirectTo); + facesService.redirectWithExternal(redirectTo, null); + + return Constants.RESULT_SUCCESS; + } + + if (this.authStep == countAuthenticationSteps) { + // Store/Update extra parameters in session attributes map + updateExtraParameters(customScriptConfiguration, this.authStep + 1, sessionIdAttributes); + + SessionId eventSessionId = authenticationService.configureSessionUser(sessionId, sessionIdAttributes); + + authenticationService.quietLogin(credentials.getUsername()); + + // Redirect to authorization workflow + logger.debug("Sending event to trigger user redirection: '{}'", credentials.getUsername()); + authenticationService.onSuccessfulLogin(eventSessionId); + + logger.info("Authentication success for User: '{}'", credentials.getUsername()); + return Constants.RESULT_SUCCESS; + } + } else { + if (StringHelper.isNotEmpty(credentials.getUsername())) { + boolean authenticated = authenticationService.authenticate(credentials.getUsername(), + credentials.getPassword()); + if (authenticated) { + SessionId eventSessionId = authenticationService.configureSessionUser(sessionId, + sessionIdAttributes); + + // Redirect to authorization workflow + logger.debug("Sending event to trigger user redirection: '{}'", credentials.getUsername()); + authenticationService.onSuccessfulLogin(eventSessionId); + } else { + // Force session lastUsedAt update if authentication attempt is failed + sessionIdService.updateSessionId(sessionId); + } + + logger.info("Authentication success for User: '{}'", credentials.getUsername()); + return Constants.RESULT_SUCCESS; + } + } + + return Constants.RESULT_FAILURE; + } + + protected void handleSessionInvalid() { + errorHandlerService.handleError(INVALID_SESSION_MESSAGE, + AuthorizeErrorResponseType.AUTHENTICATION_SESSION_INVALID, + "Create authorization request to start new authentication session."); + } + + protected void handleScriptError() { + handleScriptError(AUTHENTICATION_ERROR_MESSAGE); + } + + protected void handleScriptError(String facesMessageId) { + errorHandlerService.handleError(facesMessageId, AuthorizeErrorResponseType.INVALID_AUTHENTICATION_METHOD, + "Contact administrator to fix specific ACR method issue."); + } + + protected void handlePermissionsError() { + errorHandlerService.handleError("login.youDontHavePermission", AuthorizeErrorResponseType.ACCESS_DENIED, + "Contact administrator to grant access to resource."); + } + + protected void handleLoginError(String facesMessageId) { + errorHandlerService.handleError(facesMessageId, AuthorizeErrorResponseType.LOGIN_REQUIRED, + "User should log into into system."); + } + + private boolean updateSession(SessionId sessionId, Map sessionIdAttributes) { + sessionId.setSessionAttributes(sessionIdAttributes); + boolean updateResult = sessionIdService.updateSessionId(sessionId, true, true, true); + if (!updateResult) { + logger.debug("Failed to update session entry: '{}'", sessionId.getId()); + return false; + } + + return true; + } + + private boolean userAuthenticationService() { + if (externalAuthenticationService.isEnabled(AuthenticationScriptUsageType.SERVICE)) { + CustomScriptConfiguration customScriptConfiguration = externalAuthenticationService + .determineCustomScriptConfiguration(AuthenticationScriptUsageType.SERVICE, 1, this.authAcr); + + if (customScriptConfiguration == null) { + logger.error("Failed to get CustomScriptConfiguration. auth_step: '{}', acr: '{}'", this.authStep, + this.authAcr); + } else { + this.authAcr = customScriptConfiguration.getName(); + + boolean result = externalAuthenticationService.executeExternalAuthenticate(customScriptConfiguration, + null, 1); + logger.info("Authentication result for '{}'. auth_step: '{}', result: '{}'", credentials.getUsername(), + this.authStep, result); + + if (result) { + authenticationService.configureEventUser(); + + logger.info("Authentication success for User: '{}'", credentials.getUsername()); + return true; + } + logger.info("Authentication failed for User: '{}'", credentials.getUsername()); + } + } + + if (StringHelper.isNotEmpty(credentials.getUsername())) { + boolean authenticated = authenticationService.authenticate(credentials.getUsername(), + credentials.getPassword()); + if (authenticated) { + authenticationService.configureEventUser(); + + logger.info("Authentication success for User: '{}'", credentials.getUsername()); + return true; + } + logger.info("Authentication failed for User: '{}'", credentials.getUsername()); + } + + return false; + } + + private void setIdentityWorkingParameters(Map sessionIdAttributes) { + Map authExternalAttributes = authenticationService + .getExternalScriptExtraParameters(sessionIdAttributes); + + HashMap workingParameters = identity.getWorkingParameters(); + for (Entry authExternalAttributeEntry : authExternalAttributes.entrySet()) { + String authExternalAttributeName = authExternalAttributeEntry.getKey(); + String authExternalAttributeType = authExternalAttributeEntry.getValue(); + + if (sessionIdAttributes.containsKey(authExternalAttributeName)) { + String authExternalAttributeValue = sessionIdAttributes.get(authExternalAttributeName); + Object typedValue = requestParameterService.getTypedValue(authExternalAttributeValue, + authExternalAttributeType); + + workingParameters.put(authExternalAttributeName, typedValue); + } + } + } + + public String prepareAuthenticationForStep() { + SessionId sessionId = sessionIdService.getSessionId(); + lastResult = prepareAuthenticationForStep(sessionId); + + if (Constants.RESULT_SUCCESS.equals(lastResult)) { + } else if (Constants.RESULT_FAILURE.equals(lastResult)) { + handleScriptError(); + } else if (Constants.RESULT_NO_PERMISSIONS.equals(lastResult)) { + handlePermissionsError(); + } else if (Constants.RESULT_EXPIRED.equals(lastResult)) { + handleSessionInvalid(); + } + + return lastResult; + } + + public String prepareAuthenticationForStep(SessionId sessionId) { + Map sessionIdAttributes = sessionIdService.getSessionAttributes(sessionId); + if (sessionIdAttributes == null) { + logger.debug("Unable to get attributes from session"); + return Constants.RESULT_EXPIRED; + } + + // Set current state into identity to allow use in login form and + // authentication scripts + identity.setSessionId(sessionId); + + if (!externalAuthenticationService.isEnabled(AuthenticationScriptUsageType.INTERACTIVE)) { + return Constants.RESULT_SUCCESS; + } + + initCustomAuthenticatorVariables(sessionIdAttributes); + if (StringHelper.isEmpty(this.authAcr)) { + return Constants.RESULT_SUCCESS; + } + + if ((this.authStep == null) || (this.authStep < 1)) { + return Constants.RESULT_NO_PERMISSIONS; + } + + CustomScriptConfiguration customScriptConfiguration = externalAuthenticationService + .getCustomScriptConfiguration(AuthenticationScriptUsageType.INTERACTIVE, this.authAcr); + if (customScriptConfiguration == null) { + logger.error("Failed to get CustomScriptConfiguration. auth_step: '{}', acr: '{}'", this.authStep, + this.authAcr); + return Constants.RESULT_FAILURE; + } + + // Check if all previous steps had passed + boolean passedPreviousSteps = isPassedPreviousAuthSteps(sessionIdAttributes, this.authStep); + if (!passedPreviousSteps) { + logger.error("There are authentication steps not marked as passed. acr: '{}', auth_step: '{}'", + this.authAcr, this.authStep); + return Constants.RESULT_FAILURE; + } + + // Restore identity working parameters from session + setIdentityWorkingParameters(sessionIdAttributes); + + String currentauthAcr = customScriptConfiguration.getName(); + + customScriptConfiguration = externalAuthenticationService.determineExternalAuthenticatorForWorkflow( + AuthenticationScriptUsageType.INTERACTIVE, customScriptConfiguration); + if (customScriptConfiguration == null) { + return Constants.RESULT_FAILURE; + } else { + String determinedauthAcr = customScriptConfiguration.getName(); + if (!StringHelper.equalsIgnoreCase(currentauthAcr, determinedauthAcr)) { + // Redirect user to alternative login workflow + String redirectTo = externalAuthenticationService + .executeExternalGetPageForStep(customScriptConfiguration, this.authStep); + + if (StringHelper.isEmpty(redirectTo)) { + redirectTo = "/login.xhtml"; + } + + CustomScriptConfiguration determinedCustomScriptConfiguration = externalAuthenticationService + .getCustomScriptConfiguration(AuthenticationScriptUsageType.INTERACTIVE, determinedauthAcr); + if (determinedCustomScriptConfiguration == null) { + logger.error("Failed to get determined CustomScriptConfiguration. auth_step: '{}', acr: '{}'", + this.authStep, this.authAcr); + return Constants.RESULT_FAILURE; + } + + logger.debug("Redirect to page: '{}'. Force to use acr: '{}'", redirectTo, determinedauthAcr); + + determinedauthAcr = determinedCustomScriptConfiguration.getName(); + String determinedAuthLevel = Integer.toString(determinedCustomScriptConfiguration.getLevel()); + + sessionIdAttributes.put("acr", determinedauthAcr); + sessionIdAttributes.put("auth_level", determinedAuthLevel); + sessionIdAttributes.put("auth_step", Integer.toString(1)); + + // Remove old session parameters from session + if (!appConfiguration.getKeepAuthenticatorAttributesOnAcrChange()) { + authenticationService.clearExternalScriptExtraParameters(sessionIdAttributes); + } + + if (sessionId != null) { + boolean updateResult = updateSession(sessionId, sessionIdAttributes); + if (!updateResult) { + return Constants.RESULT_EXPIRED; + } + } + + facesService.redirectWithExternal(redirectTo, null); + + return Constants.RESULT_SUCCESS; + } + } + + Boolean result = externalAuthenticationService.executeExternalPrepareForStep(customScriptConfiguration, + externalContext.getRequestParameterValuesMap(), this.authStep); + if ((result != null) && result) { + // Store/Update extra parameters in session attributes map + updateExtraParameters(customScriptConfiguration, this.authStep, sessionIdAttributes); + + if (sessionId != null) { + boolean updateResult = updateSession(sessionId, sessionIdAttributes); + if (!updateResult) { + return Constants.RESULT_FAILURE; + } + } + + return Constants.RESULT_SUCCESS; + } else { + return Constants.RESULT_FAILURE; + } + } + + public boolean authenticateBySessionId(String sessionIdString) { + if (StringUtils.isNotBlank(sessionIdString)) { + try { + SessionId sessionId = sessionIdService.getSessionId(sessionIdString); + return authenticateBySessionId(sessionId); + } catch (Exception e) { + logger.trace(e.getMessage(), e); + } + } + + return false; + } + + public boolean authenticateBySessionId(SessionId sessionId) { + if (sessionId == null) { + return false; + } + String p_sessionId = sessionId.getId(); + + logger.trace("authenticateBySessionId, sessionId = '{}', session = '{}', state= '{}'", p_sessionId, sessionId, + sessionId.getState()); + // IMPORTANT : authenticate by session id only if state of session is + // authenticated! + if (SessionIdState.AUTHENTICATED == sessionId.getState()) { + final User user = authenticationService.getUserOrRemoveSession(sessionId); + if (user != null) { + try { + authenticationService.quietLogin(user.getUserId()); + + authenticationService.configureEventUser(sessionId); + } catch (Exception e) { + logger.trace(e.getMessage(), e); + } + + return true; + } + } + + return false; + } + + private void initCustomAuthenticatorVariables(Map sessionIdAttributes) { + if (sessionIdAttributes == null) { + logger.error("Failed to restore attributes from session attributes"); + return; + } + + this.authStep = StringHelper.toInteger(sessionIdAttributes.get("auth_step"), null); + this.authAcr = sessionIdAttributes.get(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE); + } + + private boolean authenticationFailed() { + addMessage(FacesMessage.SEVERITY_ERROR, "login.errorMessage"); + handleScriptError(null); + return false; + } + + private void markAuthStepAsPassed(Map sessionIdAttributes, Integer authStep) { + String key = String.format("auth_step_passed_%d", authStep); + sessionIdAttributes.put(key, Boolean.TRUE.toString()); + } + + private boolean isAuthStepPassed(Map sessionIdAttributes, Integer authStep) { + String key = String.format("auth_step_passed_%d", authStep); + if (sessionIdAttributes.containsKey(key) && Boolean.parseBoolean(sessionIdAttributes.get(key))) { + return true; + } + + return false; + } + + private boolean isPassedPreviousAuthSteps(Map sessionIdAttributes, Integer authStep) { + for (int i = 1; i < authStep; i++) { + boolean isAuthStepPassed = isAuthStepPassed(sessionIdAttributes, i); + if (!isAuthStepPassed) { + return false; + } + } + + return true; + } + + private void updateExtraParameters(CustomScriptConfiguration customScriptConfiguration, int step, + Map sessionIdAttributes) { + List extraParameters = externalAuthenticationService + .executeExternalGetExtraParametersForStep(customScriptConfiguration, step); + authenticationService.updateExtraParameters(sessionIdAttributes, extraParameters); + } + + public void configureSessionClient(Client client) { + authenticationService.configureSessionClient(client); + } + + public void addMessage(Severity severity, String summary) { + String message = languageBean.getMessage(summary); + facesMessages.add(severity, message); + } + + private SessionId getSessionId(HttpServletRequest servletRequest) { + if (this.curentSessionId == null && identity.getSessionId() != null) { + return curentSessionId = identity.getSessionId(); + } + if (this.curentSessionId == null) { + this.curentSessionId = sessionIdService.getSessionId(servletRequest); + } + return this.curentSessionId; + } + + public String getMaskedNumber() { + String result = getFullNumber(); + if (result != null && result.length() > 7) { + String sub = result.substring(4, 6); + result = result.replace(sub, "XX"); + } + return result; + } + + private String getFullNumber() { + String phone = null; + SessionId sessionId = sessionIdService.getSessionId(); + if (sessionId != null) { + if (phone == null || phone.isEmpty()) { + phone = sessionId.getSessionAttributes().get("mobile_number"); + } + if (phone == null || phone.isEmpty()) { + phone = sessionId.getSessionAttributes().get("mobile"); + } + } + return phone == null ? "UNKNOW USER PHONE." : phone; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/auth/MTLSService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/auth/MTLSService.java new file mode 100644 index 00000000..961c62f6 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/auth/MTLSService.java @@ -0,0 +1,138 @@ +package org.gluu.oxauth.auth; + +import com.google.common.base.Strings; +import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.authorize.AuthorizeRequestParam; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.common.Prompt; +import org.gluu.oxauth.model.crypto.AbstractCryptoProvider; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.jwk.JSONWebKey; +import org.gluu.oxauth.model.jwk.JSONWebKeySet; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.model.session.SessionIdState; +import org.gluu.oxauth.model.token.TokenErrorResponseType; +import org.gluu.oxauth.model.util.CertUtils; +import org.gluu.oxauth.service.SessionIdService; +import org.gluu.oxauth.util.ServerUtil; +import org.json.JSONObject; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.inject.Named; +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * @author Yuriy Zabrovarnyy + */ +@ApplicationScoped +public class MTLSService { + + @Inject + private Logger log; + + @Inject + private Authenticator authenticator; + + @Inject + private SessionIdService sessionIdService; + + @Inject + private AbstractCryptoProvider cryptoProvider; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + public boolean processMTLS(HttpServletRequest httpRequest, HttpServletResponse httpResponse, FilterChain filterChain, Client client) throws Exception { + log.debug("Trying to authenticate client {} via {} ...", client.getClientId(), + client.getAuthenticationMethod()); + + final String clientCertAsPem = httpRequest.getHeader("X-ClientCert"); + if (StringUtils.isBlank(clientCertAsPem)) { + log.debug("Client certificate is missed in `X-ClientCert` header, client_id: {}.", client.getClientId()); + return false; + } + + X509Certificate cert = CertUtils.x509CertificateFromPem(clientCertAsPem); + if (cert == null) { + log.debug("Failed to parse client certificate, client_id: {}.", client.getClientId()); + return false; + } + final String cn = CertUtils.getCN(cert); + if (!cn.equals(client.getClientId())) { + log.error("Client certificate CN does not match clientId. Reject call, CN: " + cn + ", clientId: " + client.getClientId()); + throw new WebApplicationException(Response.status(Response.Status.UNAUTHORIZED).entity(errorResponseFactory.getErrorAsJson(TokenErrorResponseType.INVALID_CLIENT, httpRequest.getParameter("state"), "")).build()); + } + + if (client.getAuthenticationMethod() == AuthenticationMethod.TLS_CLIENT_AUTH) { + + final String subjectDn = client.getAttributes().getTlsClientAuthSubjectDn(); + if (StringUtils.isBlank(subjectDn)) { + log.debug("SubjectDN is not set for client {} which is required to authenticate it via `tls_client_auth`.", client.getClientId()); + return false; + } + + // we check only `subjectDn`, the PKI certificate validation is performed by apache/httpd + if (CertUtils.equalsRdn(subjectDn, cert.getSubjectDN().getName())) { + log.debug("Client {} authenticated via `tls_client_auth`.", client.getClientId()); + authenticatedSuccessfully(client, httpRequest); + + filterChain.doFilter(httpRequest, httpResponse); + return true; + } + } + + if (client.getAuthenticationMethod() == AuthenticationMethod.SELF_SIGNED_TLS_CLIENT_AUTH) { // disable it + final PublicKey publicKey = cert.getPublicKey(); + final byte[] encodedKey = publicKey.getEncoded(); + + JSONObject jsonWebKeys = ServerUtil.getJwks(client); + + if (jsonWebKeys == null) { + log.debug("Unable to load json web keys for client: {}, jwks_uri: {}, jks: {}", client.getClientId(), + client.getJwksUri(), client.getJwks()); + return false; + } + + final JSONWebKeySet keySet = JSONWebKeySet.fromJSONObject(jsonWebKeys); + for (JSONWebKey key : keySet.getKeys()) { + if (ArrayUtils.isEquals(encodedKey, + cryptoProvider.getPublicKey(key.getKid(), jsonWebKeys, null).getEncoded())) { + log.debug("Client {} authenticated via `self_signed_tls_client_auth`, matched kid: {}.", + client.getClientId(), key.getKid()); + authenticatedSuccessfully(client, httpRequest); + + filterChain.doFilter(httpRequest, httpResponse); + return true; + } + } + } + return false; + } + + private void authenticatedSuccessfully(Client client, HttpServletRequest httpRequest) { + authenticator.configureSessionClient(client); + + List prompts = Prompt.fromString(httpRequest.getParameter(AuthorizeRequestParam.PROMPT), " "); + if (prompts.contains(Prompt.LOGIN)) { + return; // skip session authentication if we have prompt=login + } + + SessionId sessionIdObject = sessionIdService.getSessionId(httpRequest); + if (sessionIdObject == null || sessionIdObject.getState() != SessionIdState.AUTHENTICATED) { + return; + } + + authenticator.authenticateBySessionId(sessionIdObject); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/auth/SelectAccountAction.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/auth/SelectAccountAction.java new file mode 100644 index 00000000..ac488013 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/auth/SelectAccountAction.java @@ -0,0 +1,399 @@ +package org.gluu.oxauth.auth; + +import java.io.UnsupportedEncodingException; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.RequestScoped; +import javax.faces.context.ExternalContext; +import javax.inject.Inject; +import javax.inject.Named; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang.StringUtils; +import org.gluu.jsf2.service.FacesService; +import org.gluu.oxauth.model.common.User; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.security.Identity; +import org.gluu.oxauth.service.CookieService; +import org.gluu.oxauth.service.RequestParameterService; +import org.gluu.oxauth.service.SessionIdService; +import org.slf4j.Logger; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; + +/** + * @author Yuriy Zabrovarnyy + */ +@RequestScoped +@Named +public class SelectAccountAction { + + private static final String FORM_ID = "selectForm"; + private static final String LOGIN_BUTTON_REF = FORM_ID + ":loginButton"; + + @Inject + private Logger log; + + @Inject + private Identity identity; + + @Inject + private SessionIdService sessionIdService; + + @Inject + private FacesService facesService; + + @Inject + private CookieService cookieService; + + @Inject + private ExternalContext externalContext; + + @Inject + private RequestParameterService requestParameterService; + + @Inject + private Authenticator authenticator; + + + // OAuth 2.0 request parameters + private String scope; + private String responseType; + private String clientId; + private String redirectUri; + private String state; + + // OpenID Connect request parameters + private String responseMode; + private String nonce; + private String display; + private String prompt; + private Integer maxAge; + private String uiLocales; + private String idTokenHint; + private String loginHint; + private String acrValues; + private String amrValues; + private String request; + private String requestUri; + private String codeChallenge; + private String codeChallengeMethod; + private String claims; + private String authReqId; + private String bindingMessage; + private String sessionId; + private String allowedScope; + + private List currentSessions = Lists.newArrayList(); + private String selectedSessionId; + + @PostConstruct + public void prepare() { + currentSessions = Lists.newArrayList(); + Set uids = Sets.newHashSet(); + for (SessionId sessionId : sessionIdService.getCurrentSessions()) { + final User user = sessionIdService.getUser(sessionId); + if (user == null) { + log.error("Failed to get user for session. Skipping it from current_sessions, id: " + sessionId.getId()); + continue; + } + final String uid = StringUtils.isNotBlank(user.getUserId()) ? user.getUserId() : user.getDn(); + if (!currentSessions.contains(sessionId) && !uids.contains(uid)) { + log.trace("User: {}, sessionId: {}", uid, sessionId.getId()); + currentSessions.add(sessionId); + uids.add(uid); + } + } + log.trace("Found {} sessions", currentSessions.size()); + } + + public List getCurrentSessions() { + return currentSessions; + } + + public void select() { + try { + log.debug("Selected account: " + selectedSessionId); + clearSessionIdCookie(); + SessionId selectedSession = currentSessions.stream().filter(s -> s.getId().equals(selectedSessionId)).findAny().get(); + cookieService.createSessionIdCookie(selectedSession, false); + identity.setSessionId(selectedSession); + authenticator.authenticateBySessionId(selectedSessionId); + String uri = buildAuthorizationUrl(); + log.trace("RedirectTo: {}", uri); + facesService.redirectToExternalURL(uri); + } catch (UnsupportedEncodingException e) { + log.error(e.getMessage(), e); + } + } + + public String getName(SessionId sessionId) { + final User user = sessionId.getUser(); + final String displayName = user.getAttribute("displayName"); + if (StringUtils.isNotBlank(displayName)) { + return displayName; + } + if (StringUtils.isNotBlank(displayName)) { + return user.getUserId(); + } + return user.getDn(); + } + + public void login() { + try { + clearSessionIdCookie(); + String uri = buildAuthorizationUrl(); + log.trace("RedirectTo: {}", uri); + facesService.redirectToExternalURL(uri); + } catch (UnsupportedEncodingException e) { + log.error(e.getMessage(), e); + } + } + + public void clearSessionIdCookie() { + final Object response = externalContext.getResponse(); + if (!(response instanceof HttpServletResponse)) { + log.error("Unknown http response."); + return; + } + + HttpServletResponse httpResponse = (HttpServletResponse) response; + cookieService.removeSessionIdCookie(httpResponse); + cookieService.removeOPBrowserStateCookie(httpResponse); + + if (identity != null) { + identity.logout(); + } + log.trace("Removed session_id and opbs cookies."); + } + + private String buildAuthorizationUrl() throws UnsupportedEncodingException { + final HttpServletRequest httpRequest = (HttpServletRequest) externalContext.getRequest(); + return httpRequest.getContextPath() + "/restv1/authorize?" + requestParameterService.parametersAsString(getFilteredParameters()); + } + + private Map getFilteredParameters() { + final Map parameterMap = externalContext.getRequestParameterMap(); + final Map filtered = Maps.newHashMap(); + final String formIdWithColon = FORM_ID + ":"; + + for (Map.Entry entry : parameterMap.entrySet()) { + final String key = entry.getKey(); + if (key.equals("javax.faces.ViewState") || key.equals(FORM_ID) || key.contains(LOGIN_BUTTON_REF)) { + continue; + } + if (key.startsWith(formIdWithColon)) { + filtered.put(StringUtils.removeStart(key, formIdWithColon), entry.getValue()); + continue; + } + filtered.put(StringUtils.removeStart(key, formIdWithColon), entry.getValue()); + } + return filtered; + } + + public String getScope() { + return scope; + } + + public void setScope(String scope) { + this.scope = scope; + } + + public String getResponseType() { + return responseType; + } + + public void setResponseType(String responseType) { + this.responseType = responseType; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getRedirectUri() { + return redirectUri; + } + + public void setRedirectUri(String redirectUri) { + this.redirectUri = redirectUri; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public String getResponseMode() { + return responseMode; + } + + public void setResponseMode(String responseMode) { + this.responseMode = responseMode; + } + + public String getNonce() { + return nonce; + } + + public void setNonce(String nonce) { + this.nonce = nonce; + } + + public String getDisplay() { + return display; + } + + public void setDisplay(String display) { + this.display = display; + } + + public String getPrompt() { + return prompt; + } + + public void setPrompt(String prompt) { + this.prompt = prompt; + } + + public Integer getMaxAge() { + return maxAge; + } + + public void setMaxAge(Integer maxAge) { + this.maxAge = maxAge; + } + + public String getUiLocales() { + return uiLocales; + } + + public void setUiLocales(String uiLocales) { + this.uiLocales = uiLocales; + } + + public String getIdTokenHint() { + return idTokenHint; + } + + public void setIdTokenHint(String idTokenHint) { + this.idTokenHint = idTokenHint; + } + + public String getLoginHint() { + return loginHint; + } + + public void setLoginHint(String loginHint) { + this.loginHint = loginHint; + } + + public String getAcrValues() { + return acrValues; + } + + public void setAcrValues(String acrValues) { + this.acrValues = acrValues; + } + + public String getAmrValues() { + return amrValues; + } + + public void setAmrValues(String amrValues) { + this.amrValues = amrValues; + } + + public String getRequest() { + return request; + } + + public void setRequest(String request) { + this.request = request; + } + + public String getRequestUri() { + return requestUri; + } + + public void setRequestUri(String requestUri) { + this.requestUri = requestUri; + } + + public String getCodeChallenge() { + return codeChallenge; + } + + public void setCodeChallenge(String codeChallenge) { + this.codeChallenge = codeChallenge; + } + + public String getCodeChallengeMethod() { + return codeChallengeMethod; + } + + public void setCodeChallengeMethod(String codeChallengeMethod) { + this.codeChallengeMethod = codeChallengeMethod; + } + + public String getClaims() { + return claims; + } + + public void setClaims(String claims) { + this.claims = claims; + } + + public String getAuthReqId() { + return authReqId; + } + + public void setAuthReqId(String authReqId) { + this.authReqId = authReqId; + } + + public String getSessionId() { + return sessionId; + } + + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + public String getAllowedScope() { + return allowedScope; + } + + public void setAllowedScope(String allowedScope) { + this.allowedScope = allowedScope; + } + + public String getBindingMessage() { + return bindingMessage; + } + + public void setBindingMessage(String bindingMessage) { + this.bindingMessage = bindingMessage; + } + + public String getSelectedSessionId() { + return selectedSessionId; + } + + public void setSelectedSessionId(String selectedSessionId) { + this.selectedSessionId = selectedSessionId; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/AuthorizeAction.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/AuthorizeAction.java new file mode 100644 index 00000000..4f7fe66f --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/AuthorizeAction.java @@ -0,0 +1,951 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.authorize.ws.rs; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.text.StringEscapeUtils; +import org.apache.logging.log4j.util.Strings; +import org.gluu.jsf2.message.FacesMessages; +import org.gluu.jsf2.service.FacesService; +import org.gluu.model.AuthenticationScriptUsageType; +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.oxauth.auth.Authenticator; +import org.gluu.oxauth.i18n.LanguageBean; +import org.gluu.oxauth.model.auth.AuthenticationMode; +import org.gluu.oxauth.model.authorize.*; +import org.gluu.oxauth.model.common.*; +import org.gluu.oxauth.model.config.Constants; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.crypto.AbstractCryptoProvider; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.exception.AcrChangedException; +import org.gluu.oxauth.model.exception.InvalidJwtException; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.ldap.ClientAuthorization; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.model.session.SessionIdState; +import org.gluu.oxauth.model.util.Base64Util; +import org.gluu.oxauth.model.util.JwtUtil; +import org.gluu.oxauth.model.util.Util; +import org.gluu.oxauth.security.Identity; +import org.gluu.oxauth.service.*; +import org.gluu.oxauth.service.ciba.CibaRequestService; +import org.gluu.oxauth.service.external.ExternalAuthenticationService; +import org.gluu.oxauth.service.external.ExternalConsentGatheringService; +import org.gluu.oxauth.service.external.ExternalPostAuthnService; +import org.gluu.oxauth.service.external.context.ExternalPostAuthnContext; +import org.gluu.oxauth.util.ServerUtil; +import org.gluu.persist.exception.EntryPersistenceException; +import org.gluu.service.net.NetworkService; +import org.gluu.util.StringHelper; +import org.gluu.util.locale.LocaleUtil; +import org.slf4j.Logger; + +import javax.enterprise.context.RequestScoped; +import javax.faces.application.FacesMessage; +import javax.faces.context.ExternalContext; +import javax.faces.context.FacesContext; +import javax.inject.Inject; +import javax.inject.Named; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLEncoder; +import java.util.*; + +import static org.gluu.oxauth.service.DeviceAuthorizationService.SESSION_USER_CODE; +/** + * @author Javier Rojas Blum + * @author Yuriy Movchan + * @version March 4, 2020 + */ +@RequestScoped +@Named +public class AuthorizeAction { + + @Inject + private Logger log; + + @Inject + private ClientService clientService; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private SessionIdService sessionIdService; + + @Inject + private RedirectionUriService redirectionUriService; + + @Inject + private ClientAuthorizationsService clientAuthorizationsService; + + @Inject + private ExternalAuthenticationService externalAuthenticationService; + + @Inject + private ExternalConsentGatheringService externalConsentGatheringService; + + @Inject + private AuthenticationMode defaultAuthenticationMode; + + @Inject + private LanguageBean languageBean; + + @Inject + private NetworkService networkService; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private FacesService facesService; + + @Inject + private FacesMessages facesMessages; + + @Inject + private FacesContext facesContext; + + @Inject + private ExternalContext externalContext; + + @Inject + private ConsentGathererService consentGatherer; + + @Inject + private AuthorizeService authorizeService; + + @Inject + private RequestParameterService requestParameterService; + + @Inject + private ScopeChecker scopeChecker; + + @Inject + private ErrorHandlerService errorHandlerService; + + @Inject + private AbstractCryptoProvider cryptoProvider; + + @Inject + private CookieService cookieService; + + @Inject + private Authenticator authenticator; + + @Inject + private AuthenticationService authenticationService; + + @Inject + private ExternalPostAuthnService externalPostAuthnService; + + @Inject + private CibaRequestService cibaRequestService; + + @Inject + private Identity identity; + + @Inject + private AuthorizeRestWebServiceValidator authorizeRestWebServiceValidator; + + // OAuth 2.0 request parameters + private String scope; + private String responseType; + private String clientId; + private String redirectUri; + private String state; + + // OpenID Connect request parameters + private String responseMode; + private String nonce; + private String display; + private String prompt; + private Integer maxAge; + private String uiLocales; + private String idTokenHint; + private String loginHint; + private String acrValues; + private String amrValues; + private String request; + private String requestUri; + private String codeChallenge; + private String codeChallengeMethod; + private String claims; + + // CIBA Request parameter + private String authReqId; + + // custom oxAuth parameters + private String sessionId; + + private String allowedScope; + + public void checkUiLocales() { + List uiLocalesList = null; + if (StringUtils.isNotBlank(uiLocales)) { + uiLocalesList = Util.splittedStringAsList(uiLocales, " "); + + List supportedLocales = languageBean.getSupportedLocales(); + Locale matchingLocale = LocaleUtil.localeMatch(uiLocalesList, supportedLocales); + + if (matchingLocale != null) { + languageBean.setLocale(matchingLocale); + } + } else { + Locale requestedLocale = facesContext.getExternalContext().getRequestLocale(); + if (requestedLocale != null) { + languageBean.setLocale(requestedLocale); + return; + } + + Locale defaultLocale = facesContext.getApplication().getDefaultLocale(); + if (defaultLocale != null) { + languageBean.setLocale(defaultLocale); + } + } + } + + public void checkPermissionGranted() { + try { + checkPermissionGrantedInternal(); + } catch (Exception e) { + log.error("Failed to perform checkPermissionGranted()", e); + permissionDenied(); + } + } + + public void checkPermissionGrantedInternal() throws IOException { + if ((clientId == null) || clientId.isEmpty()) { + log.debug("Permission denied. client_id should be not empty."); + permissionDenied(); + return; + } + + Client client = null; + try { + client = clientService.getClient(clientId); + } catch (EntryPersistenceException ex) { + log.debug("Permission denied. Failed to find client by inum '{}' in LDAP.", clientId, ex); + permissionDenied(); + return; + } + + if (client == null) { + log.debug("Permission denied. Failed to find client_id '{}' in LDAP.", clientId); + permissionDenied(); + return; + } + + // Fix the list of scopes in the authorization page. oxAuth #739 + Set grantedScopes = scopeChecker.checkScopesPolicy(client, scope); + allowedScope = org.gluu.oxauth.model.util.StringUtils.implode(grantedScopes, " "); + + SessionId session = getSession(); + List prompts = Prompt.fromString(prompt, " "); + + try { + redirectUri = authorizeRestWebServiceValidator.validateRedirectUri(client, redirectUri, state, session != null ? session.getSessionAttributes().get(SESSION_USER_CODE) : null, (HttpServletRequest) externalContext.getRequest()); + } catch (WebApplicationException e) { + log.debug(e.getMessage(), e); + permissionDenied(); + return; + } + + try { + session = sessionIdService.assertAuthenticatedSessionCorrespondsToNewRequest(session, acrValues); + } catch (AcrChangedException e) { + log.debug("There is already existing session which has another acr then {}, session: {}", acrValues, session.getId()); + if (e.isForceReAuthentication()) { + session = handleAcrChange(session, prompts); + } else { + log.error("ACR is changed, please provide a supported and enabled acr value"); + permissionDenied(); + return; + } + } + + if (session == null || StringUtils.isBlank(session.getUserDn()) || SessionIdState.AUTHENTICATED != session.getState()) { + Map parameterMap = externalContext.getRequestParameterMap(); + Map requestParameterMap = requestParameterService.getAllowedParameters(parameterMap); + + String redirectTo = "/login.xhtml"; + + boolean useExternalAuthenticator = externalAuthenticationService.isEnabled(AuthenticationScriptUsageType.INTERACTIVE); + if (useExternalAuthenticator) { + List acrValuesList = sessionIdService.acrValuesList(this.acrValues); + if (acrValuesList.isEmpty()) { + acrValuesList = Arrays.asList(defaultAuthenticationMode.getName()); + } + + CustomScriptConfiguration customScriptConfiguration = externalAuthenticationService.determineCustomScriptConfiguration(AuthenticationScriptUsageType.INTERACTIVE, acrValuesList); + + if (customScriptConfiguration == null) { + log.error("Failed to get CustomScriptConfiguration. auth_step: {}, acr_values: {}", 1, this.acrValues); + permissionDenied(); + return; + } + + String acr = customScriptConfiguration.getName(); + + requestParameterMap.put(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, acr); + requestParameterMap.put("auth_step", Integer.toString(1)); + + String tmpRedirectTo = externalAuthenticationService.executeExternalGetPageForStep(customScriptConfiguration, 1); + if (StringHelper.isNotEmpty(tmpRedirectTo)) { + log.trace("Redirect to person authentication login page: {}", tmpRedirectTo); + redirectTo = tmpRedirectTo; + } + } + + // Store Remote IP + String remoteIp = networkService.getRemoteIp(); + requestParameterMap.put(Constants.REMOTE_IP, remoteIp); + + // User Code used in Device Authz flow + if (session != null && session.getSessionAttributes().containsKey(SESSION_USER_CODE)) { + String userCode = session.getSessionAttributes().get(SESSION_USER_CODE); + requestParameterMap.put(SESSION_USER_CODE, userCode); + } + + // Create unauthenticated session + SessionId unauthenticatedSession = sessionIdService.generateUnauthenticatedSessionId(null, new Date(), SessionIdState.UNAUTHENTICATED, requestParameterMap, false); + unauthenticatedSession.setSessionAttributes(requestParameterMap); + unauthenticatedSession.addPermission(clientId, false); + + // Copy ACR script parameters + if (appConfiguration.getKeepAuthenticatorAttributesOnAcrChange()) { + authenticationService.copyAuthenticatorExternalAttributes(session, unauthenticatedSession); + } + + // #1030, fix for flow 4 - transfer previous session permissions to new session + if (session != null && session.getPermissionGrantedMap() != null && session.getPermissionGrantedMap().getPermissionGranted() != null) { + for (Map.Entry entity : session.getPermissionGrantedMap().getPermissionGranted().entrySet()) { + unauthenticatedSession.addPermission(entity.getKey(), entity.getValue()); + } + sessionIdService.remove(session); // #1030, remove previous session + } + + boolean persisted = sessionIdService.persistSessionId(unauthenticatedSession, !prompts.contains(Prompt.NONE)); // always persist is prompt is not none + if (persisted && log.isTraceEnabled()) { + log.trace("Session '{}' persisted to LDAP", unauthenticatedSession.getId()); + } + + this.sessionId = unauthenticatedSession.getId(); + cookieService.createSessionIdCookie(unauthenticatedSession, false); + cookieService.creatRpOriginIdCookie(redirectUri); + identity.setSessionId(unauthenticatedSession); + + Map loginParameters = new HashMap(); + if (requestParameterMap.containsKey(AuthorizeRequestParam.LOGIN_HINT)) { + loginParameters.put(AuthorizeRequestParam.LOGIN_HINT, requestParameterMap.get(AuthorizeRequestParam.LOGIN_HINT)); + } + + boolean enableRedirect = StringHelper.toBoolean(System.getProperty("gluu.enable-redirect", "false"), false); + if (!enableRedirect && redirectTo.toLowerCase().endsWith("xhtml")) { + if (redirectTo.toLowerCase().endsWith("postlogin.xhtml")) { + authenticator.authenticateWithOutcome(); + } else { + authenticator.prepareAuthenticationForStep(unauthenticatedSession); + facesService.renderView(redirectTo); + } + } else { + facesService.redirectWithExternal(redirectTo, loginParameters); + } + + return; + } + + String userCode = session.getSessionAttributes().get(SESSION_USER_CODE); + if (StringUtils.isBlank(userCode) && StringUtils.isBlank(redirectionUriService.validateRedirectionUri(clientId, redirectUri))) { + ExternalContext externalContext = facesContext.getExternalContext(); + externalContext.setResponseStatus(HttpServletResponse.SC_BAD_REQUEST); + externalContext.setResponseContentType(MediaType.APPLICATION_JSON); + externalContext.getResponseOutputWriter().write(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.INVALID_REQUEST_REDIRECT_URI, state, "")); + facesContext.responseComplete(); + } + + if (log.isTraceEnabled()) { + log.trace("checkPermissionGranted, userDn = " + session.getUserDn()); + } + + if (prompts.contains(Prompt.SELECT_ACCOUNT)) { + Map requestParameterMap = requestParameterService.getAllowedParameters(externalContext.getRequestParameterMap()); + facesService.redirect("/selectAccount.xhtml", requestParameterMap); + return; + } + + if (prompts.contains(Prompt.NONE) && prompts.size() > 1) { + invalidRequest(); + return; + } + + ExternalPostAuthnContext postAuthnContext = new ExternalPostAuthnContext(client, session, (HttpServletRequest)externalContext.getRequest(), (HttpServletResponse) externalContext.getResponse()); + final boolean forceAuthorization = externalPostAuthnService.externalForceAuthorization(client, postAuthnContext); + + final boolean hasConsentPrompt = prompts.contains(Prompt.CONSENT); + if (!hasConsentPrompt && !forceAuthorization) { + if (appConfiguration.getTrustedClientEnabled() && client.getTrustedClient()) { + // if trusted client = true, then skip authorization page and grant access directly + permissionGranted(session); + return; + } else if (ServerUtil.isTrue(appConfiguration.getSkipAuthorizationForOpenIdScopeAndPairwiseId()) + && SubjectType.PAIRWISE.toString().equals(client.getSubjectType()) && hasOnlyOpenidScope()) { + // If a client has only openid scope and pairwise id, person should not have to authorize. oxAuth-743 + permissionGranted(session); + return; + } + + final User user = sessionIdService.getUser(session); + ClientAuthorization clientAuthorization = clientAuthorizationsService.find( + user.getAttribute("inum"), + client.getClientId()); + if (clientAuthorization != null && clientAuthorization.getScopes() != null && + Arrays.asList(clientAuthorization.getScopes()).containsAll( + org.gluu.oxauth.model.util.StringUtils.spaceSeparatedToList(scope))) { + permissionGranted(session); + return; + } + } + + if (externalConsentGatheringService.isEnabled()) { + if (consentGatherer.isConsentGathered()) { + log.trace("Consent-gathered flow passed successfully"); + permissionGranted(session); + return; + } + + log.trace("Starting external consent-gathering flow"); + + boolean result = consentGatherer.configure(session.getUserDn(), clientId, state); + if (!result) { + log.error("Failed to initialize external consent-gathering flow."); + permissionDenied(); + return; + } + } + } + + private SessionId handleAcrChange(SessionId session, List prompts) { + if (session != null) { + if (session.getState() == SessionIdState.AUTHENTICATED) { + + if (!prompts.contains(Prompt.LOGIN)) { + prompts.add(Prompt.LOGIN); + } + session.getSessionAttributes().put("prompt", org.gluu.oxauth.model.util.StringUtils.implode(prompts, " ")); + session.setState(SessionIdState.UNAUTHENTICATED); + + // Update Remote IP + String remoteIp = networkService.getRemoteIp(); + session.getSessionAttributes().put(Constants.REMOTE_IP, remoteIp); + + final boolean isSessionPersisted = sessionIdService.reinitLogin(session, false); + if (!isSessionPersisted) { + sessionIdService.updateSessionId(session); + } + } + } + return session; + } + + private SessionId getSession() { + return authorizeService.getSession(sessionId); + } + + public List getScopes() { + return authorizeService.getScopes(allowedScope); + } + + public List getRequestedClaims() { + Set result = new HashSet(); + String requestJwt = request; + + if (StringUtils.isBlank(requestJwt) && StringUtils.isNotBlank(requestUri)) { + try { + URI reqUri = new URI(requestUri); + String reqUriHash = reqUri.getFragment(); + String reqUriWithoutFragment = reqUri.getScheme() + ":" + reqUri.getSchemeSpecificPart(); + + javax.ws.rs.client.Client clientRequest = ClientBuilder.newClient(); + try { + Response clientResponse = clientRequest.target(reqUriWithoutFragment).request().buildGet().invoke(); + clientRequest.close(); + + int status = clientResponse.getStatus(); + if (status == 200) { + String entity = clientResponse.readEntity(String.class); + + if (StringUtils.isBlank(reqUriHash)) { + requestJwt = entity; + } else { + String hash = Base64Util.base64urlencode(JwtUtil.getMessageDigestSHA256(entity)); + if (StringUtils.equals(reqUriHash, hash)) { + requestJwt = entity; + } + } + } + } finally { + clientRequest.close(); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + + if (StringUtils.isNotBlank(requestJwt)) { + try { + Client client = clientService.getClient(clientId); + + if (client != null) { + JwtAuthorizationRequest jwtAuthorizationRequest = new JwtAuthorizationRequest(appConfiguration, cryptoProvider, request, client); + + if (jwtAuthorizationRequest.getUserInfoMember() != null) { + for (Claim claim : jwtAuthorizationRequest.getUserInfoMember().getClaims()) { + result.add(claim.getName()); + } + } + + if (jwtAuthorizationRequest.getIdTokenMember() != null) { + for (Claim claim : jwtAuthorizationRequest.getIdTokenMember().getClaims()) { + result.add(claim.getName()); + } + } + } + } catch (EntryPersistenceException | InvalidJwtException e) { + log.error(e.getMessage(), e); + } + } + + return new ArrayList<>(result); + } + + /** + * Returns the scope of the access request. + * + * @return The scope of the access request. + */ + public String getScope() { + return scope; + } + + /** + * Sets the scope of the access request. + * + * @param scope The scope of the access request. + */ + public void setScope(String scope) { + this.scope = scope; + } + + /** + * Returns the response type: code for requesting an authorization code (authorization code grant) or + * token for requesting an access token (implicit grant). + * + * @return The response type. + */ + public String getResponseType() { + return responseType; + } + + /** + * Sets the response type. + * + * @param responseType The response type. + */ + public void setResponseType(String responseType) { + this.responseType = responseType; + } + + /** + * Returns the client identifier. + * + * @return The client identifier. + */ + public String getClientId() { + return clientId; + } + + /** + * Sets the client identifier. + * + * @param clientId The client identifier. + */ + public void setClientId(String clientId) { + this.clientId = clientId; + } + + /** + * Returns the redirection URI. + * + * @return The redirection URI. + */ + public String getRedirectUri() { + return redirectUri; + } + + /** + * Sets the redirection URI. + * + * @param redirectUri The redirection URI. + */ + public void setRedirectUri(String redirectUri) { + this.redirectUri = redirectUri; + } + + /** + * Returns an opaque value used by the client to maintain state between the request and callback. The authorization + * server includes this value when redirecting the user-agent back to the client. The parameter should be used for + * preventing cross-site request forgery. + * + * @return The state between the request and callback. + */ + public String getState() { + return state; + } + + /** + * Sets the state between the request and callback. + * + * @param state The state between the request and callback. + */ + public void setState(String state) { + this.state = state; + } + + /** + * Returns the mechanism to be used for returning parameters from the Authorization Endpoint. + * + * @return The response mode. + */ + public String getResponseMode() { + return responseMode; + } + + /** + * Sets the mechanism to be used for returning parameters from the Authorization Endpoint. + * + * @param responseMode The response mode. + */ + public void setResponseMode(String responseMode) { + this.responseMode = responseMode; + } + + /** + * Return a string value used to associate a user agent session with an ID Token, and to mitigate replay attacks. + * + * @return The nonce value. + */ + public String getNonce() { + return nonce; + } + + /** + * Sets a string value used to associate a user agent session with an ID Token, and to mitigate replay attacks. + * + * @param nonce The nonce value. + */ + public void setNonce(String nonce) { + this.nonce = nonce; + } + + /** + * Returns an ASCII string value that specifies how the Authorization Server displays the authentication page + * to the End-User. + * + * @return The display value. + */ + public String getDisplay() { + return display; + } + + /** + * Sets an ASCII string value that specifies how the Authorization Server displays the authentication page + * to the End-User. + * + * @param display The display value + */ + public void setDisplay(String display) { + this.display = display; + } + + /** + * Returns a space delimited list of ASCII strings that can contain the values + * login, consent, select_account, and none. + * + * @return A list of prompt options. + */ + public String getPrompt() { + return prompt; + } + + /** + * Sets a space delimited list of ASCII strings that can contain the values + * login, consent, select_account, and none. + * + * @param prompt A list of prompt options. + */ + public void setPrompt(String prompt) { + this.prompt = prompt; + } + + public Integer getMaxAge() { + return maxAge; + } + + public void setMaxAge(Integer maxAge) { + this.maxAge = maxAge; + } + + public String getUiLocales() { + return uiLocales; + } + + public void setUiLocales(String uiLocales) { + this.uiLocales = uiLocales; + } + + public String getIdTokenHint() { + return idTokenHint; + } + + public void setIdTokenHint(String idTokenHint) { + this.idTokenHint = idTokenHint; + } + + public String getLoginHint() { + return loginHint; + } + + public void setLoginHint(String loginHint) { + this.loginHint = StringEscapeUtils.escapeEcmaScript(loginHint); + } + + public String getAcrValues() { + return acrValues; + } + + public void setAcrValues(String acrValues) { + this.acrValues = acrValues; + } + + public String getAmrValues() { + return amrValues; + } + + public void setAmrValues(String amrValues) { + this.amrValues = amrValues; + } + + /** + * Returns a JWT encoded OpenID Request Object. + * + * @return A JWT encoded OpenID Request Object. + */ + public String getRequest() { + return request; + } + + /** + * Sets a JWT encoded OpenID Request Object. + * + * @param request A JWT encoded OpenID Request Object. + */ + public void setRequest(String request) { + this.request = request; + } + + /** + * Returns an URL that points to an OpenID Request Object. + * + * @return An URL that points to an OpenID Request Object. + */ + public String getRequestUri() { + return requestUri; + } + + /** + * Sets an URL that points to an OpenID Request Object. + * + * @param requestUri An URL that points to an OpenID Request Object. + */ + public void setRequestUri(String requestUri) { + this.requestUri = requestUri; + } + + public String getSessionId() { + return sessionId; + } + + public void setSessionId(String p_sessionId) { + sessionId = p_sessionId; + } + + public void permissionGranted() { + final SessionId session = getSession(); + permissionGranted(session); + } + + public void permissionGranted(SessionId session) { + final HttpServletRequest httpRequest = (HttpServletRequest) externalContext.getRequest(); + authorizeService.permissionGranted(httpRequest, session); + } + + public void permissionDenied() { + final SessionId session = getSession(); + authorizeService.permissionDenied(session); + } + + private void authenticationFailedSessionInvalid() { + facesMessages.add(FacesMessage.SEVERITY_ERROR, "login.errorSessionInvalidMessage"); + facesService.redirect("/error.xhtml"); + } + + public void invalidRequest() { + log.trace("invalidRequest"); + StringBuilder sb = new StringBuilder(); + + sb.append(redirectUri); + if (redirectUri != null && redirectUri.contains("?")) { + sb.append("&"); + } else { + sb.append("?"); + } + sb.append(errorResponseFactory.getErrorAsQueryString(AuthorizeErrorResponseType.INVALID_REQUEST, + getState())); + + facesService.redirectToExternalURL(sb.toString()); + } + + public void consentRequired() { + StringBuilder sb = new StringBuilder(); + + sb.append(redirectUri); + if (redirectUri != null && redirectUri.contains("?")) { + sb.append("&"); + } else { + sb.append("?"); + } + sb.append(errorResponseFactory.getErrorAsQueryString(AuthorizeErrorResponseType.CONSENT_REQUIRED, getState())); + + facesService.redirectToExternalURL(sb.toString()); + } + + public String getCodeChallenge() { + return codeChallenge; + } + + public void setCodeChallenge(String codeChallenge) { + this.codeChallenge = codeChallenge; + } + + public String getCodeChallengeMethod() { + return codeChallengeMethod; + } + + public void setCodeChallengeMethod(String codeChallengeMethod) { + this.codeChallengeMethod = codeChallengeMethod; + } + + public String getClaims() { + return claims; + } + + public void setClaims(String claims) { + this.claims = claims; + } + + public String getAuthReqId() { + return authReqId; + } + + public void setAuthReqId(String authReqId) { + this.authReqId = authReqId; + } + + public String getBindingMessage() { + String bindingMessage = null; + + if (Strings.isNotBlank(getAuthReqId())) { + final CibaRequestCacheControl cibaRequestCacheControl = cibaRequestService.getCibaRequest(authReqId); + + if (cibaRequestCacheControl != null) { + bindingMessage = cibaRequestCacheControl.getBindingMessage(); + } + } + + return bindingMessage; + } + + public String encodeParameters(String url, Map parameters) { + if (parameters.isEmpty()) return url; + + StringBuilder builder = new StringBuilder(url); + for (Map.Entry param : parameters.entrySet()) { + String parameterName = param.getKey(); + if (!containsParameter(url, parameterName)) { + Object parameterValue = param.getValue(); + if (parameterValue instanceof Iterable) { + for (Object value : (Iterable) parameterValue) { + builder.append('&') + .append(parameterName) + .append('='); + if (value != null) { + builder.append(encode(value)); + } + } + } else { + builder.append('&') + .append(parameterName) + .append('='); + if (parameterValue != null) { + builder.append(encode(parameterValue)); + } + } + } + } + if (url.indexOf('?') < 0) { + builder.setCharAt(url.length(), '?'); + } + return builder.toString(); + } + + private boolean containsParameter(String url, String parameterName) { + return url.indexOf('?' + parameterName + '=') > 0 || + url.indexOf('&' + parameterName + '=') > 0; + } + + private String encode(Object value) { + try { + return URLEncoder.encode(String.valueOf(value), "UTF-8"); + } catch (UnsupportedEncodingException iee) { + throw new RuntimeException(iee); + } + } + + private boolean hasOnlyOpenidScope() { + return getScopes() != null && getScopes().size() == 1 && getScopes().get(0).getId().equals("openid"); + } + + protected void handleSessionInvalid() { + errorHandlerService.handleError(Authenticator.INVALID_SESSION_MESSAGE, AuthorizeErrorResponseType.AUTHENTICATION_SESSION_INVALID, "Create authorization request to start new authentication session."); + } + + + protected void handleScriptError(String facesMessageId) { + errorHandlerService.handleError(Authenticator.AUTHENTICATION_ERROR_MESSAGE, AuthorizeErrorResponseType.INVALID_AUTHENTICATION_METHOD, "Contact administrator to fix specific ACR method issue."); + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/AuthorizeRestWebService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/AuthorizeRestWebService.java new file mode 100644 index 00000000..fda492f4 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/AuthorizeRestWebService.java @@ -0,0 +1,194 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.authorize.ws.rs; + +import org.gluu.oxauth.model.authorize.AuthorizeRequestParam; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; + +/** + *

+ * Provides interface for request authorization through REST web services. + *

+ *

+ * An authorization grant is a credential representing the resource owner's + * authorization (to access its protected resources) used by the client to + * obtain an access token. + *

+ * + * @author Javier Rojas Blum + * @version October 7, 2019 + */ +public interface AuthorizeRestWebService { + + /** + * Requests authorization. + * + * @param scope The scope of the access request. + * @param responseType The response type informs the authorization server of the desired response type: + * code, token, id_token + * a combination of them. The response type parameter is mandatory. + * @param clientId The client identifier. + * @param redirectUri Redirection URI + * @param state An opaque value used by the client to maintain state between + * the request and callback. The authorization server includes + * this value when redirecting the user-agent back to the client. + * The parameter should be used for preventing cross-site request + * forgery. + * @param responseMode Informs the Authorization Server of the mechanism to be used for returning parameters + * from the Authorization Endpoint. This use of this parameter is NOT RECOMMENDED when the + * Response Mode that would be requested is the default mode specified for the Response Type. + * @param nonce A string value used to associate a user agent session with an ID Token, + * and to mitigate replay attacks. + * @param display An ASCII string value that specifies how the Authorization Server displays the + * authentication page to the End-User. + * @param prompt A space delimited list of ASCII strings that can contain the values login, consent, + * select_account, and none. + * @param maxAge Maximum Authentication Age. Specifies the allowable elapsed time in seconds since the + * last time the End-User was actively authenticated. + * @param uiLocales End-User's preferred languages and scripts for the user interface, represented as a + * space-separated list of BCP47 [RFC5646] language tag values, ordered by preference. + * @param idTokenHint Previously issued ID Token passed to the Authorization Server as a hint about the + * End-User's current or past authenticated session with the Client. + * @param loginHint Hint to the Authorization Server about the login identifier the End-User might use to + * log in (if necessary). + * @param acrValues Requested Authentication Context Class Reference values. Space-separated string that + * specifies the acr values that the Authorization Server is being requested to use for + * processing this Authentication Request, with the values appearing in order of preference. + * @param amrValues Requested Authentication Methods References. JSON array of strings that are identifiers + * for authentication methods used in the authentication. For instance, values might indicate + * that both password and OTP authentication methods were used. The definition of particular + * values to be used in the amr Claim is beyond the scope of this specification.The amr value + * is an array of case sensitive strings. + * @param request A JWT encoded OpenID Request Object. + * @param requestUri An URL that points to an OpenID Request Object. + * @param requestSessionId request session id + * @param sessionId session id + * @param originHeaders + * @param codeChallenge PKCE code challenge + * @param codeChallengeMethod PKCE code challenge method + * @param authReqId A unique identifier to identify the CIBA authentication request made by the Client. + * @param httpRequest http request + * @param securityContext An injectable interface that provides access to security + * related information. + * @return

+ * When the responseType parameter is set to code: + *

+ *

+ * If the resource owner grants the access request, the + * authorization server issues an authorization code and delivers it + * to the client by adding the following parameters to the query + * component of the redirection URI using the + * application/x-www-form-urlencoded format: + *

+ *
+ *
code
+ *
+ * The authorization code generated by the authorization server.
+ *
state
+ *
+ * If the state parameter was present in the client authorization + * request. The exact value received from the client.
+ *
+ *

+ *

+ * When the responseType parameter is set to token: + *

+ *

+ * If the resource owner grants the access request, the + * authorization server issues an access token and delivers it to + * the client by adding the following parameters to the fragment + * component of the redirection URI using the + * application/x-www-form-urlencoded format. + *

+ *
+ *
access_token
+ *
The access token issued by the authorization server.
+ *
token_type
+ *
The type of the token issued. Value is case insensitive.
+ *
expires_in
+ *
The lifetime in seconds of the access token. For example, the + * value 3600 denotes that the access token will expire in one hour + * from the time the response was generated.
+ *
scope
+ *
The scope of the access token.
+ *
state
+ *
If the state parameter was present in the client + * authorization request. The exact value received from the client.
+ *
+ */ + @GET + @Path("/authorize") + @Produces({MediaType.TEXT_PLAIN}) + Response requestAuthorizationGet( + @QueryParam("scope") String scope, + @QueryParam("response_type") String responseType, + @QueryParam("client_id") String clientId, + @QueryParam("redirect_uri") String redirectUri, + @QueryParam("state") String state, + @QueryParam("response_mode") String responseMode, + @QueryParam("nonce") String nonce, + @QueryParam("display") String display, + @QueryParam("prompt") String prompt, + @QueryParam("max_age") Integer maxAge, + @QueryParam("ui_locales") String uiLocales, + @QueryParam("id_token_hint") String idTokenHint, + @QueryParam("login_hint") String loginHint, + @QueryParam("acr_values") String acrValues, + @QueryParam("amr_values") String amrValues, + @QueryParam("request") String request, + @QueryParam("request_uri") String requestUri, + @QueryParam("request_session_id") String requestSessionId, + @QueryParam("session_id") String sessionId, + @QueryParam("origin_headers") String originHeaders, + @QueryParam("code_challenge") String codeChallenge, + @QueryParam("code_challenge_method") String codeChallengeMethod, + @QueryParam(AuthorizeRequestParam.CUSTOM_RESPONSE_HEADERS) String customResponseHeaders, + @QueryParam("claims") String claims, + @QueryParam("auth_req_id") String authReqId, + @Context HttpServletRequest httpRequest, + @Context HttpServletResponse httpResponse, + @Context SecurityContext securityContext); + + @POST + @Path("/authorize") + @Produces({MediaType.TEXT_PLAIN}) + Response requestAuthorizationPost( + @FormParam("scope") String scope, + @FormParam("response_type") String responseType, + @FormParam("client_id") String clientId, + @FormParam("redirect_uri") String redirectUri, + @FormParam("state") String state, + @QueryParam("response_mode") String responseMode, + @FormParam("nonce") String nonce, + @FormParam("display") String display, + @FormParam("prompt") String prompt, + @FormParam("max_age") Integer maxAge, + @FormParam("ui_locales") String uiLocales, + @FormParam("id_token_hint") String idTokenHint, + @FormParam("login_hint") String loginHint, + @FormParam("acr_values") String acrValues, + @FormParam("amr_values") String amrValues, + @FormParam("request") String request, + @FormParam("request_uri") String requestUri, + @FormParam("request_session_id") String requestSessionId, + @FormParam("session_id") String sessionId, + @FormParam("origin_headers") String originHeaders, + @QueryParam("code_challenge") String codeChallenge, + @QueryParam("code_challenge_method") String codeChallengeMethod, + @QueryParam(AuthorizeRequestParam.CUSTOM_RESPONSE_HEADERS) String customResponseHeaders, + @QueryParam("claims") String claims, + @Context HttpServletRequest httpRequest, + @Context HttpServletResponse httpResponse, + @Context SecurityContext securityContext); +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/AuthorizeRestWebServiceImpl.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/AuthorizeRestWebServiceImpl.java new file mode 100644 index 00000000..930cca1c --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/AuthorizeRestWebServiceImpl.java @@ -0,0 +1,1056 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.authorize.ws.rs; + +import com.google.common.base.Function; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.audit.ApplicationAuditLogger; +import org.gluu.oxauth.ciba.CIBAPingCallbackService; +import org.gluu.oxauth.ciba.CIBAPushTokenDeliveryService; +import org.gluu.oxauth.model.audit.Action; +import org.gluu.oxauth.model.audit.OAuth2AuditLog; +import org.gluu.oxauth.model.authorize.*; +import org.gluu.oxauth.model.common.*; +import org.gluu.oxauth.model.config.ConfigurationFactory; +import org.gluu.oxauth.model.config.Constants; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.crypto.AbstractCryptoProvider; +import org.gluu.oxauth.model.crypto.binding.TokenBindingMessage; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.exception.AcrChangedException; +import org.gluu.oxauth.model.exception.InvalidSessionStateException; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.ldap.ClientAuthorization; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.model.session.SessionIdState; +import org.gluu.oxauth.model.token.JsonWebResponse; +import org.gluu.oxauth.model.token.JwrService; +import org.gluu.oxauth.model.util.Util; +import org.gluu.oxauth.security.Identity; +import org.gluu.oxauth.service.*; +import org.gluu.oxauth.service.ciba.CibaRequestService; +import org.gluu.oxauth.service.external.ExternalPostAuthnService; +import org.gluu.oxauth.service.external.ExternalResourceOwnerPasswordCredentialsService; +import org.gluu.oxauth.service.external.ExternalUpdateTokenService; +import org.gluu.oxauth.service.external.context.ExternalPostAuthnContext; +import org.gluu.oxauth.service.external.context.ExternalResourceOwnerPasswordCredentialsContext; +import org.gluu.oxauth.service.external.context.ExternalUpdateTokenContext; +import org.gluu.oxauth.service.external.session.SessionEvent; +import org.gluu.oxauth.service.external.session.SessionEventType; +import org.gluu.oxauth.util.QueryStringDecoder; +import org.gluu.oxauth.util.RedirectUri; +import org.gluu.oxauth.util.RedirectUtil; +import org.gluu.oxauth.util.ServerUtil; +import org.gluu.persist.exception.EntryPersistenceException; +import org.gluu.util.StringHelper; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.HttpMethod; +import javax.ws.rs.Path; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.ResponseBuilder; +import javax.ws.rs.core.SecurityContext; +import java.net.URI; +import java.util.*; +import java.util.Map.Entry; + +import static org.apache.commons.lang.BooleanUtils.isTrue; +import static org.gluu.oxauth.model.util.StringUtils.implode; + +/** + * Implementation for request authorization through REST web services. + * + * @author Javier Rojas Blum + * @version May 9, 2020 + */ +@Path("/") +public class AuthorizeRestWebServiceImpl implements AuthorizeRestWebService { + + @Inject + private Logger log; + + @Inject + private ApplicationAuditLogger applicationAuditLogger; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private AuthorizationGrantList authorizationGrantList; + + @Inject + private ClientService clientService; + + @Inject + private UserService userService; + + @Inject + private Identity identity; + + @Inject + private AuthenticationFilterService authenticationFilterService; + + @Inject + private SessionIdService sessionIdService; + + @Inject CookieService cookieService; + + @Inject + private ScopeChecker scopeChecker; + + @Inject + private ClientAuthorizationsService clientAuthorizationsService; + + @Inject + private RequestParameterService requestParameterService; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private ConfigurationFactory ÑonfigurationFactory; + + @Inject + private AbstractCryptoProvider cryptoProvider; + + @Inject + private AuthorizeRestWebServiceValidator authorizeRestWebServiceValidator; + + @Inject + private CIBAPushTokenDeliveryService cibaPushTokenDeliveryService; + + @Inject + private CIBAPingCallbackService cibaPingCallbackService; + + @Inject + private ExternalPostAuthnService externalPostAuthnService; + + @Inject + private CibaRequestService cibaRequestService; + + @Inject + private DeviceAuthorizationService deviceAuthorizationService; + + @Inject + private AttributeService attributeService; + + @Inject + private ExternalUpdateTokenService externalUpdateTokenService; + + @Inject + private ExternalResourceOwnerPasswordCredentialsService externalResourceOwnerPasswordCredentialsService; + + @Context + private HttpServletRequest servletRequest; + + @Override + public Response requestAuthorizationGet( + String scope, String responseType, String clientId, String redirectUri, String state, String responseMode, + String nonce, String display, String prompt, Integer maxAge, String uiLocales, String idTokenHint, + String loginHint, String acrValues, String amrValues, String request, String requestUri, + String requestSessionId, String sessionId, String originHeaders, + String codeChallenge, String codeChallengeMethod, String customResponseHeaders, String claims, String authReqId, + HttpServletRequest httpRequest, HttpServletResponse httpResponse, SecurityContext securityContext) { + return requestAuthorization(scope, responseType, clientId, redirectUri, state, responseMode, nonce, display, + prompt, maxAge, uiLocales, idTokenHint, loginHint, acrValues, amrValues, request, requestUri, + requestSessionId, sessionId, HttpMethod.GET, originHeaders, codeChallenge, codeChallengeMethod, + customResponseHeaders, claims, authReqId, httpRequest, httpResponse, securityContext); + } + + @Override + public Response requestAuthorizationPost( + String scope, String responseType, String clientId, String redirectUri, String state, String responseMode, + String nonce, String display, String prompt, Integer maxAge, String uiLocales, String idTokenHint, + String loginHint, String acrValues, String amrValues, String request, String requestUri, + String requestSessionId, String sessionId, String originHeaders, + String codeChallenge, String codeChallengeMethod, String customResponseHeaders, String claims, + HttpServletRequest httpRequest, HttpServletResponse httpResponse, SecurityContext securityContext) { + return requestAuthorization(scope, responseType, clientId, redirectUri, state, responseMode, nonce, display, + prompt, maxAge, uiLocales, idTokenHint, loginHint, acrValues, amrValues, request, requestUri, + requestSessionId, sessionId, HttpMethod.POST, originHeaders, codeChallenge, codeChallengeMethod, + customResponseHeaders, claims, null, httpRequest, httpResponse, securityContext); + } + + private Response requestAuthorization( + String scope, String responseType, String clientId, String redirectUri, String state, String respMode, + String nonce, String display, String prompt, Integer maxAge, String uiLocalesStr, String idTokenHint, + String loginHint, String acrValuesStr, String amrValuesStr, String request, String requestUri, String requestSessionId, + String sessionId, String method, String originHeaders, String codeChallenge, String codeChallengeMethod, + String customRespHeaders, String claims, String authReqId, + HttpServletRequest httpRequest, HttpServletResponse httpResponse, SecurityContext securityContext) { + scope = ServerUtil.urlDecode(scope); // it may be encoded in uma case + requestUri = ServerUtil.urlDecode(requestUri); // requestUri usually contains encoded characters. + + String tokenBindingHeader = httpRequest.getHeader("Sec-Token-Binding"); + + OAuth2AuditLog oAuth2AuditLog = new OAuth2AuditLog(ServerUtil.getIpAddress(httpRequest), Action.USER_AUTHORIZATION); + oAuth2AuditLog.setClientId(clientId); + oAuth2AuditLog.setScope(scope); + + // ATTENTION : please do not add more parameter in this debug method because it will not work with Seam 2.2.2.Final , + // there is limit of 10 parameters (hardcoded), see: org.jboss.seam.core.Interpolator#interpolate + log.debug("Attempting to request authorization: " + + "responseType = {}, clientId = {}, scope = {}, redirectUri = {}, nonce = {}, " + + "state = {}, request = {}, isSecure = {}, requestSessionId = {}, sessionId = {}", + responseType, clientId, scope, redirectUri, nonce, + state, request, securityContext.isSecure(), requestSessionId, sessionId); + + log.debug("Attempting to request authorization: " + + "acrValues = {}, amrValues = {}, originHeaders = {}, codeChallenge = {}, codeChallengeMethod = {}, " + + "customRespHeaders = {}, claims = {}, tokenBindingHeader = {}", + acrValuesStr, amrValuesStr, originHeaders, codeChallenge, codeChallengeMethod, customRespHeaders, claims, tokenBindingHeader); + + ResponseBuilder builder = Response.ok(); + + List uiLocales = Util.splittedStringAsList(uiLocalesStr, " "); + List responseTypes = ResponseType.fromString(responseType, " "); + List prompts = Prompt.fromString(prompt, " "); + List acrValues = Util.splittedStringAsList(acrValuesStr, " "); + List amrValues = Util.splittedStringAsList(amrValuesStr, " "); + ResponseMode responseMode = ResponseMode.getByValue(respMode); + + Map customParameters = requestParameterService.getCustomParameters( + QueryStringDecoder.decode(httpRequest.getQueryString(),true)); + if (HttpMethod.POST.endsWith(method)) { + requestParameterService.addCustomParameters(httpRequest, customParameters); + } + + SessionId sessionUser = identity.getSessionId(); + User user = sessionIdService.getUser(sessionUser); + + try { + Map customResponseHeaders = Util.jsonObjectArrayStringAsMap(customRespHeaders); + + updateSessionForROPC(httpRequest, sessionUser); + + Client client = authorizeRestWebServiceValidator.validateClient(clientId, state); + String deviceAuthzUserCode = deviceAuthorizationService.getUserCodeFromSession(httpRequest); + redirectUri = authorizeRestWebServiceValidator.validateRedirectUri(client, redirectUri, state, deviceAuthzUserCode, httpRequest); + log.trace("Validated URI: {}", redirectUri); + checkAcrChanged(acrValuesStr, prompts, sessionUser); // check after redirect uri is validated + + RedirectUriResponse redirectUriResponse = new RedirectUriResponse(new RedirectUri(redirectUri, responseTypes, responseMode), state, httpRequest, errorResponseFactory); + redirectUriResponse.setFapiCompatible(appConfiguration.getFapiCompatibility()); + + Set scopes = scopeChecker.checkScopesPolicy(client, scope); + boolean isPromptFromJwt = false; + + authorizeRestWebServiceValidator.validateRequestParameterSupported(request, state); + authorizeRestWebServiceValidator.validateRequestUriParameterSupported(requestUri, state); + + JwtAuthorizationRequest jwtRequest = null; + if (StringUtils.isNotBlank(request) || StringUtils.isNotBlank(requestUri)) { + try { + jwtRequest = JwtAuthorizationRequest.createJwtRequest(request, requestUri, client, redirectUriResponse, cryptoProvider, appConfiguration); + + if (jwtRequest == null) { + throw createInvalidJwtRequestException(redirectUriResponse, "Failed to parse jwt."); + } + if (StringUtils.isNotBlank(jwtRequest.getState())) { + state = jwtRequest.getState(); + redirectUriResponse.setState(state); + } + if (appConfiguration.getFapiCompatibility() && StringUtils.isBlank(jwtRequest.getState())) { + state = ""; // #1250 - FAPI : discard state if in JWT we don't have state + redirectUriResponse.setState(""); + } + + authorizeRestWebServiceValidator.validateRequestObject(jwtRequest, redirectUriResponse); + + // MUST be equal + if (!jwtRequest.getResponseTypes().containsAll(responseTypes) || !responseTypes.containsAll(jwtRequest.getResponseTypes())) { + throw createInvalidJwtRequestException(redirectUriResponse, "The responseType parameter is not the same in the JWT"); + } + if (StringUtils.isBlank(jwtRequest.getClientId()) || !jwtRequest.getClientId().equals(clientId)) { + throw createInvalidJwtRequestException(redirectUriResponse, "The clientId parameter is not the same in the JWT"); + } + + // JWT wins + if (!jwtRequest.getScopes().isEmpty()) { + if (!scopes.contains("openid")) { // spec: Even if a scope parameter is present in the Request Object value, a scope parameter MUST always be passed using the OAuth 2.0 request syntax containing the openid scope value + throw new WebApplicationException(Response + .status(Response.Status.BAD_REQUEST) + .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.INVALID_SCOPE, state, "scope parameter does not contain openid value which is required.")) + .build()); + } + scopes = scopeChecker.checkScopesPolicy(client, Lists.newArrayList(jwtRequest.getScopes())); + } + if (jwtRequest.getRedirectUri() != null && !jwtRequest.getRedirectUri().equals(redirectUri)) { + throw createInvalidJwtRequestException(redirectUriResponse, "The redirect_uri parameter is not the same in the JWT"); + } + if (StringUtils.isNotBlank(jwtRequest.getNonce())) { + nonce = jwtRequest.getNonce(); + } + if (jwtRequest.getDisplay() != null && StringUtils.isNotBlank(jwtRequest.getDisplay().getParamName())) { + display = jwtRequest.getDisplay().getParamName(); + } + if (!jwtRequest.getPrompts().isEmpty()) { + prompts = Lists.newArrayList(jwtRequest.getPrompts()); + isPromptFromJwt = true; + } + if (jwtRequest.getResponseMode() != null) { + redirectUriResponse.getRedirectUri().setResponseMode(jwtRequest.getResponseMode()); + } + + final IdTokenMember idTokenMember = jwtRequest.getIdTokenMember(); + if (idTokenMember != null) { + if (idTokenMember.getMaxAge() != null) { + maxAge = idTokenMember.getMaxAge(); + } + final Claim acrClaim = idTokenMember.getClaim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE); + if (acrClaim != null && acrClaim.getClaimValue() != null) { + acrValuesStr = acrClaim.getClaimValue().getValueAsString(); + acrValues = Util.splittedStringAsList(acrValuesStr, " "); + } + + Claim userIdClaim = idTokenMember.getClaim(JwtClaimName.SUBJECT_IDENTIFIER); + if (userIdClaim != null && userIdClaim.getClaimValue() != null + && userIdClaim.getClaimValue().getValue() != null) { + String userIdClaimValue = userIdClaim.getClaimValue().getValue(); + + if (user != null) { + String userId = user.getUserId(); + + if (!userId.equalsIgnoreCase(userIdClaimValue)) { + builder = redirectUriResponse.createErrorBuilder(AuthorizeErrorResponseType.USER_MISMATCHED); + applicationAuditLogger.sendMessage(oAuth2AuditLog); + return builder.build(); + } + } + } + } + requestParameterService.getCustomParameters(jwtRequest, customParameters); + } catch (WebApplicationException e) { + throw e; + } catch (Exception e) { + log.error("Invalid JWT authorization request. Message : " + e.getMessage(), e); + throw createInvalidJwtRequestException(redirectUriResponse, "Invalid JWT authorization request"); + } + } + if (!cibaRequestService.hasCibaCompatibility(client)) { + if (appConfiguration.getFapiCompatibility() && jwtRequest == null) { + throw redirectUriResponse.createWebException(AuthorizeErrorResponseType.INVALID_REQUEST); + } + authorizeRestWebServiceValidator.validateRequestJwt(request, requestUri, redirectUriResponse); + } + authorizeRestWebServiceValidator.validate(responseTypes, prompts, nonce, state, redirectUri, httpRequest, client, responseMode); + + if (CollectionUtils.isEmpty(acrValues) && !ArrayUtils.isEmpty(client.getDefaultAcrValues())) { + acrValues = Lists.newArrayList(client.getDefaultAcrValues()); + } + + if (scopes.contains(ScopeConstants.OFFLINE_ACCESS) && !client.getTrustedClient()) { + if (!responseTypes.contains(ResponseType.CODE)) { + log.trace("Removed (ignored) offline_scope. Can't find `code` in response_type which is required."); + scopes.remove(ScopeConstants.OFFLINE_ACCESS); + } + + if (scopes.contains(ScopeConstants.OFFLINE_ACCESS) && !prompts.contains(Prompt.CONSENT)) { + log.error("Removed offline_access. Can't find prompt=consent. Consent is required for offline_access."); + scopes.remove(ScopeConstants.OFFLINE_ACCESS); + } + } + + final boolean isResponseTypeValid = AuthorizeParamsValidator.validateResponseTypes(responseTypes, client) + && AuthorizeParamsValidator.validateGrantType(responseTypes, client.getGrantTypes(), appConfiguration.getGrantTypesSupported()); + + if (!isResponseTypeValid) { + throw new WebApplicationException(Response + .status(Response.Status.BAD_REQUEST) + .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.UNSUPPORTED_RESPONSE_TYPE, state, "")) + .build()); + } + + AuthorizationGrant authorizationGrant = null; + + User ropcUser = executeRopcIfRequired(user, httpRequest, httpResponse); + if (ropcUser != null) { + user = ropcUser; + if (sessionUser == null) { + log.trace("Generating authenticated session."); + Map genericRequestMap = getGenericRequestMap(httpRequest); + + Map parameterMap = Maps.newHashMap(genericRequestMap); + Map requestParameterMap = requestParameterService.getAllowedParameters(parameterMap); + sessionUser = sessionIdService.generateAuthenticatedSessionId(httpRequest, user.getDn(), prompt); + sessionUser.setSessionAttributes(requestParameterMap); + + cookieService.createSessionIdCookie(sessionUser, httpRequest, httpResponse, false); + sessionIdService.updateSessionId(sessionUser); + } + } + + log.trace("User: {}, prompts: {}", user, prompts); + if (user == null) { + identity.logout(); + if (prompts.contains(Prompt.NONE)) { + if (authenticationFilterService.isEnabled()) { + Map params; + if (method.equals(HttpMethod.GET)) { + params = QueryStringDecoder.decode(httpRequest.getQueryString()); + } else { + params = getGenericRequestMap(httpRequest); + } + + String userDn = authenticationFilterService.processAuthenticationFilters(params); + if (userDn != null) { + Map genericRequestMap = getGenericRequestMap(httpRequest); + + Map parameterMap = Maps.newHashMap(genericRequestMap); + Map requestParameterMap = requestParameterService.getAllowedParameters(parameterMap); + + sessionUser = sessionIdService.generateAuthenticatedSessionId(httpRequest, userDn, prompt); + sessionUser.setSessionAttributes(requestParameterMap); + + cookieService.createSessionIdCookie(sessionUser, httpRequest, httpResponse, false); + sessionIdService.updateSessionId(sessionUser); + user = userService.getUserByDn(sessionUser.getUserDn()); + } else { + builder = redirectUriResponse.createErrorBuilder(AuthorizeErrorResponseType.LOGIN_REQUIRED); + applicationAuditLogger.sendMessage(oAuth2AuditLog); + return builder.build(); + } + } else { + builder = redirectUriResponse.createErrorBuilder(AuthorizeErrorResponseType.LOGIN_REQUIRED); + applicationAuditLogger.sendMessage(oAuth2AuditLog); + return builder.build(); + } + } else { + if (prompts.contains(Prompt.LOGIN)) { + unauthenticateSession(sessionId, httpRequest, isPromptFromJwt); + sessionId = null; + prompts.remove(Prompt.LOGIN); + } + + return redirectToAuthorizationPage(redirectUriResponse.getRedirectUri(), responseTypes, scope, clientId, + redirectUri, state, responseMode, nonce, display, prompts, maxAge, uiLocales, + idTokenHint, loginHint, acrValues, amrValues, request, requestUri, originHeaders, + codeChallenge, codeChallengeMethod, sessionId, claims, authReqId, customParameters, oAuth2AuditLog, httpRequest); + } + } + + boolean authnMaxAgeValid = authorizeRestWebServiceValidator.isAuthnMaxAgeValid(maxAge, sessionUser, client); + if (!authnMaxAgeValid) { + unauthenticateSession(sessionId, httpRequest); + sessionId = null; + + return redirectToAuthorizationPage(redirectUriResponse.getRedirectUri(), responseTypes, scope, clientId, + redirectUri, state, responseMode, nonce, display, prompts, maxAge, uiLocales, + idTokenHint, loginHint, acrValues, amrValues, request, requestUri, originHeaders, + codeChallenge, codeChallengeMethod, sessionId, claims, authReqId, customParameters, oAuth2AuditLog, httpRequest); + } + + oAuth2AuditLog.setUsername(user.getUserId()); + + ExternalPostAuthnContext postAuthnContext = new ExternalPostAuthnContext(client, sessionUser, httpRequest, httpResponse); + final boolean forceReAuthentication = externalPostAuthnService.externalForceReAuthentication(client, postAuthnContext); + if (forceReAuthentication) { + unauthenticateSession(sessionId, httpRequest); + sessionId = null; + + return redirectToAuthorizationPage(redirectUriResponse.getRedirectUri(), responseTypes, scope, clientId, + redirectUri, state, responseMode, nonce, display, prompts, maxAge, uiLocales, + idTokenHint, loginHint, acrValues, amrValues, request, requestUri, originHeaders, + codeChallenge, codeChallengeMethod, sessionId, claims, authReqId, customParameters, oAuth2AuditLog, httpRequest); + } + + final boolean forceAuthorization = externalPostAuthnService.externalForceAuthorization(client, postAuthnContext); + if (forceAuthorization) { + return redirectToAuthorizationPage(redirectUriResponse.getRedirectUri(), responseTypes, scope, clientId, + redirectUri, state, responseMode, nonce, display, prompts, maxAge, uiLocales, + idTokenHint, loginHint, acrValues, amrValues, request, requestUri, originHeaders, + codeChallenge, codeChallengeMethod, sessionId, claims, authReqId, customParameters, oAuth2AuditLog, httpRequest); + } + + ClientAuthorization clientAuthorization = null; + boolean clientAuthorizationFetched = false; + if (scopes.size() > 0) { + if (prompts.contains(Prompt.CONSENT)) { + return redirectToAuthorizationPage(redirectUriResponse.getRedirectUri(), responseTypes, scope, clientId, + redirectUri, state, responseMode, nonce, display, prompts, maxAge, uiLocales, + idTokenHint, loginHint, acrValues, amrValues, request, requestUri, originHeaders, + codeChallenge, codeChallengeMethod, sessionId, claims, authReqId, customParameters, oAuth2AuditLog, httpRequest); + } + + final boolean clientHasAllScopes = sessionIdService.hasClientAllScopes(sessionUser, clientId, scopes); + final boolean permissionGrantedForClient = isTrue(sessionUser.isPermissionGrantedForClient(clientId)); + if (client.getTrustedClient() || (clientHasAllScopes && permissionGrantedForClient)) { + log.trace("Granting access to session {}, clientTrusted: {}, clientHasAllScopes: {}, permissionGrantedForClient: {}", sessionUser.getId(), client.getTrustedClient(), clientHasAllScopes, permissionGrantedForClient); + sessionUser.addPermission(clientId, true, scopes); + sessionIdService.updateSessionId(sessionUser); + } else { + clientAuthorization = clientAuthorizationsService.find(user.getAttribute("inum"), client.getClientId()); + if (clientAuthorization == null || clientAuthorization.getScopes() == null || clientAuthorization.getScopes().length == 0) { + log.trace("Redirect to authorization page, no appropriate clientAuthorization, clientId: {}", client.getClientId()); + return redirectToAuthorizationPage(redirectUriResponse.getRedirectUri(), responseTypes, scope, clientId, + redirectUri, state, responseMode, nonce, display, prompts, maxAge, uiLocales, + idTokenHint, loginHint, acrValues, amrValues, request, requestUri, originHeaders, + codeChallenge, codeChallengeMethod, sessionId, claims, authReqId, customParameters, oAuth2AuditLog, httpRequest); + } + + clientAuthorizationFetched = true; + log.trace("ClientAuthorization - scope: " + scope + ", dn: " + clientAuthorization.getDn() + ", requestedScope: " + scopes); + if (Arrays.asList(clientAuthorization.getScopes()).containsAll(scopes)) { + log.trace("Granting access to session {}, clientAuthorization has all scopes {}", sessionUser.getId(), clientAuthorization.getScopes()); + sessionUser.addPermission(clientId, true); + sessionIdService.updateSessionId(sessionUser); + } else { + return redirectToAuthorizationPage(redirectUriResponse.getRedirectUri(), responseTypes, scope, clientId, + redirectUri, state, responseMode, nonce, display, prompts, maxAge, uiLocales, + idTokenHint, loginHint, acrValues, amrValues, request, requestUri, originHeaders, + codeChallenge, codeChallengeMethod, sessionId, claims, authReqId, customParameters, oAuth2AuditLog, httpRequest); + } + } + } + + if (prompts.contains(Prompt.LOGIN)) { + boolean sessionUnauthenticated = false; + + // workaround for #1030 - remove only authenticated session, for set up acr we set it unauthenticated and then drop in AuthorizeAction + if (identity.getSessionId().getState() == SessionIdState.AUTHENTICATED) { + sessionUnauthenticated = unauthenticateSession(sessionId, httpRequest, isPromptFromJwt); + } + sessionId = null; + prompts.remove(Prompt.LOGIN); + + if (sessionUnauthenticated || identity.getSessionId().getState() == SessionIdState.UNAUTHENTICATED) { + return redirectToAuthorizationPage(redirectUriResponse.getRedirectUri(), responseTypes, scope, clientId, + redirectUri, state, responseMode, nonce, display, prompts, maxAge, uiLocales, + idTokenHint, loginHint, acrValues, amrValues, request, requestUri, originHeaders, + codeChallenge, codeChallengeMethod, sessionId, claims, authReqId, customParameters, oAuth2AuditLog, httpRequest); + } + } + + if (prompts.contains(Prompt.CONSENT) || !sessionUser.isPermissionGrantedForClient(clientId)) { + if (!clientAuthorizationFetched) { + clientAuthorization = clientAuthorizationsService.find(user.getAttribute("inum"), client.getClientId()); + } + clientAuthorizationsService.clearAuthorizations(clientAuthorization, client.getPersistClientAuthorizations()); + + prompts.remove(Prompt.CONSENT); + + return redirectToAuthorizationPage(redirectUriResponse.getRedirectUri(), responseTypes, scope, clientId, + redirectUri, state, responseMode, nonce, display, prompts, maxAge, uiLocales, + idTokenHint, loginHint, acrValues, amrValues, request, requestUri, originHeaders, + codeChallenge, codeChallengeMethod, sessionId, claims, authReqId, customParameters, oAuth2AuditLog, httpRequest); + } + + if (prompts.contains(Prompt.SELECT_ACCOUNT)) { + return redirectToSelectAccountPage(redirectUriResponse.getRedirectUri(), responseTypes, scope, clientId, + redirectUri, state, responseMode, nonce, display, prompts, maxAge, uiLocales, + idTokenHint, loginHint, acrValues, amrValues, request, requestUri, originHeaders, + codeChallenge, codeChallengeMethod, sessionId, claims, authReqId, customParameters, oAuth2AuditLog, httpRequest); + } + + AuthorizationCode authorizationCode = null; + if (responseTypes.contains(ResponseType.CODE)) { + authorizationGrant = authorizationGrantList.createAuthorizationCodeGrant(user, client, + sessionUser.getAuthenticationTime()); + authorizationGrant.setNonce(nonce); + authorizationGrant.setJwtAuthorizationRequest(jwtRequest); + authorizationGrant.setTokenBindingHash(TokenBindingMessage.getTokenBindingIdHashFromTokenBindingMessage(tokenBindingHeader, client.getIdTokenTokenBindingCnf())); + authorizationGrant.setScopes(scopes); + authorizationGrant.setCodeChallenge(codeChallenge); + authorizationGrant.setCodeChallengeMethod(codeChallengeMethod); + authorizationGrant.setClaims(claims); + + // Store acr_values + authorizationGrant.setAcrValues(getAcrForGrant(acrValuesStr, sessionUser)); + authorizationGrant.setSessionDn(sessionUser.getDn()); + authorizationGrant.save(); // call save after object modification!!! + + authorizationCode = authorizationGrant.getAuthorizationCode(); + + redirectUriResponse.getRedirectUri().addResponseParameter("code", authorizationCode.getCode()); + } + + final ExecutionContext executionContext = new ExecutionContext(httpRequest, httpResponse); + executionContext.setGrant(authorizationGrant); + executionContext.setClient(client); + executionContext.setAppConfiguration(appConfiguration); + executionContext.setAttributeService(attributeService); + + AccessToken newAccessToken = null; + if (responseTypes.contains(ResponseType.TOKEN)) { + if (authorizationGrant == null) { + authorizationGrant = authorizationGrantList.createImplicitGrant(user, client, + sessionUser.getAuthenticationTime()); + authorizationGrant.setNonce(nonce); + authorizationGrant.setJwtAuthorizationRequest(jwtRequest); + authorizationGrant.setScopes(scopes); + authorizationGrant.setClaims(claims); + + // Store acr_values + authorizationGrant.setAcrValues(getAcrForGrant(acrValuesStr, sessionUser)); + authorizationGrant.setSessionDn(sessionUser.getDn()); + authorizationGrant.save(); // call save after object modification!!! + } + + newAccessToken = authorizationGrant.createAccessToken(httpRequest.getHeader("X-ClientCert"), executionContext); + + redirectUriResponse.getRedirectUri().addResponseParameter(AuthorizeResponseParam.ACCESS_TOKEN, newAccessToken.getCode()); + redirectUriResponse.getRedirectUri().addResponseParameter(AuthorizeResponseParam.TOKEN_TYPE, newAccessToken.getTokenType().toString()); + redirectUriResponse.getRedirectUri().addResponseParameter(AuthorizeResponseParam.EXPIRES_IN, newAccessToken.getExpiresIn() + ""); + } + + if (responseTypes.contains(ResponseType.ID_TOKEN)) { + boolean includeIdTokenClaims = Boolean.TRUE.equals(appConfiguration.getLegacyIdTokenClaims()); + if (authorizationGrant == null) { + includeIdTokenClaims = true; + authorizationGrant = authorizationGrantList.createImplicitGrant(user, client, + sessionUser.getAuthenticationTime()); + authorizationGrant.setNonce(nonce); + authorizationGrant.setJwtAuthorizationRequest(jwtRequest); + authorizationGrant.setScopes(scopes); + authorizationGrant.setClaims(claims); + + // Store authentication acr values + authorizationGrant.setAcrValues(getAcrForGrant(acrValuesStr, sessionUser)); + authorizationGrant.setSessionDn(sessionUser.getDn()); + authorizationGrant.save(); // call save after object modification, call is asynchronous!!! + } + + ExternalUpdateTokenContext context = ExternalUpdateTokenContext.of(executionContext); + Function postProcessor = externalUpdateTokenService.buildModifyIdTokenProcessor(context); + + IdToken idToken = authorizationGrant.createIdToken( + nonce, authorizationCode, newAccessToken, null, + state, authorizationGrant, includeIdTokenClaims, + JwrService.wrapWithSidFunction(TokenBindingMessage.createIdTokenTokingBindingPreprocessing(tokenBindingHeader, client.getIdTokenTokenBindingCnf()), sessionUser.getOutsideSid()), + postProcessor, executionContext); + + redirectUriResponse.getRedirectUri().addResponseParameter(AuthorizeResponseParam.ID_TOKEN, idToken.getCode()); + } + + if (authorizationGrant != null && StringHelper.isNotEmpty(acrValuesStr) && !appConfiguration.getFapiCompatibility()) { + redirectUriResponse.getRedirectUri().addResponseParameter(AuthorizeResponseParam.ACR_VALUES, acrValuesStr); + } + + if (sessionUser.getId() == null) { + final SessionId newSessionUser = sessionIdService.generateAuthenticatedSessionId(httpRequest, sessionUser.getUserDn(), prompt); + String newSessionId = newSessionUser.getId(); + sessionUser.setId(newSessionId); + log.trace("newSessionId = {}", newSessionId); + } + if (!appConfiguration.getFapiCompatibility() && appConfiguration.getSessionIdRequestParameterEnabled()) { + redirectUriResponse.getRedirectUri().addResponseParameter(AuthorizeResponseParam.SESSION_ID, sessionUser.getId()); + } + redirectUriResponse.getRedirectUri().addResponseParameter(AuthorizeResponseParam.SID, sessionUser.getOutsideSid()); + redirectUriResponse.getRedirectUri().addResponseParameter(AuthorizeResponseParam.SESSION_STATE, sessionIdService.computeSessionState(sessionUser, clientId, redirectUri)); + redirectUriResponse.getRedirectUri().addResponseParameter(AuthorizeResponseParam.STATE, state); + if (scope != null && !scope.isEmpty() && authorizationGrant != null && !appConfiguration.getFapiCompatibility()) { + scope = authorizationGrant.checkScopesPolicy(scope); + + redirectUriResponse.getRedirectUri().addResponseParameter(AuthorizeResponseParam.SCOPE, scope); + } + + clientService.updateAccessTime(client, false); + oAuth2AuditLog.setSuccess(true); + + updateSessionRpRedirect(sessionUser); + + log.trace("Preparing redirect to: {}", redirectUriResponse.getRedirectUri()); + builder = RedirectUtil.getRedirectResponseBuilder(redirectUriResponse.getRedirectUri(), httpRequest); + + if (appConfiguration.getCustomHeadersWithAuthorizationResponse()) { + for (String key : customResponseHeaders.keySet()) { + builder.header(key, customResponseHeaders.get(key)); + } + } + + if (StringUtils.isNotBlank(authReqId)) { + runCiba(authReqId, executionContext); + } + if (StringUtils.isNotBlank(deviceAuthzUserCode)) { + processDeviceAuthorization(deviceAuthzUserCode, user); + } + } catch (WebApplicationException e) { + applicationAuditLogger.sendMessage(oAuth2AuditLog); + log.debug(e.getMessage(), e); + throw e; + } catch (AcrChangedException e) { // Acr changed + log.error("ACR is changed, please provide a supported and enabled acr value"); + log.error(e.getMessage(), e); + + RedirectUri redirectUriResponse = new RedirectUri(redirectUri, responseTypes, responseMode); + redirectUriResponse.parseQueryString(errorResponseFactory.getErrorAsQueryString( + AuthorizeErrorResponseType.SESSION_SELECTION_REQUIRED, state)); + redirectUriResponse.addResponseParameter("hint", "Use prompt=login in order to alter existing session."); + applicationAuditLogger.sendMessage(oAuth2AuditLog); + return RedirectUtil.getRedirectResponseBuilder(redirectUriResponse, httpRequest).build(); + } catch (EntryPersistenceException e) { // Invalid clientId + builder = Response.status(Response.Status.UNAUTHORIZED.getStatusCode()) + .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.UNAUTHORIZED_CLIENT, state, "")) + .type(MediaType.APPLICATION_JSON_TYPE); + log.error(e.getMessage(), e); + } catch (InvalidSessionStateException ex) { // Allow to handle it via GlobalExceptionHandler + throw ex; + } catch (Exception e) { + builder = Response.status(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); // 500 + log.error(e.getMessage(), e); + } + + applicationAuditLogger.sendMessage(oAuth2AuditLog); + return builder.build(); + } + + private User executeRopcIfRequired(User user, HttpServletRequest httpRequest, HttpServletResponse httpResponse) { + if (!appConfiguration.getForceRopcInAuthorizationEndpoint()) { + return null; + } + + log.trace("Triggering ROPC at Authorization Endpoint (forced by 'forceRopcInAuthorizationEndpoint' configuration property)"); + + if (!externalResourceOwnerPasswordCredentialsService.isEnabled()) { + log.trace("Skip ROPC because no ROPC script found."); + return null; + } + + final ExternalResourceOwnerPasswordCredentialsContext context = new ExternalResourceOwnerPasswordCredentialsContext(httpRequest, httpResponse, appConfiguration, attributeService, userService); + context.setUser(user); + + if (externalResourceOwnerPasswordCredentialsService.executeExternalAuthenticate(context)) { + user = context.getUser(); + if (user != null) { + log.trace("ROPC - User {} is authenticated successfully by external script.", user.getUserId()); + return user; + } else { + log.trace("ROPC returned True but user is not set (set valid user in context.setUser())"); + } + } else { + log.trace("ROPC script returned False."); + } + + return null; + } + + private void updateSessionRpRedirect(SessionId sessionUser) { + int rpRedirectCount = Util.parseIntSilently(sessionUser.getSessionAttributes().get("successful_rp_redirect_count"), 0); + rpRedirectCount++; + + sessionUser.getSessionAttributes().put("successful_rp_redirect_count", Integer.toString(rpRedirectCount)); + sessionIdService.updateSessionId(sessionUser); + } + + private String getAcrForGrant(String acrValuesStr, SessionId sessionUser) { + final String acr = sessionIdService.getAcr(sessionUser); + return StringUtils.isNotBlank(acr) ? acr : acrValuesStr; + } + + private void runCiba(String authReqId, ExecutionContext executionContext) { + CibaRequestCacheControl cibaRequest = cibaRequestService.getCibaRequest(authReqId); + + if (cibaRequest == null || cibaRequest.getStatus() == CibaRequestStatus.EXPIRED) { + log.trace("User responded too late and the grant {} has expired, {}", authReqId, cibaRequest); + return; + } + + cibaRequestService.removeCibaRequest(authReqId); + CIBAGrant cibaGrant = authorizationGrantList.createCIBAGrant(cibaRequest); + executionContext.setGrant(cibaGrant); + + RefreshToken refreshToken = cibaGrant.createRefreshToken(executionContext); + log.debug("Issuing refresh token: {}", refreshToken.getCode()); + + AccessToken accessToken = cibaGrant.createAccessToken(executionContext.getHttpRequest().getHeader("X-ClientCert"), executionContext); + log.debug("Issuing access token: {}", accessToken.getCode()); + + ExternalUpdateTokenContext context = ExternalUpdateTokenContext.of(executionContext); + Function postProcessor = externalUpdateTokenService.buildModifyIdTokenProcessor(context); + + boolean includeIdTokenClaims = Boolean.TRUE.equals(appConfiguration.getLegacyIdTokenClaims()); + IdToken idToken = cibaGrant.createIdToken( + null, null, accessToken, refreshToken, + null, cibaGrant, includeIdTokenClaims, null, postProcessor, executionContext); + + cibaGrant.setTokensDelivered(true); + cibaGrant.save(); + + if (cibaRequest.getClient().getBackchannelTokenDeliveryMode() == BackchannelTokenDeliveryMode.PUSH) { + cibaPushTokenDeliveryService.pushTokenDelivery( + cibaGrant.getAuthReqId(), + cibaGrant.getClient().getBackchannelClientNotificationEndpoint(), + cibaRequest.getClientNotificationToken(), + accessToken.getCode(), + refreshToken.getCode(), + idToken.getCode(), + accessToken.getExpiresIn() + ); + } else if (cibaGrant.getClient().getBackchannelTokenDeliveryMode() == BackchannelTokenDeliveryMode.PING) { + cibaGrant.setTokensDelivered(false); + cibaGrant.save(); + + cibaPingCallbackService.pingCallback( + cibaGrant.getAuthReqId(), + cibaGrant.getClient().getBackchannelClientNotificationEndpoint(), + cibaRequest.getClientNotificationToken() + ); + } else if (cibaGrant.getClient().getBackchannelTokenDeliveryMode() == BackchannelTokenDeliveryMode.POLL) { + cibaGrant.setTokensDelivered(false); + cibaGrant.save(); + } + } + + private WebApplicationException createInvalidJwtRequestException(RedirectUriResponse redirectUriResponse, String reason) { + if (appConfiguration.getFapiCompatibility()) { + log.debug(reason); // in FAPI case log reason but don't send it since it's `reason` is not known. + return redirectUriResponse.createWebException(AuthorizeErrorResponseType.INVALID_REQUEST_OBJECT); + } + return redirectUriResponse.createWebException(AuthorizeErrorResponseType.INVALID_REQUEST_OBJECT, reason); + } + + private void updateSessionForROPC(HttpServletRequest httpRequest, SessionId sessionUser) { + if (sessionUser == null) { + return; + } + + Map sessionAttributes = sessionUser.getSessionAttributes(); + String authorizedGrant = sessionUser.getSessionAttributes().get(Constants.AUTHORIZED_GRANT); + if (StringHelper.isNotEmpty(authorizedGrant) && GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS == GrantType.fromString(authorizedGrant)) { + // Remove from session to avoid execution on next AuthZ request + sessionAttributes.remove(Constants.AUTHORIZED_GRANT); + + // Reset AuthZ parameters + Map parameterMap = getGenericRequestMap(httpRequest); + Map requestParameterMap = requestParameterService.getAllowedParameters(parameterMap); + sessionAttributes.putAll(requestParameterMap); + sessionIdService.updateSessionId(sessionUser, true, true, true); + } + } + + private void checkAcrChanged(String acrValuesStr, List prompts, SessionId sessionUser) throws AcrChangedException { + try { + sessionIdService.assertAuthenticatedSessionCorrespondsToNewRequest(sessionUser, acrValuesStr); + } catch (AcrChangedException e) { // Acr changed + //See https://github.com/GluuFederation/oxTrust/issues/797 + if (e.isForceReAuthentication()) { + if (!prompts.contains(Prompt.LOGIN)) { + log.info("ACR is changed, adding prompt=login to prompts"); + prompts.add(Prompt.LOGIN); + + sessionUser.setState(SessionIdState.UNAUTHENTICATED); + sessionUser.getSessionAttributes().put("prompt", org.gluu.oxauth.model.util.StringUtils.implode(prompts, " ")); + if (!sessionIdService.persistSessionId(sessionUser)) { + log.trace("Unable persist session_id, try to update it."); + sessionIdService.updateSessionId(sessionUser); + } + + sessionIdService.externalEvent(new SessionEvent(SessionEventType.UNAUTHENTICATED, sessionUser)); + } + } else { + throw e; + } + } + } + + private Map getGenericRequestMap(HttpServletRequest httpRequest) { + Map result = new HashMap<>(); + for (Entry entry : httpRequest.getParameterMap().entrySet()) { + result.put(entry.getKey(), entry.getValue()[0]); + } + + return result; + } + + private Response redirectToAuthorizationPage(RedirectUri redirectUriResponse, List responseTypes, String scope, String clientId, + String redirectUri, String state, ResponseMode responseMode, String nonce, String display, + List prompts, Integer maxAge, List uiLocales, String idTokenHint, String loginHint, + List acrValues, List amrValues, String request, String requestUri, String originHeaders, + String codeChallenge, String codeChallengeMethod, String sessionId, String claims, String authReqId, + Map customParameters, OAuth2AuditLog oAuth2AuditLog, HttpServletRequest httpRequest) { + return redirectTo("/authorize", redirectUriResponse, responseTypes, scope, clientId, redirectUri, + state, responseMode, nonce, display, prompts, maxAge, uiLocales, idTokenHint, loginHint, acrValues, amrValues, request, requestUri, originHeaders, + codeChallenge, codeChallengeMethod, sessionId, claims, authReqId, customParameters, oAuth2AuditLog, httpRequest); + } + + private Response redirectToSelectAccountPage(RedirectUri redirectUriResponse, List responseTypes, String scope, String clientId, + String redirectUri, String state, ResponseMode responseMode, String nonce, String display, + List prompts, Integer maxAge, List uiLocales, String idTokenHint, String loginHint, + List acrValues, List amrValues, String request, String requestUri, String originHeaders, + String codeChallenge, String codeChallengeMethod, String sessionId, String claims, String authReqId, + Map customParameters, OAuth2AuditLog oAuth2AuditLog, HttpServletRequest httpRequest) { + return redirectTo("/selectAccount", redirectUriResponse, responseTypes, scope, clientId, redirectUri, + state, responseMode, nonce, display, prompts, maxAge, uiLocales, idTokenHint, loginHint, acrValues, amrValues, request, requestUri, originHeaders, + codeChallenge, codeChallengeMethod, sessionId, claims, authReqId, customParameters, oAuth2AuditLog, httpRequest); + } + + private Response redirectTo(String pathToRedirect, + RedirectUri redirectUriResponse, List responseTypes, String scope, String clientId, + String redirectUri, String state, ResponseMode responseMode, String nonce, String display, + List prompts, Integer maxAge, List uiLocales, String idTokenHint, String loginHint, + List acrValues, List amrValues, String request, String requestUri, String originHeaders, + String codeChallenge, String codeChallengeMethod, String sessionId, String claims, String authReqId, + Map customParameters, OAuth2AuditLog oAuth2AuditLog, HttpServletRequest httpRequest) { + + final URI contextUri = URI.create(appConfiguration.getIssuer()).resolve(servletRequest.getContextPath() + pathToRedirect + ÑonfigurationFactory.getFacesMapping()); + + redirectUriResponse.setBaseRedirectUri(contextUri.toString()); + redirectUriResponse.setResponseMode(ResponseMode.QUERY); + + // oAuth parameters + String responseType = implode(responseTypes, " "); + if (StringUtils.isNotBlank(responseType)) { + redirectUriResponse.addResponseParameter(AuthorizeRequestParam.RESPONSE_TYPE, responseType); + } + if (StringUtils.isNotBlank(scope)) { + redirectUriResponse.addResponseParameter(AuthorizeRequestParam.SCOPE, scope); + } + if (StringUtils.isNotBlank(clientId)) { + redirectUriResponse.addResponseParameter(AuthorizeRequestParam.CLIENT_ID, clientId); + } + if (StringUtils.isNotBlank(redirectUri)) { + redirectUriResponse.addResponseParameter(AuthorizeRequestParam.REDIRECT_URI, redirectUri); + } + if (StringUtils.isNotBlank(state)) { + redirectUriResponse.addResponseParameter(AuthorizeRequestParam.STATE, state); + } + if (responseMode != null) { + redirectUriResponse.addResponseParameter(AuthorizeRequestParam.RESPONSE_MODE, responseMode.getParamName()); + } + + // OIC parameters + if (StringUtils.isNotBlank(nonce)) { + redirectUriResponse.addResponseParameter(AuthorizeRequestParam.NONCE, nonce); + } + if (StringUtils.isNotBlank(display)) { + redirectUriResponse.addResponseParameter(AuthorizeRequestParam.DISPLAY, display); + } + String prompt = implode(prompts, " "); + if (StringUtils.isNotBlank(prompt)) { + redirectUriResponse.addResponseParameter(AuthorizeRequestParam.PROMPT, prompt); + } + if (maxAge != null) { + redirectUriResponse.addResponseParameter(AuthorizeRequestParam.MAX_AGE, maxAge.toString()); + } + String uiLocalesStr = implode(uiLocales, " "); + if (StringUtils.isNotBlank(uiLocalesStr)) { + redirectUriResponse.addResponseParameter(AuthorizeRequestParam.UI_LOCALES, uiLocalesStr); + } + if (StringUtils.isNotBlank(idTokenHint)) { + redirectUriResponse.addResponseParameter(AuthorizeRequestParam.ID_TOKEN_HINT, idTokenHint); + } + if (StringUtils.isNotBlank(loginHint)) { + redirectUriResponse.addResponseParameter(AuthorizeRequestParam.LOGIN_HINT, loginHint); + } + String acrValuesStr = implode(acrValues, " "); + if (StringUtils.isNotBlank(acrValuesStr)) { + redirectUriResponse.addResponseParameter(AuthorizeRequestParam.ACR_VALUES, acrValuesStr); + } + String amrValuesStr = implode(amrValues, " "); + if (StringUtils.isNotBlank(amrValuesStr)) { + redirectUriResponse.addResponseParameter(AuthorizeRequestParam.AMR_VALUES, amrValuesStr); + } + if (StringUtils.isNotBlank(request)) { + redirectUriResponse.addResponseParameter(AuthorizeRequestParam.REQUEST, request); + } + if (StringUtils.isNotBlank(requestUri)) { + redirectUriResponse.addResponseParameter(AuthorizeRequestParam.REQUEST_URI, requestUri); + } + if (StringUtils.isNotBlank(codeChallenge)) { + redirectUriResponse.addResponseParameter(AuthorizeRequestParam.CODE_CHALLENGE, codeChallenge); + } + if (StringUtils.isNotBlank(codeChallengeMethod)) { + redirectUriResponse.addResponseParameter(AuthorizeRequestParam.CODE_CHALLENGE_METHOD, codeChallengeMethod); + } + if (StringUtils.isNotBlank(sessionId) && appConfiguration.getSessionIdRequestParameterEnabled()) { + redirectUriResponse.addResponseParameter(AuthorizeRequestParam.SESSION_ID, sessionId); + } + if (StringUtils.isNotBlank(claims)) { + redirectUriResponse.addResponseParameter(AuthorizeRequestParam.CLAIMS, claims); + } + + // CIBA param + if (StringUtils.isNotBlank(authReqId)) { + redirectUriResponse.addResponseParameter(AuthorizeRequestParam.AUTH_REQ_ID, authReqId); + } + + // mod_ox param + if (StringUtils.isNotBlank(originHeaders)) { + redirectUriResponse.addResponseParameter(AuthorizeRequestParam.ORIGIN_HEADERS, originHeaders); + } + + if (customParameters != null && customParameters.size() > 0) { + for (Map.Entry entry : customParameters.entrySet()) { + redirectUriResponse.addResponseParameter(entry.getKey(), entry.getValue()); + } + } + + ResponseBuilder builder = RedirectUtil.getRedirectResponseBuilder(redirectUriResponse, httpRequest); + applicationAuditLogger.sendMessage(oAuth2AuditLog); + return builder.build(); + } + + private boolean unauthenticateSession(String sessionId, HttpServletRequest httpRequest) { + return unauthenticateSession(sessionId, httpRequest, false); + } + + private boolean unauthenticateSession(String sessionId, HttpServletRequest httpRequest, boolean isPromptFromJwt) { + SessionId sessionUser = identity.getSessionId(); + if (isPromptFromJwt && sessionUser != null && !sessionUser.getSessionAttributes().containsKey("successful_rp_redirect_count")) { + return false; // skip unauthentication because there were no at least one successful rp redirect + } + + identity.logout(); + + if (sessionUser != null) { + sessionUser.setUserDn(null); + sessionUser.setUser(null); + sessionUser.setAuthenticationTime(null); + } + + if (StringHelper.isEmpty(sessionId)) { + sessionId = cookieService.getSessionIdFromCookie(httpRequest); + } + + SessionId persistenceSessionId = sessionIdService.getSessionId(sessionId); + if (persistenceSessionId == null) { + log.error("Failed to load session from LDAP by session_id: '{}'", sessionId); + return true; + } + + persistenceSessionId.setState(SessionIdState.UNAUTHENTICATED); + persistenceSessionId.setUserDn(null); + persistenceSessionId.setUser(null); + persistenceSessionId.setAuthenticationTime(null); + boolean result = sessionIdService.updateSessionId(persistenceSessionId); + sessionIdService.externalEvent(new SessionEvent(SessionEventType.UNAUTHENTICATED, persistenceSessionId).setHttpRequest(httpRequest)); + if (!result) { + log.error("Failed to update session_id '{}'", sessionId); + } + return result; + } + + /** + * Processes an authorization granted for device code grant type. + * @param userCode User code used in the device code flow. + * @param user Authenticated user that is giving the permissions. + */ + private void processDeviceAuthorization(String userCode, User user) { + DeviceAuthorizationCacheControl cacheData = deviceAuthorizationService.getDeviceAuthzByUserCode(userCode); + if (cacheData == null || cacheData.getStatus() == DeviceAuthorizationStatus.EXPIRED) { + log.trace("User responded too late and the authorization {} has expired, {}", userCode, cacheData); + return; + } + + deviceAuthorizationService.removeDeviceAuthRequestInCache(userCode, cacheData.getDeviceCode()); + DeviceCodeGrant deviceCodeGrant = authorizationGrantList.createDeviceGrant(cacheData, user); + + log.info("Granted device authorization request, user_code: {}, device_code: {}, grant_id: {}", userCode, cacheData.getDeviceCode(), deviceCodeGrant.getGrantId()); + } + +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/AuthorizeRestWebServiceValidator.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/AuthorizeRestWebServiceValidator.java new file mode 100644 index 00000000..5fbfd950 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/AuthorizeRestWebServiceValidator.java @@ -0,0 +1,315 @@ +package org.gluu.oxauth.authorize.ws.rs; + +import org.apache.commons.lang.BooleanUtils; +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.authorize.AuthorizeErrorResponseType; +import org.gluu.oxauth.model.authorize.AuthorizeParamsValidator; +import org.gluu.oxauth.model.authorize.JwtAuthorizationRequest; +import org.gluu.oxauth.model.common.*; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.service.ClientService; +import org.gluu.oxauth.service.DeviceAuthorizationService; +import org.gluu.oxauth.service.RedirectUriResponse; +import org.gluu.oxauth.service.RedirectionUriService; +import org.gluu.oxauth.util.RedirectUri; +import org.gluu.oxauth.util.RedirectUtil; +import org.gluu.oxauth.util.ServerUtil; +import org.gluu.persist.exception.EntryPersistenceException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.*; + +import static org.apache.commons.lang3.BooleanUtils.isTrue; +import static org.gluu.oxauth.model.ciba.BackchannelAuthenticationErrorResponseType.INVALID_REQUEST; + +/** + * @author Yuriy Zabrovarnyy + */ +@Named +public class AuthorizeRestWebServiceValidator { + + @Inject + private Logger log; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private ClientService clientService; + + @Inject + private RedirectionUriService redirectionUriService; + + @Inject + private DeviceAuthorizationService deviceAuthorizationService; + + @Inject + private AppConfiguration appConfiguration; + + public Client validateClient(String clientId, String state) { + if (StringUtils.isBlank(clientId)) { + throw new WebApplicationException(Response + .status(Response.Status.BAD_REQUEST) + .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.UNAUTHORIZED_CLIENT, state, "client_id is empty or blank.")) + .type(MediaType.APPLICATION_JSON_TYPE) + .build()); + } + + try { + final Client client = clientService.getClient(clientId); + if (client == null) { + throw new WebApplicationException(Response + .status(Response.Status.UNAUTHORIZED) + .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.UNAUTHORIZED_CLIENT, state, "Unable to find client.")) + .type(MediaType.APPLICATION_JSON_TYPE) + .build()); + } + if (client.isDisabled()) { + throw new WebApplicationException(Response + .status(Response.Status.UNAUTHORIZED) + .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.DISABLED_CLIENT, state, "Client is disabled.")) + .type(MediaType.APPLICATION_JSON_TYPE) + .build()); + } + + return client; + } catch (EntryPersistenceException e) { // Invalid clientId + throw new WebApplicationException(Response + .status(Response.Status.UNAUTHORIZED) + .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.UNAUTHORIZED_CLIENT, state, "Unable to find client on AS.")) + .type(MediaType.APPLICATION_JSON_TYPE) + .build()); + } + } + + public boolean isAuthnMaxAgeValid(Integer maxAge, SessionId sessionUser, Client client) { + if (maxAge == null) { + maxAge = client.getDefaultMaxAge(); + } + if (maxAge == null) { // if not set, it's still valid + return true; + } + + if (maxAge == 0) { // issue #2361: allow authentication for max_age=0 + if (BooleanUtils.isTrue(appConfiguration.getDisableAuthnForMaxAgeZero())) { + return false; + } + return true; + } + + + GregorianCalendar userAuthnTime = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + if (sessionUser.getAuthenticationTime() != null) { + userAuthnTime.setTime(sessionUser.getAuthenticationTime()); + } + + userAuthnTime.add(Calendar.SECOND, maxAge); + return userAuthnTime.after(ServerUtil.now()); + } + + public void validateRequestJwt(String request, String requestUri, RedirectUriResponse redirectUriResponse) { + if (appConfiguration.getFapiCompatibility() && StringUtils.isBlank(request) && StringUtils.isBlank(requestUri)) { + throw redirectUriResponse.createWebException(AuthorizeErrorResponseType.INVALID_REQUEST, "request and request_uri are both not specified which is forbidden for FAPI."); + } + if (StringUtils.isNotBlank(request) && StringUtils.isNotBlank(requestUri)) { + throw redirectUriResponse.createWebException(AuthorizeErrorResponseType.INVALID_REQUEST, "Both request and request_uri are specified which is not allowed."); + } + } + + public void validate(List responseTypes, List prompts, String nonce, String state, String redirectUri, HttpServletRequest httpRequest, Client client, ResponseMode responseMode) { + if (!AuthorizeParamsValidator.validateParams(responseTypes, prompts, nonce, appConfiguration.getFapiCompatibility())) { + if (redirectUri != null && redirectionUriService.validateRedirectionUri(client, redirectUri) != null) { + RedirectUri redirectUriResponse = new RedirectUri(redirectUri, responseTypes, responseMode); + redirectUriResponse.parseQueryString(errorResponseFactory.getErrorAsQueryString( + AuthorizeErrorResponseType.INVALID_REQUEST, state)); + throw new WebApplicationException(RedirectUtil.getRedirectResponseBuilder(redirectUriResponse, httpRequest).build()); + } else { + throw new WebApplicationException(Response + .status(Response.Status.BAD_REQUEST.getStatusCode()) + .type(MediaType.APPLICATION_JSON_TYPE) + .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.INVALID_REQUEST, state, "Invalid redirect uri.")) + .build()); + } + } + } + + public void validateRequestObject(JwtAuthorizationRequest jwtRequest, RedirectUriResponse redirectUriResponse) { + if (!jwtRequest.getAud().isEmpty() && !jwtRequest.getAud().contains(appConfiguration.getIssuer())) { + log.error("Failed to match aud to AS, aud: " + jwtRequest.getAud()); + throw redirectUriResponse.createWebException(AuthorizeErrorResponseType.INVALID_REQUEST_OBJECT); + } + + if (!appConfiguration.getFapiCompatibility()) { + return; + } + + // FAPI related validation + if (jwtRequest.getExp() == null) { + log.error("The exp claim is not set"); + throw redirectUriResponse.createWebException(AuthorizeErrorResponseType.INVALID_REQUEST_OBJECT); + } + final long expInMillis = jwtRequest.getExp() * 1000L; + final long now = new Date().getTime(); + if (expInMillis < now) { + log.error("Request object expired. Exp:" + expInMillis + ", now: " + now); + throw redirectUriResponse.createWebException(AuthorizeErrorResponseType.INVALID_REQUEST_OBJECT); + } + if (jwtRequest.getScopes() == null || jwtRequest.getScopes().isEmpty()) { + log.error("Request object does not have scope claim."); + throw redirectUriResponse.createWebException(AuthorizeErrorResponseType.INVALID_REQUEST_OBJECT); + } + if (StringUtils.isBlank(jwtRequest.getNonce())) { + log.error("Request object does not have nonce claim."); + throw redirectUriResponse.createWebException(AuthorizeErrorResponseType.INVALID_REQUEST_OBJECT); + } + if (StringUtils.isBlank(jwtRequest.getRedirectUri())) { + log.error("Request object does not have redirect_uri claim."); + throw redirectUriResponse.createWebException(AuthorizeErrorResponseType.INVALID_REQUEST_OBJECT); + } + } + + /** + * Validates expiration, audience and scopes in the JWT request. + * @param jwtRequest Object to be validated. + */ + public void validateCibaRequestObject(JwtAuthorizationRequest jwtRequest, String clientId) { + if (jwtRequest.getAud().isEmpty() || !jwtRequest.getAud().contains(appConfiguration.getIssuer())) { + log.error("Failed to match aud to AS, aud: " + jwtRequest.getAud()); + throw new WebApplicationException(Response + .status(Response.Status.BAD_REQUEST) + .entity(errorResponseFactory.getErrorAsJson(INVALID_REQUEST)) + .build()); + } + + if (!appConfiguration.getFapiCompatibility()) { + return; + } + + // FAPI related validation + if (jwtRequest.getExp() == null) { + log.error("The exp claim is not set"); + throw new WebApplicationException(Response + .status(Response.Status.BAD_REQUEST) + .entity(errorResponseFactory.getErrorAsJson(INVALID_REQUEST)) + .build()); + } + final long expInMillis = jwtRequest.getExp() * 1000L; + final long now = new Date().getTime(); + if (expInMillis < now) { + log.error("Request object expired. Exp:" + expInMillis + ", now: " + now); + throw new WebApplicationException(Response + .status(Response.Status.BAD_REQUEST) + .entity(errorResponseFactory.getErrorAsJson(INVALID_REQUEST)) + .build()); + } + if (jwtRequest.getScopes() == null || jwtRequest.getScopes().isEmpty()) { + log.error("Request object does not have scope claim."); + throw new WebApplicationException(Response + .status(Response.Status.BAD_REQUEST) + .entity(errorResponseFactory.getErrorAsJson(INVALID_REQUEST)) + .build()); + } + if (StringUtils.isEmpty(jwtRequest.getIss()) || !jwtRequest.getIss().equals(clientId)) { + log.error("Request object has a wrong iss claim, iss: " + jwtRequest.getIss()); + throw new WebApplicationException(Response + .status(Response.Status.BAD_REQUEST) + .entity(errorResponseFactory.getErrorAsJson(INVALID_REQUEST)) + .build()); + } + if (jwtRequest.getIat() == null || jwtRequest.getIat() == 0) { + log.error("Request object has a wrong iat claim, iat: " + jwtRequest.getIat()); + throw new WebApplicationException(Response + .status(Response.Status.BAD_REQUEST) + .entity(errorResponseFactory.getErrorAsJson(INVALID_REQUEST)) + .build()); + } + int nowInSeconds = Math.toIntExact(System.currentTimeMillis() / 1000); + if (jwtRequest.getNbf() == null || jwtRequest.getNbf() > nowInSeconds + || jwtRequest.getNbf() < nowInSeconds - appConfiguration.getCibaMaxExpirationTimeAllowedSec()) { + log.error("Request object has a wrong nbf claim, nbf: " + jwtRequest.getNbf()); + throw new WebApplicationException(Response + .status(Response.Status.BAD_REQUEST) + .entity(errorResponseFactory.getErrorAsJson(INVALID_REQUEST)) + .build()); + } + if (StringUtils.isEmpty(jwtRequest.getJti())) { + log.error("Request object has a wrong jti claim, jti: " + jwtRequest.getJti()); + throw new WebApplicationException(Response + .status(Response.Status.BAD_REQUEST) + .entity(errorResponseFactory.getErrorAsJson(INVALID_REQUEST)) + .build()); + } + int result = (StringUtils.isNotBlank(jwtRequest.getLoginHint()) ? 1 : 0) + + (StringUtils.isNotBlank(jwtRequest.getLoginHintToken()) ? 1 : 0) + + (StringUtils.isNotBlank(jwtRequest.getIdTokenHint()) ? 1 : 0); + if (result != 1) { + log.error("Request object has too many hints or doesnt have any"); + throw new WebApplicationException(Response + .status(Response.Status.BAD_REQUEST) + .entity(errorResponseFactory.getErrorAsJson(INVALID_REQUEST)) + .build()); + } + } + + public String validateRedirectUri(@NotNull Client client, @Nullable String redirectUri, String state, + String deviceAuthzUserCode, HttpServletRequest httpRequest) { + if (StringUtils.isNotBlank(deviceAuthzUserCode)) { + DeviceAuthorizationCacheControl deviceAuthorizationCacheControl = deviceAuthorizationService + .getDeviceAuthzByUserCode(deviceAuthzUserCode); + redirectUri = deviceAuthorizationService.getDeviceAuthorizationPage(deviceAuthorizationCacheControl, client, state, httpRequest); + } else { + redirectUri = redirectionUriService.validateRedirectionUri(client, redirectUri); + } + if (StringUtils.isNotBlank(redirectUri)) { + return redirectUri; + } + throw new WebApplicationException(Response + .status(Response.Status.BAD_REQUEST) + .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.INVALID_REQUEST_REDIRECT_URI, state, "")) + .build()); + } + + public void validateRequestParameterSupported(String request, String state) { + if (StringUtils.isBlank(request)) { + return; + } + + if (isTrue(appConfiguration.getRequestParameterSupported())) { + return; + } + + log.debug("'request' support is switched off by requestParameterSupported=false configuration property."); + throw new WebApplicationException(Response + .status(Response.Status.BAD_REQUEST) + .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.REQUEST_NOT_SUPPORTED, state, "request processing is denied by AS.")) + .build()); + + } + + public void validateRequestUriParameterSupported(String requestUri, String state) { + if (StringUtils.isBlank(requestUri)) { + return; + } + + if (isTrue(appConfiguration.getRequestUriParameterSupported())) { + return; + } + + log.debug("'request_uri' support is switched off by requestUriParameterSupported=false configuration property."); + throw new WebApplicationException(Response + .status(Response.Status.BAD_REQUEST) + .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.REQUEST_URI_NOT_SUPPORTED, state, "request_uri processing is denied by AS")) + .build()); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/ConsentGathererService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/ConsentGathererService.java new file mode 100644 index 00000000..262d66f7 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/ConsentGathererService.java @@ -0,0 +1,320 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2015, Gluu + */ + +package org.gluu.oxauth.authorize.ws.rs; + +import org.gluu.jsf2.service.FacesService; +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.oxauth.i18n.LanguageBean; +import org.gluu.oxauth.model.authorize.AuthorizeRequestParam; +import org.gluu.oxauth.model.authorize.ScopeChecker; +import org.gluu.oxauth.model.config.Constants; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.service.AuthorizeService; +import org.gluu.oxauth.service.ClientService; +import org.gluu.oxauth.service.SessionIdService; +import org.gluu.oxauth.service.common.UserService; +import org.gluu.oxauth.service.external.ExternalConsentGatheringService; +import org.gluu.oxauth.service.external.context.ConsentGatheringContext; +import org.gluu.util.StringHelper; +import org.slf4j.Logger; + +import javax.enterprise.context.RequestScoped; +import javax.faces.application.FacesMessage; +import javax.faces.context.ExternalContext; +import javax.faces.context.FacesContext; +import javax.inject.Inject; +import javax.inject.Named; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.*; + +/** + * @author Yuriy Movchan Date: 10/30/2017 + */ +@RequestScoped +@Named(value = "consentGatherer") +public class ConsentGathererService { + + @Inject + private Logger log; + + @Inject + private ExternalConsentGatheringService external; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private FacesContext facesContext; + + @Inject + private ExternalContext externalContext; + + @Inject + private FacesService facesService; + + @Inject + private LanguageBean languageBean; + + @Inject + private ConsentGatheringSessionService sessionService; + + @Inject + private UserService userService; + + @Inject + private AuthorizeService authorizeService; + + @Inject + private ClientService clientService; + + @Inject + private SessionIdService sessionIdService; + + @Inject + private ScopeChecker scopeChecker; + + private final Map pageAttributes = new HashMap(); + private ConsentGatheringContext context; + + public boolean configure(String userDn, String clientId, String state) { + final HttpServletRequest httpRequest = (HttpServletRequest) externalContext.getRequest(); + final HttpServletResponse httpResponse = (HttpServletResponse) externalContext.getResponse(); + + final SessionId session = sessionService.getConsentSession(httpRequest, httpResponse, userDn, true); + + CustomScriptConfiguration script = determineConsentScript(clientId); + if (script == null) { + log.error("Failed to determine consent-gathering script"); + return false; + } + + sessionService.configure(session, script.getName(), clientId, state); + + this.context = new ConsentGatheringContext(script.getConfigurationAttributes(), httpRequest, httpResponse, session, + pageAttributes, sessionService, userService, facesService, appConfiguration); + log.debug("Configuring consent-gathering script '{}'", script.getName()); + + int step = sessionService.getStep(session); + String redirectTo = external.getPageForStep(script, step, context); + if (StringHelper.isEmpty(redirectTo)) { + log.error("Failed to determine page for consent-gathering script"); + return false; + } + + context.persist(); + + log.trace("Redirecting to page: '{}'", redirectTo); + facesService.redirectWithExternal(redirectTo, null); + + return true; + } + + private CustomScriptConfiguration determineConsentScript(String clientId) { + if (appConfiguration.getConsentGatheringScriptBackwardCompatibility()) { + // in 4.1 and earlier we returned default consent script + return external.getDefaultExternalCustomScript(); + } + + final List consentGatheringScripts = clientService.getClient(clientId).getAttributes().getConsentGatheringScripts(); + final List scripts = external.getCustomScriptConfigurationsByDns(consentGatheringScripts); + if (!scripts.isEmpty()) { + final CustomScriptConfiguration script = Collections.max(scripts, Comparator.comparingInt(CustomScriptConfiguration::getLevel)); // flow supports single script, thus taking the one with higher level + log.debug("Determined consent gathering script `%s`", script.getName()); + return script; + } + + log.debug("There no consent gathering script configured for client `%s`. Therefore taking default consent script.", clientId); + return external.getDefaultExternalCustomScript(); + } + + public boolean authorize() { + try { + final HttpServletRequest httpRequest = (HttpServletRequest) externalContext.getRequest(); + final HttpServletResponse httpResponse = (HttpServletResponse) externalContext.getResponse(); + + final SessionId session = sessionService.getConsentSession(httpRequest, httpResponse, null, false); + if (session == null) { + log.error("Failed to restore claim-gathering session state"); + errorPage("consent.gather.invalid.session"); + return false; + } + + CustomScriptConfiguration script = getScript(session); + if (script == null) { + log.error("Failed to find script '{}' in session:", sessionService.getScriptName(session)); + errorPage("consent.gather.failed"); + return false; + } + + int step = sessionService.getStep(session); + if (!sessionService.isPassedPreviousSteps(session, step)) { + log.error("There are consent-gathering steps not marked as passed. scriptName: '{}', step: '{}'", script.getName(), step); + errorPage("consent.gather.invalid.step"); + return false; + } + + this.context = new ConsentGatheringContext(script.getConfigurationAttributes(), httpRequest, httpResponse, session, + pageAttributes, sessionService, userService, facesService, appConfiguration); + boolean authorizeResult = external.authorize(script, step, context); + log.debug("Consent-gathering result for script '{}', step: '{}', gatheredResult: '{}'", script.getName(), step, authorizeResult); + + int overridenNextStep = external.getNextStep(script, step, context); + if (!authorizeResult && overridenNextStep == -1) { + SessionId connectSession = sessionService.getConnectSession(httpRequest); + authorizeService.permissionDenied(connectSession); + return false; + } + + if (overridenNextStep != -1) { + sessionService.resetToStep(session, overridenNextStep, step); + step = overridenNextStep; + } + + int stepsCount = external.getStepsCount(script, context); + if (step < stepsCount || overridenNextStep != -1) { + int nextStep; + if (overridenNextStep != -1) { + nextStep = overridenNextStep; + } else { + nextStep = step + 1; + sessionService.markStep(session, step, true); + } + + sessionService.setStep(nextStep, session); + + String redirectTo = external.getPageForStep(script, nextStep, context); + context.persist(); + + log.trace("Redirecting to page: '{}'", redirectTo); + facesService.redirectWithExternal(redirectTo, null); + + return true; + } + + if (step == stepsCount) { + context.persist(); + onSuccess(httpRequest, session, context); + return true; + } + } catch (Exception e) { + log.error("Exception during gather() method call.", e); + } + + log.error("Failed to perform gather() method successfully."); + errorPage("consent.gather.failed"); + return false; + } + + private void onSuccess(HttpServletRequest httpRequest, SessionId session, ConsentGatheringContext context) { + sessionService.setAuthenticatedSessionState(httpRequest, context.getHttpResponse(), session); + + SessionId connectSessionId = sessionService.getConnectSession(httpRequest); + + authorizeService.permissionGranted(httpRequest, connectSessionId); + } + + public String prepareForStep() { + try { + final HttpServletRequest httpRequest = (HttpServletRequest) externalContext.getRequest(); + final HttpServletResponse httpResponse = (HttpServletResponse) externalContext.getResponse(); + + final SessionId session = sessionService.getConsentSession(httpRequest, httpResponse, null, false); + if (session == null || session.getSessionAttributes().isEmpty()) { + log.error("Failed to restore claim-gathering session state"); + return result(Constants.RESULT_EXPIRED); + } + + CustomScriptConfiguration script = getScript(session); + if (script == null) { + log.error("Failed to find script '{}' in session:", sessionService.getScriptName(session)); + return result(Constants.RESULT_FAILURE); + } + + int step = sessionService.getStep(session); + if (step < 1) { + log.error("Invalid step: {}", step); + return result(Constants.RESULT_INVALID_STEP); + } + + if (!sessionService.isPassedPreviousSteps(session, step)) { + log.error("There are consent-gathering steps not marked as passed. scriptName: '{}', step: '{}'", script.getName(), step); + return result(Constants.RESULT_FAILURE); + } + + this.context = new ConsentGatheringContext(script.getConfigurationAttributes(), httpRequest, httpResponse, session, + pageAttributes, sessionService, userService, facesService, appConfiguration); + boolean result = external.prepareForStep(script, step, context); + log.debug("Consent-gathering prepare for step result for script '{}', step: '{}', gatheredResult: '{}'", script.getName(), step, result); + if (result) { + context.persist(); + return result(Constants.RESULT_SUCCESS); + } + } catch (Exception ex) { + log.error("Failed to prepareForStep()", ex); + } + + return result(Constants.RESULT_FAILURE); + } + + private void errorPage(String errorKey) { + addMessage(FacesMessage.SEVERITY_ERROR, errorKey); + facesService.redirect("/error.xhtml"); + } + + public String result(String resultCode) { + if (Constants.RESULT_FAILURE.equals(resultCode)) { + addMessage(FacesMessage.SEVERITY_ERROR, "consent.gather.failed"); + } else if (Constants.RESULT_INVALID_STEP.equals(resultCode)) { + addMessage(FacesMessage.SEVERITY_ERROR, "consent.gather.invalid.step"); + } else if (Constants.RESULT_EXPIRED.equals(resultCode)) { + addMessage(FacesMessage.SEVERITY_ERROR, "consent.gather.invalid.session"); + } + return resultCode; + } + + public void addMessage(FacesMessage.Severity severity, String summary) { + String msg = languageBean.getMessage(summary); + FacesMessage message = new FacesMessage(severity, msg, null); + facesContext.addMessage(null, message); + } + + public Map getPageAttributes() { + return pageAttributes; + } + + protected CustomScriptConfiguration getScript(final SessionId session) { + String scriptName = sessionService.getScriptName(session); + CustomScriptConfiguration script = external.getCustomScriptConfigurationByName(scriptName); + + return script; + } + + public boolean isConsentGathered() { + final HttpServletRequest httpRequest = (HttpServletRequest) externalContext.getRequest(); + return sessionService.isSessionStateAuthenticated(httpRequest); + } + + public ConsentGatheringContext getContext() { + return context; + } + + public List getScopes() { + if (context == null) { + return Collections.emptyList(); + } + + SessionId authenticatedSessionId = sessionIdService.getSessionId(); + // Fix the list of scopes in the authorization page. oxAuth #739 + Set grantedScopes = scopeChecker.checkScopesPolicy(context.getClient(), authenticatedSessionId.getSessionAttributes().get(AuthorizeRequestParam.SCOPE)); + String allowedScope = org.gluu.oxauth.model.util.StringUtils.implode(grantedScopes, " "); + + return authorizeService.getScopes(allowedScope); + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/ConsentGatheringSessionService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/ConsentGatheringSessionService.java new file mode 100644 index 00000000..c0777b06 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/ConsentGatheringSessionService.java @@ -0,0 +1,218 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2015, Gluu + */ + +package org.gluu.oxauth.authorize.ws.rs; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.common.User; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.model.util.Util; +import org.gluu.oxauth.service.ClientService; +import org.gluu.oxauth.service.CookieService; +import org.gluu.oxauth.service.SessionIdService; +import org.gluu.persist.exception.EntryPersistenceException; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author Yuriy Movchan + * @version December 8, 2018 + */ +@Named +public class ConsentGatheringSessionService { + + @Inject + private Logger log; + + @Inject + private SessionIdService sessionIdService; + + @Inject + private CookieService cookieService; + + @Inject + private ClientService clientService; + + public SessionId getConnectSession(HttpServletRequest httpRequest) { + String cookieId = cookieService.getSessionIdFromCookie(httpRequest); + log.trace("Cookie - session_id: {}", cookieId); + if (StringUtils.isNotBlank(cookieId)) { + return sessionIdService.getSessionId(cookieId); + } + + return null; + } + public boolean hasCookie(HttpServletRequest httpRequest) { + String cookieId = cookieService.getConsentSessionIdFromCookie(httpRequest); + + return StringUtils.isNotBlank(cookieId); + } + + public SessionId getConsentSession(HttpServletRequest httpRequest, HttpServletResponse httpResponse, String userDn, boolean create) { + String cookieId = cookieService.getConsentSessionIdFromCookie(httpRequest); + log.trace("Cookie - consent_session_id: {}", cookieId); + + if (StringUtils.isNotBlank(cookieId)) { + SessionId sessionId = sessionIdService.getSessionId(cookieId); + if (sessionId != null) { + log.trace("Loaded consent_session_id from cookie, session: {}", sessionId); + return sessionId; + } else { + log.error("Failed to load consent_session_id from cookie: {}", cookieId); + } + } else { + if (!create) { + log.error("consent_session_id cookie is not set."); + } + } + + if (!create) { + return null; + } + + log.trace("Generating new consent_session_id ..."); + SessionId session = sessionIdService.generateUnauthenticatedSessionId(userDn); + + cookieService.createCookieWithState(session.getId(), session.getSessionState(), session.getOPBrowserState(), httpRequest, httpResponse, CookieService.CONSENT_SESSION_ID_COOKIE_NAME); + log.trace("consent_session_id cookie created."); + + return session; + } + + public void setAuthenticatedSessionState(HttpServletRequest httpRequest, HttpServletResponse httpResponse, SessionId sessionId) { + SessionId connectSession = getConnectSession(httpRequest); + sessionIdService.setSessionIdStateAuthenticated(httpRequest, httpResponse, sessionId, connectSession.getUserDn()); + } + + public boolean isSessionStateAuthenticated(HttpServletRequest httpRequest) { + boolean hasSession = hasCookie(httpRequest); + + if (hasSession) { + final SessionId session = getConsentSession(httpRequest, null, null, false); + return sessionIdService.isSessionIdAuthenticated(session); + } else { + return false; + } + } + + public boolean persist(SessionId session) { + try { + if (sessionIdService.updateSessionId(session, true)) { + log.trace("Session updated successfully. Session: " + session); + return true; + } + } catch (EntryPersistenceException e) { + try { + if (sessionIdService.persistSessionId(session, true)) { + log.trace("Session persisted successfully. Session: " + session); + return true; + } + } catch (Exception ex) { + log.error("Failed to persist session, id: " + session.getId(), ex); + } + } catch (Exception e) { + log.error("Failed to persist session, id: " + session.getId(), e); + } + return false; + } + + public int getStep(SessionId session) { + String stepString = session.getSessionAttributes().get("step"); + int step = Util.parseIntSilently(stepString); + if (step == -1) { + step = 1; + setStep(step, session); + } + return step; + } + + public void setStep(int step, SessionId session) { + session.getSessionAttributes().put("step", Integer.toString(step)); + } + + public void configure(SessionId session, String scriptName, String clientId, String state) { + setStep(1, session); + setScriptName(session, scriptName); + + setClientId(session, clientId); + persist(session); + } + + public boolean isStepPassed(SessionId session, Integer step) { + return Boolean.parseBoolean(session.getSessionAttributes().get(String.format("consent_step_passed_%d", step))); + } + + public boolean isPassedPreviousSteps(SessionId session, Integer step) { + for (int i = 1; i < step; i++) { + if (!isStepPassed(session, i)) { + return false; + } + } + return true; + } + + public void markStep(SessionId session, Integer step, boolean value) { + String key = String.format("consent_step_passed_%d", step); + if (value) { + session.getSessionAttributes().put(key, Boolean.TRUE.toString()); + } else { + session.getSessionAttributes().remove(key); + } + } + + public String getScriptName(SessionId session) { + return session.getSessionAttributes().get("gather_script_name"); + } + + public void setScriptName(SessionId session, String scriptName) { + session.getSessionAttributes().put("gather_script_name", scriptName); + } + + public String getClientId(SessionId session) { + return session.getSessionAttributes().get("client_id"); + } + + public void setClientId(SessionId session, String clientId) { + session.getSessionAttributes().put("client_id", clientId); + } + + public void resetToStep(SessionId session, int overridenNextStep, int step) { + for (int i = overridenNextStep; i <= step; i++) { + markStep(session, i, false); + } + + setStep(overridenNextStep, session); + } + + public User getUser(HttpServletRequest httpRequest, String... returnAttributes) { + return sessionIdService.getUser(getConnectSession(httpRequest)); + } + + public String getUserDn(HttpServletRequest httpRequest) { + SessionId connectSession = getConnectSession(httpRequest); + if (connectSession != null) { + return connectSession.getUserDn(); + } + + log.trace("No logged in user."); + return null; + } + + public Client getClient(SessionId session) { + String clientId = getClientId(session); + if (StringUtils.isNotBlank(clientId)) { + return clientService.getClient(clientId); + } + log.trace("client_id is not in session."); + return null; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/DeviceAuthorizationAction.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/DeviceAuthorizationAction.java new file mode 100644 index 00000000..e5e15247 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/DeviceAuthorizationAction.java @@ -0,0 +1,367 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.authorize.ws.rs; + +import org.apache.commons.lang.StringUtils; +import org.gluu.jsf2.message.FacesMessages; +import org.gluu.oxauth.i18n.LanguageBean; +import org.gluu.oxauth.model.common.DeviceAuthorizationCacheControl; +import org.gluu.oxauth.model.common.DeviceAuthorizationStatus; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.model.session.SessionIdState; +import org.gluu.oxauth.model.util.Util; +import org.gluu.oxauth.service.CookieService; +import org.gluu.oxauth.service.DeviceAuthorizationService; +import org.gluu.oxauth.service.SessionIdService; +import org.gluu.oxauth.util.RedirectUri; +import org.slf4j.Logger; + +import javax.enterprise.context.RequestScoped; +import javax.faces.application.FacesMessage; +import javax.faces.context.FacesContext; +import javax.inject.Inject; +import javax.inject.Named; +import java.io.IOException; +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import static org.gluu.oxauth.model.authorize.AuthorizeRequestParam.*; +import static org.gluu.oxauth.model.util.StringUtils.EASY_TO_READ_CHARACTERS; +import static org.gluu.oxauth.service.DeviceAuthorizationService.*; + +/** + * Action used to process all requests related to device authorization. + */ +@Named +@RequestScoped +public class DeviceAuthorizationAction implements Serializable { + + @Inject + private Logger log; + + @Inject + private FacesMessages facesMessages; + + @Inject + private LanguageBean languageBean; + + @Inject + private DeviceAuthorizationService deviceAuthorizationService; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private SessionIdService sessionIdService; + + @Inject + private CookieService cookieService; + + // Query params + private String code; + private String sessionId; + private String state; + private String sessionState; + private String error; + private String errorDescription; + private String userCode; + + // UI data + private String userCodePart1; + private String userCodePart2; + private String titleMsg; + private String descriptionMsg; + + // Internal process + private Long lastAttempt = System.currentTimeMillis(); + private byte attempts; + + /** + * Method used by the view to load all query params and set the page state. + */ + public void pageLoaded() { + log.info("Processing device authorization page request, userCode: {}, code: {}, sessionId: {}, state: {}, sessionState: {}, error: {}, errorDescription: {}", userCode, code, sessionId, state, sessionState, error, errorDescription); + if (StringUtils.isNotBlank(error)) { + this.titleMsg = error; + this.descriptionMsg = errorDescription; + } + if (this.isDeviceAuthnCompleted()) { + this.titleMsg = languageBean.getMessage("device.authorization.access.granted.title"); + this.descriptionMsg = languageBean.getMessage("device.authorization.authorization.completed.msg"); + } + + initializeSession(); + } + + /** + * Reset data in session or create a new one whether there is no session. + */ + public void initializeSession() { + SessionId sessionId = sessionIdService.getSessionId(); + Map sessionAttributes = new HashMap<>(); + if (StringUtils.isNotBlank(userCode)) { + sessionAttributes.put(SESSION_USER_CODE, userCode); + } + if (sessionId == null) { + SessionId deviceAuthzSession = sessionIdService.generateUnauthenticatedSessionId(null, new Date(), SessionIdState.UNAUTHENTICATED, sessionAttributes, false); + sessionIdService.persistSessionId(deviceAuthzSession); + cookieService.createSessionIdCookie(deviceAuthzSession, false); + log.debug("Created session for device authorization grant page, sessionId: {}", deviceAuthzSession.getId()); + } else { + if (StringUtils.isNotBlank(sessionId.getSessionAttributes().get(SESSION_LAST_ATTEMPT)) + && StringUtils.isNotBlank(sessionId.getSessionAttributes().get(SESSION_ATTEMPTS))) { + lastAttempt = Long.parseLong(sessionId.getSessionAttributes().get(SESSION_LAST_ATTEMPT)); + attempts = Byte.parseByte(sessionId.getSessionAttributes().get(SESSION_ATTEMPTS)); + } + sessionAttributes.put(SESSION_LAST_ATTEMPT, String.valueOf(lastAttempt)); + sessionAttributes.put(SESSION_ATTEMPTS, String.valueOf(attempts)); + sessionId.setSessionAttributes(sessionAttributes); + sessionIdService.updateSessionId(sessionId); + } + } + + /** + * Processes user code introduced or loaded in the veritification page and redirects whether user code is correct + * or return an error if there is something wrong. + */ + public void processUserCodeVerification() { + SessionId session = sessionIdService.getSessionId(); + if (session == null) { + facesMessages.add(FacesMessage.SEVERITY_WARN, languageBean.getMessage("error.errorEncountered")); + return; + } + + if (!preventBruteForcing(session)) { + facesMessages.add(FacesMessage.SEVERITY_WARN, languageBean.getMessage("device.authorization.brute.forcing.msg")); + return; + } + + String userCode; + if (StringUtils.isBlank(userCodePart1) && StringUtils.isBlank(userCodePart2)) { + userCode = session.getSessionAttributes().get(SESSION_USER_CODE); + } else { + userCode = userCodePart1 + '-' + userCodePart2; + } + userCode = userCode.toUpperCase(); + + if (!validateFormat(userCode)) { + facesMessages.add(FacesMessage.SEVERITY_WARN, languageBean.getMessage("device.authorization.invalid.user.code")); + return; + } + + DeviceAuthorizationCacheControl cacheData = deviceAuthorizationService.getDeviceAuthzByUserCode(userCode); + log.debug("Verifying device authorization cache data: {}", cacheData); + + String message = null; + if (cacheData != null) { + if (cacheData.getStatus() == DeviceAuthorizationStatus.PENDING) { + session.getSessionAttributes().put(SESSION_USER_CODE, userCode); + session.getSessionAttributes().remove(SESSION_LAST_ATTEMPT); + session.getSessionAttributes().remove(SESSION_ATTEMPTS); + sessionIdService.updateSessionId(session); + + redirectToAuthorization(cacheData); + } else if (cacheData.getStatus() == DeviceAuthorizationStatus.DENIED) { + message = languageBean.getMessage("device.authorization.access.denied.msg"); + } else { + message = languageBean.getMessage("device.authorization.expired.code.msg"); + } + } else { + message = languageBean.getMessage("device.authorization.invalid.user.code"); + } + + if (message != null) { + facesMessages.add(FacesMessage.SEVERITY_WARN, message); + } + } + + /** + * Prevents brute forcing for user code field from device_authorization page. + * @param session Session used to keep data related to all attemps done. + */ + private boolean preventBruteForcing(SessionId session) { + lastAttempt = Long.valueOf(session.getSessionAttributes().getOrDefault(SESSION_LAST_ATTEMPT, "0")); + attempts = Byte.parseByte(session.getSessionAttributes().getOrDefault(SESSION_ATTEMPTS, "0")); + long currentTime = System.currentTimeMillis(); + if (currentTime - lastAttempt > 500 && attempts < 5) { + lastAttempt = currentTime; + attempts++; + session.getSessionAttributes().put(SESSION_LAST_ATTEMPT, String.valueOf(lastAttempt)); + session.getSessionAttributes().put(SESSION_ATTEMPTS, String.valueOf(attempts)); + sessionIdService.updateSessionId(session); + return true; + } else { + log.trace("User has done too many failed user code verification requests, sessionId: {}", sessionIdService.getSessionId()); + return false; + } + } + + /** + * Ensures that the user code gotten from UI is well formatted. + * @param userCode User code to be processed. + */ + private boolean validateFormat(String userCode) { + String regex = "[" + EASY_TO_READ_CHARACTERS + "]{4}-[" + EASY_TO_READ_CHARACTERS + "]{4}"; + return userCode.matches(regex); + } + + /** + * Process data related to device authorization and redirects to the authorization page. + * @param cacheData Data related to the device code request. + */ + private void redirectToAuthorization(DeviceAuthorizationCacheControl cacheData) { + try { + log.info("Redirecting to authorization code flow to process device authorization, data: {}", cacheData); + String authorizationEndpoint = appConfiguration.getAuthorizationEndpoint(); + String clientId = cacheData.getClient().getClientId(); + String responseType = appConfiguration.getDeviceAuthzResponseTypeToProcessAuthz(); + String scope = Util.listAsString(cacheData.getScopes()); + String state = UUID.randomUUID().toString(); + String nonce = UUID.randomUUID().toString(); + + RedirectUri authRequest = new RedirectUri(authorizationEndpoint); + authRequest.addResponseParameter(CLIENT_ID, clientId); + authRequest.addResponseParameter(RESPONSE_TYPE, responseType); + authRequest.addResponseParameter(SCOPE, scope); + authRequest.addResponseParameter(STATE, state); + authRequest.addResponseParameter(NONCE, nonce); + + FacesContext.getCurrentInstance().getExternalContext().redirect(authRequest.toString()); + } catch (IOException e) { + log.error("Problems trying to redirect to authorization page from device authorization action", e); + String message = languageBean.getMessage("error.errorEncountered"); + facesMessages.add(FacesMessage.SEVERITY_WARN, message); + } catch (Exception e) { + log.error("Exception processing redirection", e); + String message = languageBean.getMessage("error.errorEncountered"); + facesMessages.add(FacesMessage.SEVERITY_WARN, message); + } + } + + /** + * Checks if page is loaded for a new device request. + */ + public boolean isNewRequest() { + return StringUtils.isBlank(code) && StringUtils.isBlank(sessionId) && StringUtils.isBlank(state) + && StringUtils.isBlank(error) && StringUtils.isBlank(errorDescription); + } + + /** + * Checks if page should show error messages. + */ + public boolean isErrorResponse() { + return StringUtils.isNotBlank(error) && StringUtils.isNotBlank(error); + } + + /** + * Checks if page should be shown in complete verification mode, it means that the + * user code has been shared by the url. + */ + public boolean isCompleteVerificationMode() { + return isNewRequest() && StringUtils.isNotBlank(userCode); + } + + /** + * Checks if the authorization is complete and page should show confirmation to the end-user. + */ + public boolean isDeviceAuthnCompleted() { + return StringUtils.isNotBlank(code) && StringUtils.isNotBlank(state) && StringUtils.isBlank(error); + } + + public String getUserCodePart1() { + return userCodePart1; + } + + public void setUserCodePart1(String userCodePart1) { + this.userCodePart1 = userCodePart1; + } + + public String getUserCodePart2() { + return userCodePart2; + } + + public void setUserCodePart2(String userCodePart2) { + this.userCodePart2 = userCodePart2; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getSessionId() { + return sessionId; + } + + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public String getSessionState() { + return sessionState; + } + + public void setSessionState(String sessionState) { + this.sessionState = sessionState; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + public String getErrorDescription() { + return errorDescription; + } + + public void setErrorDescription(String errorDescription) { + this.errorDescription = errorDescription; + } + + public String getUserCode() { + return userCode; + } + + public void setUserCode(String userCode) { + this.userCode = userCode; + } + + public String getTitleMsg() { + return titleMsg; + } + + public void setTitleMsg(String titleMsg) { + this.titleMsg = titleMsg; + } + + public String getDescriptionMsg() { + return descriptionMsg; + } + + public void setDescriptionMsg(String descriptionMsg) { + this.descriptionMsg = descriptionMsg; + } +} + diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/DeviceAuthorizationRestWebService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/DeviceAuthorizationRestWebService.java new file mode 100644 index 00000000..cfafb673 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/DeviceAuthorizationRestWebService.java @@ -0,0 +1,48 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.authorize.ws.rs; + +import org.gluu.oxauth.model.authorize.DeviceAuthorizationRequestParam; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.FormParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; + +/** + *

+ * Provides interface to process OAuth2 Device Flow. + *

+ */ +public interface DeviceAuthorizationRestWebService { + + + /** + * Device Authorization Request [RFC8628 3.1]. + * Generates user_code, device_code and data needed to follow the device authorization flow + * in other rest services. + * + * @param clientId REQUIRED The client identifier as described in Section 2.2 of [RFC6749]. + * @param scope The scope of the access request as defined by Section 3.3 of [RFC6749]. + */ + @POST + @Path("/device_authorization") + @Produces({MediaType.APPLICATION_JSON}) + Response deviceAuthorization( + @FormParam(DeviceAuthorizationRequestParam.CLIENT_ID) String clientId, + @FormParam(DeviceAuthorizationRequestParam.SCOPE) String scope, + @Context HttpServletRequest httpRequest, + @Context HttpServletResponse httpResponse, + @Context SecurityContext securityContext); + +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/DeviceAuthorizationRestWebServiceImpl.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/DeviceAuthorizationRestWebServiceImpl.java new file mode 100644 index 00000000..4ddde014 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/DeviceAuthorizationRestWebServiceImpl.java @@ -0,0 +1,159 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.authorize.ws.rs; + +import org.gluu.oxauth.audit.ApplicationAuditLogger; +import org.gluu.oxauth.model.audit.Action; +import org.gluu.oxauth.model.audit.OAuth2AuditLog; +import org.gluu.oxauth.model.authorize.DeviceAuthorizationResponseParam; +import org.gluu.oxauth.model.authorize.ScopeChecker; +import org.gluu.oxauth.model.common.DeviceAuthorizationCacheControl; +import org.gluu.oxauth.model.common.DeviceAuthorizationStatus; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.session.SessionClient; +import org.gluu.oxauth.model.util.StringUtils; +import org.gluu.oxauth.security.Identity; +import org.gluu.oxauth.service.ClientService; +import org.gluu.oxauth.service.DeviceAuthorizationService; +import org.gluu.oxauth.util.ServerUtil; +import org.gluu.util.StringHelper; +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.Path; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.*; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import static org.gluu.oxauth.model.authorize.DeviceAuthorizationResponseParam.*; +import static org.gluu.oxauth.model.token.TokenErrorResponseType.INVALID_CLIENT; +import static org.gluu.oxauth.model.token.TokenErrorResponseType.INVALID_GRANT; + +/** + * Implementation for device authorization rest service. + */ +@Path("/") +public class DeviceAuthorizationRestWebServiceImpl implements DeviceAuthorizationRestWebService { + + @Inject + private Logger log; + + @Inject + private ApplicationAuditLogger applicationAuditLogger; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private Identity identity; + + @Inject + private ScopeChecker scopeChecker; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private DeviceAuthorizationService deviceAuthorizationService; + + @Inject + private ClientService clientService; + + @Context + private HttpServletRequest servletRequest; + + + @Override + public Response deviceAuthorization(String clientId, String scope, HttpServletRequest httpRequest, + HttpServletResponse httpResponse, SecurityContext securityContext) { + scope = ServerUtil.urlDecode(scope); // it may be encoded + + OAuth2AuditLog oAuth2AuditLog = new OAuth2AuditLog(ServerUtil.getIpAddress(httpRequest), Action.DEVICE_CODE_AUTHORIZATION); + oAuth2AuditLog.setClientId(clientId); + oAuth2AuditLog.setScope(scope); + + try { + log.debug("Attempting to request device codes: clientId = {}, scope = {}", clientId, scope); + + SessionClient sessionClient = identity.getSessionClient(); + Client client = sessionClient != null ? sessionClient.getClient() : null; + if (client == null) { + client = clientService.getClient(clientId); + if (!clientService.isPublic(client)) { + log.trace("Client is not public and not authenticated. Skip device authorization, clientId: {}", clientId); + throw errorResponseFactory.createWebApplicationException(Response.Status.UNAUTHORIZED, INVALID_CLIENT, ""); + } + } + if (client == null) { + log.trace("Client is not unknown. Skip revoking."); + throw errorResponseFactory.createWebApplicationException(Response.Status.UNAUTHORIZED, INVALID_CLIENT, ""); + } + + if (!deviceAuthorizationService.hasDeviceCodeCompatibility(client)) { + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, INVALID_GRANT, ""); + } + + List scopes = new ArrayList<>(); + if (StringHelper.isNotEmpty(scope)) { + Set grantedScopes = scopeChecker.checkScopesPolicy(client, scope); + scopes.addAll(grantedScopes); + } + + String userCode = StringUtils.generateRandomReadableCode((byte) 8); // Entropy 20^8 which is suggested in the RFC8628 section 6.1 + String deviceCode = StringUtils.generateRandomCode((byte) 24); // Entropy 160 bits which is over userCode entropy based on RFC8628 section 5.2 + URI verificationUri = UriBuilder.fromUri(appConfiguration.getIssuer()).path("device-code").build(); + int expiresIn = appConfiguration.getDeviceAuthzRequestExpiresIn(); + int interval = appConfiguration.getDeviceAuthzTokenPollInterval(); + long lastAccess = System.currentTimeMillis(); + DeviceAuthorizationStatus status = DeviceAuthorizationStatus.PENDING; + + DeviceAuthorizationCacheControl deviceAuthorizationCacheControl = new DeviceAuthorizationCacheControl(userCode, + deviceCode, client, scopes, verificationUri, expiresIn, interval, lastAccess, status); + deviceAuthorizationService.saveInCache(deviceAuthorizationCacheControl, true, true); + log.info("Device authorization flow initiated, userCode: {}, deviceCode: {}, clientId: {}, verificationUri: {}, expiresIn: {}, interval: {}", userCode, deviceCode, clientId, verificationUri, expiresIn, interval); + + applicationAuditLogger.sendMessage(oAuth2AuditLog); + return Response.ok() + .entity(getResponseJSONObject(deviceAuthorizationCacheControl).toString(4).replace("\\/", "/")) + .type(MediaType.APPLICATION_JSON_TYPE) + .build(); + } catch (WebApplicationException wae) { + throw wae; + } catch (Exception e) { + log.error("Problems processing device authorization init flow, clientId: {}, scope: {}", clientId, scope, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .build(); + } + } + + private JSONObject getResponseJSONObject(DeviceAuthorizationCacheControl deviceAuthorizationCacheControl) throws JSONException { + URI verificationUriComplete = UriBuilder.fromUri(deviceAuthorizationCacheControl.getVerificationUri()) + .queryParam(DeviceAuthorizationResponseParam.USER_CODE, deviceAuthorizationCacheControl.getUserCode()) + .build(); + + JSONObject responseJsonObject = new JSONObject(); + + responseJsonObject.put(DEVICE_CODE, deviceAuthorizationCacheControl.getDeviceCode()); + responseJsonObject.put(USER_CODE, deviceAuthorizationCacheControl.getUserCode()); + responseJsonObject.put(VERIFICATION_URI, deviceAuthorizationCacheControl.getVerificationUri()); + responseJsonObject.put(VERIFICATION_URI_COMPLETE, verificationUriComplete.toString()); + responseJsonObject.put(EXPIRES_IN, deviceAuthorizationCacheControl.getExpiresIn()); + responseJsonObject.put(INTERVAL, deviceAuthorizationCacheControl.getInterval()); + + return responseJsonObject; + } + +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/LoginAction.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/LoginAction.java new file mode 100644 index 00000000..b727ab7a --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/LoginAction.java @@ -0,0 +1,31 @@ +package org.gluu.oxauth.authorize.ws.rs; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.inject.Named; + +import org.slf4j.Logger; + + +/** + * @author Javier Rojas Blum + * @version May 24, 2016 + */ +@RequestScoped +@Named +public class LoginAction { + + @Inject + private Logger log; + + private String loginHint; + + public String getLoginHint() { + return loginHint; + } + + public void setLoginHint(String loginHint) { + this.loginHint = loginHint; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/LogoutAction.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/LogoutAction.java new file mode 100644 index 00000000..a0e864a4 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/authorize/ws/rs/LogoutAction.java @@ -0,0 +1,302 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.authorize.ws.rs; + +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JsonMappingException; +import org.gluu.jsf2.service.FacesService; +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.oxauth.i18n.LanguageBean; +import org.gluu.oxauth.model.common.AuthorizationGrant; +import org.gluu.oxauth.model.common.AuthorizationGrantList; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.session.EndSessionRequestParam; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.model.util.Base64Util; +import org.gluu.oxauth.model.util.Util; +import org.gluu.oxauth.service.SessionIdService; +import org.gluu.oxauth.service.external.ExternalAuthenticationService; +import org.gluu.service.JsonService; +import org.gluu.util.StringHelper; +import org.slf4j.Logger; + +import javax.enterprise.context.RequestScoped; +import javax.faces.application.FacesMessage; +import javax.faces.context.FacesContext; +import javax.inject.Inject; +import javax.inject.Named; +import java.io.IOException; +import java.util.Map; + +/** + * @author Javier Rojas Blum + * @author Yuriy Movchan + * @version August 9, 2017 + */ +@RequestScoped +@Named +public class LogoutAction { + + private static final String EXTERNAL_LOGOUT = "external_logout"; + private static final String EXTERNAL_LOGOUT_DATA = "external_logout_data"; + + @Inject + private Logger log; + + @Inject + private AuthorizationGrantList authorizationGrantList; + + @Inject + private SessionIdService sessionIdService; + + @Inject + private ExternalAuthenticationService externalAuthenticationService; + + @Inject + private JsonService jsonService; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private FacesService facesService; + + @Inject + private FacesContext facesContext; + + @Inject + private LanguageBean languageBean; + + private String idTokenHint; + private String postLogoutRedirectUri; + private SessionId sessionId; + + + public String getIdTokenHint() { + return idTokenHint; + } + + public void setIdTokenHint(String idTokenHint) { + this.idTokenHint = idTokenHint; + } + + public String getPostLogoutRedirectUri() { + return postLogoutRedirectUri; + } + + public void setPostLogoutRedirectUri(String postLogoutRedirectUri) { + this.postLogoutRedirectUri = postLogoutRedirectUri; + } + + public void redirect() { + SessionId sessionId = sessionIdService.getSessionId(); + + boolean validationResult = validateParameters(); + if (!validationResult) { + try { + restoreLogoutParametersFromSession(sessionId); + } catch (IOException ex) { + logoutFailed(); + log.debug("Failed to restore logout parameters from session", ex); + } + + validationResult = validateParameters(); + if (!validationResult) { + missingLogoutParameters(); + return; + } + } + + ExternalLogoutResult externalLogoutResult = processExternalAuthenticatorLogOut(sessionId); + if (ExternalLogoutResult.FAILURE == externalLogoutResult) { + logoutFailed(); + return; + } else if (ExternalLogoutResult.REDIRECT == externalLogoutResult) { + return; + } + + StringBuilder sb = new StringBuilder(); + + // Required parameters + if (idTokenHint != null && !idTokenHint.isEmpty()) { + sb.append(EndSessionRequestParam.ID_TOKEN_HINT + "=").append(idTokenHint); + } + + if (sessionId != null && !postLogoutRedirectUri.isEmpty()) { + if (appConfiguration.getSessionIdRequestParameterEnabled()) { + sb.append("&" + EndSessionRequestParam.SESSION_ID + "=").append(sessionId.getId()); + } + sb.append("&" + EndSessionRequestParam.SID + "=").append(sessionId.getOutsideSid()); + } + + if (postLogoutRedirectUri != null && !postLogoutRedirectUri.isEmpty()) { + sb.append("&" + EndSessionRequestParam.POST_LOGOUT_REDIRECT_URI + "=").append(postLogoutRedirectUri); + } + + facesService.redirectToExternalURL("restv1/end_session?" + sb.toString()); + } + + private boolean validateParameters() { + return (StringHelper.isNotEmpty(idTokenHint) || (sessionId != null)) && StringHelper.isNotEmpty(postLogoutRedirectUri); + } + + private ExternalLogoutResult processExternalAuthenticatorLogOut(SessionId sessionId) { + if ((sessionId != null) && sessionId.getSessionAttributes().containsKey(EXTERNAL_LOGOUT)) { + log.debug("Detected callback from external system. Resuming logout."); + return ExternalLogoutResult.SUCCESS; + } + + AuthorizationGrant authorizationGrant = authorizationGrantList.getAuthorizationGrantByIdToken(idTokenHint); + if (authorizationGrant == null) { + Boolean endSessionWithAccessToken = appConfiguration.getEndSessionWithAccessToken(); + if ((endSessionWithAccessToken != null) && endSessionWithAccessToken) { + authorizationGrant = authorizationGrantList.getAuthorizationGrantByAccessToken(idTokenHint); + } + } + if ((authorizationGrant == null) && (sessionId == null)) { + return ExternalLogoutResult.FAILURE; + } + + String acrValues; + if (authorizationGrant == null) { + acrValues = sessionIdService.getAcr(sessionId); + } else { + acrValues = authorizationGrant.getAcrValues(); + } + + boolean isExternalAuthenticatorLogoutPresent = StringHelper.isNotEmpty(acrValues); + if (isExternalAuthenticatorLogoutPresent) { + log.debug("Attemptinmg to execute logout method of '{}' external authenticator.", acrValues); + + CustomScriptConfiguration customScriptConfiguration = externalAuthenticationService.getCustomScriptConfigurationByName(acrValues); + if (customScriptConfiguration == null) { + log.error("Failed to get ExternalAuthenticatorConfiguration. acr_values: {}", acrValues); + return ExternalLogoutResult.FAILURE; + } else { + boolean scriptExternalLogoutResult = externalAuthenticationService.executeExternalLogout(customScriptConfiguration, null); + ExternalLogoutResult externalLogoutResult = scriptExternalLogoutResult ? ExternalLogoutResult.SUCCESS : ExternalLogoutResult.FAILURE; + log.debug("Logout result is '{}' for session '{}', userDn: '{}'", externalLogoutResult, sessionId.getId(), sessionId.getUserDn()); + + int apiVersion = externalAuthenticationService.executeExternalGetApiVersion(customScriptConfiguration); + if (apiVersion < 3) { + // Not support redirect to external system at logout + return externalLogoutResult; + } + + log.trace("According to API version script supports logout redirects"); + String logoutExternalUrl = externalAuthenticationService.getLogoutExternalUrl(customScriptConfiguration, null); + log.debug("External logout result is '{}' for user '{}'", logoutExternalUrl, sessionId.getUserDn()); + + if (StringHelper.isEmpty(logoutExternalUrl)) { + return externalLogoutResult; + } + + // Store in session parameters needed to call end_session + try { + storeLogoutParametersInSession(sessionId); + } catch (IOException ex) { + log.debug("Failed to persist logout parameters in session", ex); + + return ExternalLogoutResult.FAILURE; + } + + // Redirect to external URL + facesService.redirectToExternalURL(logoutExternalUrl); + return ExternalLogoutResult.REDIRECT; + } + } else { + return ExternalLogoutResult.SUCCESS; + } + } + + private void storeLogoutParametersInSession(SessionId sessionId) throws JsonGenerationException, JsonMappingException, IOException { + Map sessionAttributes = sessionId.getSessionAttributes(); + + LogoutParameters logoutParameters = new LogoutParameters(idTokenHint, postLogoutRedirectUri); + + String logoutParametersJson = jsonService.objectToJson(logoutParameters); + String logoutParametersBase64 = Base64Util.base64urlencode(logoutParametersJson.getBytes(Util.UTF8_STRING_ENCODING)); + + sessionAttributes.put(EXTERNAL_LOGOUT, Boolean.toString(true)); + sessionAttributes.put(EXTERNAL_LOGOUT_DATA, logoutParametersBase64); + + sessionIdService.updateSessionId(sessionId); + } + + private boolean restoreLogoutParametersFromSession(SessionId sessionId) throws IllegalArgumentException, JsonParseException, JsonMappingException, IOException { + if (sessionId == null) { + return false; + } + + this.sessionId = sessionId; + Map sessionAttributes = sessionId.getSessionAttributes(); + + boolean restoreParameters = sessionAttributes.containsKey(EXTERNAL_LOGOUT); + if (!restoreParameters) { + return false; + } + + String logoutParametersBase64 = sessionAttributes.get(EXTERNAL_LOGOUT_DATA); + String logoutParametersJson = new String(Base64Util.base64urldecode(logoutParametersBase64), Util.UTF8_STRING_ENCODING); + + LogoutParameters logoutParameters = jsonService.jsonToObject(logoutParametersJson, LogoutParameters.class); + + this.idTokenHint = logoutParameters.getIdTokenHint(); + this.postLogoutRedirectUri = logoutParameters.getPostLogoutRedirectUri(); + + return true; + } + + public void missingLogoutParameters() { + String message = languageBean.getMessage("logout.missingParameters"); + facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, message, message)); + facesService.redirect("/error.xhtml"); + } + + public void logoutFailed() { + String message = languageBean.getMessage("logout.failedToProceed"); + facesContext.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, message, message)); + facesService.redirect("/error.xhtml"); + } + + public static class LogoutParameters { + private String idTokenHint; + private String postLogoutRedirectUri; + + public LogoutParameters() { + } + + public LogoutParameters(String idTokenHint, String postLogoutRedirectUri) { + this.idTokenHint = idTokenHint; + this.postLogoutRedirectUri = postLogoutRedirectUri; + } + + public String getIdTokenHint() { + return idTokenHint; + } + + public void setIdTokenHint(String idTokenHint) { + this.idTokenHint = idTokenHint; + } + + public String getPostLogoutRedirectUri() { + return postLogoutRedirectUri; + } + + public void setPostLogoutRedirectUri(String postLogoutRedirectUri) { + this.postLogoutRedirectUri = postLogoutRedirectUri; + } + } + + private enum ExternalLogoutResult { + SUCCESS, + FAILURE, + REDIRECT + } + +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/bcauthorize/ws/rs/BackchannelAuthorizeRestWebService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/bcauthorize/ws/rs/BackchannelAuthorizeRestWebService.java new file mode 100644 index 00000000..e8a18bcf --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/bcauthorize/ws/rs/BackchannelAuthorizeRestWebService.java @@ -0,0 +1,46 @@ +/* + * oxAuth-CIBA is available under the Gluu Enterprise License (2019). + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.bcauthorize.ws.rs; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.FormParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; + +/** + * @author Javier Rojas Blum + * @version August 20, 2019 + */ +public interface BackchannelAuthorizeRestWebService { + + @POST + @Path("/bc-authorize") + @Produces({MediaType.APPLICATION_JSON}) + Response requestBackchannelAuthorizationPost( + @FormParam("client_id") String clientId, + @FormParam("scope") String scope, + @FormParam("client_notification_token") String clientNotificationToken, + @FormParam("acr_values") String acrValues, + @FormParam("login_hint_token") String loginHintToken, + @FormParam("id_token_hint") String idTokenHint, + @FormParam("login_hint") String loginHint, + @FormParam("binding_message") String bindingMessage, + @FormParam("user_code") String userCode, + @FormParam("requested_expiry") Integer requestedExpiry, + @FormParam("request") String request, + @FormParam("request_uri") String requestUri, + @Context HttpServletRequest httpRequest, + @Context HttpServletResponse httpResponse, + @Context SecurityContext securityContext + ); +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/bcauthorize/ws/rs/BackchannelAuthorizeRestWebServiceImpl.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/bcauthorize/ws/rs/BackchannelAuthorizeRestWebServiceImpl.java new file mode 100644 index 00000000..c9849225 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/bcauthorize/ws/rs/BackchannelAuthorizeRestWebServiceImpl.java @@ -0,0 +1,349 @@ +/* + * oxAuth-CIBA is available under the Gluu Enterprise License (2019). + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.bcauthorize.ws.rs; + +import org.apache.commons.lang.StringUtils; +import org.apache.logging.log4j.util.Strings; +import org.gluu.oxauth.audit.ApplicationAuditLogger; +import org.gluu.oxauth.authorize.ws.rs.AuthorizeRestWebServiceValidator; +import org.gluu.oxauth.ciba.CIBAAuthorizeParamsValidatorService; +import org.gluu.oxauth.ciba.CIBAEndUserNotificationService; +import org.gluu.oxauth.client.JwkClient; +import org.gluu.oxauth.model.audit.Action; +import org.gluu.oxauth.model.audit.OAuth2AuditLog; +import org.gluu.oxauth.model.authorize.JwtAuthorizationRequest; +import org.gluu.oxauth.model.authorize.ScopeChecker; +import org.gluu.oxauth.model.common.*; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.crypto.AbstractCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.AlgorithmFamily; +import org.gluu.oxauth.model.crypto.signature.ECDSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.error.DefaultErrorResponse; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.exception.InvalidClaimException; +import org.gluu.oxauth.model.exception.InvalidJwtException; +import org.gluu.oxauth.model.jws.ECDSASigner; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.session.SessionClient; +import org.gluu.oxauth.security.Identity; +import org.gluu.oxauth.service.ciba.CibaRequestService; +import org.gluu.oxauth.service.common.UserService; +import org.gluu.oxauth.util.ServerUtil; +import org.gluu.util.StringHelper; +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.Path; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Set; + +import static org.gluu.oxauth.model.ciba.BackchannelAuthenticationErrorResponseType.*; +import static org.gluu.oxauth.model.ciba.BackchannelAuthenticationResponseParam.*; + +/** + * Implementation for request backchannel authorization through REST web services. + * + * @author Javier Rojas Blum + * @version April 22, 2020 + */ +@Path("/") +public class BackchannelAuthorizeRestWebServiceImpl implements BackchannelAuthorizeRestWebService { + + @Inject + private Logger log; + + @Inject + private Identity identity; + + @Inject + private UserService userService; + + @Inject + private ApplicationAuditLogger applicationAuditLogger; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private AuthorizationGrantList authorizationGrantList; + + @Inject + private ScopeChecker scopeChecker; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private CIBAAuthorizeParamsValidatorService cibaAuthorizeParamsValidatorService; + + @Inject + private CIBAEndUserNotificationService cibaEndUserNotificationService; + + @Inject + private CibaRequestService cibaRequestService; + + @Inject + private AbstractCryptoProvider cryptoProvider; + + @Inject + private AuthorizeRestWebServiceValidator authorizeRestWebServiceValidator; + + @Override + public Response requestBackchannelAuthorizationPost( + String clientId, String scope, String clientNotificationToken, String acrValues, String loginHintToken, + String idTokenHint, String loginHint, String bindingMessage, String userCodeParam, Integer requestedExpiry, + String request, String requestUri, HttpServletRequest httpRequest, + HttpServletResponse httpResponse, SecurityContext securityContext) { + scope = ServerUtil.urlDecode(scope); // it may be encoded + + OAuth2AuditLog oAuth2AuditLog = new OAuth2AuditLog(ServerUtil.getIpAddress(httpRequest), Action.BACKCHANNEL_AUTHENTICATION); + oAuth2AuditLog.setClientId(clientId); + oAuth2AuditLog.setScope(scope); + + // ATTENTION : please do not add more parameter in this debug method because it will not work with Seam 2.2.2.Final, + // there is limit of 10 parameters (hardcoded), see: org.jboss.seam.core.Interpolator#interpolate + log.debug("Attempting to request backchannel authorization: " + + "clientId = {}, scope = {}, clientNotificationToken = {}, acrValues = {}, loginHintToken = {}, " + + "idTokenHint = {}, loginHint = {}, bindingMessage = {}, userCodeParam = {}, requestedExpiry = {}, " + + "request= {}", + clientId, scope, clientNotificationToken, acrValues, loginHintToken, + idTokenHint, loginHint, bindingMessage, userCodeParam, requestedExpiry, request); + log.debug("Attempting to request backchannel authorization: " + + "isSecure = {}", securityContext.isSecure()); + + Response.ResponseBuilder builder = Response.ok(); + + if (!appConfiguration.getCibaEnabled()) { + log.warn("Trying to register a CIBA request, however CIBA config is disabled."); + builder = Response.status(Response.Status.BAD_REQUEST.getStatusCode()); + builder.entity(errorResponseFactory.getErrorAsJson(INVALID_REQUEST)); + return builder.build(); + } + + SessionClient sessionClient = identity.getSessionClient(); + Client client = null; + if (sessionClient != null) { + client = sessionClient.getClient(); + } + + if (client == null) { + builder = Response.status(Response.Status.UNAUTHORIZED.getStatusCode()); // 401 + builder.entity(errorResponseFactory.getErrorAsJson(INVALID_CLIENT)); + return builder.build(); + } + + if (!cibaRequestService.hasCibaCompatibility(client)) { + builder = Response.status(Response.Status.BAD_REQUEST.getStatusCode()); // 401 + builder.entity(errorResponseFactory.getErrorAsJson(INVALID_REQUEST)); + return builder.build(); + } + + List scopes = new ArrayList<>(); + if (StringHelper.isNotEmpty(scope)) { + Set grantedScopes = scopeChecker.checkScopesPolicy(client, scope); + scopes.addAll(grantedScopes); + } + + JwtAuthorizationRequest jwtRequest = null; + if (StringUtils.isNotBlank(request) || StringUtils.isNotBlank(requestUri)) { + jwtRequest = JwtAuthorizationRequest.createJwtRequest(request, requestUri, + client, null, cryptoProvider, appConfiguration); + if (jwtRequest == null) { + log.error("The JWT couldn't be processed"); + builder = Response.status(Response.Status.BAD_REQUEST.getStatusCode()); // 400 + builder.entity(errorResponseFactory.getErrorAsJson(INVALID_REQUEST)); + throw new WebApplicationException(builder.build()); + } + authorizeRestWebServiceValidator.validateCibaRequestObject(jwtRequest, client.getClientId()); + // JWT wins + if (!jwtRequest.getScopes().isEmpty()) { + scopes.addAll(scopeChecker.checkScopesPolicy(client, jwtRequest.getScopes())); + } + if (StringUtils.isNotBlank(jwtRequest.getClientNotificationToken())) { + clientNotificationToken = jwtRequest.getClientNotificationToken(); + } + if (StringUtils.isNotBlank(jwtRequest.getAcrValues())) { + acrValues = jwtRequest.getAcrValues(); + } + if (StringUtils.isNotBlank(jwtRequest.getLoginHintToken())) { + loginHintToken = jwtRequest.getLoginHintToken(); + } + if (StringUtils.isNotBlank(jwtRequest.getIdTokenHint())) { + idTokenHint = jwtRequest.getIdTokenHint(); + } + if (StringUtils.isNotBlank(jwtRequest.getLoginHint())) { + loginHint = jwtRequest.getLoginHint(); + } + if (StringUtils.isNotBlank(jwtRequest.getBindingMessage())) { + bindingMessage = jwtRequest.getBindingMessage(); + } + if (StringUtils.isNotBlank(jwtRequest.getUserCode())) { + userCodeParam = jwtRequest.getUserCode(); + } + if (jwtRequest.getRequestedExpiry() != null) { + requestedExpiry = jwtRequest.getRequestedExpiry(); + } else if (jwtRequest.getExp() != null) { + requestedExpiry = Math.toIntExact(jwtRequest.getExp() - System.currentTimeMillis() / 1000); + } + } + if (appConfiguration.getFapiCompatibility() && jwtRequest == null) { + builder = Response.status(Response.Status.BAD_REQUEST.getStatusCode()); // 400 + builder.entity(errorResponseFactory.getErrorAsJson(INVALID_REQUEST)); + return builder.build(); + } + User user = null; + try { + if (Strings.isNotBlank(loginHint)) { // login_hint + user = userService.getUniqueUserByAttributes(appConfiguration.getBackchannelLoginHintClaims(), loginHint); + } else if (Strings.isNotBlank(idTokenHint)) { // id_token_hint + AuthorizationGrant authorizationGrant = authorizationGrantList.getAuthorizationGrantByIdToken(idTokenHint); + if (authorizationGrant == null) { + builder = Response.status(Response.Status.BAD_REQUEST.getStatusCode()); // 400 + builder.entity(errorResponseFactory.getErrorAsJson(UNKNOWN_USER_ID)); + return builder.build(); + } + user = authorizationGrant.getUser(); + } + if (Strings.isNotBlank(loginHintToken)) { // login_hint_token + Jwt jwt = Jwt.parse(loginHintToken); + + SignatureAlgorithm algorithm = jwt.getHeader().getSignatureAlgorithm(); + String keyId = jwt.getHeader().getKeyId(); + + if (algorithm == null || Strings.isBlank(keyId)) { + builder = Response.status(Response.Status.BAD_REQUEST.getStatusCode()); // 400 + builder.entity(errorResponseFactory.getErrorAsJson(UNKNOWN_USER_ID)); + return builder.build(); + } + + boolean validSignature = false; + if (algorithm.getFamily() == AlgorithmFamily.RSA) { + RSAPublicKey publicKey = JwkClient.getRSAPublicKey(client.getJwksUri(), keyId); + RSASigner rsaSigner = new RSASigner(algorithm, publicKey); + validSignature = rsaSigner.validate(jwt); + } else if (algorithm.getFamily() == AlgorithmFamily.EC) { + ECDSAPublicKey publicKey = JwkClient.getECDSAPublicKey(client.getJwksUri(), keyId); + ECDSASigner ecdsaSigner = new ECDSASigner(algorithm, publicKey); + validSignature = ecdsaSigner.validate(jwt); + } + if (!validSignature) { + builder = Response.status(Response.Status.BAD_REQUEST.getStatusCode()); // 400 + builder.entity(errorResponseFactory.getErrorAsJson(UNKNOWN_USER_ID)); + return builder.build(); + } + + JSONObject subject = jwt.getClaims().getClaimAsJSON("subject"); + if (subject == null || !subject.has("subject_type") || !subject.has(subject.getString("subject_type"))) { + builder = Response.status(Response.Status.BAD_REQUEST.getStatusCode()); // 400 + builder.entity(errorResponseFactory.getErrorAsJson(UNKNOWN_USER_ID)); + return builder.build(); + } + + String subjectTypeKey = subject.getString("subject_type"); + String subjectTypeValue = subject.getString(subjectTypeKey); + + user = userService.getUniqueUserByAttributes(appConfiguration.getBackchannelLoginHintClaims(), subjectTypeValue); + } + } catch (InvalidJwtException e) { + log.error(e.getMessage(), e); + } catch (JSONException e) { + log.error(e.getMessage(), e); + } + if (user == null) { + builder = Response.status(Response.Status.BAD_REQUEST.getStatusCode()); // 400 + builder.entity(errorResponseFactory.getErrorAsJson(UNKNOWN_USER_ID)); + return builder.build(); + } + + try { + String userCode = (String) user.getAttribute("oxAuthBackchannelUserCode", true, false); + DefaultErrorResponse cibaAuthorizeParamsValidation = cibaAuthorizeParamsValidatorService.validateParams( + scopes, clientNotificationToken, client.getBackchannelTokenDeliveryMode(), + loginHintToken, idTokenHint, loginHint, bindingMessage, client.getBackchannelUserCodeParameter(), + userCodeParam, userCode, requestedExpiry); + if (cibaAuthorizeParamsValidation != null) { + builder = Response.status(cibaAuthorizeParamsValidation.getStatus()); + builder.entity(errorResponseFactory.errorAsJson( + cibaAuthorizeParamsValidation.getType(), cibaAuthorizeParamsValidation.getReason())); + return builder.build(); + } + + String deviceRegistrationToken = (String) user.getAttribute("oxAuthBackchannelDeviceRegistrationToken", true, false); + if (deviceRegistrationToken == null) { + builder = Response.status(Response.Status.UNAUTHORIZED.getStatusCode()); // 401 + builder.entity(errorResponseFactory.getErrorAsJson(UNAUTHORIZED_END_USER_DEVICE)); + return builder.build(); + } + + int expiresIn = requestedExpiry != null ? requestedExpiry : appConfiguration.getBackchannelAuthenticationResponseExpiresIn(); + Integer interval = client.getBackchannelTokenDeliveryMode() == BackchannelTokenDeliveryMode.PUSH ? + null : appConfiguration.getBackchannelAuthenticationResponseInterval(); + long currentTime = new Date().getTime(); + + CibaRequestCacheControl cibaRequestCacheControl = new CibaRequestCacheControl(user, client, expiresIn, scopes, + clientNotificationToken, bindingMessage, currentTime, acrValues); + + cibaRequestService.save(cibaRequestCacheControl, expiresIn); + + String authReqId = cibaRequestCacheControl.getAuthReqId(); + + // Notify End-User to obtain Consent/Authorization + cibaEndUserNotificationService.notifyEndUser( + cibaRequestCacheControl.getScopesAsString(), + cibaRequestCacheControl.getAcrValues(), + authReqId, + deviceRegistrationToken); + + builder.entity(getJSONObject( + authReqId, + expiresIn, + interval).toString(4).replace("\\/", "/")); + + builder.type(MediaType.APPLICATION_JSON_TYPE); + builder.cacheControl(ServerUtil.cacheControl(true, false)); + } catch (JSONException e) { + builder = Response.status(400); + builder.entity(errorResponseFactory.getErrorAsJson(INVALID_REQUEST)); + log.error(e.getMessage(), e); + } catch (InvalidClaimException e) { + builder = Response.status(400); + builder.entity(errorResponseFactory.getErrorAsJson(INVALID_REQUEST)); + log.error(e.getMessage(), e); + } + + applicationAuditLogger.sendMessage(oAuth2AuditLog); + return builder.build(); + } + + private JSONObject getJSONObject(String authReqId, int expiresIn, Integer interval) throws JSONException { + JSONObject responseJsonObject = new JSONObject(); + + responseJsonObject.put(AUTH_REQ_ID, authReqId); + responseJsonObject.put(EXPIRES_IN, expiresIn); + + if (interval != null) { + responseJsonObject.put(INTERVAL, interval); + } + + return responseJsonObject; + } + +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/bcauthorize/ws/rs/BackchannelDeviceRegistrationRestWebService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/bcauthorize/ws/rs/BackchannelDeviceRegistrationRestWebService.java new file mode 100644 index 00000000..443dbb51 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/bcauthorize/ws/rs/BackchannelDeviceRegistrationRestWebService.java @@ -0,0 +1,36 @@ +/* + * oxAuth-CIBA is available under the Gluu Enterprise License (2019). + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.bcauthorize.ws.rs; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.FormParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; + +/** + * @author Javier Rojas Blum + * @version October 7, 2019 + */ +public interface BackchannelDeviceRegistrationRestWebService { + + @POST + @Path("/bc-deviceRegistration") + @Produces({MediaType.APPLICATION_JSON}) + Response requestBackchannelDeviceRegistrationPost( + @FormParam("id_token_hint") String idTokenHint, + @FormParam("device_registration_token") String deviceRegistrationToken, + @Context HttpServletRequest httpRequest, + @Context HttpServletResponse httpResponse, + @Context SecurityContext securityContext + ); +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/bcauthorize/ws/rs/BackchannelDeviceRegistrationRestWebServiceImpl.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/bcauthorize/ws/rs/BackchannelDeviceRegistrationRestWebServiceImpl.java new file mode 100644 index 00000000..24f26de6 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/bcauthorize/ws/rs/BackchannelDeviceRegistrationRestWebServiceImpl.java @@ -0,0 +1,118 @@ +/* + * oxAuth-CIBA is available under the Gluu Enterprise License (2019). + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.bcauthorize.ws.rs; + +import org.gluu.oxauth.audit.ApplicationAuditLogger; +import org.gluu.oxauth.ciba.CIBADeviceRegistrationValidatorService; +import org.gluu.oxauth.model.audit.Action; +import org.gluu.oxauth.model.audit.OAuth2AuditLog; +import org.gluu.oxauth.model.ciba.BackchannelAuthenticationErrorResponseType; +import org.gluu.oxauth.model.common.AuthorizationGrant; +import org.gluu.oxauth.model.common.AuthorizationGrantList; +import org.gluu.oxauth.model.common.User; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.error.DefaultErrorResponse; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.service.common.UserService; +import org.gluu.oxauth.util.ServerUtil; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; + +import static org.gluu.oxauth.model.ciba.BackchannelAuthenticationErrorResponseType.INVALID_REQUEST; +import static org.gluu.oxauth.model.ciba.BackchannelDeviceRegistrationErrorResponseType.UNKNOWN_USER_ID; + +/** + * Implementation for request backchannel device registration through REST web services. + * + * @author Javier Rojas Blum + * @version October 7, 2019 + */ +@Path("/") +public class BackchannelDeviceRegistrationRestWebServiceImpl implements BackchannelDeviceRegistrationRestWebService { + + @Inject + private Logger log; + + @Inject + private ApplicationAuditLogger applicationAuditLogger; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private UserService userService; + + @Inject + private AuthorizationGrantList authorizationGrantList; + + @Inject + private CIBADeviceRegistrationValidatorService cibaDeviceRegistrationValidatorService; + + @Override + public Response requestBackchannelDeviceRegistrationPost( + String idTokenHint, String deviceRegistrationToken, + HttpServletRequest httpRequest, HttpServletResponse httpResponse, SecurityContext securityContext) { + + OAuth2AuditLog oAuth2AuditLog = new OAuth2AuditLog(ServerUtil.getIpAddress(httpRequest), Action.BACKCHANNEL_DEVICE_REGISTRATION); + + // ATTENTION : please do not add more parameter in this debug method because it will not work with Seam 2.2.2.Final, + // there is limit of 10 parameters (hardcoded), see: org.jboss.seam.core.Interpolator#interpolate + log.debug("Attempting to request backchannel device registration: " + + "idTokenHint = {}, deviceRegistrationToken = {}, isSecure = {}", + idTokenHint, deviceRegistrationToken, securityContext.isSecure()); + + Response.ResponseBuilder builder = Response.ok(); + + if (!appConfiguration.getCibaEnabled()) { + log.warn("Trying to register a CIBA device, however CIBA config is disabled."); + builder = Response.status(Response.Status.BAD_REQUEST.getStatusCode()); + builder.entity(errorResponseFactory.getErrorAsJson(INVALID_REQUEST)); + return builder.build(); + } + + DefaultErrorResponse cibaDeviceRegistrationValidation = cibaDeviceRegistrationValidatorService.validateParams( + idTokenHint, deviceRegistrationToken); + if (cibaDeviceRegistrationValidation != null) { + builder = Response.status(cibaDeviceRegistrationValidation.getStatus()); + builder.entity(errorResponseFactory.errorAsJson( + cibaDeviceRegistrationValidation.getType(), cibaDeviceRegistrationValidation.getReason())); + return builder.build(); + } + + User user = null; + + AuthorizationGrant authorizationGrant = authorizationGrantList.getAuthorizationGrantByIdToken(idTokenHint); + if (authorizationGrant == null) { + builder = Response.status(Response.Status.BAD_REQUEST.getStatusCode()); // 400 + builder.entity(errorResponseFactory.getErrorAsJson(BackchannelAuthenticationErrorResponseType.UNKNOWN_USER_ID)); + return builder.build(); + } + + user = authorizationGrant.getUser(); + + if (user == null) { + builder = Response.status(Response.Status.BAD_REQUEST.getStatusCode()); // 400 + builder.entity(errorResponseFactory.getErrorAsJson(UNKNOWN_USER_ID)); + return builder.build(); + } + + userService.setCustomAttribute(user, "oxAuthBackchannelDeviceRegistrationToken", deviceRegistrationToken); + userService.updateUser(user); + + applicationAuditLogger.sendMessage(oAuth2AuditLog); + return builder.build(); + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/bcauthorize/ws/rs/CIBAAuthorizeAction.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/bcauthorize/ws/rs/CIBAAuthorizeAction.java new file mode 100644 index 00000000..9bf0a5a1 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/bcauthorize/ws/rs/CIBAAuthorizeAction.java @@ -0,0 +1,89 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.bcauthorize.ws.rs; + +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.util.RedirectUri; +import org.slf4j.Logger; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.inject.Named; +import java.util.UUID; + +import static org.gluu.oxauth.model.authorize.AuthorizeRequestParam.*; + +/** + * @author Javier Rojas Blum + * @version November 19, 2019 + */ +@RequestScoped +@Named("cibaAuthorizeAction") +public class CIBAAuthorizeAction { + + @Inject + private Logger log; + + @Inject + private AppConfiguration appConfiguration; + + public String getApiKey() { + return appConfiguration.getCibaEndUserNotificationConfig().getApiKey(); + } + + public String getAuthDomain() { + return appConfiguration.getCibaEndUserNotificationConfig().getAuthDomain(); + } + + public String getDatabaseURL() { + return appConfiguration.getCibaEndUserNotificationConfig().getDatabaseURL(); + } + + public String getProjectId() { + return appConfiguration.getCibaEndUserNotificationConfig().getProjectId(); + } + + public String getStorageBucket() { + return appConfiguration.getCibaEndUserNotificationConfig().getStorageBucket(); + } + + public String getMessagingSenderId() { + return appConfiguration.getCibaEndUserNotificationConfig().getMessagingSenderId(); + } + + public String getAppId() { + return appConfiguration.getCibaEndUserNotificationConfig().getAppId(); + } + + public String getPublicVapidKey() { + return appConfiguration.getCibaEndUserNotificationConfig().getPublicVapidKey(); + } + + public String getAuthRequest() { + String authorizationEndpoint = appConfiguration.getAuthorizationEndpoint(); + String clientId = appConfiguration.getBackchannelClientId(); + String redirectUri = appConfiguration.getBackchannelRedirectUri(); + String responseType = "token id_token"; + String scope = "openid"; + String state = UUID.randomUUID().toString(); + String nonce = UUID.randomUUID().toString(); + + RedirectUri authRequest = new RedirectUri(authorizationEndpoint); + authRequest.addResponseParameter(CLIENT_ID, clientId); + authRequest.addResponseParameter(REDIRECT_URI, redirectUri); + authRequest.addResponseParameter(RESPONSE_TYPE, responseType); + authRequest.addResponseParameter(SCOPE, scope); + authRequest.addResponseParameter(STATE, state); + authRequest.addResponseParameter(NONCE, nonce); + + return authRequest.toString(); + } + + public String getBackchannelDeviceRegistrationEndpoint() { + return appConfiguration.getBackchannelDeviceRegistrationEndpoint(); + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBAAuthorizeParamsValidatorService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBAAuthorizeParamsValidatorService.java new file mode 100644 index 00000000..de2bb206 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBAAuthorizeParamsValidatorService.java @@ -0,0 +1,143 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ciba; + +import org.apache.commons.lang.BooleanUtils; +import org.apache.logging.log4j.util.Strings; +import org.gluu.oxauth.model.common.BackchannelTokenDeliveryMode; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.error.DefaultErrorResponse; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.core.Response; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +import static org.gluu.oxauth.model.ciba.BackchannelAuthenticationErrorResponseType.*; + +/** + * @author Javier Rojas Blum + * @version April 22, 2020 + */ +@ApplicationScoped +public class CIBAAuthorizeParamsValidatorService { + + @Inject + private AppConfiguration appConfiguration; + + public DefaultErrorResponse validateParams( + List scopeList, String clientNotificationToken, BackchannelTokenDeliveryMode tokenDeliveryMode, + String loginHintToken, String idTokenHint, String loginHint, String bindingMessage, + Boolean backchannelUserCodeParameter, String userCodeParam, String userCode, Integer requestedExpirity) { + if (tokenDeliveryMode == null) { + DefaultErrorResponse errorResponse = new DefaultErrorResponse(); + errorResponse.setStatus(Response.Status.BAD_REQUEST.getStatusCode()); + errorResponse.setType(UNAUTHORIZED_CLIENT); + errorResponse.setReason( + "Clients registering to use CIBA must indicate a token delivery mode."); + + return errorResponse; + } + + if (scopeList == null || !scopeList.contains("openid")) { + DefaultErrorResponse errorResponse = new DefaultErrorResponse(); + errorResponse.setStatus(Response.Status.BAD_REQUEST.getStatusCode()); + errorResponse.setType(INVALID_SCOPE); + errorResponse.setReason( + "CIBA authentication requests must contain the openid scope value."); + + return errorResponse; + } + + if (!validateOneParamNotBlank(loginHintToken, idTokenHint, loginHint)) { + DefaultErrorResponse errorResponse = new DefaultErrorResponse(); + errorResponse.setStatus(Response.Status.BAD_REQUEST.getStatusCode()); + errorResponse.setType(INVALID_REQUEST); + errorResponse.setReason( + "It is required that the Client provides one (and only one) of the hints in the authentication " + + "request, that is login_hint_token, id_token_hint or login_hint."); + + return errorResponse; + } + + if (tokenDeliveryMode == BackchannelTokenDeliveryMode.PING || tokenDeliveryMode == BackchannelTokenDeliveryMode.PUSH) { + if (Strings.isBlank(clientNotificationToken)) { + DefaultErrorResponse errorResponse = new DefaultErrorResponse(); + errorResponse.setStatus(Response.Status.BAD_REQUEST.getStatusCode()); + errorResponse.setType(INVALID_REQUEST); + errorResponse.setReason( + "The client notification token is required if the Client is registered to use Ping or Push modes."); + + return errorResponse; + } + } + + if(Strings.isNotBlank(bindingMessage)) { + final Pattern pattern = Pattern.compile(appConfiguration.getBackchannelBindingMessagePattern()); + if (!pattern.matcher(bindingMessage).matches()) { + DefaultErrorResponse errorResponse = new DefaultErrorResponse(); + errorResponse.setStatus(Response.Status.BAD_REQUEST.getStatusCode()); + errorResponse.setType(INVALID_BINDING_MESSAGE); + errorResponse.setReason("The provided binding message is unacceptable. It must match the pattern: " + pattern.pattern()); + + return errorResponse; + } + } + + if (BooleanUtils.isTrue(backchannelUserCodeParameter)) { + if (Strings.isBlank(userCodeParam)) { + DefaultErrorResponse errorResponse = new DefaultErrorResponse(); + errorResponse.setStatus(Response.Status.BAD_REQUEST.getStatusCode()); + errorResponse.setType(INVALID_USER_CODE); + errorResponse.setReason("The user code is required."); + + return errorResponse; + } else if (Strings.isBlank(userCode)) { + DefaultErrorResponse errorResponse = new DefaultErrorResponse(); + errorResponse.setStatus(Response.Status.BAD_REQUEST.getStatusCode()); + errorResponse.setType(INVALID_USER_CODE); + errorResponse.setReason("The user code is not set."); + + return errorResponse; + } else if (!userCode.equals(userCodeParam)) { + DefaultErrorResponse errorResponse = new DefaultErrorResponse(); + errorResponse.setStatus(Response.Status.BAD_REQUEST.getStatusCode()); + errorResponse.setType(INVALID_USER_CODE); + errorResponse.setReason("The user code is not valid."); + + return errorResponse; + } + } + + if (requestedExpirity != null && (requestedExpirity < 1 + || requestedExpirity > appConfiguration.getCibaMaxExpirationTimeAllowedSec())) { + DefaultErrorResponse errorResponse = new DefaultErrorResponse(); + errorResponse.setStatus(Response.Status.BAD_REQUEST.getStatusCode()); + errorResponse.setType(INVALID_REQUEST); + errorResponse.setReason("Requested expirity is not allowed."); + + return errorResponse; + } + + return null; + } + + private boolean validateOneParamNotBlank(String... params) { + List notBlankParams = new ArrayList<>(); + + for (String param : params) { + if (Strings.isNotBlank(param)) { + notBlankParams.add(param); + } + } + + return notBlankParams.size() == 1; + } + +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBAConfigurationService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBAConfigurationService.java new file mode 100644 index 00000000..e66049ec --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBAConfigurationService.java @@ -0,0 +1,58 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ciba; + +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.enterprise.context.ApplicationScoped; + +import javax.inject.Inject; + +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.*; + +/** + * @author Javier Rojas Blum + * @version August 20, 2019 + */ +@ApplicationScoped +public class CIBAConfigurationService { + + private final static Logger log = LoggerFactory.getLogger(CIBAConfigurationService.class); + + @Inject + private AppConfiguration appConfiguration; + + public void processConfiguration(JSONObject jsonConfiguration) { + try { + jsonConfiguration.put(BACKCHANNEL_AUTHENTICATION_ENDPOINT, appConfiguration.getBackchannelAuthenticationEndpoint()); + + JSONArray backchannelTokenDeliveryModesSupported = new JSONArray(); + for (String item : appConfiguration.getBackchannelTokenDeliveryModesSupported()) { + backchannelTokenDeliveryModesSupported.put(item); + } + if (backchannelTokenDeliveryModesSupported.length() > 0) { + jsonConfiguration.put(BACKCHANNEL_TOKEN_DELIVERY_MODES_SUPPORTED, backchannelTokenDeliveryModesSupported); + } + + JSONArray backchannelAuthenticationRequestSigningAlgValuesSupported = new JSONArray(); + for (String item : appConfiguration.getBackchannelAuthenticationRequestSigningAlgValuesSupported()) { + backchannelAuthenticationRequestSigningAlgValuesSupported.put(item); + } + if (backchannelAuthenticationRequestSigningAlgValuesSupported.length() > 0) { + jsonConfiguration.put(BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG_VALUES_SUPPORTED, backchannelAuthenticationRequestSigningAlgValuesSupported); + } + + jsonConfiguration.put(BACKCHANNEL_USER_CODE_PAREMETER_SUPPORTED, appConfiguration.getBackchannelUserCodeParameterSupported()); + } catch (Exception e) { + log.error("Failed to process CIBA configuration.", e); + } + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBADeviceRegistrationValidatorService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBADeviceRegistrationValidatorService.java new file mode 100644 index 00000000..fdca3574 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBADeviceRegistrationValidatorService.java @@ -0,0 +1,47 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ciba; + +import org.apache.logging.log4j.util.Strings; +import org.gluu.oxauth.model.error.DefaultErrorResponse; + +import javax.enterprise.context.ApplicationScoped; + +import javax.ws.rs.core.Response; + +import static org.gluu.oxauth.model.ciba.BackchannelAuthenticationErrorResponseType.INVALID_REQUEST; +import static org.gluu.oxauth.model.ciba.BackchannelAuthenticationErrorResponseType.UNKNOWN_USER_ID; + +/** + * @author Javier Rojas Blum + * @version October 7, 2019 + */ +@ApplicationScoped +public class CIBADeviceRegistrationValidatorService { + + public DefaultErrorResponse validateParams(String idTokenHint, String deviceRegistrationToken) { + if (Strings.isBlank(deviceRegistrationToken)) { + DefaultErrorResponse errorResponse = new DefaultErrorResponse(); + errorResponse.setStatus(Response.Status.BAD_REQUEST.getStatusCode()); // 400 + errorResponse.setType(INVALID_REQUEST); + errorResponse.setReason("The device registration token cannot be blank."); + + return errorResponse; + } + + if (Strings.isBlank(idTokenHint)) { + DefaultErrorResponse errorResponse = new DefaultErrorResponse(); + errorResponse.setStatus(Response.Status.BAD_REQUEST.getStatusCode()); // 400 + errorResponse.setType(UNKNOWN_USER_ID); + errorResponse.setReason("The id token hint cannot be blank."); + + return errorResponse; + } + + return null; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBAEndUserNotificationService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBAEndUserNotificationService.java new file mode 100644 index 00000000..25ed4e15 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBAEndUserNotificationService.java @@ -0,0 +1,99 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ciba; + +import org.gluu.oxauth.client.ciba.fcm.FirebaseCloudMessagingClient; +import org.gluu.oxauth.client.ciba.fcm.FirebaseCloudMessagingRequest; +import org.gluu.oxauth.client.ciba.fcm.FirebaseCloudMessagingResponse; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.service.ciba.CibaEncryptionService; +import org.gluu.oxauth.service.external.ExternalCibaEndUserNotificationService; +import org.gluu.oxauth.service.external.context.ExternalCibaEndUserNotificationContext; +import org.gluu.oxauth.util.RedirectUri; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.enterprise.context.ApplicationScoped; + +import javax.inject.Inject; +import java.util.UUID; + +import static org.gluu.oxauth.model.authorize.AuthorizeRequestParam.*; + +/** + * @author Javier Rojas Blum + * @version October 7, 2019 + */ +@ApplicationScoped +public class CIBAEndUserNotificationService { + + private final static Logger log = LoggerFactory.getLogger(CIBAEndUserNotificationService.class); + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private CibaEncryptionService cibaEncryptionService; + + @Inject + private ExternalCibaEndUserNotificationService externalCibaEndUserNotificationService; + + public void notifyEndUser(String scope, String acrValues, String authReqId, String deviceRegistrationToken) { + try { + if (externalCibaEndUserNotificationService.isEnabled()) { + log.debug("CIBA: Authorization request sending to the end user with custom interception scripts"); + ExternalCibaEndUserNotificationContext context = new ExternalCibaEndUserNotificationContext(scope, + acrValues, authReqId, deviceRegistrationToken, appConfiguration, cibaEncryptionService); + log.info("CIBA: Notification sent to the end user, result {}", + externalCibaEndUserNotificationService.executeExternalNotifyEndUser(context)); + } else { + this.notifyEndUserUsingFCM(scope, acrValues, authReqId, deviceRegistrationToken); + } + } catch (Exception e) { + log.info("Error when it was sending the notification to the end user to validate the Ciba authorization", e); + } + } + + /** + * Method responsible to send notifications to the end user using Firebase Cloud Messaging. + * @param deviceRegistrationToken Device already registered. + * @param scope Scope of the authorization request + * @param acrValues Acr values used to the authorzation request + * @param authReqId Authentication request id. + */ + private void notifyEndUserUsingFCM(String scope, String acrValues, String authReqId, String deviceRegistrationToken) { + String clientId = appConfiguration.getBackchannelClientId(); + String redirectUri = appConfiguration.getBackchannelRedirectUri(); + String url = appConfiguration.getCibaEndUserNotificationConfig().getNotificationUrl(); + String key = cibaEncryptionService.decrypt(appConfiguration.getCibaEndUserNotificationConfig() + .getNotificationKey(), true); + String to = deviceRegistrationToken; + String title = "oxAuth Authentication Request"; + String body = "Client Initiated Backchannel Authentication (CIBA)"; + + RedirectUri authorizationRequestUri = new RedirectUri(appConfiguration.getAuthorizationEndpoint()); + authorizationRequestUri.addResponseParameter(CLIENT_ID, clientId); + authorizationRequestUri.addResponseParameter(RESPONSE_TYPE, "id_token"); + authorizationRequestUri.addResponseParameter(SCOPE, scope); + authorizationRequestUri.addResponseParameter(ACR_VALUES, acrValues); + authorizationRequestUri.addResponseParameter(REDIRECT_URI, redirectUri); + authorizationRequestUri.addResponseParameter(STATE, UUID.randomUUID().toString()); + authorizationRequestUri.addResponseParameter(NONCE, UUID.randomUUID().toString()); + authorizationRequestUri.addResponseParameter(PROMPT, "consent"); + authorizationRequestUri.addResponseParameter(AUTH_REQ_ID, authReqId); + + String clickAction = authorizationRequestUri.toString(); + + FirebaseCloudMessagingRequest firebaseCloudMessagingRequest = new FirebaseCloudMessagingRequest(key, to, title, body, clickAction); + FirebaseCloudMessagingClient firebaseCloudMessagingClient = new FirebaseCloudMessagingClient(url); + firebaseCloudMessagingClient.setRequest(firebaseCloudMessagingRequest); + FirebaseCloudMessagingResponse firebaseCloudMessagingResponse = firebaseCloudMessagingClient.exec(); + + log.debug("CIBA: firebase cloud messaging result status " + firebaseCloudMessagingResponse.getStatus()); + } + +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBAPingCallbackService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBAPingCallbackService.java new file mode 100644 index 00000000..bc41873b --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBAPingCallbackService.java @@ -0,0 +1,44 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ciba; + +import org.gluu.oxauth.client.ciba.ping.PingCallbackClient; +import org.gluu.oxauth.client.ciba.ping.PingCallbackRequest; +import org.gluu.oxauth.client.ciba.ping.PingCallbackResponse; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.enterprise.context.ApplicationScoped; + +import javax.inject.Inject; + +/** + * @author Javier Rojas Blum + * @version December 21, 2019 + */ +@ApplicationScoped +public class CIBAPingCallbackService { + + private final static Logger log = LoggerFactory.getLogger(CIBAPingCallbackService.class); + + @Inject + private AppConfiguration appConfiguration; + + public void pingCallback(String authReqId, String clientNotificationEndpoint, String clientNotificationToken) { + PingCallbackRequest pingCallbackRequest = new PingCallbackRequest(); + + pingCallbackRequest.setClientNotificationToken(clientNotificationToken); + pingCallbackRequest.setAuthReqId(authReqId); + + PingCallbackClient pingCallbackClient = new PingCallbackClient(clientNotificationEndpoint, appConfiguration.getFapiCompatibility()); + pingCallbackClient.setRequest(pingCallbackRequest); + PingCallbackResponse pingCallbackResponse = pingCallbackClient.exec(); + + log.debug("CIBA: ping callback result status " + pingCallbackResponse.getStatus()); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBAPushErrorService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBAPushErrorService.java new file mode 100644 index 00000000..96936a44 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBAPushErrorService.java @@ -0,0 +1,42 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ciba; + +import org.gluu.oxauth.client.ciba.push.PushErrorClient; +import org.gluu.oxauth.client.ciba.push.PushErrorRequest; +import org.gluu.oxauth.client.ciba.push.PushErrorResponse; +import org.gluu.oxauth.model.ciba.PushErrorResponseType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.enterprise.context.ApplicationScoped; + +/** + * @author Javier Rojas Blum + * @version May 9, 2020 + */ +@ApplicationScoped +public class CIBAPushErrorService { + + private final static Logger log = LoggerFactory.getLogger(CIBAPushErrorService.class); + + public void pushError(String authReqId, String clientNotificationEndpoint, String clientNotificationToken, + PushErrorResponseType error, String errorDescription) { + PushErrorRequest pushErrorRequest = new PushErrorRequest(); + + pushErrorRequest.setClientNotificationToken(clientNotificationToken); + pushErrorRequest.setAuthReqId(authReqId); + pushErrorRequest.setErrorType(error); + pushErrorRequest.setErrorDescription(errorDescription); + + PushErrorClient pushErrorClient = new PushErrorClient(clientNotificationEndpoint); + pushErrorClient.setRequest(pushErrorRequest); + PushErrorResponse pushErrorResponse = pushErrorClient.exec(); + + log.debug("CIBA: push error result status " + pushErrorResponse.getStatus()); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBAPushTokenDeliveryService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBAPushTokenDeliveryService.java new file mode 100644 index 00000000..7058542c --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBAPushTokenDeliveryService.java @@ -0,0 +1,45 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ciba; + +import org.gluu.oxauth.client.ciba.push.PushTokenDeliveryClient; +import org.gluu.oxauth.client.ciba.push.PushTokenDeliveryRequest; +import org.gluu.oxauth.client.ciba.push.PushTokenDeliveryResponse; +import org.gluu.oxauth.model.common.TokenType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.enterprise.context.ApplicationScoped; + +/** + * @author Javier Rojas Blum + * @version September 4, 2019 + */ +@ApplicationScoped +public class CIBAPushTokenDeliveryService { + + private final static Logger log = LoggerFactory.getLogger(CIBAPushTokenDeliveryService.class); + + public void pushTokenDelivery(String authReqId, String clientNotificationEndpoint, String clientNotificationToken, + String accessToken, String refreshToken, String idToken, Integer expiresIn) { + PushTokenDeliveryRequest pushTokenDeliveryRequest = new PushTokenDeliveryRequest(); + + pushTokenDeliveryRequest.setClientNotificationToken(clientNotificationToken); + pushTokenDeliveryRequest.setAuthReqId(authReqId); + pushTokenDeliveryRequest.setAccessToken(accessToken); + pushTokenDeliveryRequest.setTokenType(TokenType.BEARER); + pushTokenDeliveryRequest.setRefreshToken(refreshToken); + pushTokenDeliveryRequest.setExpiresIn(expiresIn); + pushTokenDeliveryRequest.setIdToken(idToken); + + PushTokenDeliveryClient pushTokenDeliveryClient = new PushTokenDeliveryClient(clientNotificationEndpoint); + pushTokenDeliveryClient.setRequest(pushTokenDeliveryRequest); + PushTokenDeliveryResponse pushTokenDeliveryResponse = pushTokenDeliveryClient.exec(); + + log.debug("CIBA: push token delivery result status " + pushTokenDeliveryResponse.getStatus()); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBARegisterClientMetadataService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBARegisterClientMetadataService.java new file mode 100644 index 00000000..0e47e665 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBARegisterClientMetadataService.java @@ -0,0 +1,47 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ciba; + +import org.apache.commons.lang.BooleanUtils; +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.common.BackchannelTokenDeliveryMode; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.crypto.signature.AsymmetricSignatureAlgorithm; +import org.gluu.oxauth.model.registration.Client; + +import javax.enterprise.context.ApplicationScoped; + +import javax.inject.Inject; + +/** + * @author Javier Rojas Blum + * @version August 20, 2019 + */ +@ApplicationScoped +public class CIBARegisterClientMetadataService { + + @Inject + private AppConfiguration appConfiguration; + + public void updateClient(Client client, BackchannelTokenDeliveryMode backchannelTokenDeliveryMode, + String backchannelClientNotificationEndpoint, AsymmetricSignatureAlgorithm backchannelAuthenticationRequestSigningAlg, + Boolean backchannelUserCodeParameter) { + if (backchannelTokenDeliveryMode != null) { + client.setBackchannelTokenDeliveryMode(backchannelTokenDeliveryMode); + } + if (StringUtils.isNotBlank(backchannelClientNotificationEndpoint)) { + client.setBackchannelClientNotificationEndpoint(backchannelClientNotificationEndpoint); + } + if (backchannelAuthenticationRequestSigningAlg != null) { + client.setBackchannelAuthenticationRequestSigningAlg(backchannelAuthenticationRequestSigningAlg); + } + if (BooleanUtils.isTrue(appConfiguration.getBackchannelUserCodeParameterSupported()) + && backchannelUserCodeParameter != null) { + client.setBackchannelUserCodeParameter(backchannelUserCodeParameter); + } + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBARegisterClientResponseService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBARegisterClientResponseService.java new file mode 100644 index 00000000..bc0af05f --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBARegisterClientResponseService.java @@ -0,0 +1,39 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ciba; + +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.util.Util; +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.enterprise.context.ApplicationScoped; + +import static org.gluu.oxauth.model.register.RegisterRequestParam.*; + +/** + * @author Javier Rojas Blum + * @version August 20, 2019 + */ +@ApplicationScoped +public class CIBARegisterClientResponseService { + + private final static Logger log = LoggerFactory.getLogger(CIBARegisterClientResponseService.class); + + public void updateResponse(JSONObject responseJsonObject, Client client) { + try { + Util.addToJSONObjectIfNotNull(responseJsonObject, BACKCHANNEL_TOKEN_DELIVERY_MODE.toString(), client.getBackchannelTokenDeliveryMode()); + Util.addToJSONObjectIfNotNull(responseJsonObject, BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT.toString(), client.getBackchannelClientNotificationEndpoint()); + Util.addToJSONObjectIfNotNull(responseJsonObject, BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG.toString(), client.getBackchannelAuthenticationRequestSigningAlg()); + Util.addToJSONObjectIfNotNull(responseJsonObject, BACKCHANNEL_USER_CODE_PARAMETER.toString(), client.getBackchannelUserCodeParameter()); + } catch (JSONException e) { + log.error("Failed to update response.", e); + } + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBARegisterParamsValidatorService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBARegisterParamsValidatorService.java new file mode 100644 index 00000000..11b629ab --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/ciba/CIBARegisterParamsValidatorService.java @@ -0,0 +1,126 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.ciba; + +import org.apache.logging.log4j.util.Strings; +import org.gluu.oxauth.model.common.BackchannelTokenDeliveryMode; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.SubjectType; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.crypto.signature.AsymmetricSignatureAlgorithm; +import org.gluu.oxauth.model.util.Util; +import org.json.JSONArray; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.enterprise.context.ApplicationScoped; + +import javax.inject.Inject; +import javax.ws.rs.HttpMethod; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.Response; + +import java.util.List; + +import static org.gluu.oxauth.model.common.BackchannelTokenDeliveryMode.*; +import static org.gluu.oxauth.model.common.GrantType.CIBA; + +/** + * @author Javier Rojas Blum + * @version May 20, 2020 + */ +@ApplicationScoped +public class CIBARegisterParamsValidatorService { + + private final static Logger log = LoggerFactory.getLogger(CIBARegisterParamsValidatorService.class); + + @Inject + private AppConfiguration appConfiguration; + + public boolean validateParams( + BackchannelTokenDeliveryMode backchannelTokenDeliveryMode, String backchannelClientNotificationEndpoint, + AsymmetricSignatureAlgorithm backchannelAuthenticationRequestSigningAlg, Boolean backchannelUserCodeParameter, + List grantTypes, SubjectType subjectType, String sectorIdentifierUri, String jwks, String jwksUri) { + try { + // Not CIBA Registration + if (backchannelTokenDeliveryMode == null && Strings.isBlank(backchannelClientNotificationEndpoint) && backchannelAuthenticationRequestSigningAlg == null) { + return true; + } + + // Required parameter. + if (backchannelTokenDeliveryMode == null + || !appConfiguration.getBackchannelTokenDeliveryModesSupported().contains(backchannelTokenDeliveryMode.getValue())) { + return false; + } + + // Required if the token delivery mode is set to ping or push. + if ((backchannelTokenDeliveryMode == PING || backchannelTokenDeliveryMode == PUSH) + && Strings.isBlank(backchannelClientNotificationEndpoint)) { + return false; + } + + // Grant type urn:openid:params:grant-type:ciba is required if the token delivery mode is set to ping or poll. + if (backchannelTokenDeliveryMode == PING || backchannelTokenDeliveryMode == POLL) { + if (!appConfiguration.getGrantTypesSupported().contains(CIBA) || !grantTypes.contains(CIBA)) { + return false; + } + } + + // If the server does not support backchannel_user_code_parameter_supported, the default value is false. + if (appConfiguration.getBackchannelUserCodeParameterSupported() == null || appConfiguration.getBackchannelUserCodeParameterSupported() == false) { + backchannelUserCodeParameter = false; + } + + if (subjectType != null && subjectType == SubjectType.PAIRWISE) { + + if (backchannelTokenDeliveryMode == PING || backchannelTokenDeliveryMode == POLL) { + if (Strings.isBlank(jwks) && Strings.isBlank(jwksUri)) { + return false; + } + } + + if (Strings.isNotBlank(sectorIdentifierUri)) { + javax.ws.rs.client.Client clientRequest = ClientBuilder.newClient(); + String entity = null; + try { + Response clientResponse = clientRequest.target(sectorIdentifierUri).request().buildGet().invoke(); + int status = clientResponse.getStatus(); + + if (status != 200) { + return false; + } + + entity = clientResponse.readEntity(String.class); + } finally { + clientRequest.close(); + } + + JSONArray sectorIdentifierJsonArray = new JSONArray(entity); + + if (backchannelTokenDeliveryMode == PING || backchannelTokenDeliveryMode == POLL) { + // If a sector_identifier_uri is explicitly provided, then the jwks_uri must be included in the list of + // URIs pointed to by the sector_identifier_uri. + if (!Strings.isBlank(jwksUri) && !Util.asList(sectorIdentifierJsonArray).contains(jwksUri)) { + return false; + } + } else if (backchannelTokenDeliveryMode == PUSH) { + // In case a sector_identifier_uri is explicitly provided, then the backchannel_client_notification_endpoint + // must be included in the list of URIs pointed to by the sector_identifier_uri. + if (!Util.asList(sectorIdentifierJsonArray).contains(backchannelClientNotificationEndpoint)) { + return false; + } + } + } + } + } catch (Exception e) { + log.trace(e.getMessage(), e); + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/clientinfo/ws/rs/ClientInfoRestWebService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/clientinfo/ws/rs/ClientInfoRestWebService.java new file mode 100644 index 00000000..d5ee72d6 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/clientinfo/ws/rs/ClientInfoRestWebService.java @@ -0,0 +1,37 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.clientinfo.ws.rs; + +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; + +/** + * Provides interface for Client Info REST web services + * + * @author Javier Rojas Blum Date: 07.19.2012 + */ +public interface ClientInfoRestWebService { + + @GET + @Path("/clientinfo") + @Produces({MediaType.APPLICATION_JSON}) + Response requestClientInfoGet( + @QueryParam("access_token") String accessToken, + @HeaderParam("Authorization") String authorization, + @Context SecurityContext securityContext); + + @POST + @Path("/clientinfo") + @Produces({MediaType.APPLICATION_JSON}) + Response requestClientInfoPost( + @FormParam("access_token") String accessToken, + @HeaderParam("Authorization") String authorization, + @Context SecurityContext securityContext); +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/clientinfo/ws/rs/ClientInfoRestWebServiceImpl.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/clientinfo/ws/rs/ClientInfoRestWebServiceImpl.java new file mode 100644 index 00000000..d11bb1af --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/clientinfo/ws/rs/ClientInfoRestWebServiceImpl.java @@ -0,0 +1,135 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.clientinfo.ws.rs; + +import org.gluu.model.GluuAttribute; +import org.gluu.oxauth.model.clientinfo.ClientInfoErrorResponseType; +import org.gluu.oxauth.model.clientinfo.ClientInfoParamsValidator; +import org.gluu.oxauth.model.common.AbstractToken; +import org.gluu.oxauth.model.common.AuthorizationGrant; +import org.gluu.oxauth.model.common.AuthorizationGrantList; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.service.AttributeService; +import org.gluu.oxauth.service.ClientService; +import org.gluu.oxauth.service.ScopeService; +import org.gluu.oxauth.service.token.TokenService; +import org.gluu.oxauth.util.ServerUtil; +import org.json.JSONException; +import org.json.JSONObject; +import org.oxauth.persistence.model.Scope; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; +import java.util.Set; + +/** + * Provides interface for Client Info REST web services + * + * @author Javier Rojas Blum + * @version 0.9 March 27, 2015 + */ +@Path("/") +public class ClientInfoRestWebServiceImpl implements ClientInfoRestWebService { + + @Inject + private Logger log; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private AuthorizationGrantList authorizationGrantList; + + @Inject + private ScopeService scopeService; + + @Inject + private ClientService clientService; + + @Inject + private AttributeService attributeService; + + @Inject + private TokenService tokenService; + + @Override + public Response requestClientInfoGet(String accessToken, String authorization, SecurityContext securityContext) { + return requestClientInfo(accessToken, authorization, securityContext); + } + + @Override + public Response requestClientInfoPost(String accessToken, String authorization, SecurityContext securityContext) { + return requestClientInfo(accessToken, authorization, securityContext); + } + + public Response requestClientInfo(String accessToken, String authorization, SecurityContext securityContext) { + if (tokenService.isBearerAuthToken(authorization)) { + accessToken = tokenService.getBearerToken(authorization); + } + log.debug("Attempting to request Client Info, Access token = {}, Is Secure = {}", + new Object[] { accessToken, securityContext.isSecure() }); + Response.ResponseBuilder builder = Response.ok(); + + if (!ClientInfoParamsValidator.validateParams(accessToken)) { + builder = Response.status(400); + builder.entity(errorResponseFactory.errorAsJson(ClientInfoErrorResponseType.INVALID_REQUEST, "Failed to validate access token.")); + } else { + AuthorizationGrant authorizationGrant = authorizationGrantList.getAuthorizationGrantByAccessToken(accessToken); + + if (authorizationGrant == null) { + log.trace("Failed to find authorization grant for access token."); + return Response.status(400).entity(errorResponseFactory.getErrorAsJson(ClientInfoErrorResponseType.INVALID_TOKEN,"","Unable to find grant object associated with access token.")).build(); + } + + final AbstractToken token = authorizationGrant.getAccessToken(accessToken); + if (token == null || !token.isValid()) { + log.trace("Invalid access token."); + return Response.status(400).entity(errorResponseFactory.getErrorAsJson(ClientInfoErrorResponseType.INVALID_TOKEN,"","Invalid access token.")).build(); + } + + builder.cacheControl(ServerUtil.cacheControlWithNoStoreTransformAndPrivate()); + builder.header("Pragma", "no-cache"); + builder.entity(getJSonResponse(authorizationGrant.getClient(), authorizationGrant.getScopes())); + } + + return builder.build(); + } + + /** + * Builds a JSon String with the response parameters. + */ + public String getJSonResponse(Client client, Set scopes) { + JSONObject jsonObj = new JSONObject(); + + try { + for (String scopeName : scopes) { + Scope scope = scopeService.getScopeById(scopeName); + + if (scope.getOxAuthClaims() != null) { + for (String claimDn : scope.getOxAuthClaims()) { + GluuAttribute attribute = attributeService.getAttributeByDn(claimDn); + + String attributeName = attribute.getName(); + Object attributeValue = clientService.getAttribute(client, attribute.getName()); + + jsonObj.put(attributeName, attributeValue); + } + } + } + } catch (JSONException e) { + log.error(e.getMessage(), e); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + + return jsonObj.toString(); + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/crypto/cert/CertificateParser.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/crypto/cert/CertificateParser.java new file mode 100644 index 00000000..299c700c --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/crypto/cert/CertificateParser.java @@ -0,0 +1,56 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.crypto.cert; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.IOUtils; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.openssl.PEMParser; +import org.gluu.util.security.SecurityProviderUtility; + +public class CertificateParser { + + public static X509Certificate parsePem(String pemEncodedCert) throws CertificateException { + StringReader sr = new StringReader(pemEncodedCert); + PEMParser pemReader = new PEMParser(sr); + try { + X509CertificateHolder certificateHolder = ((X509CertificateHolder) pemReader.readObject()); + if (certificateHolder == null) { + return null; + } + + X509Certificate cert = new JcaX509CertificateConverter().setProvider(SecurityProviderUtility.getBCProvider()).getCertificate(certificateHolder); + + return cert; + } catch (IOException ex) { + throw new CertificateException(ex); + } finally { + IOUtils.closeQuietly(pemReader); + } + } + + public static X509Certificate parseDer(String base64DerEncodedCert) throws CertificateException { + return parseDer(Base64.decodeBase64(base64DerEncodedCert)); + } + + public static X509Certificate parseDer(byte[] derEncodedCert) throws CertificateException { + return parseDer(new ByteArrayInputStream(derEncodedCert)); + } + + public static X509Certificate parseDer(InputStream is) throws CertificateException { + return (X509Certificate) CertificateFactory.getInstance("X.509", SecurityProviderUtility.getBCProvider()).generateCertificate(is); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/crypto/random/ChallengeGenerator.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/crypto/random/ChallengeGenerator.java new file mode 100644 index 00000000..91818a83 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/crypto/random/ChallengeGenerator.java @@ -0,0 +1,12 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.crypto.random; + +public interface ChallengeGenerator { + + byte[] generateChallenge(); +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/crypto/random/RandomChallengeGenerator.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/crypto/random/RandomChallengeGenerator.java new file mode 100644 index 00000000..077d0811 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/crypto/random/RandomChallengeGenerator.java @@ -0,0 +1,25 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.crypto.random; + +import java.security.SecureRandom; + +import javax.inject.Named; + +@Named("randomChallengeGenerator") +public class RandomChallengeGenerator implements ChallengeGenerator { + + private final SecureRandom random = new SecureRandom(); + + @Override + public byte[] generateChallenge() { + byte[] randomBytes = new byte[32]; + random.nextBytes(randomBytes); + + return randomBytes; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/crypto/signature/SHA256withECDSASignatureVerification.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/crypto/signature/SHA256withECDSASignatureVerification.java new file mode 100644 index 00000000..12bbaf81 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/crypto/signature/SHA256withECDSASignatureVerification.java @@ -0,0 +1,85 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.crypto.signature; + +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.cert.X509Certificate; + +import javax.inject.Named; + +import org.bouncycastle.asn1.sec.SECNamedCurves; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.jce.spec.ECParameterSpec; +import org.bouncycastle.jce.spec.ECPublicKeySpec; +import org.bouncycastle.math.ec.ECPoint; +import org.gluu.oxauth.model.exception.SignatureException; +import org.gluu.util.security.SecurityProviderUtility; + +@Named +public class SHA256withECDSASignatureVerification implements SignatureVerification { + + @Override + public boolean checkSignature(X509Certificate certificate, byte[] signedBytes, byte[] signature) throws SignatureException { + return checkSignature(certificate.getPublicKey(), signedBytes, signature); + } + + @Override + public boolean checkSignature(PublicKey publicKey, byte[] signedBytes, byte[] signature) throws SignatureException { + boolean isValid = false; + try { + Signature ecdsaSignature = Signature.getInstance("SHA256withECDSA", SecurityProviderUtility.getBCProvider()); + ecdsaSignature.initVerify(publicKey); + ecdsaSignature.update(signedBytes); + + isValid = ecdsaSignature.verify(signature); + } catch (GeneralSecurityException ex) { + throw new SignatureException(ex); + } + + return isValid; + } + + @Override + public PublicKey decodePublicKey(byte[] encodedPublicKey) throws SignatureException { + X9ECParameters curve = SECNamedCurves.getByName("secp256r1"); + ECPoint point = curve.getCurve().decodePoint(encodedPublicKey); + + try { + return KeyFactory.getInstance("ECDSA").generatePublic( + new ECPublicKeySpec(point, + new ECParameterSpec( + curve.getCurve(), + curve.getG(), + curve.getN(), + curve.getH() + ) + ) + ); + } catch (GeneralSecurityException ex) { + throw new SignatureException(ex); + } + } + + @Override + public byte[] hash(byte[] bytes) { + try { + return MessageDigest.getInstance("SHA-256").digest(bytes); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + @Override + public byte[] hash(String str) { + return hash(str.getBytes()); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/crypto/signature/SignatureVerification.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/crypto/signature/SignatureVerification.java new file mode 100644 index 00000000..66dd645d --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/crypto/signature/SignatureVerification.java @@ -0,0 +1,26 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.crypto.signature; + +import java.security.PublicKey; +import java.security.cert.X509Certificate; + +import org.gluu.oxauth.model.exception.SignatureException; + +public interface SignatureVerification { + + boolean checkSignature(X509Certificate attestationCertificate, byte[] signedBytes, byte[] signature) throws SignatureException; + + boolean checkSignature(PublicKey publicKey, byte[] signedBytes, byte[] signature) throws SignatureException; + + PublicKey decodePublicKey(byte[] encodedPublicKey) throws SignatureException; + + byte[] hash(byte[] bytes); + + byte[] hash(String str); + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/exception/GlobalExceptionHandler.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/exception/GlobalExceptionHandler.java new file mode 100644 index 00000000..65a44bcb --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/exception/GlobalExceptionHandler.java @@ -0,0 +1,72 @@ +package org.gluu.oxauth.exception; + +import java.util.Iterator; + +import javax.faces.FacesException; +import javax.faces.application.ConfigurableNavigationHandler; +import javax.faces.context.ExceptionHandler; +import javax.faces.context.ExceptionHandlerWrapper; +import javax.faces.context.ExternalContext; +import javax.faces.context.FacesContext; +import javax.faces.event.ExceptionQueuedEvent; +import javax.faces.event.ExceptionQueuedEventContext; + +import org.apache.commons.lang.exception.ExceptionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Created by eugeniuparvan on 8/29/17. + */ +public class GlobalExceptionHandler extends ExceptionHandlerWrapper { + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + private ExceptionHandler wrapped; + + GlobalExceptionHandler(ExceptionHandler exception) { + this.wrapped = exception; + } + + @Override + public ExceptionHandler getWrapped() { + return this.wrapped; + } + + public void handle() throws FacesException { + final Iterator i = getUnhandledExceptionQueuedEvents().iterator(); + + while (i.hasNext()) { + ExceptionQueuedEvent event = i.next(); + ExceptionQueuedEventContext context = (ExceptionQueuedEventContext) event.getSource(); + + Throwable t = context.getException(); + final FacesContext fc = FacesContext.getCurrentInstance(); + final ExternalContext externalContext = fc.getExternalContext(); + try { + if (isInvalidSessionStateException(t)) { + log.error(t.getMessage(), t); + performRedirect(externalContext, "/error_session.htm"); + } else { + log.error(t.getMessage(), t); + performRedirect(externalContext, "/error_service.htm"); + } + fc.renderResponse(); + } finally { + i.remove(); + } + } + getWrapped().handle(); + } + + private boolean isInvalidSessionStateException(Throwable t) { + return ExceptionUtils.getRootCause(t) instanceof org.gluu.oxauth.model.exception.InvalidSessionStateException; + } + + private void performRedirect(ExternalContext externalContext, String viewId) { + try { + externalContext.redirect(externalContext.getRequestContextPath() + viewId); + } catch (Exception e) { + log.error("Can't perform redirect to viewId: " + viewId, e); + } + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/exception/GlobalExceptionHandlerFactory.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/exception/GlobalExceptionHandlerFactory.java new file mode 100644 index 00000000..f5c8def8 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/exception/GlobalExceptionHandlerFactory.java @@ -0,0 +1,20 @@ +package org.gluu.oxauth.exception; + +import javax.faces.context.ExceptionHandler; +import javax.faces.context.ExceptionHandlerFactory; + +/** + * Created by eugeniuparvan on 8/29/17. + */ +public class GlobalExceptionHandlerFactory extends ExceptionHandlerFactory { + private ExceptionHandlerFactory exceptionHandlerFactory; + + public GlobalExceptionHandlerFactory(ExceptionHandlerFactory exceptionHandlerFactory) { + this.exceptionHandlerFactory = exceptionHandlerFactory; + } + + @Override + public ExceptionHandler getExceptionHandler() { + return new GlobalExceptionHandler(exceptionHandlerFactory.getExceptionHandler()); + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/exception/InvalidSchemaUpdateException.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/exception/InvalidSchemaUpdateException.java new file mode 100644 index 00000000..2ba545dc --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/exception/InvalidSchemaUpdateException.java @@ -0,0 +1,28 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.exception; + +/** + * @author Yuriy Movchan Date: 10.15.2010 + */ +public class InvalidSchemaUpdateException extends RuntimeException { + + private static final long serialVersionUID = 3071969232087073304L; + + public InvalidSchemaUpdateException(Throwable root) { + super(root); + } + + public InvalidSchemaUpdateException(String string, Throwable root) { + super(string, root); + } + + public InvalidSchemaUpdateException(String s) { + super(s); + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/exception/UncaughtException.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/exception/UncaughtException.java new file mode 100644 index 00000000..4bdcf679 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/exception/UncaughtException.java @@ -0,0 +1,66 @@ +package org.gluu.oxauth.exception; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.enterprise.inject.Vetoed; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; +import java.net.URI; + +/** + * Created by eugeniuparvan on 8/29/17. + */ +@Provider +@Vetoed +public class UncaughtException extends Throwable implements ExceptionMapper { + + private static final long serialVersionUID = 1L; + + private static final String ERROR_PAGE = "/error_service.htm"; + + private Logger log = LoggerFactory.getLogger(UncaughtException.class); + + @Context + private HttpServletRequest httpRequest; + + @Context + private UriInfo uriInfo; + + public UncaughtException() { + } + + + @Override + public Response toResponse(Throwable exception) { + try { + if (exception instanceof WebApplicationException) { + final Response response = ((WebApplicationException) exception).getResponse(); + if (response != null && response.getStatus() > 0) { + return response; + } + } + log.error("Jersey error.", exception); + return Response.temporaryRedirect(new URI(getRedirectURI())).build(); + } catch (Exception e) { + log.error("Jersey error.", e); + return Response.status(500).entity("Something bad happened. Please try again later!").type("text/plain").build(); + } + } + + private String getRedirectURI() throws Exception { + String baseUri = uriInfo.getBaseUri().toString(); + String contextPath = httpRequest.getContextPath(); + + int startIndex = baseUri.indexOf(contextPath); + if (startIndex == -1) + throw new Exception("Can't build redirect URI"); + + return baseUri.substring(0, startIndex + contextPath.length()) + ERROR_PAGE; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/exception/fido/u2f/BadConfigurationException.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/exception/fido/u2f/BadConfigurationException.java new file mode 100644 index 00000000..da4bf3ad --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/exception/fido/u2f/BadConfigurationException.java @@ -0,0 +1,20 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.exception.fido.u2f; + +public class BadConfigurationException extends RuntimeException { + + private static final long serialVersionUID = -1914683110856700400L; + + public BadConfigurationException(String message) { + super(message); + } + + public BadConfigurationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/exception/fido/u2f/DeviceCompromisedException.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/exception/fido/u2f/DeviceCompromisedException.java new file mode 100644 index 00000000..517a4e15 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/exception/fido/u2f/DeviceCompromisedException.java @@ -0,0 +1,30 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.exception.fido.u2f; + +import org.gluu.oxauth.model.fido.u2f.DeviceRegistration; + +public class DeviceCompromisedException extends Exception { + + private static final long serialVersionUID = -2098466708327419261L; + + private final DeviceRegistration registration; + + public DeviceCompromisedException(DeviceRegistration registration, String message, Throwable cause) { + super(message, cause); + this.registration = registration; + } + + public DeviceCompromisedException(DeviceRegistration registration, String message) { + super(message); + this.registration = registration; + } + + public DeviceRegistration getDeviceRegistration() { + return registration; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/exception/fido/u2f/InvalidDeviceCounterException.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/exception/fido/u2f/InvalidDeviceCounterException.java new file mode 100644 index 00000000..20444bfb --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/exception/fido/u2f/InvalidDeviceCounterException.java @@ -0,0 +1,18 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.exception.fido.u2f; + +import org.gluu.oxauth.model.fido.u2f.DeviceRegistration; + +public class InvalidDeviceCounterException extends DeviceCompromisedException { + + private static final long serialVersionUID = -3393844723613998052L; + + public InvalidDeviceCounterException(DeviceRegistration registration) { + super(registration, "The device's internal counter was was smaller than expected. It's possible that the device has been cloned!"); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/exception/fido/u2f/InvalidKeyHandleDeviceException.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/exception/fido/u2f/InvalidKeyHandleDeviceException.java new file mode 100644 index 00000000..f017429e --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/exception/fido/u2f/InvalidKeyHandleDeviceException.java @@ -0,0 +1,21 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.exception.fido.u2f; + +public class InvalidKeyHandleDeviceException extends Exception { + + private static final long serialVersionUID = 4324358428668365475L; + + public InvalidKeyHandleDeviceException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidKeyHandleDeviceException(String message) { + super(message); + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/exception/fido/u2f/NoEligableDevicesException.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/exception/fido/u2f/NoEligableDevicesException.java new file mode 100644 index 00000000..d30bfa16 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/exception/fido/u2f/NoEligableDevicesException.java @@ -0,0 +1,37 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.exception.fido.u2f; + +import java.util.Collections; +import java.util.List; + +import org.gluu.oxauth.model.fido.u2f.DeviceRegistration; + +public class NoEligableDevicesException extends Exception { + + private static final long serialVersionUID = -7685552584573073454L; + + private final List deviceRegistrations; + + public NoEligableDevicesException(List deviceRegistrations, String message, Throwable cause) { + super(message, cause); + this.deviceRegistrations = Collections.unmodifiableList(deviceRegistrations); + } + + public NoEligableDevicesException(List deviceRegistrations, String message) { + super(message); + this.deviceRegistrations = Collections.unmodifiableList(deviceRegistrations); + } + + public List getDeviceRegistrations() { + return deviceRegistrations; + } + + public boolean hasDevices() { + return !deviceRegistrations.isEmpty(); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/filter/CorsFilter.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/filter/CorsFilter.java new file mode 100644 index 00000000..2dbc9b7d --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/filter/CorsFilter.java @@ -0,0 +1,168 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.filter; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import javax.inject.Inject; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.annotation.WebFilter; +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.codec.binary.Base64; +import org.gluu.oxauth.model.config.ConfigurationFactory; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.util.Util; +import org.gluu.oxauth.service.ClientService; +import org.gluu.server.filters.AbstractCorsFilter; +import org.gluu.util.StringHelper; +import org.slf4j.Logger; + +/** + * CORS Filter to support both Tomcat and Jetty + * + * @author Yuriy Movchan + * @author Javier Rojas Blum + * @version March 20, 2018 + */ +@WebFilter( + filterName = "CorsFilter", + asyncSupported = true, + urlPatterns = {"/.well-known/*", "/restv1/*", "/opiframe"}) +public class CorsFilter extends AbstractCorsFilter { + + @Inject + private Logger log; + + @Inject + private ConfigurationFactory configurationFactory; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private ClientService clientService; + + private boolean filterEnabled; + + public CorsFilter() { + super(); + } + + @Override + public void init(final FilterConfig filterConfig) throws ServletException { + // Initialize defaults + parseAndStore(DEFAULT_ALLOWED_ORIGINS, DEFAULT_ALLOWED_HTTP_METHODS, + DEFAULT_ALLOWED_HTTP_HEADERS, DEFAULT_EXPOSED_HEADERS, + DEFAULT_SUPPORTS_CREDENTIALS, DEFAULT_PREFLIGHT_MAXAGE, + DEFAULT_DECORATE_REQUEST); + + AppConfiguration appConfiguration = configurationFactory.getAppConfiguration(); + + if (filterConfig != null) { + String filterName = filterConfig.getFilterName(); + CorsFilterConfig corsFilterConfig = new CorsFilterConfig(filterName, appConfiguration); + + String configEnabled = corsFilterConfig + .getInitParameter(PARAM_CORS_ENABLED); + String configAllowedOrigins = corsFilterConfig + .getInitParameter(PARAM_CORS_ALLOWED_ORIGINS); + String configAllowedHttpMethods = corsFilterConfig + .getInitParameter(PARAM_CORS_ALLOWED_METHODS); + String configAllowedHttpHeaders = corsFilterConfig + .getInitParameter(PARAM_CORS_ALLOWED_HEADERS); + String configExposedHeaders = corsFilterConfig + .getInitParameter(PARAM_CORS_EXPOSED_HEADERS); + String configSupportsCredentials = corsFilterConfig + .getInitParameter(PARAM_CORS_SUPPORT_CREDENTIALS); + String configPreflightMaxAge = corsFilterConfig + .getInitParameter(PARAM_CORS_PREFLIGHT_MAXAGE); + String configDecorateRequest = corsFilterConfig + .getInitParameter(PARAM_CORS_REQUEST_DECORATE); + + if (configEnabled != null) { + this.filterEnabled = Boolean.parseBoolean(configEnabled); + } + + parseAndStore(configAllowedOrigins, configAllowedHttpMethods, + configAllowedHttpHeaders, configExposedHeaders, + configSupportsCredentials, configPreflightMaxAge, + configDecorateRequest); + } + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + if (this.filterEnabled) { + try { + Collection clientAllowedOrigins = doFilterImpl(servletRequest); + setContextClientAllowedOrigins(servletRequest, clientAllowedOrigins); + } catch (Exception ex) { + log.error("Failed to process request", ex); + } + super.doFilter(servletRequest, servletResponse, filterChain); + } else { + filterChain.doFilter(servletRequest, servletResponse); + } + } + + protected Collection doFilterImpl(ServletRequest servletRequest) + throws UnsupportedEncodingException, IOException, ServletException { + List clientAuthorizedOrigins = null; + + if (StringHelper.isNotEmpty(servletRequest.getParameter("client_id"))) { + String clientId = servletRequest.getParameter("client_id"); + Client client = clientService.getClient(clientId); + if (client != null) { + String[] authorizedOriginsArray = client.getAuthorizedOrigins(); + if (authorizedOriginsArray != null && authorizedOriginsArray.length > 0) { + clientAuthorizedOrigins = Arrays.asList(authorizedOriginsArray); + } + } + } else { + final HttpServletRequest httpRequest = (HttpServletRequest) servletRequest; + String header = httpRequest.getHeader("Authorization"); + if (httpRequest.getRequestURI().endsWith("/token")) { + if (header != null && header.startsWith("Basic ")) { + String base64Token = header.substring(6); + String token = new String(Base64.decodeBase64(base64Token), Util.UTF8_STRING_ENCODING); + + String username = ""; + int delim = token.indexOf(":"); + + if (delim != -1) { + username = URLDecoder.decode(token.substring(0, delim), Util.UTF8_STRING_ENCODING); + } + + Client client = clientService.getClient(username); + + if (client != null) { + String[] authorizedOriginsArray = client.getAuthorizedOrigins(); + if (authorizedOriginsArray != null && authorizedOriginsArray.length > 0) { + clientAuthorizedOrigins = Arrays.asList(authorizedOriginsArray); + } + } + } + } + } + + return clientAuthorizedOrigins; + } +} + diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/filter/CorsFilterConfig.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/filter/CorsFilterConfig.java new file mode 100644 index 00000000..b3edb374 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/filter/CorsFilterConfig.java @@ -0,0 +1,119 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.filter; + +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.FilterConfig; +import javax.servlet.ServletContext; + +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.configuration.CorsConfigurationFilter; + +/** + * @author Javier Rojas Blum + * @version February 15, 2017 + */ +public class CorsFilterConfig implements FilterConfig { + + private String filterName; + private Map initParameters; + + /** + * Key to retrieve if filter enabled from {@link CorsConfigurationFilter}. + */ + public static final String PARAM_CORS_ENABLED = "cors.enabled"; + + /** + * Key to retrieve allowed origins from {@link CorsConfigurationFilter}. + */ + public static final String PARAM_CORS_ALLOWED_ORIGINS = "cors.allowed.origins"; + + /** + * Key to retrieve allowed methods from {@link CorsConfigurationFilter}. + */ + public static final String PARAM_CORS_ALLOWED_METHODS = "cors.allowed.methods"; + + /** + * Key to retrieve allowed headers from {@link CorsConfigurationFilter}. + */ + public static final String PARAM_CORS_ALLOWED_HEADERS = "cors.allowed.headers"; + + /** + * Key to retrieve exposed headers from {@link CorsConfigurationFilter}. + */ + public static final String PARAM_CORS_EXPOSED_HEADERS = "cors.exposed.headers"; + + /** + * Key to retrieve support credentials from {@link CorsConfigurationFilter}. + */ + public static final String PARAM_CORS_SUPPORT_CREDENTIALS = "cors.support.credentials"; + + /** + * Key to retrieve logging enabled from {@link CorsConfigurationFilter}. + */ + public static final String PARAM_CORS_LOGGING_ENABLED = "cors.logging.enabled"; + + /** + * Key to retrieve preflight max age from {@link CorsConfigurationFilter}. + */ + public static final String PARAM_CORS_PREFLIGHT_MAXAGE = "cors.preflight.maxage"; + + /** + * Key to determine if request should be decorated {@link CorsConfigurationFilter}. + */ + public static final String PARAM_CORS_REQUEST_DECORATE = "cors.request.decorate"; + + public CorsFilterConfig(String filterName, AppConfiguration appConfiguration) { + this.filterName = filterName; + initParameters = new HashMap(); + + List corsConfigurationFilters = appConfiguration.getCorsConfigurationFilters(); + for (CorsConfigurationFilter corsConfigurationFilter : corsConfigurationFilters) { + if (filterName.equals(corsConfigurationFilter.getFilterName())) { + initParameters.put(PARAM_CORS_ENABLED, corsConfigurationFilter.getCorsEnabled().toString()); + initParameters.put(PARAM_CORS_ALLOWED_ORIGINS, corsConfigurationFilter.getCorsAllowedOrigins()); + initParameters.put(PARAM_CORS_ALLOWED_METHODS, corsConfigurationFilter.getCorsAllowedMethods()); + initParameters.put(PARAM_CORS_ALLOWED_HEADERS, corsConfigurationFilter.getCorsAllowedHeaders()); + initParameters.put(PARAM_CORS_EXPOSED_HEADERS, corsConfigurationFilter.getCorsExposedHeaders()); + initParameters.put(PARAM_CORS_SUPPORT_CREDENTIALS, corsConfigurationFilter.getCorsSupportCredentials().toString()); + initParameters.put(PARAM_CORS_LOGGING_ENABLED, corsConfigurationFilter.getCorsLoggingEnabled().toString()); + initParameters.put(PARAM_CORS_PREFLIGHT_MAXAGE, corsConfigurationFilter.getCorsPreflightMaxAge().toString()); + initParameters.put(PARAM_CORS_REQUEST_DECORATE, corsConfigurationFilter.getCorsRequestDecorate().toString()); + } + } + + } + + @Override + public String getFilterName() { + return filterName; + } + + @Override + public ServletContext getServletContext() { + return null; + } + + @Override + public String getInitParameter(String name) { + if (initParameters == null) { + return (null); + } + + return initParameters.get(name); + } + + @Override + public Enumeration getInitParameterNames() { + return Collections.enumeration(initParameters.keySet()); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/gluu/ws/rs/GluuConfigurationWS.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/gluu/ws/rs/GluuConfigurationWS.java new file mode 100644 index 00000000..d4f6e2e8 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/gluu/ws/rs/GluuConfigurationWS.java @@ -0,0 +1,124 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.gluu.ws.rs; + +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import org.apache.commons.lang.StringUtils; +import org.gluu.model.GluuAttribute; +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.gluu.GluuConfiguration; +import org.gluu.oxauth.model.gluu.GluuErrorResponseType; +import org.gluu.oxauth.service.AttributeService; +import org.gluu.oxauth.service.ScopeService; +import org.gluu.oxauth.service.external.ExternalAuthenticationService; +import org.gluu.oxauth.util.ServerUtil; +import org.oxauth.persistence.model.Scope; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import java.util.*; + +/** + * Created by eugeniuparvan on 8/5/16. + */ +@Path("/.well-known/gluu-configuration") +public class GluuConfigurationWS { + + @Inject + private Logger log; + + @Inject + private ScopeService scopeService; + + @Inject + private AttributeService attributeService; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private ExternalAuthenticationService externalAuthenticationService; + + @GET + @Produces({"application/json"}) + public Response getConfiguration() { + try { + final GluuConfiguration conf = new GluuConfiguration(); + + conf.setIdGenerationEndpoint(appConfiguration.getIdGenerationEndpoint()); + conf.setIntrospectionEndpoint(appConfiguration.getIntrospectionEndpoint()); + conf.setAuthLevelMapping(createAuthLevelMapping()); + conf.setScopeToClaimsMapping(createScopeToClaimsMapping()); + + // convert manually to avoid possible conflicts between resteasy + // providers, e.g. jettison, jackson + final String entity = ServerUtil.asPrettyJson(conf); + log.trace("Gluu configuration: {}", entity); + + return Response.ok(entity).build(); + } catch (Throwable ex) { + log.error(ex.getMessage(), ex); + throw new WebApplicationException(Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(errorResponseFactory.getErrorResponse(GluuErrorResponseType.SERVER_ERROR)).build()); + } + } + + public Map> createAuthLevelMapping() { + Map> map = Maps.newHashMap(); + try { + for (CustomScriptConfiguration script : externalAuthenticationService.getCustomScriptConfigurationsMap()) { + String acr = script.getName(); + int level = script.getLevel(); + + Set acrs = map.get(level); + if (acrs == null) { + acrs = Sets.newHashSet(); + map.put(level, acrs); + } + acrs.add(acr); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return map; + } + + private Map> createScopeToClaimsMapping() { + Map> result = new HashMap>(); + try { + for (Scope scope : scopeService.getAllScopesList()) { + final Set claimsList = new HashSet(); + result.put(scope.getId(), claimsList); + + final List claimIdList = scope.getOxAuthClaims(); + if (claimIdList != null && !claimIdList.isEmpty()) { + for (String claimDn : claimIdList) { + final GluuAttribute attribute = attributeService.getAttributeByDn(claimDn); + final String claimName = attribute.getOxAuthClaimName(); + if (StringUtils.isNotBlank(claimName)) { + claimsList.add(claimName); + } + } + } + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return result; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/i18n/ApplicationFacesLocalizationConfigPopulator.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/i18n/ApplicationFacesLocalizationConfigPopulator.java new file mode 100644 index 00000000..2e450f70 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/i18n/ApplicationFacesLocalizationConfigPopulator.java @@ -0,0 +1,13 @@ +package org.gluu.oxauth.i18n; + +import org.gluu.jsf2.customization.FacesLocalizationConfigPopulator; + +public class ApplicationFacesLocalizationConfigPopulator extends FacesLocalizationConfigPopulator { + private static final String LANGUAGE_FILE_PATTERN = "^oxauth_(.*)\\.properties$"; + + @Override + public String getLanguageFilePattern() { + return LANGUAGE_FILE_PATTERN; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/i18n/CustomResourceBundle.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/i18n/CustomResourceBundle.java new file mode 100644 index 00000000..56b9e38a --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/i18n/CustomResourceBundle.java @@ -0,0 +1,20 @@ +package org.gluu.oxauth.i18n; + +import org.gluu.jsf2.i18n.ExtendedResourceBundle; + +/** + * Custom i18n resource loader + * + * @author Yuriy Movchan + * @version 02/23/2018 + */ +public class CustomResourceBundle extends ExtendedResourceBundle { + + private static final String BASE_NAME = "oxauth"; + + @Override + public String getBaseName() { + return BASE_NAME; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/i18n/LanguageBean.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/i18n/LanguageBean.java new file mode 100644 index 00000000..1ad23724 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/i18n/LanguageBean.java @@ -0,0 +1,175 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.i18n; + +import java.io.Serializable; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.ResourceBundle; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Observes; +import javax.faces.context.FacesContext; +import javax.inject.Inject; +import javax.inject.Named; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang3.tuple.Pair; +import org.apache.logging.log4j.util.Strings; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.service.cdi.event.ConfigurationUpdate; +import org.gluu.util.StringHelper; +import org.gluu.util.locale.LocaleUtil; +import org.slf4j.Logger; + +/** + * @version August 9, 2017 + */ +@Named("language") +@ApplicationScoped +public class LanguageBean implements Serializable { + + private static final long serialVersionUID = -6723715664277907737L; + + private static final String COOKIE_NAME = "org.gluu.i18n.Locale"; + private static final int DEFAULT_MAX_AGE = 31536000; // 1 year in seconds + private static final String COOKIE_PATH = "/"; + + private static final Locale defaultLocale = Locale.ENGLISH; + + @Inject + private Logger log; + + private List supportedLocales; + + public void initSupportedLocales(@Observes @ConfigurationUpdate AppConfiguration appConfiguration) { + this.supportedLocales = buildSupportedLocales(appConfiguration); + } + + @Deprecated + // We need to keep it till 5.0 for compatibility with old xhtml files + public String getLocaleCode() { + try { + Locale locale = getCookieLocale(); + if (locale != null) { + setLocale(locale); + } + + return locale.toLanguageTag(); + } catch (Exception e) { + return defaultLocale.getLanguage(); + } + } + + public Locale getLocale() { + try { + Locale locale = getCookieLocale(); + if (locale != null) { + return locale; + } + } catch (Exception ex) { + log.trace("Failed to get locale from cookie", ex); + } + + return defaultLocale; + } + + public void setLocaleCode(String requestedLocaleCode) { + for (Locale supportedLocale : supportedLocales) { + if (!Strings.isEmpty(supportedLocale.getLanguage()) && supportedLocale.getLanguage().equals(requestedLocaleCode)) { + Locale locale = new Locale(requestedLocaleCode); + FacesContext.getCurrentInstance().getViewRoot().setLocale(locale); + setCookieValue(locale.toLanguageTag()); + break; + } + } + } + + public void setLocale(Locale requestedLocale) { + for (Locale supportedLocale : supportedLocales) { + if (supportedLocale.equals(requestedLocale)) { + FacesContext.getCurrentInstance().getViewRoot().setLocale(supportedLocale); + setCookieValue(supportedLocale.toLanguageTag()); + break; + } + } + + // If there is no supported locale attempt to find it by language + setLocaleCode(requestedLocale.getLanguage()); + } + + public List getSupportedLocales() { + return supportedLocales; + } + + private List buildSupportedLocales(AppConfiguration appConfiguration) { + List uiLocales = appConfiguration.getUiLocalesSupported(); + + List supportedLocales = new LinkedList(); + for (String uiLocale : uiLocales) { + Pair> locales = LocaleUtil.toLocaleList(uiLocale); + + supportedLocales.addAll(locales.getRight()); + } + + return supportedLocales; + } + + public String getMessage(String key) { + FacesContext context = FacesContext.getCurrentInstance(); + ResourceBundle bundle = context.getApplication().getResourceBundle(context, "msgs"); + String result; + try { + result = bundle.getString(key); + } catch (MissingResourceException e) { + result = "???" + key + "??? not found"; + } + return result; + } + + private void setCookieValue(String value) { + FacesContext ctx = FacesContext.getCurrentInstance(); + + if (ctx == null) + return; + HttpServletResponse response = (HttpServletResponse) ctx.getExternalContext().getResponse(); + Cookie cookie = new Cookie(COOKIE_NAME, value); + cookie.setMaxAge(DEFAULT_MAX_AGE); + cookie.setPath(COOKIE_PATH); + cookie.setSecure(true); + cookie.setVersion(1); + response.addCookie(cookie); + } + + private String getCookieValue() { + Cookie cookie = getCookie(); + return cookie == null ? null : cookie.getValue(); + } + + private Locale getCookieLocale() { + String cookieValue = getCookieValue(); + if (StringHelper.isEmpty(cookieValue)) { + return null; + } + + Locale locale = Locale.forLanguageTag(cookieValue); + + return locale; + } + + private Cookie getCookie() { + FacesContext ctx = FacesContext.getCurrentInstance(); + if (ctx != null) { + return (Cookie) ctx.getExternalContext().getRequestCookieMap().get(COOKIE_NAME); + } else { + return null; + } + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/idgen/ws/rs/IdGenService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/idgen/ws/rs/IdGenService.java new file mode 100644 index 00000000..4e70ca26 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/idgen/ws/rs/IdGenService.java @@ -0,0 +1,52 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.idgen.ws.rs; + +import javax.inject.Inject; + +import org.gluu.oxauth.model.common.IdType; +import org.gluu.oxauth.service.common.api.IdGenerator; +import org.gluu.oxauth.service.external.ExternalIdGeneratorService; +import org.gluu.util.StringHelper; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 24/06/2013 + */ +@ApplicationScoped +public class IdGenService implements IdGenerator { + + @Inject + private Logger log; + + @Inject + private InumGenerator inumGenerator; + + @Inject + private ExternalIdGeneratorService externalIdGeneratorService; + + public String generateId(IdType p_idType, String p_idPrefix) { + return generateId(p_idType.getType(), p_idPrefix); + } + + @Override + public String generateId(String p_idType, String p_idPrefix) { + if (externalIdGeneratorService.isEnabled()) { + final String generatedId = externalIdGeneratorService.executeExternalDefaultGenerateIdMethod("oxauth", p_idType, p_idPrefix); + + if (StringHelper.isNotEmpty(generatedId)) { + return generatedId; + } + } + + return inumGenerator.generateId(p_idType, p_idPrefix); + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/idgen/ws/rs/InumGenerator.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/idgen/ws/rs/InumGenerator.java new file mode 100644 index 00000000..b1d1d23b --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/idgen/ws/rs/InumGenerator.java @@ -0,0 +1,154 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.idgen.ws.rs; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.inject.Inject; + +import org.apache.commons.lang.StringUtils; +import org.gluu.model.GluuAttribute; +import org.gluu.oxauth.model.common.IdType; +import org.gluu.oxauth.model.common.User; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.persist.model.base.DummyEntry; +import org.gluu.search.filter.Filter; +import org.gluu.util.INumGenerator; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; + +import org.gluu.oxauth.model.config.BaseDnConfiguration; +import org.gluu.oxauth.model.config.StaticConfiguration; +import org.gluu.oxauth.model.gluu.GluuConfiguration; +import org.gluu.oxauth.model.registration.Client; + +/** + * Inum ID generator. Generates inum: e.g. @!1111!0001!1234. + * + * @author Yuriy Zabrovarnyy + * @version 0.9, 26/06/2013 + */ +@ApplicationScoped +public class InumGenerator { + + public static final String SEPARATOR = "!"; + + private static final int MAX = 100; + + private final Pattern baseRdnPattern = Pattern.compile(".+o=([\\w\\!\\@\\.]+)$"); + + @Inject + private Logger log; + @Inject + private PersistenceEntryManager ldapEntryManager; + + @Inject + private StaticConfiguration staticConfiguration; + + public String generateId(String p_idType, String p_idPrefix) { + final IdType idType = IdType.fromString(p_idType); + if (idType != null) { + return generateId(idType, p_idPrefix); + } else { + log.error("Unable to identify id type: {}", p_idType); + } + return ""; + } + + public String generateId(IdType p_idType, String p_idPrefix) { + String inum; + int counter = 0; + + try { + while (true) { + final StringBuilder sb = new StringBuilder(); + sb.append(p_idPrefix). + append(InumGenerator.SEPARATOR). + append(p_idType.getInum()). + append(InumGenerator.SEPARATOR); + + if ((IdType.CLIENTS == p_idType) || (IdType.PEOPLE == p_idType)) { + sb.append(INumGenerator.generate(4)); + } else { + sb.append(INumGenerator.generate(2)); + } + + inum = sb.toString(); + if (StringUtils.isBlank(inum)) { + log.error("Unable to generate inum: {}", inum); + break; + } + + if (!contains(inum, p_idType)) { + break; + } + + /* Just to make sure it doesn't get into an infinite loop */ + if (counter > MAX) { + log.error("Infinite loop problem while generating new inum"); + return ""; + } + counter++; + } + } catch (Exception e) { + log.error(e.getMessage(), e); + inum = e.getMessage(); + } + log.trace("Generated inum: {}", inum); + return inum; + } + + public boolean contains(String inum, IdType type) { + final String baseDn = baseDn(type); + final Filter filter = Filter.createEqualityFilter("inum", inum); + Class entryClass = getEntryClass(type); + final List entries = ldapEntryManager.findEntries(baseDn, entryClass, filter); + return entries != null && !entries.isEmpty(); + } + + private Class getEntryClass(IdType type) { + switch (type) { + case CLIENTS: + return Client.class; + case CONFIGURATION: + return GluuConfiguration.class; + case ATTRIBUTE: + return GluuAttribute.class; + case PEOPLE: + return User.class; + } + + return DummyEntry.class; + } + + public String baseDn(IdType p_type) { + final BaseDnConfiguration baseDn = staticConfiguration.getBaseDn(); + switch (p_type) { + case CLIENTS: + return baseDn.getClients(); + case CONFIGURATION: + return baseDn.getConfiguration(); + case ATTRIBUTE: + return baseDn.getAttributes(); + case PEOPLE: + return baseDn.getPeople(); + } + + // if not able to identify baseDn by type then return organization baseDn, e.g. o=gluu + Matcher m = baseRdnPattern.matcher(baseDn.getClients()); + if (m.matches()) { + return m.group(1); + } + + log.error("Use fallback DN: o=gluu, for ID generator, please check oxAuth configuration, clientDn must be valid DN"); + return "o=gluu"; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/introspection/ws/rs/IntrospectionWebService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/introspection/ws/rs/IntrospectionWebService.java new file mode 100644 index 00000000..87f99b98 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/introspection/ws/rs/IntrospectionWebService.java @@ -0,0 +1,307 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.introspection.ws.rs; + +import com.google.common.collect.Lists; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.claims.Audience; +import org.gluu.oxauth.model.authorize.AuthorizeErrorResponseType; +import org.gluu.oxauth.model.common.*; +import org.gluu.oxauth.model.config.WebKeysConfiguration; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.token.JwtSigner; +import org.gluu.oxauth.model.uma.UmaScopeType; +import org.gluu.oxauth.model.util.Util; +import org.gluu.oxauth.service.AttributeService; +import org.gluu.oxauth.service.ClientService; +import org.gluu.oxauth.service.external.ExternalIntrospectionService; +import org.gluu.oxauth.service.external.context.ExternalIntrospectionContext; +import org.gluu.oxauth.service.token.TokenService; +import org.gluu.oxauth.util.ServerUtil; +import org.gluu.util.Pair; +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Iterator; + +import static org.apache.commons.lang3.BooleanUtils.isTrue; + +/** + * @author Yuriy Zabrovarnyy + * @version June 30, 2018 + */ +@Path("/introspection") +public class IntrospectionWebService { + + private static final Pair EMPTY = new Pair<>(null, false); + + @Inject + private Logger log; + @Inject + private AppConfiguration appConfiguration; + @Inject + private TokenService tokenService; + @Inject + private ErrorResponseFactory errorResponseFactory; + @Inject + private AuthorizationGrantList authorizationGrantList; + @Inject + private ClientService clientService; + @Inject + private ExternalIntrospectionService externalIntrospectionService; + @Inject + private AttributeService attributeService; + @Inject + private WebKeysConfiguration webKeysConfiguration; + + @GET + @Produces(MediaType.APPLICATION_JSON) + public Response introspectGet(@HeaderParam("Authorization") String p_authorization, + @QueryParam("token") String p_token, + @QueryParam("token_type_hint") String tokenTypeHint, + @QueryParam("response_as_jwt") String responseAsJwt, + @Context HttpServletRequest httpRequest, + @Context HttpServletResponse httpResponse + ) { + return introspect(p_authorization, p_token, tokenTypeHint, responseAsJwt, httpRequest, httpResponse); + } + + @POST + @Produces(MediaType.APPLICATION_JSON) + public Response introspectPost(@HeaderParam("Authorization") String p_authorization, + @FormParam("token") String p_token, + @FormParam("token_type_hint") String tokenTypeHint, + @FormParam("response_as_jwt") String responseAsJwt, + @Context HttpServletRequest httpRequest, + @Context HttpServletResponse httpResponse) { + return introspect(p_authorization, p_token, tokenTypeHint, responseAsJwt, httpRequest, httpResponse); + } + + private AuthorizationGrant validateAuthorization(String p_authorization, String p_token) throws IOException { + final boolean skipAuthorization = ServerUtil.isTrue(appConfiguration.getIntrospectionSkipAuthorization()); + log.trace("skipAuthorization: {}", skipAuthorization); + if (skipAuthorization) { + return null; + } + + if (StringUtils.isBlank(p_authorization)) { + log.trace("Bad request: Authorization header or token is blank."); + throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(errorResponseFactory.errorAsJson(AuthorizeErrorResponseType.INVALID_REQUEST, "")).build()); + } + + final Pair pair = getAuthorizationGrant(p_authorization, p_token); + final AuthorizationGrant authorizationGrant = pair.getFirst(); + if (authorizationGrant == null) { + log.debug("Authorization grant is null."); + if (isTrue(pair.getSecond())) { + log.debug("Returned {\"active\":false}."); + throw new WebApplicationException(Response.status(Response.Status.OK) + .entity("{\"active\":false}") + .type(MediaType.APPLICATION_JSON_TYPE) + .build()); + } + throw new WebApplicationException(Response.status(Response.Status.UNAUTHORIZED) + .type(MediaType.APPLICATION_JSON_TYPE) + .entity(errorResponseFactory.errorAsJson(AuthorizeErrorResponseType.ACCESS_DENIED, "Authorization grant is null.")) + .build()); + } + + final AbstractToken authorizationAccessToken = authorizationGrant.getAccessToken(tokenService.getToken(p_authorization)); + + if ((authorizationAccessToken == null || !authorizationAccessToken.isValid()) && !pair.getSecond()) { + log.error("Access token is not valid. Valid: " + (authorizationAccessToken != null && authorizationAccessToken.isValid()) + ", basicClientAuthentication: " + pair.getSecond()); + throw new WebApplicationException(Response.status(Response.Status.UNAUTHORIZED).type(MediaType.APPLICATION_JSON_TYPE).entity(errorResponseFactory.errorAsJson(AuthorizeErrorResponseType.ACCESS_DENIED, "Access token is not valid")).build()); + } + + if (ServerUtil.isTrue(appConfiguration.getIntrospectionAccessTokenMustHaveUmaProtectionScope()) && + !authorizationGrant.getScopesAsString().contains(UmaScopeType.PROTECTION.getValue())) { // #562 - make uma_protection optional + final String reason = "access_token used to access introspection endpoint does not have uma_protection scope, however in oxauth configuration `checkUmaProtectionScopePresenceDuringIntrospection` is true"; + log.trace(reason); + throw new WebApplicationException(Response.status(Response.Status.UNAUTHORIZED).entity(errorResponseFactory.errorAsJson(AuthorizeErrorResponseType.ACCESS_DENIED, reason)).type(MediaType.APPLICATION_JSON_TYPE).build()); + } + return authorizationGrant; + } + + private Response introspect(String p_authorization, String p_token, String tokenTypeHint, String responseAsJwt, HttpServletRequest httpRequest, HttpServletResponse httpResponse) { + try { + log.trace("Introspect token, authorization: {}, token to introspect: {}, tokenTypeHint: {}", p_authorization, p_token, tokenTypeHint); + + AuthorizationGrant authorizationGrant = validateAuthorization(p_authorization, p_token); + + if (StringUtils.isBlank(p_token)) { + log.trace("Bad request: Token is blank."); + return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(errorResponseFactory.errorAsJson(AuthorizeErrorResponseType.INVALID_REQUEST, "")).build(); + } + + final IntrospectionResponse response = new IntrospectionResponse(false); + + final AuthorizationGrant grantOfIntrospectionToken = authorizationGrantList.getAuthorizationGrantByAccessToken(p_token); + + AbstractToken tokenToIntrospect = null; + if (grantOfIntrospectionToken != null) { + tokenToIntrospect = grantOfIntrospectionToken.getAccessToken(p_token); + + response.setActive(tokenToIntrospect.isValid()); + response.setExpiresAt(ServerUtil.dateToSeconds(tokenToIntrospect.getExpirationDate())); + response.setIssuedAt(ServerUtil.dateToSeconds(tokenToIntrospect.getCreationDate())); + response.setAcrValues(grantOfIntrospectionToken.getAcrValues()); + response.setScope(grantOfIntrospectionToken.getScopes() != null ? grantOfIntrospectionToken.getScopes() : Lists.newArrayList()); // #433 + response.setClientId(grantOfIntrospectionToken.getClientId()); + response.setSub(grantOfIntrospectionToken.getSub()); + response.setUsername(grantOfIntrospectionToken.getUserId()); + response.setIssuer(appConfiguration.getIssuer()); + response.setAudience(grantOfIntrospectionToken.getClientId()); + + if (tokenToIntrospect instanceof AccessToken) { + AccessToken accessToken = (AccessToken) tokenToIntrospect; + response.setTokenType(accessToken.getTokenType() != null ? accessToken.getTokenType().getName() : TokenType.BEARER.getName()); + } + } else { + log.debug("Failed to find grant for access_token: " + p_token + ". Return 200 with active=false."); + } + JSONObject responseAsJsonObject = createResponseAsJsonObject(response, grantOfIntrospectionToken); + + ExternalIntrospectionContext context = new ExternalIntrospectionContext(authorizationGrant, httpRequest, httpResponse, appConfiguration, attributeService); + context.setGrantOfIntrospectionToken(grantOfIntrospectionToken); + if (externalIntrospectionService.executeExternalModifyResponse(responseAsJsonObject, context)) { + log.trace("Successfully run extenal introspection scripts."); + } else { + responseAsJsonObject = createResponseAsJsonObject(response, grantOfIntrospectionToken); + log.trace("Canceled changes made by external introspection script since method returned `false`."); + } + + // Make scopes conform as required by spec, see #1499 + if (response.getScope()!= null && !appConfiguration.getIntrospectionResponseScopesBackwardCompatibility()) { + String scopes = StringUtils.join(response.getScope().toArray(), " "); + responseAsJsonObject.put("scope", scopes); + } + if (Boolean.TRUE.toString().equalsIgnoreCase(responseAsJwt)) { + return Response.status(Response.Status.OK).entity(createResponseAsJwt(responseAsJsonObject, grantOfIntrospectionToken)).build(); + } + + final String entity = responseAsJsonObject.toString(); + if (log.isTraceEnabled()) { + log.trace("Response entity: {}", entity); + } + return Response.status(Response.Status.OK).entity(entity).type(MediaType.APPLICATION_JSON_TYPE).build(); + + } catch (WebApplicationException e) { + log.error(e.getMessage(), e); + throw e; + } catch (Exception e) { + log.error(e.getMessage(), e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).type(MediaType.APPLICATION_JSON_TYPE).build(); + } + } + + private String createResponseAsJwt(JSONObject response, AuthorizationGrant grant) throws Exception { + final JwtSigner jwtSigner = JwtSigner.newJwtSigner(appConfiguration, webKeysConfiguration, grant.getClient()); + final Jwt jwt = jwtSigner.newJwt(); + Audience.setAudience(jwt.getClaims(), grant.getClient()); + + Iterator keysIter = response.keys(); + while (keysIter.hasNext()) { + String key = keysIter.next(); + Object value = response.opt(key); + if (value != null) { + try { + jwt.getClaims().setClaimObject(key, value, false); + } catch (Exception e) { + log.error("Failed to put claims into jwt. Key: " + key + ", response: " + response.toString(), e); + } + } + } + + if (log.isTraceEnabled()) { + log.trace("Response before signing: {}", jwt.getClaims().toJsonString()); + } + return jwtSigner.sign().toString(); + } + + private JSONObject createResponseAsJsonObject(IntrospectionResponse response, AuthorizationGrant grantOfIntrospectionToken) throws JSONException, IOException { + final JSONObject result = new JSONObject(ServerUtil.asJson(response)); + + if (log.isTraceEnabled()) { + log.trace("grantOfIntrospectionToken: {}, x5ts256: {}", (grantOfIntrospectionToken != null), (grantOfIntrospectionToken != null ? grantOfIntrospectionToken.getX5ts256() : "")); + } + + if (grantOfIntrospectionToken != null && StringUtils.isNotBlank(grantOfIntrospectionToken.getX5ts256())) { + JSONObject cnf = result.optJSONObject("cnf"); + if (cnf == null) { + cnf = new JSONObject(); + result.put("cnf", cnf); + } + + cnf.put("x5t#S256", grantOfIntrospectionToken.getX5ts256()); + } + + return result; + } + + /** + * @return we return pair of authorization grant or otherwise true - if it's basic client authentication or false if it is not + * @throws UnsupportedEncodingException when encoding is not supported + */ + private Pair getAuthorizationGrant(String authorization, String accessToken) throws UnsupportedEncodingException { + AuthorizationGrant grant = tokenService.getBearerAuthorizationGrant(authorization); + if (grant != null) { + final String authorizationAccessToken = tokenService.getBearerToken(authorization); + final AbstractToken accessTokenObject = grant.getAccessToken(authorizationAccessToken); + if (accessTokenObject != null && accessTokenObject.isValid()) { + return new Pair<>(grant, false); + } else { + log.error("Access token is not valid: " + authorizationAccessToken); + return EMPTY; + } + } + + grant = tokenService.getBasicAuthorizationGrant(authorization); + if (grant != null) { + return new Pair<>(grant, false); + } + if (tokenService.isBasicAuthToken(authorization)) { + + String encodedCredentials = tokenService.getBasicToken(authorization); + + String token = new String(Base64.decodeBase64(encodedCredentials), StandardCharsets.UTF_8); + + int delim = token.indexOf(":"); + + if (delim != -1) { + String clientId = URLDecoder.decode(token.substring(0, delim), Util.UTF8_STRING_ENCODING); + String password = URLDecoder.decode(token.substring(delim + 1), Util.UTF8_STRING_ENCODING); + if (clientService.authenticate(clientId, password)) { + grant = authorizationGrantList.getAuthorizationGrantByAccessToken(accessToken); + if (isTrue(appConfiguration.getIntrospectionRestrictBasicAuthnToOwnTokens()) && grant != null && !grant.getClientId().equals(clientId)) { + log.trace("Failed to match grant object clientId and client id provided during authentication."); + return EMPTY; + } + return new Pair<>(grant, true); + } else { + log.trace("Failed to perform basic authentication for client: {}", clientId); + } + } + } + return EMPTY; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/jwk/ws/rs/JwkRestWebService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/jwk/ws/rs/JwkRestWebService.java new file mode 100644 index 00000000..b6df77c2 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/jwk/ws/rs/JwkRestWebService.java @@ -0,0 +1,49 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.jwk.ws.rs; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; + +/** + *

+ * Provides interface for JWK REST web services + *

+ *

+ * A JSON Web Key (JWK) is a JSON data structure that represents a set of public keys as a JSON object [RFC4627]. + * The JWK format is used to represent bare keys. JSON Web Keys are referenced in JSON Web Signatures (JWSs) + * using the jku (JSON Key URL) header parameter. + *

+ *

+ * It is sometimes useful to be able to reference public key representations, for instance, in order to verify the + * signature on content signed with the corresponding private key. The JSON Web Key (JWK) data structure provides a + * convenient JSON representation for sets of public keys utilizing either the Elliptic Curve or RSA families of + * algorithms. + *

+ * + * @author Javier Rojas Blum Date: 11.15.2011 + */ +public interface JwkRestWebService { + + /** + * The JWK endpoint. + * + * @param securityContext An injectable interface that provides access to security + * related information. + * @return The JSON Web Key data structure JWK. A JWK consists of a JWK Container Object, which is a JSON object + * that contains an array of JWK Key Objects as a member. + */ + @GET + @Path("/jwks") + @Produces({MediaType.APPLICATION_JSON}) + Response requestJwk(@Context SecurityContext securityContext); +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/jwk/ws/rs/JwkRestWebServiceImpl.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/jwk/ws/rs/JwkRestWebServiceImpl.java new file mode 100644 index 00000000..652645d8 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/jwk/ws/rs/JwkRestWebServiceImpl.java @@ -0,0 +1,73 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.jwk.ws.rs; + +import org.gluu.oxauth.model.config.WebKeysConfiguration; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.jwk.JSONWebKey; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Provides interface for JWK REST web services + * + * @author Javier Rojas Blum + * @version June 15, 2016 + */ +@Path("/") +public class JwkRestWebServiceImpl implements JwkRestWebService { + + @Inject + private Logger log; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private WebKeysConfiguration webKeysConfiguration; + + @Override + public Response requestJwk(SecurityContext sec) { + log.debug("Attempting to request JWK, Is Secure = {}", sec.isSecure()); + Response.ResponseBuilder builder = Response.ok(); + + try { + WebKeysConfiguration webKeysConfiguration = new WebKeysConfiguration(); + webKeysConfiguration.setKeys(this.filterKeys(this.webKeysConfiguration.getKeys())); + builder.entity(webKeysConfiguration.toString()); + } catch (Exception e) { + log.error(e.getMessage(), e); + builder = Response.status(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); // 500 + } + + return builder.build(); + } + + /** + * Method responsible to filter keys and return a new list of keys with all + * algorithms that it is inside Json config attribute called "jwksAlgorithmsSupported" + * @param allKeys All keys that should be filtered + * @return Filtered list + */ + private List filterKeys(List allKeys) { + List jwksAlgorithmsSupported = appConfiguration.getJwksAlgorithmsSupported(); + if (allKeys == null || allKeys.size() == 0 + || jwksAlgorithmsSupported == null || jwksAlgorithmsSupported.size() == 0) { + return allKeys; + } + return allKeys.stream().filter( + (key) -> jwksAlgorithmsSupported.contains(key.getAlg().getParamName()) + ).collect(Collectors.toList()); + } + +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/GluuOrganization.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/GluuOrganization.java new file mode 100644 index 00000000..53090acc --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/GluuOrganization.java @@ -0,0 +1,232 @@ +package org.gluu.oxauth.model; + +/* + * oxTrust is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +import java.io.Serializable; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +import org.gluu.model.GluuStatus; +import org.gluu.persist.annotation.AttributeName; +import org.gluu.persist.annotation.DataEntry; +import org.gluu.persist.annotation.ObjectClass; +import org.gluu.persist.model.base.Entry; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; + +/** + * Group + * + * @author Yuriy Movchan Date: 11.02.2010 + */ +@DataEntry(sortBy = "displayName") +@ObjectClass(value = "gluuOrganization") +@JsonInclude(Include.NON_NULL) +public class GluuOrganization extends Entry implements Serializable { + + private static final long serialVersionUID = -8284018077740582699L; + + @NotNull + @Size(min = 0, max = 60, message = "Length of the Display Name should not exceed 60") + @AttributeName + private String displayName; + + @NotNull + @Size(min = 0, max = 60, message = "Length of the Description should not exceed 60") + @AttributeName + private String description; + + @AttributeName(name = "memberOf") + private String member; + + @AttributeName(name = "c") + private String countryName; + + @AttributeName(name = "o") + private String organization; + + @AttributeName(name = "gluuStatus") + private GluuStatus status; + + @AttributeName(name = "gluuManagerGroup") + private String managerGroup; + + @AttributeName(name = "oxTrustLogoPath") + private String oxTrustLogoPath; + + @AttributeName(name = "oxTrustFaviconPath") + private String oxTrustFaviconPath; + + @AttributeName(name = "oxAuthLogoPath") + private String oxAuthLogoPath; + + @AttributeName(name = "oxAuthFaviconPath") + private String oxAuthFaviconPath; + + @AttributeName(name = "idpLogoPath") + private String idpLogoPath; + + @AttributeName(name = "idpFaviconPath") + private String idpFaviconPath; + + @AttributeName(name = "gluuThemeColor") + private String themeColor; + + @AttributeName(name = "gluuOrgShortName") + private String shortName; + + @AttributeName(name = "gluuCustomMessage") + private String[] customMessages; + + @AttributeName(name = "title") + private String title; + + public String getOrganizationTitle() { + if (title == null || title.trim().equals("")) { + return "Gluu"; + } + return title; + } + + public String getCountryName() { + return countryName; + } + + public void setCountryName(String countryName) { + this.countryName = countryName; + } + + public String[] getCustomMessages() { + return customMessages; + } + + public void setCustomMessages(String[] customMessages) { + this.customMessages = customMessages; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public String getManagerGroup() { + return managerGroup; + } + + public void setManagerGroup(String managerGroup) { + this.managerGroup = managerGroup; + } + + public String getMember() { + return member; + } + + public void setMember(String member) { + this.member = member; + } + + public String getOrganization() { + return organization; + } + + public void setOrganization(String organization) { + this.organization = organization; + } + + public String getShortName() { + return shortName; + } + + public void setShortName(String shortName) { + this.shortName = shortName; + } + + public GluuStatus getStatus() { + return status; + } + + public void setStatus(GluuStatus status) { + this.status = status; + } + + public String getThemeColor() { + return themeColor; + } + + public void setThemeColor(String themeColor) { + this.themeColor = themeColor; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getOxTrustLogoPath() { + return oxTrustLogoPath; + } + + public void setOxTrustLogoPath(String oxTrustLogoPath) { + this.oxTrustLogoPath = oxTrustLogoPath; + } + + public String getOxTrustFaviconPath() { + return oxTrustFaviconPath; + } + + public void setOxTrustFaviconPath(String oxTrustFaviconPath) { + this.oxTrustFaviconPath = oxTrustFaviconPath; + } + + public String getOxAuthLogoPath() { + return oxAuthLogoPath; + } + + public void setOxAuthLogoPath(String oxAuthLogoPath) { + this.oxAuthLogoPath = oxAuthLogoPath; + } + + public String getOxAuthFaviconPath() { + return oxAuthFaviconPath; + } + + public void setOxAuthFaviconPath(String oxAuthFaviconPath) { + this.oxAuthFaviconPath = oxAuthFaviconPath; + } + + public String getIdpLogoPath() { + return idpLogoPath; + } + + public void setIdpLogoPath(String idpLogoPath) { + this.idpLogoPath = idpLogoPath; + } + + public String getIdpFaviconPath() { + return idpFaviconPath; + } + + public void setIdpFaviconPath(String idpFaviconPath) { + this.idpFaviconPath = idpFaviconPath; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/audit/Action.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/audit/Action.java new file mode 100644 index 00000000..2b14e7a4 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/audit/Action.java @@ -0,0 +1,39 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.audit; + +/** + * @version August 20, 2019 + */ +public enum Action { + CLIENT_REGISTRATION("CLIENT_REGISTRATION"), + CLIENT_UPDATE("CLIENT_UPDATE"), + CLIENT_READ("CLIENT_READ"), + CLIENT_DELETE("CLIENT_DELETE"), + USER_AUTHORIZATION("USER_AUTHORIZATION"), + BACKCHANNEL_AUTHENTICATION("BACKCHANNEL_AUTHENTICATION"), + BACKCHANNEL_DEVICE_REGISTRATION("BACKCHANNEL_DEVICE_REGISTRATION"), + USER_INFO("USER_INFO"), + TOKEN_REQUEST("TOKEN_REQUEST"), + TOKEN_VALIDATE("TOKEN_VALIDATE"), + TOKEN_REVOCATION("TOKEN_REVOCATION"), + SESSION_UNAUTHENTICATED("SESSION_UNAUTHENTICATED"), + SESSION_AUTHENTICATED("SESSION_AUTHENTICATED"), + SESSION_DESTROYED("SESSION_DESTROYED"), + DEVICE_CODE_AUTHORIZATION("DEVICE_CODE_AUTHORIZATION"); + + private String value; + + Action(String value) { + this.value = value; + } + + @Override + public String toString() { + return value; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/audit/OAuth2AuditLog.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/audit/OAuth2AuditLog.java new file mode 100644 index 00000000..04cc9ee1 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/audit/OAuth2AuditLog.java @@ -0,0 +1,86 @@ +package org.gluu.oxauth.model.audit; + +import java.util.Date; + +import org.apache.commons.lang.StringUtils; +import org.gluu.net.InetAddressUtility; +import org.gluu.oxauth.model.common.AuthorizationGrant; + + +public class OAuth2AuditLog { + + private final String ip; + private final Action action; + private final Date timestamp; + private final String macAddress; + private boolean isSuccess; + + + private String clientId; + + private String username; + private String scope; + + public OAuth2AuditLog(String ip, Action action) { + this.ip = ip; + this.action = action; + this.timestamp = new Date(); + this.macAddress = InetAddressUtility.getMACAddressOrNull(); + this.isSuccess = false; + } + + public void updateOAuth2AuditLog(AuthorizationGrant authorizationGrant, boolean success) { + this.setClientId(authorizationGrant.getClientId()); + this.setUsername(authorizationGrant.getUserId()); + this.setScope(StringUtils.join(authorizationGrant.getScopes(), " ")); + this.setSuccess(success); + } + + public String getIp() { + return ip; + } + + public Action getAction() { + return action; + } + + public Date getTimestamp() { + return timestamp; + } + + public String getMacAddress() { + return macAddress; + } + + public boolean isSuccess() { + return isSuccess; + } + + public void setSuccess(boolean success) { + isSuccess = success; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getScope() { + return scope; + } + + public void setScope(String scope) { + this.scope = scope; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/auth/AuthenticationMode.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/auth/AuthenticationMode.java new file mode 100644 index 00000000..2d56c520 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/auth/AuthenticationMode.java @@ -0,0 +1,32 @@ +package org.gluu.oxauth.model.auth; + +import java.io.Serializable; + +import javax.enterprise.inject.Vetoed; + +/** + * @author Yuriy Movchan + * Date: 03/17/2017 + */ +@Vetoed +public class AuthenticationMode implements Serializable { + + private static final long serialVersionUID = -3187893527945584013L; + + private String name; + + public AuthenticationMode() {} + + public AuthenticationMode(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/AuthorizeParamsValidator.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/AuthorizeParamsValidator.java new file mode 100644 index 00000000..9d6952b6 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/AuthorizeParamsValidator.java @@ -0,0 +1,85 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.authorize; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.Prompt; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.registration.Client; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +/** + * Validates the parameters received for the authorize web service. + * + * @author Javier Rojas Blum + * @version July 19, 2017 + */ +public class AuthorizeParamsValidator { + + /** + * Validates the parameters for an authorization request. + * + * @param responseTypes The response types. This parameter is mandatory. + * @return Returns true when all the parameters are valid. + */ + public static boolean validateParams(List responseTypes, List prompts, String nonce, boolean fapiCompatibility) { + if (fapiCompatibility && responseTypes.size() == 1 && responseTypes.contains(ResponseType.CODE)) { + return false; + } + + boolean existsNonce = StringUtils.isNotBlank(nonce); + if (!existsNonce && ((responseTypes.contains(ResponseType.CODE) && responseTypes.contains(ResponseType.ID_TOKEN)) + || (responseTypes.contains(ResponseType.ID_TOKEN) && responseTypes.size() == 1) + || (responseTypes.contains(ResponseType.ID_TOKEN) && responseTypes.contains(ResponseType.TOKEN)) + || (responseTypes.contains(ResponseType.TOKEN) && responseTypes.size() == 1))) { + return false; + } + + boolean validParams = !responseTypes.isEmpty(); + return validParams && noNonePrompt(prompts); + } + + public static boolean noNonePrompt(List prompts) { + return !(prompts.contains(Prompt.NONE) && prompts.size() > 1); + } + + public static boolean validateResponseTypes(List responseTypes, Client client) { + if (responseTypes == null || responseTypes.isEmpty() || client == null || client.getResponseTypes() == null) { + return false; + } + + List clientSupportedResponseTypes = Arrays.asList(client.getResponseTypes()); + + return clientSupportedResponseTypes.containsAll(responseTypes); + } + + public static boolean validateGrantType(List responseTypes, GrantType[] clientGrantTypesArray, Set grantTypesSupported) { + List clientGrantTypes = Arrays.asList(clientGrantTypesArray); + + if (responseTypes == null || grantTypesSupported == null) { + return false; + } + if (responseTypes.contains(ResponseType.CODE)) { + GrantType requestedGrantType = GrantType.AUTHORIZATION_CODE; + if (!clientGrantTypes.contains(requestedGrantType) || !grantTypesSupported.contains(requestedGrantType)) { + return false; + } + } + if (responseTypes.contains(ResponseType.TOKEN) || responseTypes.contains(ResponseType.ID_TOKEN)) { + GrantType requestedGrantType = GrantType.IMPLICIT; + if (!clientGrantTypes.contains(requestedGrantType) || !grantTypesSupported.contains(requestedGrantType)) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/Claim.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/Claim.java new file mode 100644 index 00000000..85dade0f --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/Claim.java @@ -0,0 +1,37 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.authorize; + +/** + * @author Javier Rojas Blum Date: 03.09.2012 + */ +public class Claim { + + private String name; + private ClaimValue claimValue; + + public Claim(String name, ClaimValue claimValue) { + this.name = name; + this.claimValue = claimValue; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public ClaimValue getClaimValue() { + return claimValue; + } + + public void setClaimValue(ClaimValue claimValue) { + this.claimValue = claimValue; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/ClaimValue.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/ClaimValue.java new file mode 100644 index 00000000..89478386 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/ClaimValue.java @@ -0,0 +1,124 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.authorize; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Javier Rojas Blum Date: 03.09.2012 + */ +public class ClaimValue { + + private ClaimValueType claimValueType; + private List values; + private String value; + private Boolean essential; + + private ClaimValue() { + } + + public static ClaimValue createNull() { + ClaimValue claimValue = new ClaimValue(); + claimValue.claimValueType = ClaimValueType.NULL; + + return claimValue; + } + + public static ClaimValue createEssential(boolean essentialValue) { + ClaimValue claimValue = new ClaimValue(); + claimValue.setEssential(essentialValue); + + // todo do we need code below ? + if (essentialValue) { + claimValue.claimValueType = ClaimValueType.ESSENTIAL_TRUE; + } else { + claimValue.claimValueType = ClaimValueType.ESSENTIAL_FALSE; + } + return claimValue; + } + + public static ClaimValue createValueList(List values) { + ClaimValue claimValue = new ClaimValue(); + claimValue.claimValueType = ClaimValueType.VALUE_LIST; + + claimValue.values = new ArrayList(values); + + return claimValue; + } + + public static ClaimValue createSingleValue(String value) { + ClaimValue claimValue = new ClaimValue(); + claimValue.claimValueType = ClaimValueType.SINGLE_VALUE; + + claimValue.value = value; + + return claimValue; + } + + public Boolean getEssential() { + return essential; + } + + public void setEssential(Boolean essential) { + this.essential = essential; + } + + public ClaimValueType getClaimValueType() { + return claimValueType; + } + + public List getValues() { + return values; + } + + public String getValue() { + return value; + } + + public String getValueAsString() { + if (values != null && !values.isEmpty()) { + return String.join(" ", values); + } + return value; + } + + public JSONObject toJSONObject() throws JSONException { + JSONObject obj = null; + + switch (claimValueType) { + case NULL: + break; + case ESSENTIAL_TRUE: + obj = new JSONObject(); + obj.put("essential", true); + break; + case ESSENTIAL_FALSE: + obj = new JSONObject(); + obj.put("essential", false); + break; + case VALUE_LIST: + JSONArray arr = new JSONArray(); + for (String value : values) { + arr.put(value); + } + obj = new JSONObject(); + obj.put("values", arr); + break; + case SINGLE_VALUE: + obj = new JSONObject(); + obj.put("value", value); + break; + } + + return obj; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/ClaimValueType.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/ClaimValueType.java new file mode 100644 index 00000000..1088a282 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/ClaimValueType.java @@ -0,0 +1,18 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.authorize; + +/** + * @author Javier Rojas Blum Date: 03.21.2012 + */ +public enum ClaimValueType { + NULL, + ESSENTIAL_TRUE, + ESSENTIAL_FALSE, + VALUE_LIST, + SINGLE_VALUE; +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/IdTokenMember.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/IdTokenMember.java new file mode 100644 index 00000000..80aee5c9 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/IdTokenMember.java @@ -0,0 +1,83 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.authorize; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.util.Util; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * @author Javier Rojas Blum Date: 03.09.2012 + */ +public class IdTokenMember { + + private List claims; + private Integer maxAge; + + public IdTokenMember(JSONObject jsonObject) throws JSONException { + claims = new ArrayList(); + + for (Iterator iterator = jsonObject.keys(); iterator.hasNext(); ) { + String claimName = iterator.next(); + ClaimValue claimValue = null; + + if (claimName != null && claimName.equals("max_age") && jsonObject.has("max_age")) { + maxAge = jsonObject.getInt("max_age"); + } else if (jsonObject.isNull(claimName)) { + claimValue = ClaimValue.createNull(); + } else { + JSONObject claimValueJsonObject = jsonObject.getJSONObject(claimName); + + if (claimValueJsonObject.has("values")) { + JSONArray claimValueJsonArray = claimValueJsonObject.getJSONArray("values"); + List claimValueArr = Util.asList(claimValueJsonArray); + claimValue = ClaimValue.createValueList(claimValueArr); + } else if (claimValueJsonObject.has("value")) { + String value = claimValueJsonObject.getString("value"); + claimValue = ClaimValue.createSingleValue(value); + } + if (claimValueJsonObject.has("essential")) { + final boolean essential = claimValueJsonObject.getBoolean("essential"); + if (claimValue != null) { + claimValue.setEssential(essential); + } else { + claimValue = ClaimValue.createEssential(essential); + } + } + } + + Claim claim = new Claim(claimName, claimValue); + claims.add(claim); + } + } + + public List getClaims() { + return claims; + } + + public Integer getMaxAge() { + return maxAge; + } + + public Claim getClaim(String claimName) { + if (StringUtils.isNotBlank(claimName)) { + for (Claim claim : claims) { + if (claimName.equals(claim.getName())) { + return claim; + } + } + } + + return null; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/JwtAuthorizationRequest.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/JwtAuthorizationRequest.java new file mode 100644 index 00000000..36ac9192 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/JwtAuthorizationRequest.java @@ -0,0 +1,526 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.authorize; + +import com.google.common.collect.Lists; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.ArrayUtils; +import org.gluu.oxauth.model.common.Display; +import org.gluu.oxauth.model.common.Prompt; +import org.gluu.oxauth.model.common.ResponseMode; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.crypto.AbstractCryptoProvider; +import org.gluu.oxauth.model.crypto.encryption.BlockEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.encryption.KeyEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.exception.InvalidJwtException; +import org.gluu.oxauth.model.jwe.Jwe; +import org.gluu.oxauth.model.jwe.JweDecrypterImpl; +import org.gluu.oxauth.model.jwt.JwtHeader; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.util.Base64Util; +import org.gluu.oxauth.model.util.JwtUtil; +import org.gluu.oxauth.model.util.URLPatternList; +import org.gluu.oxauth.model.util.Util; +import org.gluu.oxauth.service.ClientService; +import org.gluu.oxauth.service.RedirectUriResponse; +import org.gluu.oxauth.service.RedirectionUriService; +import org.gluu.oxauth.util.ServerUtil; +import org.gluu.service.cdi.util.CdiUtil; +import org.jetbrains.annotations.Nullable; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.Response; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Javier Rojas Blum + * @version November 20, 2018 + */ +public class JwtAuthorizationRequest { + + private final static Logger log = LoggerFactory.getLogger(JwtAuthorizationRequest.class); + + // Header + private String type; + private String algorithm; + private String encryptionAlgorithm; + private String keyId; + + // Payload + private List responseTypes; + private String clientId; + private List scopes; + private String redirectUri; + private String nonce; + private String state; + private List aud = Lists.newArrayList(); + private Display display; + private List prompts; + private UserInfoMember userInfoMember; + private IdTokenMember idTokenMember; + private Integer exp; + private String iss; + private Integer iat; + private Integer nbf; + private String jti; + private String clientNotificationToken; + private String acrValues; + private String loginHintToken; + private String idTokenHint; + private String loginHint; + private String bindingMessage; + private String userCode; + private Integer requestedExpiry; + private ResponseMode responseMode; + + private String encodedJwt; + private String payload; + + private AppConfiguration appConfiguration; + + public JwtAuthorizationRequest(AppConfiguration appConfiguration, AbstractCryptoProvider cryptoProvider, String encodedJwt, Client client) throws InvalidJwtException { + try { + this.appConfiguration = appConfiguration; + this.responseTypes = new ArrayList<>(); + this.scopes = new ArrayList<>(); + this.prompts = new ArrayList<>(); + this.encodedJwt = encodedJwt; + + if (StringUtils.isEmpty(encodedJwt)) { + throw new InvalidJwtException("The JWT is null or empty"); + } + + + String[] parts = encodedJwt.split("\\."); + + if (parts.length == 5) { + String encodedHeader = parts[0]; + + JwtHeader jwtHeader = new JwtHeader(encodedHeader); + + keyId = jwtHeader.getKeyId(); + KeyEncryptionAlgorithm keyEncryptionAlgorithm = KeyEncryptionAlgorithm.fromName( + jwtHeader.getClaimAsString(JwtHeaderName.ALGORITHM)); + BlockEncryptionAlgorithm blockEncryptionAlgorithm = BlockEncryptionAlgorithm.fromName( + jwtHeader.getClaimAsString(JwtHeaderName.ENCRYPTION_METHOD)); + + JweDecrypterImpl jweDecrypter = null; + if ("RSA".equals(keyEncryptionAlgorithm.getFamily())) { + PrivateKey privateKey = cryptoProvider.getPrivateKey(keyId); + jweDecrypter = new JweDecrypterImpl(privateKey); + } else { + ClientService clientService = CdiUtil.bean(ClientService.class); + jweDecrypter = new JweDecrypterImpl(clientService.decryptSecret(client.getClientSecret()).getBytes(StandardCharsets.UTF_8)); + } + jweDecrypter.setKeyEncryptionAlgorithm(keyEncryptionAlgorithm); + jweDecrypter.setBlockEncryptionAlgorithm(blockEncryptionAlgorithm); + + Jwe jwe = jweDecrypter.decrypt(encodedJwt); + + loadHeader(jwe.getHeader().toJsonString()); + loadPayload(jwe.getClaims().toJsonString()); + } else if (parts.length == 2 || parts.length == 3) { + String encodedHeader = parts[0]; + String encodedClaim = parts[1]; + String encodedSignature = StringUtils.EMPTY; + if (parts.length == 3) { + encodedSignature = parts[2]; + } + + String signingInput = encodedHeader + "." + encodedClaim; + String header = new String(Base64Util.base64urldecode(encodedHeader), StandardCharsets.UTF_8); + String payload = new String(Base64Util.base64urldecode(encodedClaim), StandardCharsets.UTF_8); + payload = payload.replace("\\", ""); + + loadHeader(header); + + SignatureAlgorithm sigAlg = SignatureAlgorithm.fromString(algorithm); + if (sigAlg == null) { + throw new InvalidJwtException("The JWT algorithm is not supported"); + } + if (sigAlg == SignatureAlgorithm.NONE && appConfiguration.getFapiCompatibility()) { + throw new InvalidJwtException("None algorithm is not allowed for FAPI"); + } + if (!validateSignature(cryptoProvider, sigAlg, client, signingInput, encodedSignature)) { + throw new InvalidJwtException("The JWT signature is not valid"); + } + + loadPayload(payload); + } else { + throw new InvalidJwtException("The JWT is not well formed"); + } + + } catch (Exception e) { + throw new InvalidJwtException(e); + } + } + + public String getEncodedJwt() { + return encodedJwt; + } + + private void loadHeader(String header) throws JSONException { + JSONObject jsonHeader = new JSONObject(header); + + if (jsonHeader.has("typ")) { + type = jsonHeader.getString("typ"); + } + if (jsonHeader.has("alg")) { + algorithm = jsonHeader.getString("alg"); + } + if (jsonHeader.has("enc")) { + encryptionAlgorithm = jsonHeader.getString("enc"); + } + if (jsonHeader.has("kid")) { + keyId = jsonHeader.getString("kid"); + } + } + + private void loadPayload(String payload) throws JSONException, UnsupportedEncodingException { + this.payload = payload; + + JSONObject jsonPayload = new JSONObject(payload); + + if (jsonPayload.has("response_type")) { + JSONArray responseTypeJsonArray = jsonPayload.optJSONArray("response_type"); + if (responseTypeJsonArray != null) { + for (int i = 0; i < responseTypeJsonArray.length(); i++) { + ResponseType responseType = ResponseType.fromString(responseTypeJsonArray.getString(i)); + responseTypes.add(responseType); + } + } else { + responseTypes.addAll(ResponseType.fromString(jsonPayload.getString("response_type"), " ")); + } + } + if (jsonPayload.has("exp")) { + exp = jsonPayload.getInt("exp"); + } + if (jsonPayload.has("aud")) { + final String audStr = jsonPayload.optString("aud"); + if (StringUtils.isNotBlank(audStr)) { + this.aud.add(audStr); + } + final JSONArray audArray = jsonPayload.optJSONArray("aud"); + if (audArray != null && audArray.length() > 0) { + this.aud.addAll(Util.asList(audArray)); + } + } + clientId = jsonPayload.optString("client_id", null); + if (jsonPayload.has("scope")) { + JSONArray scopesJsonArray = jsonPayload.optJSONArray("scope"); + if (scopesJsonArray != null) { + for (int i = 0; i < scopesJsonArray.length(); i++) { + String scope = scopesJsonArray.getString(i); + scopes.add(scope); + } + } else { + String scopeStringList = jsonPayload.getString("scope"); + scopes.addAll(Util.splittedStringAsList(scopeStringList, " ")); + } + } + if (jsonPayload.has("redirect_uri")) { + redirectUri = URLDecoder.decode(jsonPayload.getString("redirect_uri"), "UTF-8"); + } + nonce = jsonPayload.optString("nonce", null); + state = jsonPayload.optString("state", null); + if (jsonPayload.has("display")) { + display = Display.fromString(jsonPayload.getString("display")); + } + if (jsonPayload.has("prompt")) { + JSONArray promptJsonArray = jsonPayload.optJSONArray("prompt"); + if (promptJsonArray != null) { + for (int i = 0; i < promptJsonArray.length(); i++) { + Prompt prompt = Prompt.fromString(promptJsonArray.getString(i)); + prompts.add(prompt); + } + } else { + prompts.addAll(Prompt.fromString(jsonPayload.getString("prompt"), " ")); + } + } + if (jsonPayload.has("claims")) { + JSONObject claimsJsonObject = jsonPayload.getJSONObject("claims"); + + if (claimsJsonObject.has("userinfo")) { + userInfoMember = new UserInfoMember(claimsJsonObject.getJSONObject("userinfo")); + } + if (claimsJsonObject.has("id_token")) { + idTokenMember = new IdTokenMember(claimsJsonObject.getJSONObject("id_token")); + } + } + iss = jsonPayload.optString("iss", null); + if (jsonPayload.has("exp")) { + exp = jsonPayload.getInt("exp"); + } + if (jsonPayload.has("iat")) { + iat = jsonPayload.getInt("iat"); + } + if (jsonPayload.has("nbf")) { + nbf = jsonPayload.getInt("nbf"); + } + jti = jsonPayload.optString("jti", null); + clientNotificationToken = jsonPayload.optString("client_notification_token", null); + acrValues = jsonPayload.optString("acr_values", null); + loginHintToken = jsonPayload.optString("login_hint_token", null); + idTokenHint = jsonPayload.optString("id_token_hint", null); + loginHint = jsonPayload.optString("login_hint", null); + bindingMessage = jsonPayload.optString("binding_message", null); + userCode = jsonPayload.optString("user_code", null); + + if (jsonPayload.has("requested_expiry")) { + // requested_expirity is an exception, it could be String or Number. + if (jsonPayload.get("requested_expiry") instanceof Number) { + requestedExpiry = jsonPayload.getInt("requested_expiry"); + } else { + requestedExpiry = Integer.parseInt(jsonPayload.getString("requested_expiry")); + } + } + if (jsonPayload.has("response_mode")) { + responseMode = ResponseMode.getByValue(jsonPayload.optString("response_mode")); + } + } + + private boolean validateSignature(AbstractCryptoProvider cryptoProvider, SignatureAlgorithm signatureAlgorithm, Client client, String signingInput, String signature) throws Exception { + ClientService clientService = CdiUtil.bean(ClientService.class); + String sharedSecret = clientService.decryptSecret(client.getClientSecret()); + JSONObject jwks = ServerUtil.getJwks(client); + return cryptoProvider.verifySignature(signingInput, signature, keyId, jwks, sharedSecret, signatureAlgorithm); + } + + public String getEncryptionAlgorithm() { + return encryptionAlgorithm; + } + + public String getKeyId() { + return keyId; + } + + public String getType() { + return type; + } + + public String getAlgorithm() { + return algorithm; + } + + public List getResponseTypes() { + return responseTypes; + } + + public String getClientId() { + return clientId; + } + + public List getScopes() { + return scopes; + } + + public String getRedirectUri() { + return redirectUri; + } + + public String getNonce() { + return nonce; + } + + public String getState() { + return state; + } + + public Display getDisplay() { + return display; + } + + public List getPrompts() { + return prompts; + } + + public UserInfoMember getUserInfoMember() { + return userInfoMember; + } + + public IdTokenMember getIdTokenMember() { + return idTokenMember; + } + + public Integer getExp() { + return exp; + } + + public List getAud() { + if (aud == null) aud = Lists.newArrayList(); + return aud; + } + + public String getPayload() { + return payload; + } + + public String getIss() { + return iss; + } + + public Integer getIat() { + return iat; + } + + public Integer getNbf() { + return nbf; + } + + public String getJti() { + return jti; + } + + public String getClientNotificationToken() { + return clientNotificationToken; + } + + public String getAcrValues() { + return acrValues; + } + + public String getLoginHintToken() { + return loginHintToken; + } + + public String getIdTokenHint() { + return idTokenHint; + } + + public String getLoginHint() { + return loginHint; + } + + public String getBindingMessage() { + return bindingMessage; + } + + public String getUserCode() { + return userCode; + } + + public Integer getRequestedExpiry() { + return requestedExpiry; + } + + public ResponseMode getResponseMode() { + return responseMode; + } + + @Nullable + private static String queryRequest(@Nullable String requestUri, @Nullable RedirectUriResponse redirectUriResponse, + AppConfiguration appConfiguration) { + if (StringUtils.isBlank(requestUri)) { + return null; + } + boolean validRequestUri = false; + try { + URI reqUri = new URI(requestUri); + String reqUriHash = reqUri.getFragment(); + String reqUriWithoutFragment = reqUri.getScheme() + ":" + reqUri.getSchemeSpecificPart(); + + javax.ws.rs.client.Client clientRequest = ClientBuilder.newClient(); + String request = null; + try { + Response clientResponse = clientRequest.target(reqUriWithoutFragment).request().buildGet().invoke(); + int status = clientResponse.getStatus(); + + if (status == 200) { + request = clientResponse.readEntity(String.class); + + if (StringUtils.isBlank(reqUriHash) || !appConfiguration.getRequestUriHashVerificationEnabled()) { + validRequestUri = true; + } else { + String hash = Base64Util.base64urlencode(JwtUtil.getMessageDigestSHA256(request)); + validRequestUri = StringUtils.equals(reqUriHash, hash); + } + } + } finally { + clientRequest.close(); + } + + if (!validRequestUri && redirectUriResponse != null) { + throw redirectUriResponse.createWebException(AuthorizeErrorResponseType.INVALID_REQUEST_URI, "Invalid request uri."); + } + return request; + } catch (WebApplicationException e) { + throw e; + } catch (Exception e) { + log.error(e.getMessage(), e); + return null; + } + } + + public static JwtAuthorizationRequest createJwtRequest(String request, String requestUri, Client client, RedirectUriResponse redirectUriResponse, AbstractCryptoProvider cryptoProvider, AppConfiguration appConfiguration) { + validateRequestUri(requestUri, client, appConfiguration, redirectUriResponse != null ? redirectUriResponse.getState() : null); + final String requestFromClient = queryRequest(requestUri, redirectUriResponse, appConfiguration); + if (StringUtils.isNotBlank(requestFromClient)) { + request = requestFromClient; + } + + if (StringUtils.isBlank(request)) { + return null; + } + + try { + return new JwtAuthorizationRequest(appConfiguration, cryptoProvider, request, client); + } catch (WebApplicationException e) { + throw e; + } catch (Exception e) { + log.error("Invalid JWT authorization request. " + e.getMessage(), e); + } + return null; + } + + public static void validateRequestUri(String requestUri, Client client, AppConfiguration appConfiguration, String state) { + validateRequestUri(requestUri, client, appConfiguration, state, CdiUtil.bean(ErrorResponseFactory.class)); + } + + public static void validateRequestUri(String requestUri, Client client, AppConfiguration appConfiguration, String state, ErrorResponseFactory errorResponseFactory) { + if (StringUtils.isBlank(requestUri)) { + return; // nothing to validate + } + + // client.requestUris() - validation + if (ArrayUtils.isNotEmpty(client.getRequestUris()) && !RedirectionUriService.isUriEqual(requestUri, client.getRequestUris())) { + log.debug("request_uri is forbidden by client request uris."); + throw new WebApplicationException(Response + .status(Response.Status.BAD_REQUEST) + .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.INVALID_REQUEST_URI, state, "")) + .build()); + } + + // check black list + final List blackList = appConfiguration.getRequestUriBlockList(); + if (!blackList.isEmpty()) { + URLPatternList urlPatternList = new URLPatternList(blackList); + if (urlPatternList.isUrlListed(requestUri)) { + log.debug("request_uri is forbidden by requestUriBlackList configuration."); + throw new WebApplicationException(Response + .status(Response.Status.BAD_REQUEST) + .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.INVALID_REQUEST_URI, state, "")) + .build()); + } + } + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/ScopeChecker.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/ScopeChecker.java new file mode 100644 index 00000000..f46196c4 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/ScopeChecker.java @@ -0,0 +1,94 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.authorize; + +import com.google.common.collect.Sets; + +import javax.enterprise.context.ApplicationScoped; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.service.ScopeService; +import org.gluu.oxauth.service.SpontaneousScopeService; +import org.gluu.oxauth.service.external.ExternalSpontaneousScopeService; +import org.gluu.oxauth.service.external.context.SpontaneousScopeExternalContext; +import org.slf4j.Logger; + +import javax.inject.Inject; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Validates the scopes received for the authorize web service. + * + * @author Yuriy Zabrovarnyy + * @author Yuriy Movchan + * @author Javier Rojas Blum + * @version January 30, 2018 + */ +@ApplicationScoped +public class ScopeChecker { + + @Inject + private Logger log; + + @Inject + private ScopeService scopeService; + + @Inject + private SpontaneousScopeService spontaneousScopeService; + + @Inject + private ExternalSpontaneousScopeService externalSpontaneousScopeService; + + public Set checkScopesPolicy(Client client, String scope) { + if (StringUtils.isBlank(scope)) { + return Sets.newHashSet(); + } + return checkScopesPolicy(client, Arrays.asList(scope.split(" "))); + } + + public Set checkScopesPolicy(Client client, List scopesRequested) { + log.debug("Checking scopes policy for: " + scopesRequested); + Set grantedScopes = new HashSet<>(); + + if (scopesRequested == null || scopesRequested.isEmpty() || client == null) { + return grantedScopes; + } + + String[] scopesAllowed = client.getScopes() != null ? client.getScopes() : new String[0]; + + for (String scopeRequested : scopesRequested) { + if (StringUtils.isBlank(scopeRequested)) { + continue; + } + + List scopesAllowedIds = scopeService.getScopeIdsByDns(Arrays.asList(scopesAllowed)); + if (scopesAllowedIds.contains(scopeRequested)) { + grantedScopes.add(scopeRequested); + continue; + } + + if (spontaneousScopeService.isAllowedBySpontaneousScopes(client, scopeRequested)) { + grantedScopes.add(scopeRequested); + + SpontaneousScopeExternalContext context = new SpontaneousScopeExternalContext(client, scopeRequested, grantedScopes, spontaneousScopeService); + externalSpontaneousScopeService.executeExternalManipulateScope(context); + + if (context.isAllowSpontaneousScopePersistence()) { + spontaneousScopeService.createSpontaneousScopeIfNeeded(Sets.newHashSet(client.getAttributes().getSpontaneousScopes()), scopeRequested, client.getClientId()); + } + } + } + + log.debug("Granted scopes: " + grantedScopes); + + return grantedScopes; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/UserInfoMember.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/UserInfoMember.java new file mode 100644 index 00000000..e0a467fd --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/authorize/UserInfoMember.java @@ -0,0 +1,68 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.authorize; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.gluu.oxauth.model.util.Util; + +/** + * @author Javier Rojas Blum Date: 03.09.2012 + */ +public class UserInfoMember { + + private List claims; + private List preferredLocales; + + public UserInfoMember(JSONObject jsonObject) throws JSONException { + claims = new ArrayList(); + + for (Iterator iterator = jsonObject.keys(); iterator.hasNext(); ) { + String claimName = iterator.next(); + ClaimValue claimValue = null; + + if (jsonObject.isNull(claimName)) { + claimValue = ClaimValue.createNull(); + } else { + JSONObject claimValueJsonObject = jsonObject.getJSONObject(claimName); + if (claimValueJsonObject.has("essential")) { + boolean essential = claimValueJsonObject.getBoolean("essential"); + claimValue = ClaimValue.createEssential(essential); + } else if (claimValueJsonObject.has("values")) { + JSONArray claimValueJsonArray = claimValueJsonObject.getJSONArray("values"); + List claimValueArr = Util.asList(claimValueJsonArray); + claimValue = ClaimValue.createValueList(claimValueArr); + } + } + + Claim claim = new Claim(claimName, claimValue); + claims.add(claim); + } + + preferredLocales = new ArrayList(); + if (jsonObject.has("preferred_locales")) { + JSONArray preferredLocalesJsonArray = jsonObject.getJSONArray("preferred_locales"); + + for (int i = 0; i < preferredLocalesJsonArray.length(); i++) { + preferredLocales.add(preferredLocalesJsonArray.getString(i)); + } + } + } + + public List getClaims() { + return claims; + } + + public List getPreferredLocales() { + return preferredLocales; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/clientinfo/ClientInfoErrorResponseType.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/clientinfo/ClientInfoErrorResponseType.java new file mode 100644 index 00000000..25c1d47e --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/clientinfo/ClientInfoErrorResponseType.java @@ -0,0 +1,46 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.clientinfo; + +import org.gluu.oxauth.model.error.IErrorType; + +/** + * @author Javier Rojas Date: 07.19.2012 + */ +public enum ClientInfoErrorResponseType implements IErrorType { + + INVALID_REQUEST("invalid_request"), + INVALID_TOKEN("invalid_token"); + + private final String paramName; + + private ClientInfoErrorResponseType(String paramName) { + this.paramName = paramName; + } + + public static ClientInfoErrorResponseType fromString(String param) { + if (param != null) { + for (ClientInfoErrorResponseType err : ClientInfoErrorResponseType.values()) { + if (param.equals(err.paramName)) { + return err; + } + } + } + + return null; + } + + @Override + public String toString() { + return paramName; + } + + @Override + public String getParameter() { + return paramName; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/clientinfo/ClientInfoParamsValidator.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/clientinfo/ClientInfoParamsValidator.java new file mode 100644 index 00000000..3801b73a --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/clientinfo/ClientInfoParamsValidator.java @@ -0,0 +1,25 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.clientinfo; + +/** + * Validates the parameters received for the client info web service. + * + * @author Javier Rojas Blum Date: 07.19.2012 + */ +public class ClientInfoParamsValidator { + + /** + * Validates the parameters for a client info request. + * + * @param accessToken + * @return Returns true when all the parameters are valid. + */ + public static boolean validateParams(String accessToken) { + return accessToken != null && !accessToken.isEmpty(); + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AbstractAuthorizationGrant.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AbstractAuthorizationGrant.java new file mode 100644 index 00000000..914e3c51 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AbstractAuthorizationGrant.java @@ -0,0 +1,502 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import org.gluu.oxauth.model.authorize.JwtAuthorizationRequest; +import org.gluu.oxauth.model.authorize.ScopeChecker; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.ldap.TokenLdap; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.util.CertUtils; +import org.gluu.oxauth.service.external.ExternalUpdateTokenService; +import org.gluu.oxauth.service.external.context.ExternalUpdateTokenContext; +import org.gluu.oxauth.util.TokenHashUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * @author Yuriy Zabrovarnyy + * @author Javier Rojas Blum + * @author Yuriy Movchan + * @version November 28, 2018 + */ + +public abstract class AbstractAuthorizationGrant implements IAuthorizationGrant { + + private static final Logger log = LoggerFactory.getLogger(AbstractAuthorizationGrant.class); + + @Inject + protected AppConfiguration appConfiguration; + + @Inject + protected ScopeChecker scopeChecker; + + @Inject + private ExternalUpdateTokenService externalUpdateTokenService; + + private User user; + private AuthorizationGrantType authorizationGrantType; + private Client client; + private Set scopes; + + private String grantId; + private JwtAuthorizationRequest jwtAuthorizationRequest; + private Date authenticationTime; + private TokenLdap tokenLdap; + private AccessToken longLivedAccessToken; + private IdToken idToken; + private AuthorizationCode authorizationCode; + private String tokenBindingHash; + private String x5ts256; + private String nonce; + private String codeChallenge; + private String codeChallengeMethod; + private String claims; + + private String acrValues; + private String sessionDn; + + protected final ConcurrentMap accessTokens = new ConcurrentHashMap(); + protected final ConcurrentMap refreshTokens = new ConcurrentHashMap(); + + public AbstractAuthorizationGrant() { + } + + protected AbstractAuthorizationGrant(User user, AuthorizationGrantType authorizationGrantType, Client client, + Date authenticationTime) { + init(user, authorizationGrantType, client, authenticationTime); + } + + protected void init(User user, AuthorizationGrantType authorizationGrantType, Client client, + Date authenticationTime) { + this.authenticationTime = authenticationTime != null ? new Date(authenticationTime.getTime()) : null; + this.user = user; + this.authorizationGrantType = authorizationGrantType; + this.client = client; + this.scopes = new CopyOnWriteArraySet(); + this.grantId = UUID.randomUUID().toString(); + } + + @Override + public synchronized String getGrantId() { + return grantId; + } + + @Override + public synchronized void setGrantId(String p_grantId) { + grantId = p_grantId; + } + + /** + * Returns the {@link AuthorizationCode}. + * + * @return The authorization code. + */ + @Override + public AuthorizationCode getAuthorizationCode() { + return authorizationCode; + } + + /** + * Sets the {@link AuthorizationCode}. + * + * @param authorizationCode The authorization code. + */ + @Override + public void setAuthorizationCode(AuthorizationCode authorizationCode) { + this.authorizationCode = authorizationCode; + } + + public String getTokenBindingHash() { + return tokenBindingHash; + } + + public void setTokenBindingHash(String tokenBindingHash) { + this.tokenBindingHash = tokenBindingHash; + } + + public String getX5ts256() { + return x5ts256; + } + + public void setX5ts256(String x5ts256) { + this.x5ts256 = x5ts256; + } + + @Override + public String getNonce() { + return nonce; + } + + @Override + public void setNonce(String nonce) { + this.nonce = nonce; + } + + public String getCodeChallenge() { + return codeChallenge; + } + + public void setCodeChallenge(String codeChallenge) { + this.codeChallenge = codeChallenge; + } + + public String getCodeChallengeMethod() { + return codeChallengeMethod; + } + + public void setCodeChallengeMethod(String codeChallengeMethod) { + this.codeChallengeMethod = codeChallengeMethod; + } + + public String getClaims() { + return claims; + } + + public void setClaims(String claims) { + this.claims = claims; + } + + /** + * Returns a list with all the issued refresh tokens codes. + * + * @return List with all the issued refresh tokens codes. + */ + @Override + public Set getRefreshTokensCodes() { + return refreshTokens.keySet(); + } + + /** + * Returns a list with all the issued access tokens codes. + * + * @return List with all the issued access tokens codes. + */ + @Override + public Set getAccessTokensCodes() { + return accessTokens.keySet(); + } + + /** + * Returns a list with all the issued access tokens. + * + * @return List with all the issued access tokens. + */ + @Override + public List getAccessTokens() { + return new ArrayList(accessTokens.values()); + } + + @Override + public void setScopes(Collection scopes) { + this.scopes.clear(); + this.scopes.addAll(scopes); + } + + @Override + public AccessToken getLongLivedAccessToken() { + return longLivedAccessToken; + } + + @Override + public void setLongLivedAccessToken(AccessToken longLivedAccessToken) { + this.longLivedAccessToken = longLivedAccessToken; + } + + @Override + public IdToken getIdToken() { + return idToken; + } + + @Override + public void setIdToken(IdToken idToken) { + this.idToken = idToken; + } + + @Override + public TokenLdap getTokenLdap() { + return tokenLdap; + } + + @Override + public void setTokenLdap(TokenLdap p_tokenLdap) { + this.tokenLdap = p_tokenLdap; + } + + /** + * Returns the resource owner's. + * + * @return The resource owner's. + */ + @Override + public User getUser() { + return user; + } + + public String getAcrValues() { + return acrValues; + } + + public void setAcrValues(String acrValues) { + this.acrValues = acrValues; + } + + public String getSessionDn() { + return sessionDn; + } + + public void setSessionDn(String sessionDn) { + this.sessionDn = sessionDn; + } + + /** + * Checks the scopes policy configured according to the type of the + * authorization grant to limit the issued token scopes. + * + * @param requestedScopes A space-delimited list of values in which the order of values + * does not matter. + * @return A space-delimited list of scopes + */ + @Override + public String checkScopesPolicy(String requestedScopes) { + this.scopes.clear(); + + Set grantedScopes = scopeChecker.checkScopesPolicy(client, requestedScopes); + this.scopes.addAll(grantedScopes); + + final StringBuilder grantedScopesSb = new StringBuilder(); + for (String scope : scopes) { + grantedScopesSb.append(" ").append(scope); + } + + final String grantedScopesSt = grantedScopesSb.toString().trim(); + + return grantedScopesSt; + } + + @Override + public AccessToken createAccessToken(String certAsPem, ExecutionContext executionContext) { + int lifetime = appConfiguration.getAccessTokenLifetime(); + // oxAuth #830 Client-specific access token expiration + if (client != null && client.getAccessTokenLifetime() != null && client.getAccessTokenLifetime() > 0) { + lifetime = client.getAccessTokenLifetime(); + } + + int lifetimeFromScript = externalUpdateTokenService.getAccessTokenLifetimeInSeconds(ExternalUpdateTokenContext.of(executionContext)); + if (lifetimeFromScript > 0) { + lifetime = lifetimeFromScript; + log.trace("Override access token lifetime with value from script: {}", lifetimeFromScript); + } + + AccessToken accessToken = new AccessToken(lifetime); + + accessToken.setAuthMode(getAcrValues()); + accessToken.setSessionDn(getSessionDn()); + accessToken.setX5ts256(CertUtils.confirmationMethodHashS256(certAsPem)); + + return accessToken; + } + + @Override + public RefreshToken createRefreshToken(ExecutionContext executionContext) { + int lifetime = appConfiguration.getRefreshTokenLifetime(); + if (client.getRefreshTokenLifetime() != null && client.getRefreshTokenLifetime() > 0) { + lifetime = client.getRefreshTokenLifetime(); + log.debug("Overwritten refresh_token lifetime from client, clientId: {} .", client.getClientId()); + } + + final int refreshTokenLifetimeFromScript = executionContext.getRefreshTokenLifetimeFromScript(); + if (refreshTokenLifetimeFromScript > 0) { + lifetime = refreshTokenLifetimeFromScript; + log.debug("Overwritten refresh_token lifetime from script with {} value.", refreshTokenLifetimeFromScript); + } + + RefreshToken refreshToken = new RefreshToken(lifetime); + + refreshToken.setAuthMode(getAcrValues()); + refreshToken.setSessionDn(getSessionDn()); + + return refreshToken; + } + + @Override + public String getUserId() { + if (user == null) { + return null; + } + + return user.getUserId(); + } + + @Override + public String getUserDn() { + if (user == null) { + return null; + } + + return user.getDn(); + } + + /** + * Returns the {@link AuthorizationGrantType}. + * + * @return The authorization grant type. + */ + @Override + public AuthorizationGrantType getAuthorizationGrantType() { + return authorizationGrantType; + } + + /** + * Returns the {@link org.gluu.oxauth.model.registration.Client}. An + * application making protected resource requests on behalf of the resource + * owner and with its authorization. + * + * @return The client. + */ + @Override + public Client getClient() { + return client; + } + + @Override + public String getClientId() { + if (client == null) { + return null; + } + + return client.getClientId(); + } + + @Override + public String getClientDn() { + if (client == null) { + return null; + } + + return client.getDn(); + } + + @Override + public Date getAuthenticationTime() { + return authenticationTime; + } + + public void setAuthenticationTime(Date authenticationTime) { + this.authenticationTime = authenticationTime; + } + + /** + * Returns a list of the scopes granted to the client. + * + * @return List of the scopes granted to the client. + */ + @Override + public Set getScopes() { + return scopes; + } + + @Override + public JwtAuthorizationRequest getJwtAuthorizationRequest() { + return jwtAuthorizationRequest; + } + + @Override + public void setJwtAuthorizationRequest(JwtAuthorizationRequest p_jwtAuthorizationRequest) { + jwtAuthorizationRequest = p_jwtAuthorizationRequest; + } + + @Override + public void setAccessTokens(List accessTokens) { + put(this.accessTokens, accessTokens); + } + + private static void put(ConcurrentMap p_map, List p_list) { + p_map.clear(); + if (p_list != null && !p_list.isEmpty()) { + for (T t : p_list) { + p_map.put(t.getCode(), t); + } + } + } + + /** + * Returns a list with all the issued refresh tokens. + * + * @return List with all the issued refresh tokens. + */ + @Override + public List getRefreshTokens() { + return new ArrayList(refreshTokens.values()); + } + + @Override + public void setRefreshTokens(List refreshTokens) { + put(this.refreshTokens, refreshTokens); + } + + /** + * Gets the refresh token instance from the refresh token list given its + * code. + * + * @param refreshTokenCode The code of the refresh token. + * @return The refresh token instance or null if not found. + */ + @Override + public RefreshToken getRefreshToken(String refreshTokenCode) { + if (log.isTraceEnabled()) { + log.trace("Looking for the refresh token: " + refreshTokenCode + " for an authorization grant of type: " + + getAuthorizationGrantType()); + } + return refreshTokens.get(TokenHashUtil.hash(refreshTokenCode)); + } + + /** + * Gets the access token instance from the id token list or the access token + * list given its code. + * + * @param tokenCode The code of the access token. + * @return The access token instance or null if not found. + */ + @Override + public AbstractToken getAccessToken(String tokenCode) { + + String hashedTokenCode = TokenHashUtil.hash(tokenCode); + + final IdToken idToken = getIdToken(); + if (idToken != null) { + if (idToken.getCode().equals(hashedTokenCode)) { + return idToken; + } + } + + final AccessToken longLivedAccessToken = getLongLivedAccessToken(); + if (longLivedAccessToken != null) { + if (longLivedAccessToken.getCode().equals(hashedTokenCode)) { + return longLivedAccessToken; + } + } + + return accessTokens.get(hashedTokenCode); + } + + @Override + public String toString() { + return "AbstractAuthorizationGrant{" + "user=" + user + ", authorizationCode=" + authorizationCode + ", client=" + + client + ", grantId='" + grantId + '\'' + ", nonce='" + nonce + '\'' + ", acrValues='" + acrValues + + '\'' + ", sessionDn='" + sessionDn + '\'' + ", codeChallenge='" + codeChallenge + '\'' + + ", codeChallengeMethod='" + codeChallengeMethod + '\'' + ", authenticationTime=" + authenticationTime + + ", scopes=" + scopes + ", authorizationGrantType=" + authorizationGrantType + ", tokenBindingHash=" + tokenBindingHash + + ", x5ts256=" + x5ts256 + ", claims=" + claims + '}'; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AbstractToken.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AbstractToken.java new file mode 100644 index 00000000..ece6741b --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AbstractToken.java @@ -0,0 +1,291 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.token.HandleTokenFactory; +import org.gluu.oxauth.model.util.HashUtil; +import org.gluu.oxauth.util.ServerUtil; +import org.gluu.persist.annotation.AttributeName; +import org.gluu.persist.annotation.Expiration; +import org.gluu.persist.model.base.Deletable; + +import java.io.Serializable; +import java.time.Duration; +import java.util.Calendar; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +/** + *

+ * Base class for the access token, refresh token and authorization code. + *

+ *

+ * When created, a token is valid for a given lifetime, and after this period of + * time, it will be marked as expired automatically by a background process. + *

+ *

+ * When required, the token can be marked as revoked. + *

+ * + * @author Javier Rojas Blum + * @version March 14, 2019 + */ +public abstract class AbstractToken implements Serializable, Deletable { + + @AttributeName(name = "tknCde", consistency = true) + private String code; + @AttributeName(name = "iat") + private Date creationDate; + @AttributeName(name = "exp") + private Date expirationDate; + @AttributeName(name = "del") + private boolean deletable = true; + private boolean revoked; + private boolean expired; + + private String authMode; + + @AttributeName(name = "ssnId") + private String sessionDn; + private String x5ts256; + + @Expiration + private int ttl; + + /** + * Creates and initializes the values of an abstract token. + * + * @param lifeTime The life time of the token. + */ + public AbstractToken(int lifeTime) { + if (lifeTime <= 0) { + throw new IllegalArgumentException("Lifetime of the token is less or equal to zero."); + } + ttl = lifeTime; + Calendar calendar = Calendar.getInstance(); + creationDate = calendar.getTime(); + calendar.add(Calendar.SECOND, lifeTime); + expirationDate = calendar.getTime(); + + code = HandleTokenFactory.generateHandleToken(); + + revoked = false; + expired = false; + } + + protected AbstractToken(String code, Date creationDate, Date expirationDate) { + this.code = code; + this.creationDate = creationDate; + this.expirationDate = expirationDate; + + checkExpired(); + } + + public int getTtl() { + initTtl(); + return ttl; + } + + private void initTtl() { + if (ttl > 0) { + return; + } + ttl = ServerUtil.calculateTtl(creationDate, expirationDate); + if (ttl > 0) { + return; + } + // unable to calculate ttl (expiration or creation date is not set), thus defaults it to 1 day + ttl = (int) TimeUnit.DAYS.toSeconds(1); + } + + public void resetTtlFromExpirationDate() { + this.ttl = (int) Duration.between(new Date().toInstant(), getExpirationDate().toInstant()).getSeconds(); + } + + /** + * Checks whether the token has expired and if true, marks itself as expired. + */ + public void checkExpired() { + checkExpired(new Date()); + } + + /** + * Checks whether the token has expired and if true, marks itself as expired. + */ + public void checkExpired(Date now) { + if (now.after(expirationDate)) { + expired = true; + } + } + + /** + * Checks whether a token is valid, it is valid if it is not revoked and not + * expired. + * + * @return Returns true if the token is valid. + */ + public boolean isValid() { + return !revoked && !expired; + } + + /** + * Returns the token code. + * + * @return The Code of the token. + */ + public String getCode() { + return code; + } + + /** + * Sets the token code. + * + * @param code The code of the token. + */ + public void setCode(String code) { + this.code = code; + } + + /** + * Returns the creation date of the token. + * + * @return The creation date. + */ + public Date getCreationDate() { + return creationDate != null ? new Date(creationDate.getTime()) : null; + } + + /** + * Sets the creation date of the token. + * + * @param creationDate The creation date. + */ + public void setCreationDate(Date creationDate) { + this.creationDate = creationDate != null ? new Date(creationDate.getTime()) : null; + } + + /** + * Returns the expiration date of the token. + * + * @return The expiration date. + */ + public Date getExpirationDate() { + return expirationDate != null ? new Date(expirationDate.getTime()) : null; + } + + /** + * Sets the expiration date of the token. + * + * @param expirationDate The expiration date. + */ + public void setExpirationDate(Date expirationDate) { + this.expirationDate = expirationDate != null ? new Date(expirationDate.getTime()) : null; + } + + /** + * Returns true if the token has been revoked. + * + * @return true if the token has been revoked. + */ + public boolean isRevoked() { + return revoked; + } + + /** + * Sets the value of the revoked flag to indicate whether the token has been + * revoked. + * + * @param revoked Revoke or not. + */ + public synchronized void setRevoked(boolean revoked) { + this.revoked = revoked; + } + + /** + * Return true if the token has expired. + * + * @return true if the token has expired. + */ + public boolean isExpired() { + return expired; + } + + /** + * Sets the value of the expired flag to indicate whether the token has + * expired. + * + * @param expired Expire or not. + */ + public synchronized void setExpired(boolean expired) { + this.expired = expired; + } + + /** + * Returns the authentication mode. + * + * @return The authentication mode. + */ + public String getAuthMode() { + return authMode; + } + + /** + * Sets the authentication mode. + * + * @param authMode The authentication mode. + */ + public void setAuthMode(String authMode) { + this.authMode = authMode; + } + + public String getX5ts256() { + return x5ts256; + } + + public void setX5ts256(String x5ts256) { + this.x5ts256 = x5ts256; + } + + public String getSessionDn() { + return sessionDn; + } + + public void setSessionDn(String sessionDn) { + this.sessionDn = sessionDn; + } + + @Override + public Boolean isDeletable() { + return deletable; + } + + public void setDeletable(boolean deletable) { + this.deletable = deletable; + } + + /** + * Returns the lifetime in seconds of the token. + * + * @return The lifetime in seconds of the token. + */ + public int getExpiresIn() { + int expiresIn = 0; + + checkExpired(); + if (isValid()) { + long diff = expirationDate.getTime() - new Date().getTime(); + expiresIn = diff != 0 ? (int) (diff / 1000) : 0; + } + + return expiresIn; + } + + public static String getHash(String input, SignatureAlgorithm signatureAlgorithm) { + return HashUtil.getHash(input, signatureAlgorithm); + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AccessToken.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AccessToken.java new file mode 100644 index 00000000..2d8d15b0 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AccessToken.java @@ -0,0 +1,73 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import java.util.Date; + +import org.gluu.oxauth.model.common.TokenType; + +/** + *

+ * Access token (as well as any access token type-specific attributes) MUST be + * kept confidential in transit and storage, and only shared among the + * authorization server, the resource servers the access token is valid for, and + * the client to whom the access token is issued. + *

+ *

+ * When using the implicit grant type, the access token is transmitted in the + * URI fragment, which can expose it to unauthorized parties. + *

+ *

+ * The authorization server MUST ensure that access tokens cannot be generated, + * modified, or guessed to produce valid access tokens by unauthorized parties. + *

+ *

+ * The client SHOULD request access tokens with the minimal scope and lifetime + * necessary. The authorization server SHOULD take the client identity into + * account when choosing how to honor the requested scope and lifetime, and MAY + * issue an access token with a less rights than requested. + *

+ * + * @author Javier Rojas Blum Date: 09.29.2011 + */ +public class AccessToken extends AbstractToken { + + private TokenType tokenType; + + /** + *

+ * Constructs an access token. + *

+ *

+ * When created, a token is valid for a given lifetime, and after this + * period of time, it will be marked as expired automatically by a + * background process. + *

+ *

+ * When required, the token can be marked as revoked. + *

+ * + * @param lifeTime The life time of the token. + */ + public AccessToken(int lifeTime) { + super(lifeTime); + this.tokenType = TokenType.BEARER; + } + + public AccessToken(String tokenCode, Date creationDate, Date expirationDate) { + super(tokenCode, creationDate, expirationDate); + } + + /** + * Returns the {@link TokenType}. + * + * @return The token type. + */ + public TokenType getTokenType() { + return tokenType; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AuthorizationCode.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AuthorizationCode.java new file mode 100644 index 00000000..7f5835d1 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AuthorizationCode.java @@ -0,0 +1,94 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import java.util.Date; + +/** + *

+ * The authorization code is obtained by using an authorization server as an + * intermediary between the client and resource owner. Instead of requesting + * authorization directly from the resource owner, the client directs the + * resource owner to an authorization server (via its user- agent as defined in + * [RFC2616]), which in turn directs the resource owner back to the client with + * the authorization code. + *

+ *

+ * Before directing the resource owner back to the client with the authorization + * code, the authorization server authenticates the resource owner and obtains + * authorization. Because the resource owner only authenticates with the + * authorization server, the resource owner's credentials are never shared with + * the client. + *

+ *

+ * The authorization code provides a few important security benefits such as the + * ability to authenticate the client, and the transmission of the access token + * directly to the client without passing it through the resource owner's + * user-agent, potentially exposing it to others, including the resource owner. + *

+ * + * @author Javier Rojas Blum Date: 09.29.2011 + */ +public class AuthorizationCode extends AbstractToken { + + private boolean used; + + /** + *

+ * Constructs an authorization code. + *

+ *

+ * When created, a token is valid for a given lifetime, and after this + * period of time, it will be marked as expired automatically by a + * background process. + *

+ *

+ * When required, the token can be marked as revoked. + *

+ * + * @param lifeTime The life time of the token. + */ + public AuthorizationCode(int lifeTime) { + super(lifeTime); + used = false; + } + + public AuthorizationCode(String code, Date creationDate, Date expirationDate) { + super(code, creationDate, expirationDate); + used = false; + checkExpired(); + } + + /** + * Checks whether a token is valid. An authorization code is valid if + * it has not been used before, not revoked and not expired. + */ + @Override + public boolean isValid() { + return super.isValid() && !used; + } + + /** + * Returns whether an authorization code has been used. + * + * @return true if the authorization code has been used. + */ + public boolean isUsed() { + return used; + } + + /** + * Sets the flag to indicate whether a token has been used. + * The authorization code must be used only once and after + * it must be marked as used. + * + * @param used Used or not. + */ + public synchronized void setUsed(boolean used) { + this.used = used; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AuthorizationCodeGrant.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AuthorizationCodeGrant.java new file mode 100644 index 00000000..99e7bdee --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AuthorizationCodeGrant.java @@ -0,0 +1,89 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import org.gluu.oxauth.model.registration.Client; + +import java.util.Date; + +/** + *

+ * The authorization code is obtained by using an authorization server as an + * intermediary between the client and resource owner. Instead of requesting + * authorization directly from the resource owner, the client directs the + * resource owner to an authorization server (via its user- agent as defined in + * [RFC2616]), which in turn directs the resource owner back to the client with + * the authorization code. + *

+ *

+ * Before directing the resource owner back to the client with the authorization + * code, the authorization server authenticates the resource owner and obtains + * authorization. Because the resource owner only authenticates with the + * authorization server, the resource owner's credentials are never shared with + * the client. + *

+ *

+ * The authorization code provides a few important security benefits such as the + * ability to authenticate the client, and the transmission of the access token + * directly to the client without passing it through the resource owner's + * user-agent, potentially exposing it to others, including the resource owner. + *

+ * + * @author Javier Rojas Blum Date: 09.29.2011 + * @author Yuriy Movchan + */ +public class AuthorizationCodeGrant extends AuthorizationGrant { + + public AuthorizationCodeGrant() {} + + /** + * Constructs and authorization code grant. + * + * @param user The resource owner. + * @param client An application making protected resource requests on behalf of the resource owner and + * with its authorization. + * @param authenticationTime The Claim Value is the number of seconds from 1970-01-01T0:0:0Z as measured in UTC + * until the date/time that the End-User authentication occurred. + */ + public AuthorizationCodeGrant(User user, Client client, Date authenticationTime) { + init(user, client, authenticationTime); + } + + public void init(User user, Client client, Date authenticationTime) { + super.init(user, AuthorizationGrantType.AUTHORIZATION_CODE, client, authenticationTime); + setAuthorizationCode(new AuthorizationCode(appConfiguration.getAuthorizationCodeLifetime())); + setIsCachedWithNoPersistence(true); + } + + @Override + public GrantType getGrantType() { + return GrantType.AUTHORIZATION_CODE; + } + + /** + * Revokes all the issued tokens. + */ + @Override + public void revokeAllTokens() { + super.revokeAllTokens(); + if (getAuthorizationCode() != null) { + getAuthorizationCode().setRevoked(true); + } + } + + /** + * Checks all tokens for expiration. Each token will check itself and mark + * as expired when needed. + */ + @Override + public void checkExpiredTokens() { + super.checkExpiredTokens(); + if (getAuthorizationCode() != null) { + getAuthorizationCode().checkExpired(); + } + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AuthorizationGrant.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AuthorizationGrant.java new file mode 100644 index 00000000..5546bef6 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AuthorizationGrant.java @@ -0,0 +1,480 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import com.google.common.base.Function; +import com.google.common.collect.Lists; +import org.apache.commons.lang.BooleanUtils; +import org.apache.commons.lang.StringUtils; +import org.gluu.model.metric.MetricType; +import org.gluu.oxauth.claims.Audience; +import org.gluu.oxauth.model.authorize.JwtAuthorizationRequest; +import org.gluu.oxauth.model.config.WebKeysConfiguration; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.ldap.TokenLdap; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.token.HandleTokenFactory; +import org.gluu.oxauth.model.token.IdTokenFactory; +import org.gluu.oxauth.model.token.JsonWebResponse; +import org.gluu.oxauth.model.token.JwtSigner; +import org.gluu.oxauth.model.util.JwtUtil; +import org.gluu.oxauth.service.*; +import org.gluu.oxauth.service.external.ExternalIntrospectionService; +import org.gluu.oxauth.service.external.ExternalUpdateTokenService; +import org.gluu.oxauth.service.external.context.ExternalIntrospectionContext; +import org.gluu.oxauth.service.external.context.ExternalUpdateTokenContext; +import org.gluu.oxauth.service.stat.StatService; +import org.gluu.oxauth.util.TokenHashUtil; +import org.gluu.service.CacheService; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.ws.rs.WebApplicationException; +import java.util.Date; +import java.util.List; +import java.util.Set; + +/** + * Base class for all the types of authorization grant. + * + * @author Javier Rojas Blum + * @author Yuriy Movchan + * @version April 10, 2020 + */ +public abstract class AuthorizationGrant extends AbstractAuthorizationGrant { + + private static final Logger log = LoggerFactory.getLogger(AuthorizationGrant.class); + + @Inject + private CacheService cacheService; + + @Inject + private GrantService grantService; + + @Inject + private IdTokenFactory idTokenFactory; + + @Inject + private WebKeysConfiguration webKeysConfiguration; + + @Inject + private ClientService clientService; + + @Inject + private ExternalIntrospectionService externalIntrospectionService; + + @Inject + private AttributeService attributeService; + + @Inject + private SectorIdentifierService sectorIdentifierService; + + @Inject + private MetricService metricService; + + @Inject + private StatService statService; + + @Inject + private ExternalUpdateTokenService externalUpdateTokenService; + + private boolean isCachedWithNoPersistence = false; + + public AuthorizationGrant() { + } + + public AuthorizationGrant(User user, AuthorizationGrantType authorizationGrantType, Client client, + Date authenticationTime) { + super(user, authorizationGrantType, client, authenticationTime); + } + + public void init(User user, AuthorizationGrantType authorizationGrantType, Client client, Date authenticationTime) { + super.init(user, authorizationGrantType, client, authenticationTime); + } + + public IdToken createIdToken( + IAuthorizationGrant grant, String nonce, + AuthorizationCode authorizationCode, AccessToken accessToken, RefreshToken refreshToken, + String state, Set scopes, boolean includeIdTokenClaims, Function preProcessing, + Function postProcessing, ExecutionContext executionContext) throws Exception { + JsonWebResponse jwr = idTokenFactory.createJwr(grant, nonce, authorizationCode, accessToken, refreshToken, + state, scopes, includeIdTokenClaims, preProcessing, postProcessing, executionContext); + final IdToken idToken = new IdToken(jwr.toString(), jwr.getClaims().getClaimAsDate(JwtClaimName.ISSUED_AT), + jwr.getClaims().getClaimAsDate(JwtClaimName.EXPIRATION_TIME)); + if (log.isTraceEnabled()) + log.trace("Created id_token:" + idToken.getCode() ); + return idToken; + } + + @Override + public String checkScopesPolicy(String scope) { + final String result = super.checkScopesPolicy(scope); + save(); + return result; + } + + @Override + public void save() { + if (isCachedWithNoPersistence) { + if (getAuthorizationGrantType() == AuthorizationGrantType.AUTHORIZATION_CODE) { + saveInCache(); + } else if (getAuthorizationGrantType() == AuthorizationGrantType.CIBA) { + saveInCache(); + } else { + throw new UnsupportedOperationException( + "Grant caching is not supported for : " + getAuthorizationGrantType()); + } + } else { + if (BooleanUtils.isTrue(appConfiguration.getUseCacheForAllImplicitFlowObjects()) && isImplicitFlow()) { + saveInCache(); + return; + } + saveImpl(); + } + } + + private void saveInCache() { + CacheGrant cachedGrant = new CacheGrant(this, appConfiguration); + cacheService.put(cachedGrant.getExpiresIn(), cachedGrant.cacheKey(), cachedGrant); + } + + public boolean isImplicitFlow() { + return getAuthorizationGrantType() == null || getAuthorizationGrantType() == AuthorizationGrantType.IMPLICIT; + } + + private void saveImpl() { + String grantId = getGrantId(); + if (grantId != null && StringUtils.isNotBlank(grantId)) { + final List grants = grantService.getGrantsByGrantId(grantId); + if (grants != null && !grants.isEmpty()) { + for (TokenLdap t : grants) { + initTokenFromGrant(t); + log.debug("Saving grant: " + grantId + ", code_challenge: " + getCodeChallenge()); + grantService.mergeSilently(t); + } + } + } + } + + private void initTokenFromGrant(TokenLdap token) { + final String nonce = getNonce(); + if (nonce != null) { + token.setNonce(nonce); + } + token.setScope(getScopesAsString()); + token.setAuthMode(getAcrValues()); + token.setSessionDn(getSessionDn()); + token.setAuthenticationTime(getAuthenticationTime()); + token.setCodeChallenge(getCodeChallenge()); + token.setCodeChallengeMethod(getCodeChallengeMethod()); + token.setClaims(getClaims()); + + final JwtAuthorizationRequest jwtRequest = getJwtAuthorizationRequest(); + if (jwtRequest != null && StringUtils.isNotBlank(jwtRequest.getEncodedJwt())) { + token.setJwtRequest(jwtRequest.getEncodedJwt()); + } + } + + @Override + public AccessToken createAccessToken(String certAsPem, ExecutionContext context) { + try { + context.setGrant(this); + + final AccessToken accessToken = super.createAccessToken(certAsPem, context); + if (accessToken.getExpiresIn() < 0) { + log.trace("Failed to create access token with negative expiration time"); + return null; + } + + JwtSigner jwtSigner = null; + if (getClient().isAccessTokenAsJwt()) { + jwtSigner = createAccessTokenAsJwt(accessToken, context); + } + + boolean externalOk = externalUpdateTokenService.modifyAccessToken(accessToken, ExternalUpdateTokenContext.of(context, jwtSigner)); + if (!externalOk) { + log.trace("External script forbids access token creation."); + return null; + } + + if (getClient().isAccessTokenAsJwt() && jwtSigner != null) { + final String accessTokenCode = jwtSigner.sign().toString(); + if (log.isTraceEnabled()) + log.trace("Created access token JWT: {}", accessTokenCode + ", claims: " + jwtSigner.getJwt().getClaims().toJsonString()); + + accessToken.setCode(accessTokenCode); + } + + final TokenLdap tokenEntity = asToken(accessToken); + context.setAccessTokenEntity(tokenEntity); + + persist(tokenEntity); + + statService.reportAccessToken(getGrantType()); + metricService.incCounter(MetricType.OXAUTH_TOKEN_ACCESS_TOKEN_COUNT); + + if (log.isTraceEnabled()) + log.trace("Created plain access token: {}", accessToken.getCode()); + + return accessToken; + } catch (WebApplicationException e) { + throw e; + } catch (Exception e) { + log.error(e.getMessage(), e); + return null; + } + } + + private JwtSigner createAccessTokenAsJwt(AccessToken accessToken, ExecutionContext context) throws Exception { + final User user = getUser(); + final Client client = getClient(); + + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm + .fromString(appConfiguration.getDefaultSignatureAlgorithm()); + if (client.getAccessTokenSigningAlg() != null + && SignatureAlgorithm.fromString(client.getAccessTokenSigningAlg()) != null) { + signatureAlgorithm = SignatureAlgorithm.fromString(client.getAccessTokenSigningAlg()); + } + + final JwtSigner jwtSigner = new JwtSigner(appConfiguration, webKeysConfiguration, signatureAlgorithm, + client.getClientId(), clientService.decryptSecret(client.getClientSecret())); + final Jwt jwt = jwtSigner.newJwt(); + jwt.getClaims().setClaim("scope", Lists.newArrayList(getScopes())); + jwt.getClaims().setClaim("client_id", getClientId()); + jwt.getClaims().setClaim("username", user != null ? user.getAttribute("displayName") : null); + jwt.getClaims().setClaim("token_type", accessToken.getTokenType().getName()); + jwt.getClaims().setClaim("code", accessToken.getCode()); // guarantee uniqueness : without it we can get race condition + jwt.getClaims().setExpirationTime(accessToken.getExpirationDate()); + jwt.getClaims().setIssuedAt(accessToken.getCreationDate()); + jwt.getClaims().setSubjectIdentifier(getSub()); + jwt.getClaims().setClaim("x5t#S256", accessToken.getX5ts256()); + Audience.setAudience(jwt.getClaims(), getClient()); + + if (client.getAttributes().getRunIntrospectionScriptBeforeAccessTokenAsJwtCreationAndIncludeClaims()) { + runIntrospectionScriptAndInjectValuesIntoJwt(jwt, context); + } + + return jwtSigner; + } + + private void runIntrospectionScriptAndInjectValuesIntoJwt(Jwt jwt, ExecutionContext executionContext) { + JSONObject responseAsJsonObject = new JSONObject(); + + ExternalIntrospectionContext context = new ExternalIntrospectionContext(this, executionContext.getHttpRequest(), executionContext.getHttpResponse(), appConfiguration, attributeService); + context.setAccessTokenAsJwt(jwt); + if (externalIntrospectionService.executeExternalModifyResponse(responseAsJsonObject, context)) { + log.trace("Successfully run external introspection scripts."); + + if (context.isTranferIntrospectionPropertiesIntoJwtClaims()) { + log.trace("Transfering claims into jwt ..."); + JwtUtil.transferIntoJwtClaims(responseAsJsonObject, jwt); + log.trace("Transfered."); + } + } + } + + @Override + public RefreshToken createRefreshToken(ExecutionContext executionContext) { + try { + final int refreshTokenLifetimeInSeconds = externalUpdateTokenService.getRefreshTokenLifetimeInSeconds(ExternalUpdateTokenContext.of(executionContext)); + executionContext.setRefreshTokenLifetimeFromScript(refreshTokenLifetimeInSeconds); + + final RefreshToken refreshToken = super.createRefreshToken(executionContext); + if (refreshToken.getExpiresIn() > 0) { + final TokenLdap entity = asToken(refreshToken); + executionContext.setRefreshTokenEntity(entity); + + boolean externalOk = externalUpdateTokenService.modifyRefreshToken(refreshToken, ExternalUpdateTokenContext.of(executionContext)); + if (!externalOk) { + log.trace("External script forbids refresh token creation."); + return null; + } + + persist(entity); + } + + statService.reportRefreshToken(getGrantType()); + metricService.incCounter(MetricType.OXAUTH_TOKEN_REFRESH_TOKEN_COUNT); + + if (log.isTraceEnabled()) + log.trace("Created refresh token: " + refreshToken.getCode()); + + return refreshToken; + } catch (Exception e) { + log.error(e.getMessage(), e); + return null; + } + } + + public RefreshToken createRefreshToken(ExecutionContext executionContext, Date expirationDate) { + try { + RefreshToken refreshToken = new RefreshToken(HandleTokenFactory.generateHandleToken(), new Date(), expirationDate); + + refreshToken.setAuthMode(getAcrValues()); + refreshToken.setSessionDn(getSessionDn()); + + if (refreshToken.getExpiresIn() > 0) { + final TokenLdap entity = asToken(refreshToken); + executionContext.setRefreshTokenEntity(entity); + + boolean externalOk = externalUpdateTokenService.modifyRefreshToken(refreshToken, ExternalUpdateTokenContext.of(executionContext)); + if (!externalOk) { + log.trace("External script forbids refresh token creation."); + return null; + } + + persist(entity); + statService.reportRefreshToken(getGrantType()); + metricService.incCounter(MetricType.OXAUTH_TOKEN_REFRESH_TOKEN_COUNT); + + if (log.isTraceEnabled()) + log.trace("Created refresh token: " + refreshToken.getCode()); + + return refreshToken; + } + + log.debug("Token expiration date is in the past. Skip creation."); + return null; + } catch (Exception e) { + log.error(e.getMessage(), e); + return null; + } + } + + @Override + public IdToken createIdToken( + String nonce, AuthorizationCode authorizationCode, AccessToken accessToken, RefreshToken refreshToken, + String state, AuthorizationGrant authorizationGrant, boolean includeIdTokenClaims, Function preProcessing, + Function postProcessing, ExecutionContext executionContext) { + try { + final IdToken idToken = createIdToken(this, nonce, authorizationCode, accessToken, refreshToken, + state, getScopes(), includeIdTokenClaims, preProcessing, postProcessing, executionContext); + final String acrValues = authorizationGrant.getAcrValues(); + final String sessionDn = authorizationGrant.getSessionDn(); + if (idToken.getExpiresIn() > 0) { + final TokenLdap tokenLdap = asToken(idToken); + tokenLdap.setAuthMode(acrValues); + tokenLdap.setSessionDn(sessionDn); + persist(tokenLdap); + } + + setAcrValues(acrValues); + setSessionDn(sessionDn); + + statService.reportIdToken(getGrantType()); + metricService.incCounter(MetricType.OXAUTH_TOKEN_ID_TOKEN_COUNT); + + return idToken; + } catch (WebApplicationException e) { + throw e; + } catch (Exception e) { + log.error(e.getMessage(), e); + return null; + } + } + + public void persist(TokenLdap p_token) { + grantService.persist(p_token); + } + + public void persist(AuthorizationCode p_code) { + persist(asToken(p_code)); + } + + public TokenLdap asToken(IdToken p_token) { + final TokenLdap result = asTokenLdap(p_token); + result.setTokenTypeEnum(org.gluu.oxauth.model.ldap.TokenType.ID_TOKEN); + return result; + } + + public TokenLdap asToken(RefreshToken p_token) { + final TokenLdap result = asTokenLdap(p_token); + result.setTokenTypeEnum(org.gluu.oxauth.model.ldap.TokenType.REFRESH_TOKEN); + return result; + } + + public TokenLdap asToken(AuthorizationCode p_authorizationCode) { + final TokenLdap result = asTokenLdap(p_authorizationCode); + result.setTokenTypeEnum(org.gluu.oxauth.model.ldap.TokenType.AUTHORIZATION_CODE); + return result; + } + + public TokenLdap asToken(AccessToken p_accessToken) { + final TokenLdap result = asTokenLdap(p_accessToken); + result.setTokenTypeEnum(org.gluu.oxauth.model.ldap.TokenType.ACCESS_TOKEN); + return result; + } + + public String getScopesAsString() { + final StringBuilder scopes = new StringBuilder(); + for (String s : getScopes()) { + scopes.append(s).append(" "); + } + return scopes.toString().trim(); + } + + public TokenLdap asTokenLdap(AbstractToken p_token) { + + final TokenLdap result = new TokenLdap(); + final String hashedCode = TokenHashUtil.hash(p_token.getCode()); + + result.setDn(grantService.buildDn(hashedCode)); + result.setGrantId(getGrantId()); + result.setCreationDate(p_token.getCreationDate()); + result.setExpirationDate(p_token.getExpirationDate()); + result.setTtl(p_token.getTtl()); + result.setTokenCode(hashedCode); + result.setUserId(getUserId()); + result.setClientId(getClientId()); + + result.getAttributes().setX5cs256(p_token.getX5ts256()); + + final AuthorizationGrantType grantType = getAuthorizationGrantType(); + if (grantType != null) { + result.setGrantType(grantType.getParamName()); + } + + final AuthorizationCode authorizationCode = getAuthorizationCode(); + if (authorizationCode != null) { + result.setAuthorizationCode(TokenHashUtil.hash(authorizationCode.getCode())); + } + + initTokenFromGrant(result); + + return result; + } + + @Override + public void revokeAllTokens() { + final TokenLdap tokenLdap = getTokenLdap(); + if (tokenLdap != null && StringUtils.isNotBlank(tokenLdap.getGrantId())) { + grantService.removeAllByGrantId(tokenLdap.getGrantId()); + } + } + + @Override + public void checkExpiredTokens() { + // do nothing, clean up is made via grant service: + // org.gluu.oxauth.service.GrantService.cleanUp() + } + + public String getSub() { + return sectorIdentifierService.getSub(this); + } + + public boolean isCachedWithNoPersistence() { + return isCachedWithNoPersistence; + } + + public void setIsCachedWithNoPersistence(boolean isCachedWithNoPersistence) { + this.isCachedWithNoPersistence = isCachedWithNoPersistence; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AuthorizationGrantList.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AuthorizationGrantList.java new file mode 100644 index 00000000..0cd28c93 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AuthorizationGrantList.java @@ -0,0 +1,369 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import org.apache.commons.lang.StringUtils; +import org.gluu.model.metric.MetricType; +import org.gluu.oxauth.model.authorize.JwtAuthorizationRequest; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.crypto.AbstractCryptoProvider; +import org.gluu.oxauth.model.ldap.TokenLdap; +import org.gluu.oxauth.model.ldap.TokenType; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.util.Util; +import org.gluu.oxauth.service.ClientService; +import org.gluu.oxauth.service.GrantService; +import org.gluu.oxauth.service.MetricService; +import org.gluu.oxauth.service.common.UserService; +import org.gluu.oxauth.util.ServerUtil; +import org.gluu.oxauth.util.TokenHashUtil; +import org.gluu.service.CacheService; +import org.gluu.util.StringHelper; +import org.slf4j.Logger; + +import javax.enterprise.context.Dependent; +import javax.enterprise.inject.Instance; +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +/** + * Component to hold in memory authorization grant objects. + * + * @author Javier Rojas Blum + * @version February 25, 2020 + */ +@Dependent +public class AuthorizationGrantList implements IAuthorizationGrantList { + + @Inject + private Logger log; + + @Inject + private Instance grantInstance; + + @Inject + private GrantService grantService; + + @Inject + private UserService userService; + + @Inject + private ClientService clientService; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private CacheService cacheService; + + @Inject + private AbstractCryptoProvider cryptoProvider; + + @Inject + private MetricService metricService; + + @Override + public void removeAuthorizationGrants(List authorizationGrants) { + if (authorizationGrants != null && !authorizationGrants.isEmpty()) { + for (AuthorizationGrant r : authorizationGrants) { + grantService.remove(r); + } + } + } + + @Override + public AuthorizationGrant createAuthorizationGrant(User user, Client client, Date authenticationTime) { + AuthorizationGrant grant = grantInstance.select(SimpleAuthorizationGrant.class).get(); + grant.init(user, null, client, authenticationTime); + + return grant; + } + + @Override + public AuthorizationCodeGrant createAuthorizationCodeGrant(User user, Client client, Date authenticationTime) { + AuthorizationCodeGrant grant = grantInstance.select(AuthorizationCodeGrant.class).get(); + grant.init(user, client, authenticationTime); + + CacheGrant memcachedGrant = new CacheGrant(grant, appConfiguration); + cacheService.put(grant.getAuthorizationCode().getExpiresIn(), memcachedGrant.cacheKey(), memcachedGrant); + log.trace("Put authorization grant in cache, code: " + grant.getAuthorizationCode().getCode() + ", clientId: " + grant.getClientId()); + + metricService.incCounter(MetricType.OXAUTH_TOKEN_AUTHORIZATION_CODE_COUNT); + return grant; + } + + @Override + public ImplicitGrant createImplicitGrant(User user, Client client, Date authenticationTime) { + ImplicitGrant grant = grantInstance.select(ImplicitGrant.class).get(); + grant.init(user, client, authenticationTime); + + return grant; + } + + @Override + public ClientCredentialsGrant createClientCredentialsGrant(User user, Client client) { + ClientCredentialsGrant grant = grantInstance.select(ClientCredentialsGrant.class).get(); + grant.init(user, client); + + return grant; + } + + @Override + public ResourceOwnerPasswordCredentialsGrant createResourceOwnerPasswordCredentialsGrant(User user, Client client) { + ResourceOwnerPasswordCredentialsGrant grant = grantInstance.select(ResourceOwnerPasswordCredentialsGrant.class).get(); + grant.init(user, client); + + return grant; + } + + @Override + public CIBAGrant createCIBAGrant(CibaRequestCacheControl request) { + CIBAGrant grant = grantInstance.select(CIBAGrant.class).get(); + grant.init(request); + + CacheGrant memcachedGrant = new CacheGrant(grant, appConfiguration); + cacheService.put(request.getExpiresIn(), memcachedGrant.getAuthReqId(), memcachedGrant); + log.trace("Ciba grant saved in cache, authReqId: {}, grantId: {}", grant.getAuthReqId(), grant.getGrantId()); + return grant; + } + + @Override + public CIBAGrant getCIBAGrant(String authReqId) { + Object cachedGrant = cacheService.get(authReqId); + if (cachedGrant == null) { + // retry one time : sometimes during high load cache client may be not fast enough + cachedGrant = cacheService.get(authReqId); + log.trace("Failed to fetch CIBA grant from cache, authReqId: {}", authReqId); + } + return cachedGrant instanceof CacheGrant ? ((CacheGrant) cachedGrant).asCibaGrant(grantInstance) : null; + } + + @Override + public DeviceCodeGrant createDeviceGrant(DeviceAuthorizationCacheControl data, User user) { + DeviceCodeGrant grant = grantInstance.select(DeviceCodeGrant.class).get(); + grant.init(data, user); + + CacheGrant memcachedGrant = new CacheGrant(grant, appConfiguration); + cacheService.put(data.getExpiresIn(), memcachedGrant.getDeviceCode(), memcachedGrant); + log.trace("Device code grant saved in cache, deviceCode: {}, grantId: {}", grant.getDeviceCode(), grant.getGrantId()); + return grant; + } + + @Override + public DeviceCodeGrant getDeviceCodeGrant(String deviceCode) { + Object cachedGrant = cacheService.get(deviceCode); + if (cachedGrant == null) { + // retry one time : sometimes during high load cache client may be not fast enough + cachedGrant = cacheService.get(deviceCode); + log.trace("Failed to fetch Device code grant from cache, deviceCode: {}", deviceCode); + } + return cachedGrant instanceof CacheGrant ? ((CacheGrant) cachedGrant).asDeviceCodeGrant(grantInstance) : null; + } + + @Override + public AuthorizationCodeGrant getAuthorizationCodeGrant(String authorizationCode) { + Object cachedGrant = cacheService.get(CacheGrant.cacheKey(authorizationCode, null)); + if (cachedGrant == null) { + // retry one time : sometimes during high load cache client may be not fast enough + cachedGrant = cacheService.get(CacheGrant.cacheKey(authorizationCode, null)); + log.trace("Failed to fetch authorization grant from cache, code: " + authorizationCode); + } + return cachedGrant instanceof CacheGrant ? ((CacheGrant) cachedGrant).asCodeGrant(grantInstance) : null; + } + + @Override + public AuthorizationGrant getAuthorizationGrantByRefreshToken(String clientId, String refreshTokenCode) { + if (!ServerUtil.isTrue(appConfiguration.getPersistRefreshTokenInLdap())) { + return assertTokenType((TokenLdap) cacheService.get(TokenHashUtil.hash(refreshTokenCode)), TokenType.REFRESH_TOKEN, clientId); + } + return assertTokenType(grantService.getGrantByCode(refreshTokenCode), TokenType.REFRESH_TOKEN, clientId); + } + + public AuthorizationGrant assertTokenType(TokenLdap tokenLdap, TokenType tokenType, String clientId) { + if (tokenLdap == null || tokenLdap.getTokenTypeEnum() != tokenType) { + return null; + } + + final AuthorizationGrant grant = asGrant(tokenLdap); + if (grant == null || !grant.getClientId().equals(clientId)) { + return null; + } + return grant; + } + + @Override + public List getAuthorizationGrant(String clientId) { + final List result = new ArrayList<>(); + try { + final List entries = new ArrayList(); + entries.addAll(grantService.getGrantsOfClient(clientId)); + entries.addAll(grantService.getCacheClientTokensEntries(clientId)); + + for (TokenLdap t : entries) { + final AuthorizationGrant grant = asGrant(t); + if (grant != null) { + result.add(grant); + } + } + } catch (Exception e) { + log.trace(e.getMessage(), e); + } + return result; + } + + @Override + public AuthorizationGrant getAuthorizationGrantByAccessToken(String accessToken) { + return getAuthorizationGrantByAccessToken(accessToken, false); + } + + public AuthorizationGrant getAuthorizationGrantByAccessToken(String accessToken, boolean onlyFromCache) { + final TokenLdap tokenLdap = grantService.getGrantByCode(accessToken); + if (tokenLdap != null && (tokenLdap.getTokenTypeEnum() == org.gluu.oxauth.model.ldap.TokenType.ACCESS_TOKEN || tokenLdap.getTokenTypeEnum() == org.gluu.oxauth.model.ldap.TokenType.LONG_LIVED_ACCESS_TOKEN)) { + return asGrant(tokenLdap); + } + return null; + } + + @Override + public AuthorizationGrant getAuthorizationGrantByIdToken(String idToken) { + if (StringUtils.isBlank(idToken)) { + return null; + } + final TokenLdap tokenLdap = grantService.getGrantByCode(idToken); + if (tokenLdap != null && (tokenLdap.getTokenTypeEnum() == org.gluu.oxauth.model.ldap.TokenType.ID_TOKEN)) { + return asGrant(tokenLdap); + } + return null; + } + + public AuthorizationGrant asGrant(TokenLdap tokenLdap) { + if (tokenLdap != null) { + final AuthorizationGrantType grantType = AuthorizationGrantType.fromString(tokenLdap.getGrantType()); + if (grantType != null) { + String userId = tokenLdap.getUserId(); + User user = null; + if (StringHelper.isNotEmpty(userId)) { + user = userService.getUser(userId); + } + final Client client = clientService.getClient(tokenLdap.getClientId()); + final Date authenticationTime = tokenLdap.getAuthenticationTime(); + final String nonce = tokenLdap.getNonce(); + + AuthorizationGrant result; + switch (grantType) { + case AUTHORIZATION_CODE: + AuthorizationCodeGrant authorizationCodeGrant = grantInstance.select(AuthorizationCodeGrant.class).get(); + authorizationCodeGrant.init(user, client, authenticationTime); + + result = authorizationCodeGrant; + break; + case CLIENT_CREDENTIALS: + ClientCredentialsGrant clientCredentialsGrant = grantInstance.select(ClientCredentialsGrant.class).get(); + clientCredentialsGrant.init(user, client); + + result = clientCredentialsGrant; + break; + case IMPLICIT: + ImplicitGrant implicitGrant = grantInstance.select(ImplicitGrant.class).get(); + implicitGrant.init(user, client, authenticationTime); + + result = implicitGrant; + break; + case RESOURCE_OWNER_PASSWORD_CREDENTIALS: + ResourceOwnerPasswordCredentialsGrant resourceOwnerPasswordCredentialsGrant = grantInstance.select(ResourceOwnerPasswordCredentialsGrant.class).get(); + resourceOwnerPasswordCredentialsGrant.init(user, client); + + result = resourceOwnerPasswordCredentialsGrant; + break; + case CIBA: + CIBAGrant cibaGrant = grantInstance.select(CIBAGrant.class).get(); + cibaGrant.init(user, AuthorizationGrantType.CIBA, client, tokenLdap.getCreationDate()); + + result = cibaGrant; + break; + case DEVICE_CODE: + DeviceCodeGrant deviceCodeGrant = grantInstance.select(DeviceCodeGrant.class).get(); + deviceCodeGrant.init(user, AuthorizationGrantType.DEVICE_CODE, client, tokenLdap.getCreationDate()); + + result = deviceCodeGrant; + break; + default: + return null; + } + + final String grantId = tokenLdap.getGrantId(); + final String jwtRequest = tokenLdap.getJwtRequest(); + final String authMode = tokenLdap.getAuthMode(); + final String sessionDn = tokenLdap.getSessionDn(); + final String claims = tokenLdap.getClaims(); + + result.setTokenBindingHash(tokenLdap.getTokenBindingHash()); + result.setNonce(nonce); + result.setX5ts256(tokenLdap.getAttributes().getX5cs256()); + result.setTokenLdap(tokenLdap); + if (StringUtils.isNotBlank(grantId)) { + result.setGrantId(grantId); + } + result.setScopes(Util.splittedStringAsList(tokenLdap.getScope(), " ")); + + result.setCodeChallenge(tokenLdap.getCodeChallenge()); + result.setCodeChallengeMethod(tokenLdap.getCodeChallengeMethod()); + + if (StringUtils.isNotBlank(jwtRequest)) { + try { + result.setJwtAuthorizationRequest(new JwtAuthorizationRequest(appConfiguration, cryptoProvider, jwtRequest, client)); + } catch (Exception e) { + log.trace(e.getMessage(), e); + } + } + + result.setAcrValues(authMode); + result.setSessionDn(sessionDn); + result.setClaims(claims); + + if (tokenLdap.getTokenTypeEnum() != null) { + switch (tokenLdap.getTokenTypeEnum()) { + case AUTHORIZATION_CODE: + if (result instanceof AuthorizationCodeGrant) { + final AuthorizationCode code = new AuthorizationCode(tokenLdap.getTokenCode(), tokenLdap.getCreationDate(), tokenLdap.getExpirationDate()); + final AuthorizationCodeGrant g = (AuthorizationCodeGrant) result; + code.setX5ts256(g.getX5ts256()); + g.setAuthorizationCode(code); + } + break; + case REFRESH_TOKEN: + final RefreshToken refreshToken = new RefreshToken(tokenLdap.getTokenCode(), tokenLdap.getCreationDate(), tokenLdap.getExpirationDate()); + refreshToken.setX5ts256(result.getX5ts256()); + result.setRefreshTokens(Arrays.asList(refreshToken)); + break; + case ACCESS_TOKEN: + final AccessToken accessToken = new AccessToken(tokenLdap.getTokenCode(), tokenLdap.getCreationDate(), tokenLdap.getExpirationDate()); + accessToken.setX5ts256(result.getX5ts256()); + result.setAccessTokens(Arrays.asList(accessToken)); + break; + case ID_TOKEN: + final IdToken idToken = new IdToken(tokenLdap.getTokenCode(), tokenLdap.getCreationDate(), tokenLdap.getExpirationDate()); + idToken.setX5ts256(result.getX5ts256()); + result.setIdToken(idToken); + break; + case LONG_LIVED_ACCESS_TOKEN: + final AccessToken longLivedAccessToken = new AccessToken(tokenLdap.getTokenCode(), tokenLdap.getCreationDate(), tokenLdap.getExpirationDate()); + longLivedAccessToken.setX5ts256(result.getX5ts256()); + result.setLongLivedAccessToken(longLivedAccessToken); + break; + } + } + return result; + } + } + return null; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AuthorizationGrantType.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AuthorizationGrantType.java new file mode 100644 index 00000000..769d61e6 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/AuthorizationGrantType.java @@ -0,0 +1,113 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * An authorization grant is a credential representing the resource owner's + * authorization (to access its protected resources) used by the client to + * obtain an access token. This specification defines four grant types: + * authorization code, implicit, resource owner password credentials, and client + * credentials. + * + * @author Javier Rojas Blum + * @version August 20, 2019 + */ +public enum AuthorizationGrantType implements HasParamName { + + /** + * The authorization code is obtained by using an authorization server as an + * intermediary between the client and resource owner. Instead of requesting + * authorization directly from the resource owner, the client directs the + * resource owner to an authorization server (via its user- agent as defined + * in [RFC2616]), which in turn directs the resource owner back to the + * client with the authorization code. + */ + AUTHORIZATION_CODE("authorization_code"), + /** + * The implicit grant is a simplified authorization code flow optimized for + * clients implemented in a browser using a scripting language such as + * JavaScript. In the implicit flow, instead of issuing the client an + * authorization code, the client is issued an access token directly (as the + * result of the resource owner authorization). The grant type is implicit + * as no intermediate credentials (such as an authorization code) are issued + * (and later used to obtain an access token). + */ + IMPLICIT("implicit"), + /** + * The client credentials (or other forms of client authentication) can be + * used as an authorization grant when the authorization scope is limited to + * the protected resources under the control of the client, or to protected + * resources previously arranged with the authorization server. Client + * credentials are used as an authorization grant typically when the client + * is acting on its own behalf (the client is also the resource owner), or + * is requesting access to protected resources based on an authorization + * previously arranged with the authorization server. + */ + CLIENT_CREDENTIALS("client_credentials"), + /** + * The resource owner password credentials (i.e. username and password) can + * be used directly as an authorization grant to obtain an access token. The + * credentials should only be used when there is a high degree of trust + * between the resource owner and the client (e.g. its device operating + * system or a highly privileged application), and when other authorization + * grant types are not available (such as an authorization code). + */ + RESOURCE_OWNER_PASSWORD_CREDENTIALS("resource_owner_password_credentials"), + /** + * An extension grant for Client Initiated Backchannel Authentication. + */ + CIBA("urn:openid:params:grant-type:ciba"), + + /** + * Device Authorization Grant Type for OAuth 2.0 + */ + DEVICE_CODE("urn:ietf:params:oauth:grant-type:device_code"), + ; + + private final String paramName; + + private AuthorizationGrantType(String paramName) { + this.paramName = paramName; + } + + /** + * Returns the corresponding {@link AuthorizationGrantType} for a given parameter. + * + * @param param The parameter. + * @return The corresponding authorization grant type if found, otherwise + * null. + */ + @JsonCreator + public static AuthorizationGrantType fromString(String param) { + if (param != null) { + for (AuthorizationGrantType agt : AuthorizationGrantType.values()) { + if (param.equals(agt.paramName)) { + return agt; + } + } + } + return null; + } + + /** + * Returns a string representation of the object. In this case the parameter + * name for the authorization grant type parameter. + */ + @Override + @JsonValue + public String toString() { + return paramName; + } + + @Override + public String getParamName() { + return paramName; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/CIBAGrant.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/CIBAGrant.java new file mode 100644 index 00000000..a0f617e2 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/CIBAGrant.java @@ -0,0 +1,65 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import org.gluu.service.CacheService; + +import javax.inject.Inject; + +/** + * An extension grant with the grant type value: urn:openid:params:grant-type:ciba + * + * @author Javier Rojas Blum + * @version May 5, 2020 + */ +public class CIBAGrant extends AuthorizationGrant { + + private String authReqId; + private boolean tokensDelivered; + + @Inject + private CacheService cacheService; + + public CIBAGrant() { + } + + @Override + public GrantType getGrantType() { + return GrantType.CIBA; + } + + public void init(CibaRequestCacheControl cibaRequest) { + super.init(cibaRequest.getUser(), AuthorizationGrantType.CIBA, cibaRequest.getClient(), null); + setAuthReqId(cibaRequest.getAuthReqId()); + setAcrValues(cibaRequest.getAcrValues()); + setScopes(cibaRequest.getScopes()); + setIsCachedWithNoPersistence(true); + } + + @Override + public void save() { + CacheGrant cachedGrant = new CacheGrant(this, appConfiguration); + cacheService.put(cachedGrant.getExpiresIn(), cachedGrant.getAuthReqId(), cachedGrant); + } + + public String getAuthReqId() { + return authReqId; + } + + public void setAuthReqId(String authReqId) { + this.authReqId = authReqId; + } + + public boolean isTokensDelivered() { + return tokensDelivered; + } + + public void setTokensDelivered(boolean tokensDelivered) { + this.tokensDelivered = tokensDelivered; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/CacheGrant.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/CacheGrant.java new file mode 100644 index 00000000..f909412e --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/CacheGrant.java @@ -0,0 +1,328 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.registration.Client; + +import javax.enterprise.inject.Instance; +import java.io.Serializable; +import java.util.Date; +import java.util.Set; + +/** + * @author yuriyz + * @version November 28, 2018 + */ +public class CacheGrant implements Serializable { + + private String authorizationCodeString; + private Date authorizationCodeCreationDate; + private Date authorizationCodeExpirationDate; + + private User user; + private Client client; + private Date authenticationTime; + private Set scopes; + private String grantId; + private String tokenBindingHash; + private String nonce; + private String codeChallenge; + private String codeChallengeMethod; + private String claims; + private String deviceCode; + + private String acrValues; + private String sessionDn; + private int expiresIn = 1; + + // CIBA + private String authReqId; + private boolean tokensDelivered; + + + public CacheGrant() { + } + + public CacheGrant(AuthorizationGrant grant, AppConfiguration appConfiguration) { + if (grant.getAuthorizationCode() != null) { + authorizationCodeString = grant.getAuthorizationCode().getCode(); + authorizationCodeCreationDate = grant.getAuthorizationCode().getCreationDate(); + authorizationCodeExpirationDate = grant.getAuthorizationCode().getExpirationDate(); + } + initExpiresIn(grant, appConfiguration); + + user = grant.getUser(); + client = grant.getClient(); + authenticationTime = grant.getAuthenticationTime(); + scopes = grant.getScopes(); + tokenBindingHash = grant.getTokenBindingHash(); + grantId = grant.getGrantId(); + nonce = grant.getNonce(); + acrValues = grant.getAcrValues(); + codeChallenge = grant.getCodeChallenge(); + codeChallengeMethod = grant.getCodeChallengeMethod(); + claims = grant.getClaims(); + sessionDn = grant.getSessionDn(); + } + + public CacheGrant(CIBAGrant grant, AppConfiguration appConfiguration) { + if (grant.getAuthorizationCode() != null) { + authorizationCodeString = grant.getAuthorizationCode().getCode(); + authorizationCodeCreationDate = grant.getAuthorizationCode().getCreationDate(); + authorizationCodeExpirationDate = grant.getAuthorizationCode().getExpirationDate(); + } + initExpiresIn(grant, appConfiguration); + + user = grant.getUser(); + client = grant.getClient(); + authenticationTime = grant.getAuthenticationTime(); + scopes = grant.getScopes(); + tokenBindingHash = grant.getTokenBindingHash(); + grantId = grant.getGrantId(); + nonce = grant.getNonce(); + acrValues = grant.getAcrValues(); + codeChallenge = grant.getCodeChallenge(); + codeChallengeMethod = grant.getCodeChallengeMethod(); + claims = grant.getClaims(); + sessionDn = grant.getSessionDn(); + + authReqId = grant.getAuthReqId(); + tokensDelivered = grant.isTokensDelivered(); + } + + public CacheGrant(DeviceCodeGrant grant, AppConfiguration appConfiguration) { + if (grant.getAuthorizationCode() != null) { + authorizationCodeString = grant.getAuthorizationCode().getCode(); + authorizationCodeCreationDate = grant.getAuthorizationCode().getCreationDate(); + authorizationCodeExpirationDate = grant.getAuthorizationCode().getExpirationDate(); + } + initExpiresIn(grant, appConfiguration); + + user = grant.getUser(); + client = grant.getClient(); + authenticationTime = grant.getAuthenticationTime(); + scopes = grant.getScopes(); + tokenBindingHash = grant.getTokenBindingHash(); + grantId = grant.getGrantId(); + nonce = grant.getNonce(); + acrValues = grant.getAcrValues(); + codeChallenge = grant.getCodeChallenge(); + codeChallengeMethod = grant.getCodeChallengeMethod(); + claims = grant.getClaims(); + sessionDn = grant.getSessionDn(); + deviceCode = grant.getDeviceCode(); + } + + private void initExpiresIn(AuthorizationGrant grant, AppConfiguration appConfiguration) { + if (grant.getAuthorizationCode() != null) { + expiresIn = grant.getAuthorizationCode().getExpiresIn(); + } else { + expiresIn = appConfiguration.getAccessTokenLifetime(); + // oxAuth #830 Client-specific access token expiration + if (client != null && client.getAccessTokenLifetime() != null && client.getAccessTokenLifetime() > 0) { + expiresIn = client.getAccessTokenLifetime(); + } + } + } + + public int getExpiresIn() { + return expiresIn; + } + + public Date getAuthorizationCodeCreationDate() { + return authorizationCodeCreationDate; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public Set getScopes() { + return scopes; + } + + public void setScopes(Set scopes) { + this.scopes = scopes; + } + + public String getGrantId() { + return grantId; + } + + public void setGrantId(String grantId) { + this.grantId = grantId; + } + + public Client getClient() { + return client; + } + + public void setClient(Client client) { + this.client = client; + } + + public Date getAuthenticationTime() { + return authenticationTime; + } + + public void setAuthenticationTime(Date authenticationTime) { + this.authenticationTime = authenticationTime; + } + + public String getAuthorizationCodeString() { + return authorizationCodeString; + } + + public void setAuthorizationCodeString(String authorizationCodeString) { + this.authorizationCodeString = authorizationCodeString; + } + + public String getNonce() { + return nonce; + } + + public void setNonce(String nonce) { + this.nonce = nonce; + } + + public String getCodeChallenge() { + return codeChallenge; + } + + public void setCodeChallenge(String codeChallenge) { + this.codeChallenge = codeChallenge; + } + + public String getCodeChallengeMethod() { + return codeChallengeMethod; + } + + public void setCodeChallengeMethod(String codeChallengeMethod) { + this.codeChallengeMethod = codeChallengeMethod; + } + + public String getClaims() { + return claims; + } + + public void setClaims(String claims) { + this.claims = claims; + } + + public String getAcrValues() { + return acrValues; + } + + public void setAcrValues(String acrValues) { + this.acrValues = acrValues; + } + + public String getSessionDn() { + return sessionDn; + } + + public void setSessionDn(String sessionDn) { + this.sessionDn = sessionDn; + } + + public AuthorizationCodeGrant asCodeGrant(Instance grantInstance) { + AuthorizationCodeGrant grant = grantInstance.select(AuthorizationCodeGrant.class).get(); + grant.init(user, client, authenticationTime); + + grant.setAuthorizationCode(new AuthorizationCode(authorizationCodeString, authorizationCodeCreationDate, authorizationCodeExpirationDate)); + grant.setScopes(scopes); + grant.setGrantId(grantId); + grant.setSessionDn(sessionDn); + grant.setCodeChallenge(codeChallenge); + grant.setCodeChallengeMethod(codeChallengeMethod); + grant.setAcrValues(acrValues); + grant.setNonce(nonce); + grant.setClaims(claims); + + return grant; + } + + public CIBAGrant asCibaGrant(Instance grantInstance) { + CIBAGrant grant = grantInstance.select(CIBAGrant.class).get(); + grant.init(user, AuthorizationGrantType.CIBA, client, authenticationTime); + grant.setScopes(scopes); + grant.setGrantId(grantId); + grant.setSessionDn(sessionDn); + grant.setCodeChallenge(codeChallenge); + grant.setCodeChallengeMethod(codeChallengeMethod); + grant.setAcrValues(acrValues); + grant.setNonce(nonce); + grant.setClaims(claims); + grant.setAuthReqId(authReqId); + grant.setTokensDelivered(tokensDelivered); + + return grant; + } + + public DeviceCodeGrant asDeviceCodeGrant(Instance grantInstance) { + DeviceCodeGrant grant = grantInstance.select(DeviceCodeGrant.class).get(); + grant.init(user, AuthorizationGrantType.DEVICE_CODE, client, authenticationTime); + grant.setScopes(scopes); + grant.setGrantId(grantId); + grant.setSessionDn(sessionDn); + grant.setCodeChallenge(codeChallenge); + grant.setCodeChallengeMethod(codeChallengeMethod); + grant.setAcrValues(acrValues); + grant.setNonce(nonce); + grant.setClaims(claims); + grant.setDeviceCode(deviceCode); + + return grant; + } + + public String cacheKey() { + return cacheKey(authorizationCodeString, grantId); + } + + public static String cacheKey(String code, String grantId) { + if (StringUtils.isBlank(code)) { + return grantId; + } + return code; + } + + public String getAuthReqId() { + return authReqId; + } + + public void setAuthReqId(String authReqId) { + this.authReqId = authReqId; + } + + public boolean isTokensDelivered() { + return tokensDelivered; + } + + public void setTokensDelivered(boolean tokensDelivered) { + this.tokensDelivered = tokensDelivered; + } + + public String getDeviceCode() { + return deviceCode; + } + + @Override + public String toString() { + return "MemcachedGrant{" + + "authorizationCode=" + authorizationCodeString + + ", user=" + user + + ", client=" + client + + ", authenticationTime=" + authenticationTime + + '}'; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/CibaRequestCacheControl.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/CibaRequestCacheControl.java new file mode 100644 index 00000000..1c5653df --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/CibaRequestCacheControl.java @@ -0,0 +1,173 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.util.StringUtils; +import org.gluu.oxauth.model.util.Util; + +import java.io.Serializable; +import java.security.SecureRandom; +import java.util.List; + +/** + * Class used to keep all data about a CIBA request that should be processed and saved in Cache. + * + * @author Milton BO + * @version June 2, 2020 + */ +public class CibaRequestCacheControl implements Serializable { + + private String authReqId; + private User user; + private Client client; + private List scopes; + + private int expiresIn = 1; + private String clientNotificationToken; + private String bindingMessage; + private Long lastAccessControl; + private CibaRequestStatus status; + private boolean tokensDelivered; + private String acrValues; + + public CibaRequestCacheControl() { + } + + public CibaRequestCacheControl(User user, Client client, int expiresIn, List scopeList, + String clientNotificationToken, String bindingMessage, Long lastAccessControl, + String acrValues) { + this.authReqId = StringUtils.generateRandomCode((byte) 24); // Entropy above of 160 bits based on specs [RFC section 7.3] + this.user = user; + this.client = client; + this.scopes = scopeList; + this.status = CibaRequestStatus.PENDING; + this.expiresIn = expiresIn; + this.clientNotificationToken = clientNotificationToken; + this.bindingMessage = bindingMessage; + this.lastAccessControl = lastAccessControl; + this.tokensDelivered = false; + this.acrValues = acrValues; + } + + public String cacheKey() { + return authReqId; + } + + public String getScopesAsString() { + final StringBuilder scopes = new StringBuilder(); + for (String s : getScopes()) { + scopes.append(s).append(" "); + } + return scopes.toString().trim(); + } + + public int getExpiresIn() { + return expiresIn; + } + + public String getAuthReqId() { + return authReqId; + } + + public void setAuthReqId(String authReqId) { + this.authReqId = authReqId; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public Client getClient() { + return client; + } + + public void setClient(Client client) { + this.client = client; + } + + public List getScopes() { + return scopes; + } + + public void setScopes(List scopes) { + this.scopes = scopes; + } + + public void setExpiresIn(int expiresIn) { + this.expiresIn = expiresIn; + } + + public String getClientNotificationToken() { + return clientNotificationToken; + } + + public void setClientNotificationToken(String clientNotificationToken) { + this.clientNotificationToken = clientNotificationToken; + } + + public String getBindingMessage() { + return bindingMessage; + } + + public void setBindingMessage(String bindingMessage) { + this.bindingMessage = bindingMessage; + } + + public Long getLastAccessControl() { + return lastAccessControl; + } + + public void setLastAccessControl(Long lastAccessControl) { + this.lastAccessControl = lastAccessControl; + } + + public CibaRequestStatus getStatus() { + return status; + } + + public void setStatus(CibaRequestStatus status) { + this.status = status; + } + + public boolean isTokensDelivered() { + return tokensDelivered; + } + + public void setTokensDelivered(boolean tokensDelivered) { + this.tokensDelivered = tokensDelivered; + } + + public String getAcrValues() { + return acrValues; + } + + public void setAcrValues(String acrValues) { + this.acrValues = acrValues; + } + + @Override + public String toString() { + return "CibaRequestCacheControl{" + + ", authReqId='" + authReqId + '\'' + + ", user=" + user + + ", client=" + client + + ", scopes=" + scopes + + ", expiresIn=" + expiresIn + + ", clientNotificationToken='" + clientNotificationToken + '\'' + + ", bindingMessage='" + bindingMessage + '\'' + + ", lastAccessControl=" + lastAccessControl + + ", userAuthorization=" + status + + ", tokensDelivered=" + tokensDelivered + + ", acrValues='" + acrValues + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/CibaRequestStatus.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/CibaRequestStatus.java new file mode 100644 index 00000000..e319d058 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/CibaRequestStatus.java @@ -0,0 +1,42 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import org.apache.commons.lang.StringUtils; + +/** + * @author Javier Rojas Blum + * @version May 9, 2020 + */ +public enum CibaRequestStatus { + PENDING("pending"), + GRANTED("granted"), + DENIED("denied"), + EXPIRED("expired"), + IN_PROCESS("in_process"); + + private final String value; + + CibaRequestStatus(String name) { + value = name; + } + + public String getValue() { + return value; + } + + public static CibaRequestStatus fromValue(String value) { + if (StringUtils.isNotBlank(value)) { + for (CibaRequestStatus t : values()) { + if (t.getValue().endsWith(value)) { + return t; + } + } + } + return null; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/ClientCredentialsGrant.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/ClientCredentialsGrant.java new file mode 100644 index 00000000..2646773a --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/ClientCredentialsGrant.java @@ -0,0 +1,56 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import org.gluu.oxauth.model.registration.Client; + +/** + * The client credentials (or other forms of client authentication) can be used + * as an authorization grant when the authorization scope is limited to the + * protected resources under the control of the client, or to protected + * resources previously arranged with the authorization server. Client + * credentials are used as an authorization grant typically when the client is + * acting on its own behalf (the client is also the resource owner), or is + * requesting access to protected resources based on an authorization previously + * arranged with the authorization server. + * + * @author Javier Rojas Blum Date: 09.29.2011 + * @author Yuriy Movchan + */ +public class ClientCredentialsGrant extends AuthorizationGrant { + + public ClientCredentialsGrant() {} + + /** + * Construct a client credentials grant. + * + * @param user The resource owner. + * @param client An application making protected resource requests on behalf of + * the resource owner and with its authorization. + */ + public ClientCredentialsGrant(User user, Client client) { + init(user, client); + } + + @Override + public GrantType getGrantType() { + return GrantType.CLIENT_CREDENTIALS; + } + + public void init(User user, Client client) { + super.init(user, AuthorizationGrantType.CLIENT_CREDENTIALS, client, null); + } + + /** + * The authorization server MUST NOT issue a refresh token. + */ + @Override + public RefreshToken createRefreshToken(ExecutionContext executionContext) { + throw new UnsupportedOperationException( + "The authorization server MUST NOT issue a refresh token."); + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/ClientTokens.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/ClientTokens.java new file mode 100644 index 00000000..bc18c805 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/ClientTokens.java @@ -0,0 +1,52 @@ +package org.gluu.oxauth.model.common; + +import com.google.common.base.Preconditions; +import org.gluu.util.StringHelper; + +import java.io.Serializable; +import java.util.HashSet; +import java.util.Set; + +/** + * @author yuriyz + */ +@Deprecated // scheduled for removing +public class ClientTokens implements Serializable { + + private String clientId; + + private Set tokenHashes = new HashSet(); + + public ClientTokens(String clientId) { + this.clientId = clientId; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public Set getTokenHashes() { + return tokenHashes; + } + + public void setTokenHashes(Set tokenHashes) { + this.tokenHashes = tokenHashes; + } + + public String cacheKey() { + Preconditions.checkState(StringHelper.isNotEmpty(clientId)); + return clientId + "_tokens"; + } + + @Override + public String toString() { + return "ClientTokens{" + + "clientId='" + clientId + '\'' + + ", tokenHashes=" + tokenHashes + + '}'; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/DefaultScope.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/DefaultScope.java new file mode 100644 index 00000000..c089f3a3 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/DefaultScope.java @@ -0,0 +1,49 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +/** + * @author Javier Rojas Date: 11.30.2011 + */ +public enum DefaultScope { + OPEN_ID("openid"), + PROFILE("profile"), + EMAIL("email"), + ADDRESS("address"), + PHONE("phone"); + + private final String paramName; + + private DefaultScope(String paramName) { + this.paramName = paramName; + } + + /** + * Returns the corresponding {@link DefaultScope} for a default scope parameter. + * + * @param param The default scope parameter. + * @return The corresponding scope if found, otherwise null. + */ + public static DefaultScope fromString(String param) { + if (param != null) { + for (DefaultScope ds : DefaultScope.values()) { + if (param.equals(ds.paramName)) { + return ds; + } + } + } + return null; + } + + /** + * Returns a string representation of the object. In this case the parameter name for the default scope. + */ + @Override + public String toString() { + return paramName; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/DeviceAuthorizationCacheControl.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/DeviceAuthorizationCacheControl.java new file mode 100644 index 00000000..4c2cddaf --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/DeviceAuthorizationCacheControl.java @@ -0,0 +1,133 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import org.gluu.oxauth.model.registration.Client; + +import java.io.Serializable; +import java.net.URI; +import java.util.List; + +/** + * Class used to keep all data about an OAuth2 Device Flow request. + */ +public class DeviceAuthorizationCacheControl implements Serializable { + + private String userCode; + private String deviceCode; + private Client client; + private List scopes; + private URI verificationUri; + private int expiresIn = 1; + private int interval = 5; + private long lastAccessControl; + private DeviceAuthorizationStatus status; + + public DeviceAuthorizationCacheControl() { + } + + public DeviceAuthorizationCacheControl(String userCode, String deviceCode, Client client, List scopes, + URI verificationUri, int expiresIn, int interval, long lastAccessControl, + DeviceAuthorizationStatus status) { + this.userCode = userCode; + this.deviceCode = deviceCode; + this.client = client; + this.scopes = scopes; + this.verificationUri = verificationUri; + this.expiresIn = expiresIn; + this.interval = interval; + this.lastAccessControl = lastAccessControl; + this.status = status; + } + + public String getUserCode() { + return userCode; + } + + public void setUserCode(String userCode) { + this.userCode = userCode; + } + + public String getDeviceCode() { + return deviceCode; + } + + public void setDeviceCode(String deviceCode) { + this.deviceCode = deviceCode; + } + + public Client getClient() { + return client; + } + + public void setClient(Client client) { + this.client = client; + } + + public List getScopes() { + return scopes; + } + + public void setScopes(List scopes) { + this.scopes = scopes; + } + + public URI getVerificationUri() { + return verificationUri; + } + + public void setVerificationUri(URI verificationUri) { + this.verificationUri = verificationUri; + } + + public int getExpiresIn() { + return expiresIn; + } + + public void setExpiresIn(int expiresIn) { + this.expiresIn = expiresIn; + } + + public int getInterval() { + return interval; + } + + public void setInterval(int interval) { + this.interval = interval; + } + + public Long getLastAccessControl() { + return lastAccessControl; + } + + public void setLastAccessControl(long lastAccessControl) { + this.lastAccessControl = lastAccessControl; + } + + public DeviceAuthorizationStatus getStatus() { + return status; + } + + public void setStatus(DeviceAuthorizationStatus status) { + this.status = status; + } + + @Override + public String toString() { + return "DeviceAuthorizationCacheControl{" + + "userCode='" + userCode + '\'' + + ", deviceCode='" + deviceCode + '\'' + + ", client=" + client + + ", scopes=" + scopes + + ", verificationUri='" + verificationUri + '\'' + + ", expiresIn=" + expiresIn + + ", interval=" + interval + + ", lastAccessControl=" + lastAccessControl + + ", status=" + status + + '}'; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/DeviceAuthorizationStatus.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/DeviceAuthorizationStatus.java new file mode 100644 index 00000000..26206a66 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/DeviceAuthorizationStatus.java @@ -0,0 +1,27 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +/** + * Contains a list of values of status for OAuth2 Device Flow requests. + */ +public enum DeviceAuthorizationStatus { + PENDING("pending"), + DENIED("denied"), + EXPIRED("expired"); + + private final String value; + + DeviceAuthorizationStatus(String name) { + value = name; + } + + public String getValue() { + return value; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/DeviceCodeGrant.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/DeviceCodeGrant.java new file mode 100644 index 00000000..eb66680a --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/DeviceCodeGrant.java @@ -0,0 +1,54 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import org.apache.commons.lang.StringUtils; +import org.gluu.service.CacheService; + +import javax.inject.Inject; + +/** + * An extension grant with the grant type value: urn:ietf:params:oauth:grant-type:device_code + */ +public class DeviceCodeGrant extends AuthorizationGrant { + + private String deviceCode; + + @Inject + private CacheService cacheService; + + public DeviceCodeGrant() { + } + + @Override + public GrantType getGrantType() { + return GrantType.DEVICE_CODE; + } + + public void init(DeviceAuthorizationCacheControl cacheData, User user) { + super.init(user, AuthorizationGrantType.DEVICE_CODE, cacheData.getClient(), null); + setDeviceCode(cacheData.getDeviceCode()); + setIsCachedWithNoPersistence(true); + setScopes(cacheData.getScopes()); + } + + @Override + public void save() { + CacheGrant cachedGrant = new CacheGrant(this, appConfiguration); + String cacheKey = StringUtils.isNotBlank(cachedGrant.getDeviceCode()) ? cachedGrant.getDeviceCode() : cachedGrant.getGrantId(); + cacheService.put(cachedGrant.getExpiresIn(), cacheKey, cachedGrant); + } + + public String getDeviceCode() { + return deviceCode; + } + + public void setDeviceCode(String deviceCode) { + this.deviceCode = deviceCode; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/ExecutionContext.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/ExecutionContext.java new file mode 100644 index 00000000..ff6136a7 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/ExecutionContext.java @@ -0,0 +1,107 @@ +package org.gluu.oxauth.model.common; + +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.ldap.TokenLdap; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.service.AttributeService; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author Yuriy Zabrovarnyy + */ +public class ExecutionContext { + + private final HttpServletRequest httpRequest; + private final HttpServletResponse httpResponse; + + private Client client; + private AuthorizationGrant grant; + + private TokenLdap idTokenEntity; + private TokenLdap accessTokenEntity; + private TokenLdap refreshTokenEntity; + + private AppConfiguration appConfiguration; + private AttributeService attributeService; + + private int refreshTokenLifetimeFromScript; + + public ExecutionContext(HttpServletRequest httpRequest, HttpServletResponse httpResponse) { + this.httpRequest = httpRequest; + this.httpResponse = httpResponse; + } + + public int getRefreshTokenLifetimeFromScript() { + return refreshTokenLifetimeFromScript; + } + + public void setRefreshTokenLifetimeFromScript(int refreshTokenLifetimeFromScript) { + this.refreshTokenLifetimeFromScript = refreshTokenLifetimeFromScript; + } + + public HttpServletRequest getHttpRequest() { + return httpRequest; + } + + public HttpServletResponse getHttpResponse() { + return httpResponse; + } + + public Client getClient() { + return client; + } + + public void setClient(Client client) { + this.client = client; + } + + public AuthorizationGrant getGrant() { + return grant; + } + + public void setGrant(AuthorizationGrant grant) { + this.grant = grant; + } + + public TokenLdap getIdTokenEntity() { + return idTokenEntity; + } + + public void setIdTokenEntity(TokenLdap idTokenEntity) { + this.idTokenEntity = idTokenEntity; + } + + public TokenLdap getAccessTokenEntity() { + return accessTokenEntity; + } + + public void setAccessTokenEntity(TokenLdap accessTokenEntity) { + this.accessTokenEntity = accessTokenEntity; + } + + public TokenLdap getRefreshTokenEntity() { + return refreshTokenEntity; + } + + public void setRefreshTokenEntity(TokenLdap refreshTokenEntity) { + this.refreshTokenEntity = refreshTokenEntity; + } + + public AppConfiguration getAppConfiguration() { + return appConfiguration; + } + + public void setAppConfiguration(AppConfiguration appConfiguration) { + this.appConfiguration = appConfiguration; + } + + public AttributeService getAttributeService() { + return attributeService; + } + + public void setAttributeService(AttributeService attributeService) { + this.attributeService = attributeService; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/IAuthorizationGrant.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/IAuthorizationGrant.java new file mode 100644 index 00000000..5c0e796a --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/IAuthorizationGrant.java @@ -0,0 +1,124 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import com.google.common.base.Function; +import org.gluu.oxauth.model.authorize.JwtAuthorizationRequest; +import org.gluu.oxauth.model.ldap.TokenLdap; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.token.JsonWebResponse; + +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Set; + +/** + * @author Yuriy Zabrovarnyy + * @author Javier Rojas Blum + * @version September 4, 2019 + */ + +public interface IAuthorizationGrant { + + GrantType getGrantType(); + + String getGrantId(); + + void setGrantId(String p_grantId); + + AuthorizationCode getAuthorizationCode(); + + void setAuthorizationCode(AuthorizationCode authorizationCode); + + String getNonce(); + + void setNonce(String nonce); + + String getSub(); + + AccessToken createAccessToken(String certAsPem, ExecutionContext executionContext); + + RefreshToken createRefreshToken(ExecutionContext executionContext); + + IdToken createIdToken( + String nonce, AuthorizationCode authorizationCode, AccessToken accessToken, RefreshToken refreshToken, + String state, AuthorizationGrant authorizationGrant, boolean includeIdTokenClaims, + Function preProcessing, Function postProcessing, + ExecutionContext executionContext); + + RefreshToken getRefreshToken(String refreshTokenCode); + + AbstractToken getAccessToken(String tokenCode); + + void revokeAllTokens(); + + void checkExpiredTokens(); + + String checkScopesPolicy(String scope); + + User getUser(); + + String getUserId(); + + String getUserDn(); + + AuthorizationGrantType getAuthorizationGrantType(); + + String getClientId(); + + Client getClient(); + + String getClientDn(); + + List getAccessTokens(); + + Set getScopes(); + + Set getRefreshTokensCodes(); + + Set getAccessTokensCodes(); + + List getRefreshTokens(); + + void setRefreshTokens(List refreshTokens); + + AccessToken getLongLivedAccessToken(); + + IdToken getIdToken(); + + JwtAuthorizationRequest getJwtAuthorizationRequest(); + + void setJwtAuthorizationRequest(JwtAuthorizationRequest p_jwtAuthorizationRequest); + + Date getAuthenticationTime(); + + TokenLdap getTokenLdap(); + + void setTokenLdap(TokenLdap p_tokenLdap); + + void setLongLivedAccessToken(AccessToken longLivedAccessToken); + + void setIdToken(IdToken idToken); + + void setScopes(Collection scopes); + + void setAccessTokens(List accessTokens); + + String getAcrValues(); + + void setAcrValues(String authMode); + + String getSessionDn(); + + void setSessionDn(String sessionDn); + + /** + * Saves changes asynchronously + */ + void save(); +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/IAuthorizationGrantList.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/IAuthorizationGrantList.java new file mode 100644 index 00000000..9d33ca92 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/IAuthorizationGrantList.java @@ -0,0 +1,51 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import org.gluu.oxauth.model.registration.Client; + +import java.util.Date; +import java.util.List; + +/** + * @author Yuriy Zabrovarnyy + * @version August 20, 2019 + */ + +public interface IAuthorizationGrantList { + + void removeAuthorizationGrants(List authorizationGrants); + + AuthorizationGrant createAuthorizationGrant(User user, Client client, Date authenticationTime); + + AuthorizationCodeGrant createAuthorizationCodeGrant(User user, Client client, Date authenticationTime); + + ImplicitGrant createImplicitGrant(User user, Client client, Date authenticationTime); + + ClientCredentialsGrant createClientCredentialsGrant(User user, Client client); + + ResourceOwnerPasswordCredentialsGrant createResourceOwnerPasswordCredentialsGrant(User user, Client client); + + CIBAGrant createCIBAGrant(CibaRequestCacheControl request); + + AuthorizationCodeGrant getAuthorizationCodeGrant(String authorizationCode); + + AuthorizationGrant getAuthorizationGrantByRefreshToken(String clientId, String refreshTokenCode); + + List getAuthorizationGrant(String clientId); + + AuthorizationGrant getAuthorizationGrantByAccessToken(String tokenCode); + + AuthorizationGrant getAuthorizationGrantByIdToken(String idToken); + + CIBAGrant getCIBAGrant(String authReqId); + + DeviceCodeGrant createDeviceGrant(DeviceAuthorizationCacheControl data, User user); + + DeviceCodeGrant getDeviceCodeGrant(String deviceCode); + +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/IdToken.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/IdToken.java new file mode 100644 index 00000000..1854e60f --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/IdToken.java @@ -0,0 +1,23 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import java.util.Date; + +/** + * @author Javier Rojas Blum Date: 02.13.2012 + */ +public class IdToken extends AbstractToken { + + public IdToken(int lifeTime) { + super(lifeTime); + } + + public IdToken(String code, Date creationDate, Date expirationDate) { + super(code, creationDate, expirationDate); + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/ImplicitGrant.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/ImplicitGrant.java new file mode 100644 index 00000000..d938a707 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/ImplicitGrant.java @@ -0,0 +1,76 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import org.gluu.oxauth.model.registration.Client; + +import java.util.Date; + +/** + *

+ * The implicit grant is a simplified authorization code flow optimized for + * clients implemented in a browser using a scripting language such as + * JavaScript. In the implicit flow, instead of issuing the client an + * authorization code, the client is issued an access token directly (as the + * result of the resource owner authorization). The grant type is implicit as no + * intermediate credentials (such as an authorization code) are issued (and + * later used to obtain an access token). + *

+ *

+ * When issuing an implicit grant, the authorization server does not + * authenticate the client. In some cases, the client identity can be verified + * via the redirection URI used to deliver the access token to the client. The + * access token may be exposed to the resource owner or other applications with + * access to the resource owner's user-agent. + *

+ *

+ * Implicit grants improve the responsiveness and efficiency of some clients + * (such as a client implemented as an in-browser application) since it reduces + * the number of round trips required to obtain an access token. However, this + * convenience should be weighed against the security implications of using + * implicit grants, especially when the authorization code grant type is + * available. + *

+ * + * @author Javier Rojas Blum Date: 09.29.2011 + * @author Yuriy Movchan + */ +public class ImplicitGrant extends AuthorizationGrant { + + public ImplicitGrant() {} + + /** + * Constructs an implicit grant. + * + * @param user The resource owner. + * @param client An application making protected resource requests on behalf of the resource owner and + * with its authorization. + * @param authenticationTime The Claim Value is the number of seconds from 1970-01-01T0:0:0Z as measured in UTC + * until the date/time that the End-User authentication occurred. + */ + public ImplicitGrant(User user, Client client, Date authenticationTime) { + init(user, client, authenticationTime); + } + + @Override + public GrantType getGrantType() { + return GrantType.IMPLICIT; + } + + public void init(User user, Client client, Date authenticationTime) { + super.init(user, AuthorizationGrantType.IMPLICIT, client, authenticationTime); + } + + /** + * The authorization server MUST NOT issue a refresh token. + */ + @Override + public RefreshToken createRefreshToken(ExecutionContext executionContext) { + throw new UnsupportedOperationException( + "The authorization server MUST NOT issue a refresh token."); + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/RefreshToken.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/RefreshToken.java new file mode 100644 index 00000000..2f5bbc09 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/RefreshToken.java @@ -0,0 +1,70 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import java.util.Date; + +/** + *

+ * Authorization servers MAY issue refresh tokens to web application clients and + * native application clients. + *

+ *

+ * Refresh tokens MUST be kept confidential in transit and storage, and shared + * only among the authorization server and the client to whom the refresh tokens + * were issued. + *

+ *

+ * The authorization server MUST maintain the binding between a refresh token + * and the client to whom it was issued. The authorization server MUST verify + * the binding between the refresh token and client identity whenever the client + * identity can be authenticated. When client authentication is not possible, + * the authorization server SHOULD deploy other means to detect refresh token + * abuse. + *

+ *

+ * For example, the authorization server could employ refresh token rotation in + * which a new refresh token is issued with every access token refresh response. + * The previous refresh token is invalidated but retained by the authorization + * server. If a refresh token is compromised and subsequently used by both the + * attacker and the legitimate client, one of them will present an invalidated + * refresh token which will inform the authorization server of the breach. + *

+ *

+ * The authorization server MUST ensure that refresh tokens cannot be generated, + * modified, or guessed to produce valid refresh tokens by unauthorized parties. + *

+ * + * @author Javier Rojas Date: 09.29.2011 + * + */ +public class RefreshToken extends AbstractToken { + + /** + *

+ * Constructs a refresh token. + *

+ *

+ * When created, a token is valid for a given lifetime, and after this + * period of time, it will be marked as expired automatically by a + * background process. + *

+ *

+ * When required, the token can be marked as revoked. + *

+ * + * @param lifeTime + * The life time of the token. + */ + public RefreshToken(int lifeTime) { + super(lifeTime); + } + + public RefreshToken(String code, Date creationDate, Date expirationDate) { + super(code, creationDate, expirationDate); + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/ResourceOwnerPasswordCredentialsGrant.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/ResourceOwnerPasswordCredentialsGrant.java new file mode 100644 index 00000000..3a3c7208 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/ResourceOwnerPasswordCredentialsGrant.java @@ -0,0 +1,56 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import org.gluu.oxauth.model.registration.Client; + +/** + *

+ * The resource owner password credentials (i.e. username and password) can be + * used directly as an authorization grant to obtain an access token. The + * credentials should only be used when there is a high degree of trust between + * the resource owner and the client (e.g. its device operating system or a + * highly privileged application), and when other authorization grant types are + * not available (such as an authorization code). + *

+ *

+ * Even though this grant type requires direct client access to the resource + * owner credentials, the resource owner credentials are used for a single + * request and are exchanged for an access token. This grant type can eliminate + * the need for the client to store the resource owner credentials for future + * use, by exchanging the credentials with a long-lived access token or refresh + * token. + *

+ * + * @author Javier Rojas Blum Date: 09.29.2011 + * @author Yuriy Movchan + */ +public class ResourceOwnerPasswordCredentialsGrant extends AuthorizationGrant { + + public ResourceOwnerPasswordCredentialsGrant() {} + + /** + * Constructs a resource owner password credentials grant. + * + * @param user The resource owner. + * @param client An application making protected resource requests on behalf of + * the resource owner and with its authorization. + */ + public ResourceOwnerPasswordCredentialsGrant(User user, Client client) { + init(user, client); + } + + @Override + public GrantType getGrantType() { + return GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS; + } + + public void init(User user, Client client) { + super.init(user, AuthorizationGrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS, client, null); + } + +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/SessionTokens.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/SessionTokens.java new file mode 100644 index 00000000..f45bee88 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/SessionTokens.java @@ -0,0 +1,53 @@ +package org.gluu.oxauth.model.common; + +import com.google.common.base.Preconditions; +import org.gluu.util.StringHelper; + +import java.io.Serializable; +import java.util.HashSet; +import java.util.Set; + +/** + * @author yuriyz + */ +@Deprecated // scheduled for removing +public class SessionTokens implements Serializable { + + private String sessionDn; + + private Set tokenHashes = new HashSet(); + + public SessionTokens(String sessionDn) { + this.sessionDn = sessionDn; + } + + + public String getSessionDn() { + return sessionDn; + } + + public void setSessionDn(String sessionDn) { + this.sessionDn = sessionDn; + } + + public Set getTokenHashes() { + return tokenHashes; + } + + public void setTokenHashes(Set tokenHashes) { + this.tokenHashes = tokenHashes; + } + + public String cacheKey() { + Preconditions.checkState(StringHelper.isNotEmpty(sessionDn)); + return sessionDn + "_tokens"; + } + + @Override + public String toString() { + return "SessionTokens{" + + "sessionDn='" + sessionDn + '\'' + + ", tokenHashes=" + tokenHashes + + '}'; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/SimpleAuthorizationGrant.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/SimpleAuthorizationGrant.java new file mode 100644 index 00000000..e31a89b1 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/SimpleAuthorizationGrant.java @@ -0,0 +1,13 @@ +package org.gluu.oxauth.model.common; + +/** + * @author Yuriy Movchan + * @version 02/13/2017 + */ +public class SimpleAuthorizationGrant extends AuthorizationGrant { + + @Override + public GrantType getGrantType() { + return GrantType.NONE; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/UnmodifiableAuthorizationGrant.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/UnmodifiableAuthorizationGrant.java new file mode 100644 index 00000000..10af3873 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/common/UnmodifiableAuthorizationGrant.java @@ -0,0 +1,263 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.common; + +import com.google.common.base.Function; +import org.gluu.oxauth.model.authorize.JwtAuthorizationRequest; +import org.gluu.oxauth.model.ldap.TokenLdap; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.token.JsonWebResponse; + +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Set; + +/** + * Gives ability to use authorization grant in read-only mode. + * + * @author Yuriy Zabrovarnyy + * @author Javier Rojas Blum + * @version September 4, 2019 + */ + +public class UnmodifiableAuthorizationGrant implements IAuthorizationGrant { + + private final IAuthorizationGrant grant; + + public UnmodifiableAuthorizationGrant(IAuthorizationGrant grant) { + this.grant = grant; + } + + @Override + public GrantType getGrantType() { + return GrantType.NONE; + } + + @Override + public String getGrantId() { + return grant.getGrantId(); + } + + @Override + public void setGrantId(String p_grantId) { + throw new UnsupportedOperationException("Not allowed for UnmodifiableAuthorizationGrant."); + } + + @Override + public AuthorizationCode getAuthorizationCode() { + return grant.getAuthorizationCode(); + } + + @Override + public void setAuthorizationCode(AuthorizationCode authorizationCode) { + throw new UnsupportedOperationException("Not allowed for UnmodifiableAuthorizationGrant."); + } + + @Override + public String getNonce() { + return grant.getNonce(); + } + + @Override + public void setNonce(String nonce) { + throw new UnsupportedOperationException("Not allowed for UnmodifiableAuthorizationGrant."); + } + + @Override + public String getSub() { + return grant.getSub(); + } + + @Override + public AccessToken createAccessToken(String certAsPem, ExecutionContext executionContext) { + throw new UnsupportedOperationException("Not allowed for UnmodifiableAuthorizationGrant."); + } + + @Override + public RefreshToken createRefreshToken(ExecutionContext executionContext) { + throw new UnsupportedOperationException("Not allowed for UnmodifiableAuthorizationGrant."); + } + + @Override + public IdToken createIdToken( + String nonce, AuthorizationCode authorizationCode, AccessToken accessToken, RefreshToken refreshToken, + String state, AuthorizationGrant authorizationGrant, boolean includeIdTokenClaims, Function preProcessing, + Function postProcessing, ExecutionContext executionContext) { + throw new UnsupportedOperationException("Not allowed for UnmodifiableAuthorizationGrant."); + } + + @Override + public RefreshToken getRefreshToken(String refreshTokenCode) { + return grant.getRefreshToken(refreshTokenCode); + } + + @Override + public AbstractToken getAccessToken(String tokenCode) { + return grant.getAccessToken(tokenCode); + } + + @Override + public void revokeAllTokens() { + throw new UnsupportedOperationException("Not allowed for UnmodifiableAuthorizationGrant."); + } + + @Override + public void checkExpiredTokens() { + throw new UnsupportedOperationException("Not allowed for UnmodifiableAuthorizationGrant."); + } + + @Override + public String checkScopesPolicy(String scope) { + throw new UnsupportedOperationException("Not allowed for UnmodifiableAuthorizationGrant."); + } + + @Override + public User getUser() { + return grant.getUser(); + } + + @Override + public String getUserId() { + return grant.getUserId(); + } + + @Override + public String getUserDn() { + return grant.getUserDn(); + } + + @Override + public AuthorizationGrantType getAuthorizationGrantType() { + return grant.getAuthorizationGrantType(); + } + + @Override + public String getClientId() { + return grant.getClientId(); + } + + @Override + public Client getClient() { + return grant.getClient(); + } + + @Override + public String getClientDn() { + return grant.getClientDn(); + } + + @Override + public List getAccessTokens() { + return grant.getAccessTokens(); + } + + @Override + public Set getScopes() { + return grant.getScopes(); + } + + @Override + public Set getRefreshTokensCodes() { + return grant.getRefreshTokensCodes(); + } + + @Override + public Set getAccessTokensCodes() { + return grant.getAccessTokensCodes(); + } + + @Override + public List getRefreshTokens() { + return grant.getRefreshTokens(); + } + + @Override + public void setRefreshTokens(List refreshTokens) { + throw new UnsupportedOperationException("Not allowed for UnmodifiableAuthorizationGrant."); + } + + @Override + public AccessToken getLongLivedAccessToken() { + return grant.getLongLivedAccessToken(); + } + + @Override + public IdToken getIdToken() { + return grant.getIdToken(); + } + + @Override + public JwtAuthorizationRequest getJwtAuthorizationRequest() { + return grant.getJwtAuthorizationRequest(); + } + + @Override + public void setJwtAuthorizationRequest(JwtAuthorizationRequest p_jwtAuthorizationRequest) { + throw new UnsupportedOperationException("Not allowed for UnmodifiableAuthorizationGrant."); + } + + @Override + public Date getAuthenticationTime() { + return grant.getAuthenticationTime(); + } + + @Override + public TokenLdap getTokenLdap() { + return grant.getTokenLdap(); + } + + @Override + public void setTokenLdap(TokenLdap p_tokenLdap) { + throw new UnsupportedOperationException("Not allowed for UnmodifiableAuthorizationGrant."); + } + + @Override + public void setLongLivedAccessToken(AccessToken longLivedAccessToken) { + throw new UnsupportedOperationException("Not allowed for UnmodifiableAuthorizationGrant."); + } + + @Override + public void setIdToken(IdToken idToken) { + throw new UnsupportedOperationException("Not allowed for UnmodifiableAuthorizationGrant."); + } + + @Override + public void setScopes(Collection scopes) { + throw new UnsupportedOperationException("Not allowed for UnmodifiableAuthorizationGrant."); + } + + @Override + public void setAccessTokens(List accessTokens) { + throw new UnsupportedOperationException("Not allowed for UnmodifiableAuthorizationGrant."); + } + + @Override + public String getAcrValues() { + return grant.getAcrValues(); + } + + @Override + public void setAcrValues(String authMode) { + throw new UnsupportedOperationException("Not allowed for UnmodifiableAuthorizationGrant."); + } + + @Override + public String getSessionDn() { + return grant.getSessionDn(); + } + + @Override + public void setSessionDn(String sessionDn) { + throw new UnsupportedOperationException("Not allowed for UnmodifiableAuthorizationGrant."); + } + + @Override + public void save() { + throw new UnsupportedOperationException("Not allowed for UnmodifiableAuthorizationGrant."); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/config/Conf.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/config/Conf.java new file mode 100644 index 00000000..9418dd7a --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/config/Conf.java @@ -0,0 +1,109 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.config; + +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.error.ErrorMessages; +import org.gluu.persist.annotation.AttributeName; +import org.gluu.persist.annotation.DN; +import org.gluu.persist.annotation.DataEntry; +import org.gluu.persist.annotation.JsonObject; +import org.gluu.persist.annotation.ObjectClass; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 03/01/2013 + */ +@DataEntry +@ObjectClass(value = "oxAuthConfiguration") +public class Conf { + @DN + private String dn; + + @JsonObject + @AttributeName(name = "oxAuthConfDynamic") + private AppConfiguration dynamic; + + @JsonObject + @AttributeName(name = "oxAuthConfStatic") + private StaticConfiguration statics; + + @JsonObject + @AttributeName(name = "oxAuthConfErrors") + private ErrorMessages errors; + + @JsonObject + @AttributeName(name = "oxAuthConfWebKeys") + private WebKeysConfiguration webKeys; + + @AttributeName(name = "oxRevision") + private long revision; + + public Conf() { + } + + public String getDn() { + return dn; + } + + public void setDn(String p_dn) { + dn = p_dn; + } + + public AppConfiguration getDynamic() { + return dynamic; + } + + public void setDynamic(AppConfiguration dynamic) { + this.dynamic = dynamic; + } + + public StaticConfiguration getStatics() { + return statics; + } + + public void setStatics(StaticConfiguration statics) { + this.statics = statics; + } + + public ErrorMessages getErrors() { + return errors; + } + + public void setErrors(ErrorMessages errors) { + this.errors = errors; + } + + public WebKeysConfiguration getWebKeys() { + return webKeys; + } + + public void setWebKeys(WebKeysConfiguration webKeys) { + this.webKeys = webKeys; + } + + public long getRevision() { + return revision; + } + + public void setRevision(long revision) { + this.revision = revision; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("Conf"); + sb.append("{m_dn='").append(dn).append('\''); + sb.append(", m_dynamic='").append(dynamic).append('\''); + sb.append(", m_static='").append(statics).append('\''); + sb.append(", m_errors='").append(errors).append('\''); + sb.append(", m_webKeys='").append(webKeys).append('\''); + sb.append('}'); + return sb.toString(); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/config/ConfigurationFactory.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/config/ConfigurationFactory.java new file mode 100644 index 00000000..baf96a8a --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/config/ConfigurationFactory.java @@ -0,0 +1,578 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.config; + +import org.apache.commons.lang.StringUtils; +import org.gluu.exception.ConfigurationException; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.configuration.Configuration; +import org.gluu.oxauth.model.crypto.AbstractCryptoProvider; +import org.gluu.oxauth.model.crypto.CryptoProviderFactory; +import org.gluu.oxauth.model.error.ErrorMessages; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.event.CryptoProviderEvent; +import org.gluu.oxauth.model.jwk.JSONWebKey; +import org.gluu.oxauth.service.common.ApplicationFactory; +import org.gluu.oxauth.util.ServerUtil; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.persist.exception.BasePersistenceException; +import org.gluu.persist.model.PersistenceConfiguration; +import org.gluu.persist.service.PersistanceFactoryService; +import org.gluu.service.cdi.async.Asynchronous; +import org.gluu.service.cdi.event.*; +import org.gluu.service.timer.event.TimerEvent; +import org.gluu.service.timer.schedule.TimerSchedule; +import org.gluu.util.StringHelper; +import org.gluu.util.properties.FileConfiguration; +import org.json.JSONObject; +import org.slf4j.Logger; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Event; +import javax.enterprise.event.Observes; +import javax.enterprise.inject.Instance; +import javax.enterprise.inject.Produces; +import javax.inject.Inject; +import javax.inject.Named; +import javax.servlet.ServletContext; +import javax.servlet.ServletRegistration; +import java.io.File; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +/** + * @author Yuriy Zabrovarnyy + * @author Javier Rojas Blum + * @author Yuriy Movchan + * @version June 15, 2016 + */ +@ApplicationScoped +public class ConfigurationFactory { + + @Inject + private Logger log; + + @Inject + private Event timerEvent; + + @Inject + private Event configurationUpdateEvent; + + @Inject + private Event cryptoProviderEvent; + + @Inject + private Event event; + + @Inject @Named(ApplicationFactory.PERSISTENCE_ENTRY_MANAGER_NAME) + private Instance persistenceEntryManagerInstance; + + @Inject + private PersistanceFactoryService persistanceFactoryService; + + @Inject + private Instance configurationInstance; + + @Inject + private Instance abstractCryptoProviderInstance; + + public final static String PERSISTENCE_CONFIGUARION_RELOAD_EVENT_TYPE = "persistenceConfigurationReloadEvent"; + public final static String BASE_CONFIGUARION_RELOAD_EVENT_TYPE = "baseConfigurationReloadEvent"; + + private final static int DEFAULT_INTERVAL = 30; // 30 seconds + + static { + if (System.getProperty("gluu.base") != null) { + BASE_DIR = System.getProperty("gluu.base"); + } else if ((System.getProperty("catalina.base") != null) && (System.getProperty("catalina.base.ignore") == null)) { + BASE_DIR = System.getProperty("catalina.base"); + } else if (System.getProperty("catalina.home") != null) { + BASE_DIR = System.getProperty("catalina.home"); + } else if (System.getProperty("jboss.home.dir") != null) { + BASE_DIR = System.getProperty("jboss.home.dir"); + } else { + BASE_DIR = null; + } + } + + private static final String BASE_DIR; + private static final String DIR = BASE_DIR + File.separator + "conf" + File.separator; + + private static final String BASE_PROPERTIES_FILE = DIR + "gluu.properties"; + private static final String LDAP_PROPERTIES_FILE = "oxauth.properties"; + + private final String CONFIG_FILE_NAME = "oxauth-config.json"; + private final String ERRORS_FILE_NAME = "oxauth-errors.json"; + private final String STATIC_CONF_FILE_NAME = "oxauth-static-conf.json"; + private final String WEB_KEYS_FILE_NAME = "oxauth-web-keys.json"; + private final String SALT_FILE_NAME = "salt"; + + private String confDir, configFilePath, errorsFilePath, staticConfFilePath, webKeysFilePath, saltFilePath; + + private boolean loaded = false; + + private FileConfiguration baseConfiguration; + + private PersistenceConfiguration persistenceConfiguration; + private AppConfiguration conf; + private StaticConfiguration staticConf; + private WebKeysConfiguration jwks; + private ErrorResponseFactory errorResponseFactory; + private String cryptoConfigurationSalt; + + private String contextPath; + private String facesMapping; + + private AtomicBoolean isActive; + + private long baseConfigurationFileLastModifiedTime; + + private long loadedRevision = -1; + private boolean loadedFromLdap = true; + + @PostConstruct + public void init() { + this.isActive = new AtomicBoolean(true); + try { + this.persistenceConfiguration = persistanceFactoryService.loadPersistenceConfiguration(LDAP_PROPERTIES_FILE); + loadBaseConfiguration(); + + this.confDir = confDir(); + + this.configFilePath = confDir + CONFIG_FILE_NAME; + this.errorsFilePath = confDir + ERRORS_FILE_NAME; + this.staticConfFilePath = confDir + STATIC_CONF_FILE_NAME; + + String certsDir = this.baseConfiguration.getString("certsDir"); + if (StringHelper.isEmpty(certsDir)) { + certsDir = confDir; + } + this.webKeysFilePath = certsDir + File.separator + WEB_KEYS_FILE_NAME; + this.saltFilePath = confDir + SALT_FILE_NAME; + loadCryptoConfigurationSalt(); + } finally { + this.isActive.set(false); + } + } + + public void onServletContextActivation(@Observes ServletContext context ) { + this.contextPath = context.getContextPath(); + + this.facesMapping = ""; + ServletRegistration servletRegistration = context.getServletRegistration("Faces Servlet"); + if (servletRegistration == null) { + return; + } + + String[] mappings = servletRegistration.getMappings().toArray(new String[0]); + if (mappings.length == 0) { + return; + } + + this.facesMapping = mappings[0].replaceAll("\\*", ""); + } + + public void create() { + if (!createFromLdap(true)) { + log.error("Failed to load configuration from LDAP. Please fix it!!!."); + throw new ConfigurationException("Failed to load configuration from LDAP."); + } else { + log.info("Configuration loaded successfully."); + } + } + + public void initTimer() { + log.debug("Initializing Configuration Timer"); + + final int delay = 30; + final int interval = DEFAULT_INTERVAL; + + timerEvent.fire(new TimerEvent(new TimerSchedule(delay, interval), new ConfigurationEvent(), + Scheduled.Literal.INSTANCE)); + } + + @Asynchronous + public void reloadConfigurationTimerEvent(@Observes @Scheduled ConfigurationEvent configurationEvent) { + if (this.isActive.get()) { + return; + } + + if (!this.isActive.compareAndSet(false, true)) { + return; + } + + try { + reloadConfiguration(); + } catch (Throwable ex) { + log.error("Exception happened while reloading application configuration", ex); + } finally { + this.isActive.set(false); + } + } + + private void reloadConfiguration() { + // Reload LDAP configuration if needed + PersistenceConfiguration newPersistenceConfiguration = persistanceFactoryService.loadPersistenceConfiguration(LDAP_PROPERTIES_FILE); + + if (newPersistenceConfiguration != null) { + if (!StringHelper.equalsIgnoreCase(this.persistenceConfiguration.getFileName(), newPersistenceConfiguration.getFileName()) || (newPersistenceConfiguration.getLastModifiedTime() > this.persistenceConfiguration.getLastModifiedTime())) { + // Reload configuration only if it was modified + this.persistenceConfiguration = newPersistenceConfiguration; + event.select(LdapConfigurationReload.Literal.INSTANCE).fire(PERSISTENCE_CONFIGUARION_RELOAD_EVENT_TYPE); + } + } + + // Reload Base configuration if needed + File baseConfiguration = new File(BASE_PROPERTIES_FILE); + if (baseConfiguration.exists()) { + final long lastModified = baseConfiguration.lastModified(); + if (lastModified > baseConfigurationFileLastModifiedTime) { + // Reload configuration only if it was modified + loadBaseConfiguration(); + event.select(BaseConfigurationReload.Literal.INSTANCE).fire(BASE_CONFIGUARION_RELOAD_EVENT_TYPE); + } + } + + if (!loadedFromLdap) { + return; + } + + if (!isRevisionIncreased()) { + return; + } + + createFromLdap(false); + } + + private boolean isRevisionIncreased() { + final Conf conf = loadConfigurationFromLdap("oxRevision"); + if (conf == null) { + return false; + } + + log.trace("LDAP revision: " + conf.getRevision() + ", server revision:" + loadedRevision); + return conf.getRevision() > this.loadedRevision; + } + + private String confDir() { + final String confDir = this.baseConfiguration.getString("confDir", null); + if (StringUtils.isNotBlank(confDir)) { + return confDir; + } + + return DIR; + } + + public FileConfiguration getBaseConfiguration() { + return baseConfiguration; + } + + @Produces + @ApplicationScoped + public PersistenceConfiguration getPersistenceConfiguration() { + return persistenceConfiguration; + } + + @Produces + @ApplicationScoped + public AppConfiguration getAppConfiguration() { + return conf; + } + + @Produces + @ApplicationScoped + public StaticConfiguration getStaticConfiguration() { + return staticConf; + } + + @Produces + @ApplicationScoped + public WebKeysConfiguration getWebKeysConfiguration() { + return jwks; + } + + @Produces + @ApplicationScoped + public ErrorResponseFactory getErrorResponseFactory() { + return errorResponseFactory; + } + + public BaseDnConfiguration getBaseDn() { + return getStaticConfiguration().getBaseDn(); + } + + public String getCryptoConfigurationSalt() { + return cryptoConfigurationSalt; + } + + private boolean createFromFile() { + boolean result = reloadConfFromFile() && reloadErrorsFromFile() && reloadStaticConfFromFile() + && reloadWebkeyFromFile(); + + return result; + } + + private boolean reloadWebkeyFromFile() { + final WebKeysConfiguration webKeysFromFile = loadWebKeysFromFile(); + if (webKeysFromFile != null) { + log.info("Reloaded web keys from file: " + webKeysFilePath); + jwks = webKeysFromFile; + return true; + } else { + log.error("Failed to load web keys configuration from file: " + webKeysFilePath); + } + + return false; + } + + private boolean reloadStaticConfFromFile() { + final StaticConfiguration staticConfFromFile = loadStaticConfFromFile(); + if (staticConfFromFile != null) { + log.info("Reloaded static conf from file: " + staticConfFilePath); + staticConf = staticConfFromFile; + return true; + } else { + log.error("Failed to load static configuration from file: " + staticConfFilePath); + } + + return false; + } + + private boolean reloadErrorsFromFile() { + final ErrorMessages errorsFromFile = loadErrorsFromFile(); + if (errorsFromFile != null) { + log.info("Reloaded errors from file: " + errorsFilePath); + errorResponseFactory = new ErrorResponseFactory(errorsFromFile, conf); + return true; + } else { + log.error("Failed to load errors from file: " + errorsFilePath); + } + + return false; + } + + private boolean reloadConfFromFile() { + final AppConfiguration configFromFile = loadConfFromFile(); + if (configFromFile != null) { + log.info("Reloaded configuration from file: " + configFilePath); + conf = configFromFile; + return true; + } else { + log.error("Failed to load configuration from file: " + configFilePath); + } + + return false; + } + + public boolean reloadConfFromLdap() { + if (!isRevisionIncreased()) { + return false; + } + return createFromLdap(false); + } + + private boolean createFromLdap(boolean recoverFromFiles) { + log.info("Loading configuration from '{}' DB...", baseConfiguration.getString("persistence.type")); + try { + final Conf c = loadConfigurationFromLdap(); + if (c != null) { + init(c); + + // Destroy old configuration + if (this.loaded) { + destroy(AppConfiguration.class); + destroy(StaticConfiguration.class); + destroy(WebKeysConfiguration.class); + destroy(ErrorResponseFactory.class); + } + + this.loaded = true; + configurationUpdateEvent.select(ConfigurationUpdate.Literal.INSTANCE).fire(conf); + + destroyCryptoProviderInstance(); + AbstractCryptoProvider newAbstractCryptoProvider = abstractCryptoProviderInstance.get(); + cryptoProviderEvent.select(CryptoProviderEvent.Literal.INSTANCE).fire(newAbstractCryptoProvider); + + return true; + } + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + } + + if (recoverFromFiles) { + log.info("Unable to find configuration in LDAP, try to load configuration from file system... "); + if (createFromFile()) { + this.loadedFromLdap = false; + return true; + } + } + + return false; + } + + public void destroy(Class clazz) { + Instance confInstance = configurationInstance.select(clazz); + configurationInstance.destroy(confInstance.get()); + } + + private void destroyCryptoProviderInstance() { + log.trace("Destroyed crypto provider instance."); + + AbstractCryptoProvider abstractCryptoProvider = abstractCryptoProviderInstance.get(); + abstractCryptoProviderInstance.destroy(abstractCryptoProvider); + CryptoProviderFactory.reset(); + } + + private Conf loadConfigurationFromLdap(String... returnAttributes) { + final PersistenceEntryManager ldapManager = persistenceEntryManagerInstance.get(); + final String dn = this.baseConfiguration.getString("oxauth_ConfigurationEntryDN"); + try { + final Conf conf = ldapManager.find(dn, Conf.class, returnAttributes); + + return conf; + } catch (BasePersistenceException ex) { + ex.printStackTrace(); + log.error(ex.getMessage()); + } + + return null; + } + + private void init(Conf p_conf) { + initConfigurationConf(p_conf); + this.loadedRevision = p_conf.getRevision(); + } + + private void initConfigurationConf(Conf p_conf) { + if (p_conf.getDynamic() != null) { + conf = p_conf.getDynamic(); + } + if (p_conf.getStatics() != null) { + staticConf = p_conf.getStatics(); + } + if (p_conf.getWebKeys() != null) { + jwks = p_conf.getWebKeys(); + } else { + generateWebKeys(); + } + if (p_conf.getErrors() != null) { + errorResponseFactory = new ErrorResponseFactory(p_conf.getErrors(), p_conf.getDynamic()); + } + } + + private void generateWebKeys() { + log.info("Failed to load JWKS. Attempting to generate new JWKS..."); + + String newWebKeys = null; + try { + final AbstractCryptoProvider cryptoProvider = CryptoProviderFactory.getCryptoProvider(getAppConfiguration()); + + // Generate new JWKS + JSONObject jsonObject = AbstractCryptoProvider.generateJwks(cryptoProvider, getAppConfiguration()); + newWebKeys = jsonObject.toString(); + + // Attempt to load new JWKS + jwks = ServerUtil.createJsonMapper().readValue(newWebKeys, WebKeysConfiguration.class); + + // Store new JWKS in LDAP + Conf conf = loadConfigurationFromLdap(); + conf.setWebKeys(jwks); + + long nextRevision = conf.getRevision() + 1; + conf.setRevision(nextRevision); + + final PersistenceEntryManager ldapManager = persistenceEntryManagerInstance.get(); + ldapManager.merge(conf); + + log.info("Generated new JWKS successfully."); + log.trace("JWKS keys: " + conf.getWebKeys().getKeys().stream().map(JSONWebKey::getKid).collect(Collectors.toList())); + + log.trace("KeyStore keys: " + cryptoProvider.getKeys()); + } catch (Exception ex2) { + log.error("Failed to re-generate JWKS keys", ex2); + } + } + + private AppConfiguration loadConfFromFile() { + try { + return ServerUtil.createJsonMapper().readValue(new File(configFilePath), AppConfiguration.class); + } catch (Exception e) { + log.warn(e.getMessage(), e); + } + return null; + } + + private ErrorMessages loadErrorsFromFile() { + try { + return ServerUtil.createJsonMapper().readValue(new File(errorsFilePath), ErrorMessages.class); + } catch (Exception e) { + log.warn(e.getMessage(), e); + } + return null; + } + + private StaticConfiguration loadStaticConfFromFile() { + try { + return ServerUtil.createJsonMapper().readValue(new File(staticConfFilePath), StaticConfiguration.class); + } catch (Exception e) { + log.warn(e.getMessage(), e); + } + return null; + } + + private WebKeysConfiguration loadWebKeysFromFile() { + try { + return ServerUtil.createJsonMapper().readValue(new File(webKeysFilePath), WebKeysConfiguration.class); + } catch (Exception e) { + log.warn(e.getMessage(), e); + } + return null; + } + + private void loadBaseConfiguration() { + this.baseConfiguration = createFileConfiguration(BASE_PROPERTIES_FILE, true); + + File baseConfiguration = new File(BASE_PROPERTIES_FILE); + this.baseConfigurationFileLastModifiedTime = baseConfiguration.lastModified(); + } + + public void loadCryptoConfigurationSalt() { + try { + FileConfiguration cryptoConfiguration = createFileConfiguration(saltFilePath, true); + + this.cryptoConfigurationSalt = cryptoConfiguration.getString("encodeSalt"); + } catch (Exception ex) { + log.error("Failed to load configuration from {}", saltFilePath, ex); + throw new ConfigurationException("Failed to load configuration from " + saltFilePath, ex); + } + } + + private FileConfiguration createFileConfiguration(String fileName, boolean isMandatory) { + try { + FileConfiguration fileConfiguration = new FileConfiguration(fileName); + + return fileConfiguration; + } catch (Exception ex) { + if (isMandatory) { + log.error("Failed to load configuration from {}", fileName, ex); + throw new ConfigurationException("Failed to load configuration from " + fileName, ex); + } + } + + return null; + } + + public String getFacesMapping() { + return facesMapping; + } + + public String getContextPath() { + return contextPath; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/config/Constants.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/config/Constants.java new file mode 100644 index 00000000..827df8cf --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/config/Constants.java @@ -0,0 +1,36 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.config; + +/** + * Constants + * + * @author Yuriy Movchan Date: 10.14.2010 + */ +public final class Constants { + + public static final String RESULT_SUCCESS = "success"; + public static final String RESULT_FAILURE = "failure"; + public static final String RESULT_AUTHENTICATION_FAILED = "authentication_failed"; + public static final String RESULT_DUPLICATE = "duplicate"; + public static final String RESULT_DISABLED = "disabled"; + public static final String RESULT_NO_PERMISSIONS = "no_permissions"; + public static final String RESULT_INVALID_STEP = "invalid_step"; + public static final String RESULT_VALIDATION_ERROR = "validation_error"; + public static final String RESULT_LOGOUT = "logout"; + public static final String RESULT_EXPIRED = "expired"; + + public static final String U2F_PROTOCOL_VERSION = "U2F_V2"; + + public static final String OX_AUTH_SCOPE_TYPE_OPENID = "openid"; + public static final String REVOKE_SESSION_SCOPE = "revoke_session"; + + public static final String REMOTE_IP = "remote_ip"; + public static final String AUTHENTICATED_USER = "auth_user"; + public static final String AUTHORIZED_GRANT = "authorized_grant"; + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/config/WebKeysConfiguration.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/config/WebKeysConfiguration.java new file mode 100644 index 00000000..cdd61b24 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/config/WebKeysConfiguration.java @@ -0,0 +1,24 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.config; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.gluu.oxauth.model.configuration.Configuration; +import org.gluu.oxauth.model.jwk.JSONWebKeySet; + +import javax.enterprise.inject.Vetoed; + +/** + * @author Yuriy Movchan + * @version 03/15/2017 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@Vetoed +public class WebKeysConfiguration extends JSONWebKeySet implements Configuration { + + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/discovery/OpenIdConnectDiscoveryParamsValidator.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/discovery/OpenIdConnectDiscoveryParamsValidator.java new file mode 100644 index 00000000..e0c64ca3 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/discovery/OpenIdConnectDiscoveryParamsValidator.java @@ -0,0 +1,17 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.discovery; + +/** + * @author Javier Rojas Date: 01.28.2013 + */ +public class OpenIdConnectDiscoveryParamsValidator { + + public static boolean validateParams(String resource, String rel) { + return resource != null && !resource.isEmpty(); + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/error/ErrorMessageList.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/error/ErrorMessageList.java new file mode 100644 index 00000000..d06deb61 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/error/ErrorMessageList.java @@ -0,0 +1,36 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.error; + +import java.util.ArrayList; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +import org.gluu.model.error.ErrorMessage; + + +/** + * Represents an error message list in a configuration XML file. + * + * @author Javier Rojas Date: 09.23.2011 + * + */ +@XmlRootElement(name = "errors") +public class ErrorMessageList { + + @XmlElement(name = "error") + private ArrayList errors; + + public ArrayList getErrorList() { + return errors; + } + + public void setErrorList(ArrayList errorList) { + this.errors = errorList; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/error/ErrorMessages.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/error/ErrorMessages.java new file mode 100644 index 00000000..12897861 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/error/ErrorMessages.java @@ -0,0 +1,139 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.error; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import javax.xml.bind.annotation.*; + +import org.gluu.model.error.ErrorMessage; + +import java.util.List; + +/** + * @author Yuriy Zabrovarnyy + * @author Javier Rojas Blum + * @version August 20, 2019 + */ +@XmlRootElement(name = "errors") +@XmlAccessorType(XmlAccessType.FIELD) +@JsonIgnoreProperties(ignoreUnknown = true) +public class ErrorMessages { + + @XmlElementWrapper(name = "authorize") + @XmlElement(name = "error") + private List authorize; + @XmlElementWrapper(name = "client-info") + @XmlElement(name = "error") + private List clientInfo; + @XmlElementWrapper(name = "end-session") + @XmlElement(name = "error") + private List endSession; + @XmlElementWrapper(name = "register") + @XmlElement(name = "error") + private List register; + @XmlElementWrapper(name = "token") + @XmlElement(name = "error") + private List token; + @XmlElementWrapper(name = "revoke") + @XmlElement(name = "error") + private List revoke; + @XmlElementWrapper(name = "uma") + @XmlElement(name = "error") + private List uma; + @XmlElementWrapper(name = "user-info") + @XmlElement(name = "error") + private List userInfo; + + @XmlElementWrapper(name = "fido") + @XmlElement(name = "error") + private List fido; + + @XmlElementWrapper(name = "backchannelAuthentication") + @XmlElement(name = "error") + private List backchannelAuthentication; + + public List getAuthorize() { + return authorize; + } + + public void setAuthorize(List p_authorize) { + authorize = p_authorize; + } + + public List getClientInfo() { + return clientInfo; + } + + public void setClientInfo(List p_clientInfo) { + clientInfo = p_clientInfo; + } + + public List getEndSession() { + return endSession; + } + + public void setEndSession(List p_endSession) { + endSession = p_endSession; + } + + public List getRegister() { + return register; + } + + public void setRegister(List p_register) { + register = p_register; + } + + public List getToken() { + return token; + } + + public void setToken(List p_token) { + token = p_token; + } + + public List getRevoke() { + return revoke; + } + + public void setRevoke(List p_revoke) { + revoke = p_revoke; + } + + public List getUma() { + return uma; + } + + public void setUma(List p_uma) { + uma = p_uma; + } + + public List getUserInfo() { + return userInfo; + } + + public void setUserInfo(List p_userInfo) { + userInfo = p_userInfo; + } + + public List getFido() { + return fido; + } + + public void setFido(List fido) { + this.fido = fido; + } + + public List getBackchannelAuthentication() { + return backchannelAuthentication; + } + + public void setBackchannelAuthentication(List backchannelAuthentication) { + this.backchannelAuthentication = backchannelAuthentication; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/error/ErrorResponseFactory.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/error/ErrorResponseFactory.java new file mode 100644 index 00000000..b50e4b72 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/error/ErrorResponseFactory.java @@ -0,0 +1,176 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.error; + +import org.gluu.model.error.ErrorMessage; +import org.gluu.oxauth.model.authorize.AuthorizeErrorResponseType; +import org.gluu.oxauth.model.ciba.BackchannelAuthenticationErrorResponseType; +import org.gluu.oxauth.model.clientinfo.ClientInfoErrorResponseType; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.configuration.Configuration; +import org.gluu.oxauth.model.fido.u2f.U2fErrorResponseType; +import org.gluu.oxauth.model.register.RegisterErrorResponseType; +import org.gluu.oxauth.model.session.EndSessionErrorResponseType; +import org.gluu.oxauth.model.token.TokenErrorResponseType; +import org.gluu.oxauth.model.token.TokenRevocationErrorResponseType; +import org.gluu.oxauth.model.uma.UmaErrorResponseType; +import org.gluu.oxauth.model.userinfo.UserInfoErrorResponseType; +import org.gluu.oxauth.util.ServerUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.enterprise.inject.Vetoed; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.util.List; + +/** + * Provides an easy way to get Error responses based in an error response type + * + * @author Yuriy Zabrovarnyy + * @author Javier Rojas Blum + * @author Yuriy Movchan + * @version August 20, 2019 + */ +@Vetoed +public class ErrorResponseFactory implements Configuration { + + private static Logger log = LoggerFactory.getLogger(ErrorResponseFactory.class); + + private ErrorMessages messages; + private AppConfiguration appConfiguration; + + public ErrorResponseFactory() { + } + + public ErrorResponseFactory(ErrorMessages messages, AppConfiguration appConfiguration) { + this.messages = messages; + this.appConfiguration = appConfiguration; + } + + public ErrorMessages getMessages() { + return messages; + } + + public void setMessages(ErrorMessages p_messages) { + messages = p_messages; + } + + /** + * Looks for an error message. + * + * @param p_list error list + * @param type The type of the error. + * @return Error message or null if not found. + */ + private ErrorMessage getError(List p_list, IErrorType type) { + log.debug("Looking for the error with id: {}", type); + + if (p_list != null) { + for (ErrorMessage error : p_list) { + if (error.getId().equals(type.getParameter())) { + log.debug("Found error, id: {}", type); + return error; + } + } + } + + log.error("Error not found, id: {}", type); + return new ErrorMessage(type.getParameter(), type.getParameter(), null); + } + + public String getErrorAsJson(IErrorType p_type) { + return getErrorResponse(p_type).toJSonString(); + } + + public String errorAsJson(IErrorType p_type, String reason) { + final DefaultErrorResponse error = getErrorResponse(p_type); + error.setReason(appConfiguration.getErrorReasonEnabled() ? reason : ""); + return error.toJSonString(); + } + + public WebApplicationException createWebApplicationException(Response.Status status, IErrorType type, String reason) throws WebApplicationException { + return new WebApplicationException(Response + .status(status) + .entity(errorAsJson(type, reason)) + .type(MediaType.APPLICATION_JSON_TYPE) + .build()); + } + + public String getErrorAsJson(IErrorType p_type, String p_state, String reason) { + return getErrorResponse(p_type, p_state, reason).toJSonString(); + } + + public String getErrorAsQueryString(IErrorType p_type, String p_state) { + return getErrorAsQueryString(p_type, p_state, ""); + } + + public String getErrorAsQueryString(IErrorType p_type, String p_state, String reason) { + return getErrorResponse(p_type, p_state, reason).toQueryString(); + } + + public DefaultErrorResponse getErrorResponse(IErrorType type, String p_state, String reason) { + final DefaultErrorResponse response = getErrorResponse(type); + response.setState(p_state); + response.setReason(reason); + return response; + } + + public DefaultErrorResponse getErrorResponse(IErrorType type) { + final DefaultErrorResponse response = new DefaultErrorResponse(); + response.setType(type); + + if (type != null && messages != null) { + List list = null; + if (type instanceof AuthorizeErrorResponseType) { + list = messages.getAuthorize(); + } else if (type instanceof ClientInfoErrorResponseType) { + list = messages.getClientInfo(); + } else if (type instanceof EndSessionErrorResponseType) { + list = messages.getEndSession(); + } else if (type instanceof RegisterErrorResponseType) { + list = messages.getRegister(); + } else if (type instanceof TokenErrorResponseType) { + list = messages.getToken(); + } else if (type instanceof TokenRevocationErrorResponseType) { + list = messages.getRevoke(); + } else if (type instanceof UmaErrorResponseType) { + list = messages.getUma(); + } else if (type instanceof UserInfoErrorResponseType) { + list = messages.getUserInfo(); + } else if (type instanceof U2fErrorResponseType) { + list = messages.getFido(); + } else if (type instanceof BackchannelAuthenticationErrorResponseType) { + list = messages.getBackchannelAuthentication(); + } + + if (list != null) { + final ErrorMessage m = getError(list, type); + response.setErrorDescription(m.getDescription()); + response.setErrorUri(m.getUri()); + } + } + + return response; + } + + public String getJsonErrorResponse(IErrorType type) { + final DefaultErrorResponse response = getErrorResponse(type); + + JsonErrorResponse jsonErrorResponse = new JsonErrorResponse(response); + + try { + return ServerUtil.asJson(jsonErrorResponse); + } catch (IOException ex) { + log.error("Failed to generate error response", ex); + return null; + } + } + +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/error/JsonErrorResponse.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/error/JsonErrorResponse.java new file mode 100644 index 00000000..0fc606dd --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/error/JsonErrorResponse.java @@ -0,0 +1,83 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.error; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import org.jboss.resteasy.annotations.providers.jaxb.IgnoreMediaTypes; + +/** + * JSON error response + * + * @author Yuriy Movchan Date: 05/20/2015 + */ +@IgnoreMediaTypes("application/*+json") // try to ignore jettison as it's recommended here: http://docs.jboss.org/resteasy/docs/2.3.4.Final/userguide/html/json.html +@JsonPropertyOrder({ "status", "error" }) +public class JsonErrorResponse { + + @JsonProperty(value = "status") + private String status; + + @JsonProperty(value = "error") + private String error; + + @JsonProperty(value = "error_description") + private String errorDescription; + + @JsonProperty(value = "error_uri") + private String errorUri; + + public JsonErrorResponse() { + } + + public JsonErrorResponse(DefaultErrorResponse response) { + this.error = response.getType().getParameter(); + this.errorDescription = response.getErrorDescription(); + this.errorUri = response.getErrorUri(); + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + public String getErrorDescription() { + return errorDescription; + } + + public void setErrorDescription(String errorDescription) { + this.errorDescription = errorDescription; + } + + public String getErrorUri() { + return errorUri; + } + + public void setErrorUri(String errorUri) { + this.errorUri = errorUri; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("JsonErrorResponse [status=").append(status).append(", error=").append(error).append(", errorDescription=") + .append(errorDescription).append(", errorUri=").append(errorUri).append("]"); + return builder.toString(); + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/exception/AcrChangedException.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/exception/AcrChangedException.java new file mode 100644 index 00000000..ef8f148f --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/exception/AcrChangedException.java @@ -0,0 +1,40 @@ +package org.gluu.oxauth.model.exception; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 16/06/2015 + */ + +public class AcrChangedException extends Exception { + + private boolean forceReAuthentication; + + public AcrChangedException() { + forceReAuthentication = true; + } + + public AcrChangedException(boolean forceReAuthentication) { + this.forceReAuthentication = forceReAuthentication; + } + + public AcrChangedException(Throwable cause) { + super(cause); + } + + public AcrChangedException(String message) { + super(message); + } + + public AcrChangedException(String message, Throwable cause) { + super(message, cause); + } + + public boolean isForceReAuthentication() { + return forceReAuthentication; + } + + public void setForceReAuthentication(boolean forceReAuthentication) { + this.forceReAuthentication = forceReAuthentication; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/exception/InvalidSessionStateException.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/exception/InvalidSessionStateException.java new file mode 100644 index 00000000..6ff81fa4 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/exception/InvalidSessionStateException.java @@ -0,0 +1,21 @@ +package org.gluu.oxauth.model.exception; + +/** + * Indicates that current session should be invalidated + * + * @author Yuriy Movchan Date: 06/04//2019 + * + */ +public class InvalidSessionStateException extends RuntimeException { + + private static final long serialVersionUID = -2256375601182225949L; + + public InvalidSessionStateException() { + super(); + } + + public InvalidSessionStateException(String message) { + super(message); + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/exception/InvalidStateException.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/exception/InvalidStateException.java new file mode 100644 index 00000000..f5d7f84e --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/exception/InvalidStateException.java @@ -0,0 +1,21 @@ +package org.gluu.oxauth.model.exception; + +/** + * Runtime exception to stop code execution if something is not right + * + * @author Yuriy Movchan Date: 09/08//2016 + * + */ +public class InvalidStateException extends RuntimeException { + + private static final long serialVersionUID = 6256375601182225949L; + + public InvalidStateException() { + super(); + } + + public InvalidStateException(String message) { + super(message); + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/fido/u2f/AuthenticateRequestMessageLdap.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/fido/u2f/AuthenticateRequestMessageLdap.java new file mode 100644 index 00000000..8758a110 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/fido/u2f/AuthenticateRequestMessageLdap.java @@ -0,0 +1,59 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ +package org.gluu.oxauth.model.fido.u2f; + +import java.io.Serializable; +import java.util.Date; + +import org.gluu.oxauth.model.fido.u2f.protocol.AuthenticateRequestMessage; +import org.gluu.persist.annotation.AttributeName; +import org.gluu.persist.annotation.JsonObject; + +/** + * U2F authentication requests + * + * @author Yuriy Movchan + * @version August 9, 2017 + */ +public class AuthenticateRequestMessageLdap extends RequestMessageLdap implements Serializable { + + private static final long serialVersionUID = -1142931562244920584L; + + @JsonObject + @AttributeName(name = "oxRequest") + private AuthenticateRequestMessage authenticateRequestMessage; + + public AuthenticateRequestMessageLdap() { + } + + public AuthenticateRequestMessageLdap(AuthenticateRequestMessage authenticateRequestMessage) { + this.authenticateRequestMessage = authenticateRequestMessage; + this.requestId = authenticateRequestMessage.getRequestId(); + } + + public AuthenticateRequestMessageLdap(String dn, String id, Date creationDate, String sessionId, String userInum, + AuthenticateRequestMessage authenticateRequestMessage) { + super(dn, id, authenticateRequestMessage.getRequestId(), creationDate, sessionId, userInum); + this.authenticateRequestMessage = authenticateRequestMessage; + } + + public AuthenticateRequestMessage getAuthenticateRequestMessage() { + return authenticateRequestMessage; + } + + public void setAuthenticateRequestMessage(AuthenticateRequestMessage authenticateRequestMessage) { + this.authenticateRequestMessage = authenticateRequestMessage; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("AuthenticateRequestMessageLdap [id=").append(id).append(", authenticateRequestMessage=").append(authenticateRequestMessage) + .append(", requestId=").append(requestId).append(", creationDate=").append(creationDate).append("]"); + return builder.toString(); + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/fido/u2f/DeviceRegistration.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/fido/u2f/DeviceRegistration.java new file mode 100644 index 00000000..c3151f53 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/fido/u2f/DeviceRegistration.java @@ -0,0 +1,327 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ +package org.gluu.oxauth.model.fido.u2f; + +import java.io.Serializable; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +import org.gluu.oxauth.exception.fido.u2f.InvalidDeviceCounterException; +import org.gluu.oxauth.model.fido.u2f.exception.BadInputException; +import org.gluu.oxauth.model.fido.u2f.protocol.DeviceData; +import org.gluu.oxauth.model.util.Base64Util; +import org.gluu.persist.annotation.AttributeName; +import org.gluu.persist.annotation.DataEntry; +import org.gluu.persist.annotation.Expiration; +import org.gluu.persist.annotation.JsonObject; +import org.gluu.persist.annotation.ObjectClass; +import org.gluu.persist.model.base.BaseEntry; + +/** + * U2F Device registration + * + * @author Yuriy Movchan Date: 05/14/2015 + */ +@DataEntry(sortBy = "creationDate", sortByName = "creationDate") +@ObjectClass(value = "oxDeviceRegistration") +public class DeviceRegistration extends BaseEntry implements Serializable { + + private static final long serialVersionUID = -4542931562244920585L; + + @AttributeName(ignoreDuringUpdate = true, name = "oxId") + private String id; + + @AttributeName + private String displayName; + + @AttributeName + private String description; + + @AttributeName(name = "oxNickName") + private String nickname; + + @AttributeName(name = "personInum") + protected String userInum; + + @JsonObject + @AttributeName(name = "oxDeviceRegistrationConf") + private DeviceRegistrationConfiguration deviceRegistrationConfiguration; + + @JsonObject + @AttributeName(name = "oxDeviceNotificationConf") + private String deviceNotificationConf; + + @AttributeName(name = "oxCounter") + private long counter; + + @AttributeName(name = "oxStatus") + private DeviceRegistrationStatus status; + + @AttributeName(name = "oxApplication") + private String application; + + @AttributeName(name = "oxDeviceKeyHandle") + private String keyHandle; + + @AttributeName(name = "oxDeviceHashCode") + private Integer keyHandleHashCode; + + @JsonObject + @AttributeName(name = "oxDeviceData") + private DeviceData deviceData; + + @AttributeName(name = "creationDate") + private Date creationDate; + + @AttributeName(name = "oxLastAccessTime") + private Date lastAccessTime; + + @AttributeName(name = "exp") + private Date expirationDate; + + @AttributeName(name = "del") + private boolean deletable = true; + + @Expiration + private Integer ttl; + + public DeviceRegistration() {} + + public DeviceRegistration(String userInum, String keyHandle, String publicKey, String attestationCert, long counter, DeviceRegistrationStatus status, + String application, Integer keyHandleHashCode, Date creationDate) { + this.deviceRegistrationConfiguration = new DeviceRegistrationConfiguration(publicKey, attestationCert); + this.counter = counter; + this.status = status; + this.application = application; + this.userInum = userInum; + this.keyHandle = keyHandle; + this.keyHandleHashCode = keyHandleHashCode; + this.creationDate = creationDate; + } + + public DeviceRegistration(String userInum, String keyHandle, String publicKey, X509Certificate attestationCert, long counter) throws BadInputException { + this.userInum = userInum; + this.keyHandle = keyHandle; + try { + String attestationCertDecoded = Base64Util.base64urlencode(attestationCert.getEncoded()); + this.deviceRegistrationConfiguration = new DeviceRegistrationConfiguration(publicKey, attestationCertDecoded); + } catch (CertificateEncodingException e) { + throw new BadInputException("Malformed attestation certificate", e); + } + + this.counter = counter; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getNickname() { + return nickname; + } + + public void setNickname(String nickname) { + this.nickname = nickname; + } + + public String getUserInum() { + return userInum; + } + + public void setUserInum(String userInum) { + this.userInum = userInum; + } + + public DeviceRegistrationConfiguration getDeviceRegistrationConfiguration() { + return deviceRegistrationConfiguration; + } + + public void setDeviceRegistrationConfiguration(DeviceRegistrationConfiguration deviceRegistrationConfiguration) { + this.deviceRegistrationConfiguration = deviceRegistrationConfiguration; + } + + public String getDeviceNotificationConf() { + return deviceNotificationConf; + } + + public void setDeviceNotificationConf(String deviceNotificationConf) { + this.deviceNotificationConf = deviceNotificationConf; + } + + public long getCounter() { + return counter; + } + + public void setCounter(long counter) { + this.counter = counter; + } + + public DeviceRegistrationStatus getStatus() { + return status; + } + + public void setStatus(DeviceRegistrationStatus status) { + this.status = status; + } + + public String getApplication() { + return application; + } + + public void setApplication(String application) { + this.application = application; + } + + public String getKeyHandle() { + return keyHandle; + } + + public void setKeyHandle(String keyHandle) { + this.keyHandle = keyHandle; + } + + public Integer getKeyHandleHashCode() { + return keyHandleHashCode; + } + + public void setKeyHandleHashCode(Integer keyHandleHashCode) { + this.keyHandleHashCode = keyHandleHashCode; + } + + public Date getCreationDate() { + return creationDate; + } + + public void setCreationDate(Date creationDate) { + this.creationDate = creationDate; + } + + public void clearExpiration() { + this.expirationDate = null; + this.deletable = false; + this.ttl = 0; + } + + public void setExpiration() { + if (creationDate != null) { + final int expiration = 90; + Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + calendar.setTime(creationDate); + calendar.add(Calendar.SECOND, expiration); + this.expirationDate = calendar.getTime(); + this.deletable = true; + this.ttl = expiration; + } + } + + public Integer getTtl() { + return ttl; + } + + public void setTtl(Integer ttl) { + this.ttl = ttl; + } + + public DeviceData getDeviceData() { + return deviceData; + } + + public void setDeviceData(DeviceData deviceData) { + this.deviceData = deviceData; + } + + public Date getLastAccessTime() { + return lastAccessTime; + } + + public void setLastAccessTime(Date lastAccessTime) { + this.lastAccessTime = lastAccessTime; + } + + public boolean isCompromised() { + return DeviceRegistrationStatus.COMPROMISED == this.status; + } + + public void markCompromised() { + this.status = DeviceRegistrationStatus.COMPROMISED; + } + + public void checkAndUpdateCounter(long clientCounter) throws InvalidDeviceCounterException { + if (clientCounter == Integer.MAX_VALUE) { + // TODO: Remove in 6.0.It's enough period to migrate broken iOS counter + // Handle special case when counter value is max positive integer value + counter = -1; + } else { + if (clientCounter <= counter) { + markCompromised(); + throw new InvalidDeviceCounterException(this); + } + counter = clientCounter; + } + } + + public Date getExpirationDate() { + return expirationDate; + } + + public void setExpirationDate(Date expirationDate) { + this.expirationDate = expirationDate; + } + + public boolean isDeletable() { + return deletable; + } + + public void setDeletable(boolean deletable) { + this.deletable = deletable; + } + + @Override + public String toString() { + return "DeviceRegistration{" + + "id='" + id + '\'' + + ", displayName='" + displayName + '\'' + + ", description='" + description + '\'' + + ", nickname='" + nickname + '\'' + + ", deviceRegistrationConfiguration=" + deviceRegistrationConfiguration + + ", deviceNotificationConf='" + deviceNotificationConf + '\'' + + ", counter=" + counter + + ", status=" + status + + ", application='" + application + '\'' + + ", keyHandle='" + keyHandle + '\'' + + ", keyHandleHashCode=" + keyHandleHashCode + + ", deviceData=" + deviceData + + ", creationDate=" + creationDate + + ", lastAccessTime=" + lastAccessTime + + ", expirationDate=" + expirationDate + + ", deletable=" + deletable + + "} " + super.toString(); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/fido/u2f/DeviceRegistrationConfiguration.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/fido/u2f/DeviceRegistrationConfiguration.java new file mode 100644 index 00000000..b0e1aa6a --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/fido/u2f/DeviceRegistrationConfiguration.java @@ -0,0 +1,53 @@ +package org.gluu.oxauth.model.fido.u2f; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.gluu.oxauth.crypto.cert.CertificateParser; +import org.gluu.oxauth.model.util.Base64Util; + +/** + * U2F Device registration key + * + * @author Yuriy Movchan Date: 05/29/2015 + */ +public class DeviceRegistrationConfiguration { + + @JsonProperty + public final String publicKey; + + @JsonProperty + public final String attestationCert; + + public DeviceRegistrationConfiguration(@JsonProperty("publicKey") String publicKey, + @JsonProperty("attestationCert") String attestationCert) { + this.publicKey = publicKey; + this.attestationCert = attestationCert; + } + + public String getPublicKey() { + return publicKey; + } + + public String getAttestationCert() { + return attestationCert; + } + + @JsonIgnore + public X509Certificate getAttestationCertificate() throws CertificateException, NoSuchFieldException { + if (attestationCert == null) { + throw new NoSuchFieldException(); + } + return CertificateParser.parseDer(Base64Util.base64urldecode(attestationCert)); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("DeviceRegistrationConfiguration [publicKey=").append(publicKey).append(", attestationCert=").append(attestationCert).append("]"); + return builder.toString(); + } + +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/fido/u2f/DeviceRegistrationResult.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/fido/u2f/DeviceRegistrationResult.java new file mode 100644 index 00000000..bb8d82ff --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/fido/u2f/DeviceRegistrationResult.java @@ -0,0 +1,50 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ +package org.gluu.oxauth.model.fido.u2f; + +import java.io.Serializable; + +/** + * U2F Device registration with status + * + * @author Yuriy Movchan Date: 03/22/2016 + */ +public class DeviceRegistrationResult implements Serializable { + + private static final long serialVersionUID = -1542131162244920584L; + + private DeviceRegistration deviceRegistration; + + private Status status; + + public DeviceRegistrationResult() {} + + public DeviceRegistrationResult(DeviceRegistration deviceRegistration, Status status) { + this.deviceRegistration = deviceRegistration; + this.status = status; + } + + public DeviceRegistration getDeviceRegistration() { + return deviceRegistration; + } + + public void setDeviceRegistration(DeviceRegistration deviceRegistration) { + this.deviceRegistration = deviceRegistration; + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public enum Status { + APPROVED, CANCELED; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/fido/u2f/RegisterRequestMessageLdap.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/fido/u2f/RegisterRequestMessageLdap.java new file mode 100644 index 00000000..2caa8a3f --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/fido/u2f/RegisterRequestMessageLdap.java @@ -0,0 +1,59 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ +package org.gluu.oxauth.model.fido.u2f; + +import java.io.Serializable; +import java.util.Date; + +import org.gluu.oxauth.model.fido.u2f.protocol.RegisterRequestMessage; +import org.gluu.persist.annotation.AttributeName; +import org.gluu.persist.annotation.JsonObject; + +/** + * U2F registration requests + * + * @author Yuriy Movchan + * @version August 9, 2017 + */ +public class RegisterRequestMessageLdap extends RequestMessageLdap implements Serializable { + + private static final long serialVersionUID = -2242931562244920584L; + + @JsonObject + @AttributeName(name = "oxRequest") + private RegisterRequestMessage registerRequestMessage; + + public RegisterRequestMessageLdap() { + } + + public RegisterRequestMessageLdap(RegisterRequestMessage registerRequestMessage) { + this.registerRequestMessage = registerRequestMessage; + this.requestId = registerRequestMessage.getRequestId(); + } + + public RegisterRequestMessageLdap(String dn, String id, Date creationDate, String sessionId, String userInum, + RegisterRequestMessage registerRequestMessage) { + super(dn, id, registerRequestMessage.getRequestId(), creationDate, sessionId, userInum); + this.registerRequestMessage = registerRequestMessage; + } + + public RegisterRequestMessage getRegisterRequestMessage() { + return registerRequestMessage; + } + + public void setRegisterRequestMessage(RegisterRequestMessage registerRequestMessage) { + this.registerRequestMessage = registerRequestMessage; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("RegisterRequestMessageLdap [id=").append(id).append(", registerRequestMessage=").append(registerRequestMessage).append(", requestId=") + .append(requestId).append(", creationDate=").append(creationDate).append("]"); + return builder.toString(); + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/fido/u2f/RequestMessageLdap.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/fido/u2f/RequestMessageLdap.java new file mode 100644 index 00000000..ac0a3641 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/fido/u2f/RequestMessageLdap.java @@ -0,0 +1,134 @@ +package org.gluu.oxauth.model.fido.u2f; + +import org.gluu.persist.annotation.Expiration; +import org.gluu.persist.model.base.BaseEntry; +import org.gluu.persist.annotation.AttributeName; +import org.gluu.persist.annotation.DataEntry; +import org.gluu.persist.annotation.ObjectClass; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +/** + * U2F base request + * + * @author Yuriy Movchan + * @version August 9, 2017 + */ +@DataEntry(sortBy = "creationDate", sortByName = "creationDate") +@ObjectClass(value = "oxU2fRequest") +public class RequestMessageLdap extends BaseEntry { + + @AttributeName(ignoreDuringUpdate = true, name = "oxId") + protected String id; + + @AttributeName(name = "oxRequestId") + protected String requestId; + + @AttributeName(name = "creationDate") + protected Date creationDate; + + @AttributeName(name = "oxSessionStateId") + protected String sessionId; + + @AttributeName(name = "personInum") + protected String userInum; + + @AttributeName(name = "exp") + private Date expirationDate; + + @AttributeName(name = "del") + private boolean deletable = true; + + @Expiration + private Integer ttl; + + public RequestMessageLdap() { + } + + public RequestMessageLdap(String dn) { + super(dn); + } + + public RequestMessageLdap(String dn, String id, String requestId, Date creationDate, String sessionId, String userInum) { + super(dn); + this.id = id; + this.requestId = requestId; + this.creationDate = creationDate; + this.sessionId = sessionId; + this.userInum = userInum; + + final int expiration = 90; + Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + calendar.setTime(creationDate); + calendar.add(Calendar.SECOND, expiration); + this.expirationDate = calendar.getTime(); + this.ttl = expiration; + } + + public Integer getTtl() { + return ttl; + } + + public void setTtl(Integer ttl) { + this.ttl = ttl; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getRequestId() { + return requestId; + } + + public void setRequestId(String requestId) { + this.requestId = requestId; + } + + public Date getCreationDate() { + return creationDate; + } + + public void setCreationDate(Date creationDate) { + this.creationDate = creationDate; + } + + public String getSessionId() { + return sessionId; + } + + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + public String getUserInum() { + return userInum; + } + + public void setUserInum(String userInum) { + this.userInum = userInum; + } + + public Date getExpirationDate() { + return expirationDate; + } + + public void setExpirationDate(Date expirationDate) { + this.expirationDate = expirationDate; + } + + public boolean isDeletable() { + return deletable; + } + + public void setDeletable(boolean deletable) { + this.deletable = deletable; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/ldap/CIBARequest.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/ldap/CIBARequest.java new file mode 100644 index 00000000..5570d49d --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/ldap/CIBARequest.java @@ -0,0 +1,118 @@ +package org.gluu.oxauth.model.ldap; + +import org.gluu.persist.annotation.AttributeName; +import org.gluu.persist.annotation.DN; +import org.gluu.persist.annotation.DataEntry; +import org.gluu.persist.annotation.ObjectClass; + +import java.io.Serializable; +import java.util.Date; + +/** + * Object class used to save information of every CIBA request. + * + * @author Milton BO + * @version May 27, 2020 + */ +@DataEntry +@ObjectClass(value = "cibaRequest") +public class CIBARequest implements Serializable { + + @DN + private String dn; + + @AttributeName(name = "authReqId") + private String authReqId; + + @AttributeName(name = "clnId", consistency = true) + private String clientId; + + @AttributeName(name = "usrId", consistency = true) + private String userId; + + @AttributeName(name = "creationDate") + private Date creationDate; + + @AttributeName(name = "exp") + private Date expirationDate; + + @AttributeName(name = "oxStatus") + private String status; + + + public String getDn() { + return dn; + } + + public void setDn(String dn) { + this.dn = dn; + } + + public String getAuthReqId() { + return authReqId; + } + + public void setAuthReqId(String authReqId) { + this.authReqId = authReqId; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public Date getCreationDate() { + return creationDate; + } + + public void setCreationDate(Date creationDate) { + this.creationDate = creationDate; + } + + public Date getExpirationDate() { + return expirationDate; + } + + public void setExpirationDate(Date expirationDate) { + this.expirationDate = expirationDate; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CIBARequest that = (CIBARequest) o; + + if (!dn.equals(that.dn)) return false; + if (!authReqId.equals(that.authReqId)) return false; + + return true; + } + + @Override + public int hashCode() { + int result = dn.hashCode(); + result = 31 * result + authReqId.hashCode(); + return result; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/ldap/ClientAuthorization.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/ldap/ClientAuthorization.java new file mode 100644 index 00000000..91740d3f --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/ldap/ClientAuthorization.java @@ -0,0 +1,123 @@ +package org.gluu.oxauth.model.ldap; + +import org.gluu.persist.annotation.*; + +import java.io.Serializable; +import java.util.Date; + +/** + * @author Javier Rojas Blum + * @version October 16, 2015 + */ +@DataEntry +@ObjectClass(value = "oxClientAuthorization") +public class ClientAuthorization implements Serializable { + + @DN + private String dn; + + @AttributeName(name = "oxId") + private String id; + + @AttributeName(name = "oxAuthClientId", consistency = true) + private String clientId; + + @AttributeName(name = "oxAuthUserId", consistency = true) + private String userId; + + @AttributeName(name = "oxAuthScope") + private String[] scopes; + + @AttributeName(name = "exp") + private Date expirationDate; + + @AttributeName(name = "del") + private boolean deletable = true; + + @Expiration + private Integer ttl; + + public Integer getTtl() { + return ttl; + } + + public void setTtl(Integer ttl) { + this.ttl = ttl; + } + + public String getDn() { + return dn; + } + + public void setDn(String dn) { + this.dn = dn; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String[] getScopes() { + return scopes; + } + + public void setScopes(String[] scopes) { + this.scopes = scopes; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public Date getExpirationDate() { + return expirationDate; + } + + public void setExpirationDate(Date expirationDate) { + this.expirationDate = expirationDate; + } + + public boolean isDeletable() { + return deletable; + } + + public void setDeletable(boolean deletable) { + this.deletable = deletable; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ClientAuthorization that = (ClientAuthorization) o; + + if (!dn.equals(that.dn)) return false; + if (!id.equals(that.id)) return false; + + return true; + } + + @Override + public int hashCode() { + int result = dn.hashCode(); + result = 31 * result + id.hashCode(); + return result; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/ldap/SchemaEntry.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/ldap/SchemaEntry.java new file mode 100644 index 00000000..de29f731 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/ldap/SchemaEntry.java @@ -0,0 +1,99 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.ldap; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import org.gluu.persist.model.base.BaseEntry; +import org.gluu.persist.annotation.AttributeName; + +/** + * Schema attribute + * + * @author Yuriy Movchan Date: 10.14.2010 + */ +@org.gluu.persist.annotation.SchemaEntry +public final class SchemaEntry extends BaseEntry implements Serializable { + + private static final long serialVersionUID = 3819004894646725606L; + + @AttributeName + private List attributeTypes = new ArrayList(); + + @AttributeName + private List objectClasses = new ArrayList(); + + public final List getAttributeTypes() { + return attributeTypes; + } + + public final void setAttributeTypes(List attributeTypes) { + this.attributeTypes = attributeTypes; + } + + public final void addAttributeType(String attributeType) { + this.attributeTypes.add(attributeType); + } + + public final List getObjectClasses() { + return objectClasses; + } + + public final void setObjectClasses(List objectClasses) { + this.objectClasses = objectClasses; + } + + public final void addObjectClass(String objectClass) { + this.objectClasses.add(objectClass); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((attributeTypes == null) ? 0 : attributeTypes.hashCode()); + result = prime * result + ((objectClasses == null) ? 0 : objectClasses.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + SchemaEntry other = (SchemaEntry) obj; + if (attributeTypes == null) { + if (other.attributeTypes != null) { + return false; + } + } else if (!attributeTypes.equals(other.attributeTypes)) { + return false; + } + if (objectClasses == null) { + if (other.objectClasses != null) { + return false; + } + } else if (!objectClasses.equals(other.objectClasses)) { + return false; + } + return true; + } + + @Override + public String toString() { + return String.format("SchemaAttribute [dn=%s, attributeTypes=%s, objectClasses=%s]", getDn(), attributeTypes, objectClasses); + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/ldap/TokenAttributes.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/ldap/TokenAttributes.java new file mode 100644 index 00000000..7d95cf46 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/ldap/TokenAttributes.java @@ -0,0 +1,47 @@ +package org.gluu.oxauth.model.ldap; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * @author Yuriy Zabrovarnyy + */ +@JsonIgnoreProperties( + ignoreUnknown = true +) +public class TokenAttributes implements Serializable { + + @JsonProperty("x5cs256") + private String x5cs256; + @JsonProperty("attributes") + private Map attributes; + + public String getX5cs256() { + return x5cs256; + } + + public void setX5cs256(String x5cs256) { + this.x5cs256 = x5cs256; + } + + public Map getAttributes() { + if (attributes == null) attributes = new HashMap<>(); + return attributes; + } + + public void setAttributes(Map attributes) { + this.attributes = attributes; + } + + @Override + public String toString() { + return "TokenAttributes{" + + "attributes='" + attributes + '\'' + + "x5cs256='" + x5cs256 + '\'' + + '}'; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/ldap/TokenLdap.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/ldap/TokenLdap.java new file mode 100644 index 00000000..e88e4f74 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/ldap/TokenLdap.java @@ -0,0 +1,314 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.ldap; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.persist.annotation.*; + +import java.io.Serializable; +import java.util.Date; + +/** + * @author Yuriy Zabrovarnyy + * @author Javier Rojas Blum + * @version September 6, 2017 + */ + +@DataEntry +@ObjectClass(value = "token") +public class TokenLdap implements Serializable { + + @DN + private String dn; + @AttributeName(name = "grtId", consistency = true) + private String grantId; + @AttributeName(name = "usrId") + private String userId; + @AttributeName(name = "clnId") + private String clientId; + @AttributeName(name = "iat") + private Date creationDate; + @AttributeName(name = "exp") + private Date expirationDate; + @AttributeName(name = "del") + private boolean deletable = true; + @AttributeName(name = "authnTime") + private Date authenticationTime; + @AttributeName(name = "scp") + private String scope; + @AttributeName(name = "tknCde", consistency = true) + private String tokenCode; + @AttributeName(name = "tknTyp") + private String tokenType; + @AttributeName(name = "grtTyp") + private String grantType; + @AttributeName(name = "jwtReq") + private String jwtRequest; + @AttributeName(name = "authzCode", consistency = true) + private String authorizationCode; + @AttributeName(name = "nnc") + private String nonce; + @AttributeName(name = "chlng") + private String codeChallenge; + @AttributeName(name = "chlngMth") + private String codeChallengeMethod; + @AttributeName(name = "clms") + private String claims; + @AttributeName(name = "tknBndCnf") + private String tokenBindingHash; + + @AttributeName(name = "acr") + private String authMode; + + @AttributeName(name = "ssnId", consistency = true) + private String sessionDn; + @Expiration + private Integer ttl; + + @AttributeName(name = "attr") + @JsonObject + private TokenAttributes attributes; + + private boolean isFromCache; + + public TokenLdap() { + } + + public TokenAttributes getAttributes() { + if (attributes == null) { + attributes = new TokenAttributes(); + } + return attributes; + } + + public Integer getTtl() { + return ttl; + } + + public void setTtl(Integer ttl) { + this.ttl = ttl; + } + + public final void setAttributes(TokenAttributes attributes) { + this.attributes = attributes; + } + + public boolean isDeletable() { + return deletable; + } + + public void setDeletable(boolean deletable) { + this.deletable = deletable; + } + + public String getAuthorizationCode() { + return authorizationCode; + } + + public void setAuthorizationCode(String p_authorizationCode) { + authorizationCode = p_authorizationCode; + } + + public String getTokenBindingHash() { + return tokenBindingHash; + } + + public void setTokenBindingHash(String tokenBindingHash) { + this.tokenBindingHash = tokenBindingHash; + } + + public String getNonce() { + return nonce; + } + + public void setNonce(String nonce) { + this.nonce = nonce; + } + + public String getGrantId() { + return grantId; + } + + public void setGrantId(String p_grantId) { + grantId = p_grantId; + } + + public Date getAuthenticationTime() { + return authenticationTime; + } + + public void setAuthenticationTime(Date p_authenticationTime) { + authenticationTime = p_authenticationTime; + } + + public Date getCreationDate() { + return creationDate; + } + + public void setCreationDate(Date p_creationDate) { + creationDate = p_creationDate; + } + + public String getDn() { + return dn; + } + + public void setDn(String p_dn) { + dn = p_dn; + } + + public Date getExpirationDate() { + return expirationDate; + } + + public void setExpirationDate(Date p_expirationDate) { + expirationDate = p_expirationDate; + } + + public String getGrantType() { + return grantType; + } + + public void setGrantType(String p_grantType) { + grantType = p_grantType; + } + + public String getScope() { + return scope; + } + + public void setScope(String p_scope) { + scope = p_scope; + } + + public String getTokenCode() { + return tokenCode; + } + + public void setTokenCode(String p_tokenCode) { + tokenCode = p_tokenCode; + } + + public String getTokenType() { + return tokenType; + } + + public void setTokenType(String p_tokenType) { + tokenType = p_tokenType; + } + + public TokenType getTokenTypeEnum() { + return TokenType.fromValue(tokenType); + } + + public void setTokenTypeEnum(TokenType p_tokenType) { + if (p_tokenType != null) { + tokenType = p_tokenType.getValue(); + } + } + + public String getUserId() { + return userId; + } + + public void setUserId(String p_userId) { + userId = p_userId; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getJwtRequest() { + return jwtRequest; + } + + public void setJwtRequest(String p_jwtRequest) { + jwtRequest = p_jwtRequest; + } + + public String getAuthMode() { + return authMode; + } + + public void setAuthMode(String authMode) { + this.authMode = authMode; + } + + public String getCodeChallenge() { + return codeChallenge; + } + + public void setCodeChallenge(String codeChallenge) { + this.codeChallenge = codeChallenge; + } + + public String getCodeChallengeMethod() { + return codeChallengeMethod; + } + + public void setCodeChallengeMethod(String codeChallengeMethod) { + this.codeChallengeMethod = codeChallengeMethod; + } + + public String getClaims() { + return claims; + } + + public void setClaims(String claims) { + this.claims = claims; + } + + public String getSessionDn() { + return sessionDn; + } + + public void setSessionDn(String sessionDn) { + this.sessionDn = sessionDn; + } + + public boolean isFromCache() { + return isFromCache; + } + + public void setIsFromCache(boolean isFromCache) { + this.isFromCache = isFromCache; + } + + public final void setFromCache(boolean isFromCache) { + this.isFromCache = isFromCache; + } + + public boolean isImplicitFlow() { + return StringUtils.isBlank(grantType) || grantType.equals(GrantType.IMPLICIT.getValue()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + TokenLdap tokenLdap = (TokenLdap) o; + + if (tokenCode != null ? !tokenCode.equals(tokenLdap.tokenCode) : tokenLdap.tokenCode != null) return false; + if (tokenType != null ? !tokenType.equals(tokenLdap.tokenType) : tokenLdap.tokenType != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = tokenCode != null ? tokenCode.hashCode() : 0; + result = 31 * result + (tokenType != null ? tokenType.hashCode() : 0); + return result; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/ldap/TokenType.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/ldap/TokenType.java new file mode 100644 index 00000000..7f78f255 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/ldap/TokenType.java @@ -0,0 +1,42 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.ldap; + +import org.apache.commons.lang.StringUtils; + +/** +* @author Yuriy Zabrovarnyy +* @version 0.9, 08/01/2013 +*/ +public enum TokenType { + ID_TOKEN("id_token"), + ACCESS_TOKEN("access_token"), + LONG_LIVED_ACCESS_TOKEN("access_token"), + REFRESH_TOKEN("refresh_token"), + AUTHORIZATION_CODE("authorization_code"); + + private final String value; + + TokenType(String name) { + value = name; + } + + public String getValue() { + return value; + } + + public static TokenType fromValue(String value) { + if (StringUtils.isNotBlank(value)) { + for (TokenType t : values()) { + if (t.getValue().endsWith(value)) { + return t; + } + } + } + return null; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/ldap/UserGroup.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/ldap/UserGroup.java new file mode 100644 index 00000000..d76510f7 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/ldap/UserGroup.java @@ -0,0 +1,100 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.ldap; + +import org.gluu.persist.annotation.AttributeName; +import org.gluu.persist.annotation.DN; +import org.gluu.persist.annotation.DataEntry; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 27/07/2012 + */ + +@DataEntry +public class UserGroup { + @DN + private String dn; + @AttributeName(name = "displayName") + private String displayName; + @AttributeName(name = "member") + private String[] member; + @AttributeName(name = "gluuGroupType") + private String groupType; + @AttributeName(name = "gluuStatus") + private String status; + @AttributeName(name = "iname") + private String iname; + @AttributeName(name = "inum") + private String inum; + @AttributeName(name = "owner") + private String owner; + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String p_displayName) { + displayName = p_displayName; + } + + public String getDn() { + return dn; + } + + public void setDn(String p_dn) { + dn = p_dn; + } + + public String getGroupType() { + return groupType; + } + + public void setGroupType(String p_groupType) { + groupType = p_groupType; + } + + public String getIname() { + return iname; + } + + public void setIname(String p_iname) { + iname = p_iname; + } + + public String getInum() { + return inum; + } + + public void setInum(String p_inum) { + inum = p_inum; + } + + public String[] getMember() { + return member; + } + + public void setMember(String[] p_member) { + member = p_member; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String p_owner) { + owner = p_owner; + } + + public String getStatus() { + return status; + } + + public void setStatus(String p_status) { + status = p_status; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/registration/RegisterParamsValidator.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/registration/RegisterParamsValidator.java new file mode 100644 index 00000000..428b7e89 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/registration/RegisterParamsValidator.java @@ -0,0 +1,451 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.registration; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.common.SubjectType; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.register.ApplicationType; +import org.gluu.oxauth.model.register.RegisterErrorResponseType; +import org.gluu.oxauth.model.util.Pair; +import org.gluu.oxauth.model.util.URLPatternList; +import org.gluu.oxauth.model.util.Util; +import org.gluu.oxauth.util.ServerUtil; +import org.json.JSONArray; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.apache.commons.lang.BooleanUtils.isTrue; + +/** + * Validates the parameters received for the register web service. + * + * @author Javier Rojas Blum + * @version October 22, 2019 + */ +@ApplicationScoped +public class RegisterParamsValidator { + + @Inject + private Logger log; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + private static final String HTTP = "http"; + private static final String HTTPS = "https"; + private static final String LOCALHOST = "localhost"; + private static final String LOOPBACK = "127.0.0.1"; + + /** + * Validates the parameters for a register request. + * + * @param applicationType The Application Type: native or web. + * @param subjectType The subject_type requested for responses to this Client. + * @param grantTypes Grant Types that the Client is declaring that it will restrict itself to using. + * @param redirectUris Space-separated list of redirect URIs. + * @return Whether the parameters of client register is valid or not. + */ + public Pair validateParamsClientRegister( + ApplicationType applicationType, SubjectType subjectType, + List grantTypes, List responseTypes, + List redirectUris) { + if (applicationType == null) { + return new Pair<>(false, "application_type is not valid."); + } + + if (grantTypes != null && + (grantTypes.contains(GrantType.AUTHORIZATION_CODE) || grantTypes.contains(GrantType.IMPLICIT) + || (responseTypes.contains(ResponseType.CODE) && !grantTypes.contains(GrantType.DEVICE_CODE)) + || responseTypes.contains(ResponseType.TOKEN) || responseTypes.contains(ResponseType.ID_TOKEN))) { + if (redirectUris == null || redirectUris.isEmpty()) { + return new Pair<>(false, "Redirect uris are empty."); + } + } + + if (subjectType == null || !appConfiguration.getSubjectTypesSupported().contains(subjectType.toString())) { + log.debug("Parameter subject_type is not valid."); + return new Pair<>(false, "Parameter subject_type is not valid."); + } + + return new Pair<>(true, ""); + } + + /** + * Validates all algorithms received for a register client request. It throws a WebApplicationException + * whether a validation doesn't pass. + * + * @param registerRequest Object containing all parameters received to register a client. + */ + public void validateAlgorithms( RegisterRequest registerRequest ) { + if ( registerRequest.getIdTokenSignedResponseAlg() != null + && registerRequest.getIdTokenSignedResponseAlg() != SignatureAlgorithm.NONE && + ! appConfiguration.getIdTokenSigningAlgValuesSupported().contains( + registerRequest.getIdTokenSignedResponseAlg().toString()) ) { + log.debug("Parameter id_token_signed_response_alg is not valid."); + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, + RegisterErrorResponseType.INVALID_CLIENT_METADATA, "Parameter id_token_signed_response_alg is not valid."); + } + + if ( registerRequest.getIdTokenEncryptedResponseAlg() != null && + ! appConfiguration.getIdTokenEncryptionAlgValuesSupported().contains( + registerRequest.getIdTokenEncryptedResponseAlg().toString()) ) { + log.debug("Parameter id_token_encrypted_response_alg is not valid."); + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, + RegisterErrorResponseType.INVALID_CLIENT_METADATA, "Parameter id_token_encrypted_response_alg is not valid."); + } + + if ( registerRequest.getIdTokenEncryptedResponseEnc() != null && + ! appConfiguration.getIdTokenEncryptionEncValuesSupported().contains( + registerRequest.getIdTokenEncryptedResponseEnc().toString()) ) { + log.debug("Parameter id_token_encrypted_response_enc is not valid."); + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, + RegisterErrorResponseType.INVALID_CLIENT_METADATA, "Parameter id_token_encrypted_response_enc is not valid."); + } + + if ( registerRequest.getUserInfoSignedResponseAlg() != null && + ! appConfiguration.getUserInfoSigningAlgValuesSupported().contains( + registerRequest.getUserInfoSignedResponseAlg().toString()) ) { + log.debug("Parameter userinfo_signed_response_alg is not valid."); + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, + RegisterErrorResponseType.INVALID_CLIENT_METADATA, "Parameter userinfo_signed_response_alg is not valid."); + } + + if ( registerRequest.getUserInfoEncryptedResponseAlg() != null && + ! appConfiguration.getUserInfoEncryptionAlgValuesSupported().contains( + registerRequest.getUserInfoEncryptedResponseAlg().toString()) ) { + log.debug("Parameter userinfo_encrypted_response_alg is not valid."); + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, + RegisterErrorResponseType.INVALID_CLIENT_METADATA, "Parameter userinfo_encrypted_response_alg is not valid."); + } + + if ( registerRequest.getUserInfoEncryptedResponseEnc() != null && + ! appConfiguration.getUserInfoEncryptionEncValuesSupported().contains( + registerRequest.getUserInfoEncryptedResponseEnc().toString()) ) { + log.debug("Parameter userinfo_encrypted_response_enc is not valid."); + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, + RegisterErrorResponseType.INVALID_CLIENT_METADATA, "Parameter userinfo_encrypted_response_enc is not valid."); + } + + if ( registerRequest.getRequestObjectSigningAlg() != null && + ! appConfiguration.getRequestObjectSigningAlgValuesSupported().contains( + registerRequest.getRequestObjectSigningAlg().toString()) ) { + log.debug("Parameter request_object_signing_alg is not valid."); + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, + RegisterErrorResponseType.INVALID_CLIENT_METADATA, "Parameter request_object_signing_alg is not valid."); + } + + if ( registerRequest.getRequestObjectEncryptionAlg() != null && + ! appConfiguration.getRequestObjectEncryptionAlgValuesSupported().contains( + registerRequest.getRequestObjectEncryptionAlg().toString()) ) { + log.debug("Parameter request_object_encryption_alg is not valid."); + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, + RegisterErrorResponseType.INVALID_CLIENT_METADATA, "Parameter request_object_encryption_alg is not valid."); + } + + if ( registerRequest.getRequestObjectEncryptionEnc() != null && + ! appConfiguration.getRequestObjectEncryptionEncValuesSupported().contains( + registerRequest.getRequestObjectEncryptionEnc().toString()) ) { + log.debug("Parameter request_object_encryption_enc is not valid."); + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, + RegisterErrorResponseType.INVALID_CLIENT_METADATA, "Parameter request_object_encryption_enc is not valid."); + } + + if ( registerRequest.getTokenEndpointAuthMethod() != null && + ! appConfiguration.getTokenEndpointAuthMethodsSupported().contains( + registerRequest.getTokenEndpointAuthMethod().toString()) ) { + log.debug("Parameter token_endpoint_auth_method is not valid."); + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, + RegisterErrorResponseType.INVALID_CLIENT_METADATA, "Parameter token_endpoint_auth_method is not valid."); + } + + if ( registerRequest.getTokenEndpointAuthSigningAlg() != null && + ! appConfiguration.getTokenEndpointAuthSigningAlgValuesSupported().contains( + registerRequest.getTokenEndpointAuthSigningAlg().toString()) ) { + log.debug("Parameter token_endpoint_auth_signing_alg is not valid."); + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, + RegisterErrorResponseType.INVALID_CLIENT_METADATA, "Parameter token_endpoint_auth_signing_alg is not valid."); + } + } + + /** + * Validates the parameters for a client read request. + * + * @param clientId Unique Client identifier. + * @param accessToken Access Token obtained out of band to authorize the registrant. + * @return Whether the parameters of client read is valid or not. + */ + public boolean validateParamsClientRead(String clientId, String accessToken) { + return StringUtils.isNotBlank(clientId) && StringUtils.isNotBlank(accessToken); + } + + /** + * @param grantTypes Grant Types that the Client is declaring that it will restrict itself to using. + * @param applicationType The Application Type: native or web. + * @param subjectType Subject Type requested for responses to this Client. + * @param redirectUris Redirection URI values used by the Client. + * @param sectorIdentifierUrl A HTTPS scheme URL to be used in calculating Pseudonymous Identifiers by the OP. + * The URL contains a file with a single JSON array of redirect_uri values. + * @return Whether the Redirect URI parameters are valid or not. + */ + public boolean validateRedirectUris(List grantTypes, List responseTypes, + ApplicationType applicationType, SubjectType subjectType, + List redirectUris, String sectorIdentifierUrl) { + boolean valid = true; + Set redirectUriHosts = new HashSet(); + + if (redirectUris != null && !redirectUris.isEmpty()) { + for (String redirectUri : redirectUris) { + if (redirectUri == null || redirectUri.contains("#")) { + valid = false; + } else { + URI uri = null; + try { + uri = new URI(redirectUri); + } catch (URISyntaxException e) { + log.debug("Failed to parse redirect_uri: {}, error: {}", redirectUri, e.getMessage()); + valid = false; + continue; + } + redirectUriHosts.add(uri.getHost()); + switch (applicationType) { + case WEB: + if (HTTP.equalsIgnoreCase(uri.getScheme())) { + if (!LOCALHOST.equalsIgnoreCase(uri.getHost()) && !LOOPBACK.equalsIgnoreCase(uri.getHost())) { + log.debug("Invalid protocol for redirect_uri: " + + redirectUri + + " (only https protocol is allowed for application_type=web or localhost/127.0.0.1 for http)"); + valid = false; + } + } + break; + case NATIVE: + // to conform "OAuth 2.0 for Native Apps" https://tools.ietf.org/html/draft-wdenniss-oauth-native-apps-00 + // we allow registration with custom schema for native apps. +// if (!HTTP.equalsIgnoreCase(uri.getScheme())) { +// valid = false; +// } else if (!LOCALHOST.equalsIgnoreCase(uri.getHost())) { +// valid = false; +// } + break; + } + } + } + } else if (!grantTypes.contains(GrantType.AUTHORIZATION_CODE) && !grantTypes.contains(GrantType.IMPLICIT) && + (!responseTypes.contains(ResponseType.CODE) || grantTypes.contains(GrantType.DEVICE_CODE)) + && !responseTypes.contains(ResponseType.TOKEN) && !responseTypes.contains(ResponseType.ID_TOKEN)) { + // It is valid for grant types: password, client_credentials, urn:ietf:params:oauth:grant-type:uma-ticket and urn:openid:params:grant-type:ciba + valid = true; + } else { + valid = false; + } + + + /* + * Providers that use pairwise sub (subject) values SHOULD utilize the sector_identifier_uri value + * provided in the Subject Identifier calculation for pairwise identifiers. + * + * If the Client has not provided a value for sector_identifier_uri in Dynamic Client Registration, + * the Sector Identifier used for pairwise identifier calculation is the host component of the + * registered redirect_uri. + * + * If there are multiple hostnames in the registered redirect_uris, the Client MUST register a + * sector_identifier_uri. + */ + if (subjectType != null && subjectType.equals(SubjectType.PAIRWISE) && StringUtils.isBlank(sectorIdentifierUrl)) { + if (redirectUriHosts.size() > 1) { + valid = false; + } + } + + // Validate Sector Identifier URL + boolean noRedirectUriInSectorIdentifierUri = false; + if (valid && StringUtils.isNotBlank(sectorIdentifierUrl)) { + try { + URI uri = new URI(sectorIdentifierUrl); + if (!HTTPS.equalsIgnoreCase(uri.getScheme())) { + valid = false; + } + + javax.ws.rs.client.Client clientRequest = ClientBuilder.newClient(); + String entity = null; + try { + Response clientResponse = clientRequest.target(sectorIdentifierUrl).request().buildGet().invoke(); + int status = clientResponse.getStatus(); + + if (status == 200) { + entity = clientResponse.readEntity(String.class); + + JSONArray sectorIdentifierJsonArray = new JSONArray(entity); + valid = Util.asList(sectorIdentifierJsonArray).containsAll(redirectUris); + } + } finally { + clientRequest.close(); + } + } catch (Exception e) { + log.debug(e.getMessage(), e); + valid = false; + } finally { + if (!valid) { + noRedirectUriInSectorIdentifierUri = true; + } + } + } + + // Validate Redirect Uris checking the white list and black list + if (valid || isTrue(appConfiguration.getAllowWildcardRedirectUri())) { + valid = checkWhiteListRedirectUris(redirectUris) && checkBlackListRedirectUris(redirectUris); + } + + if (noRedirectUriInSectorIdentifierUri) { + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, RegisterErrorResponseType.INVALID_CLIENT_METADATA, "Failed to validate redirect uris. No redirect_uri in sector_identifier_uri content."); + } + + return valid; + } + + public boolean validateInitiateLoginUri(String initiateLoginUri) { + boolean valid = false; + + try { + URI uri = new URI(initiateLoginUri); + if (HTTPS.equalsIgnoreCase(uri.getScheme())) { + valid = true; + } + } catch (URISyntaxException e) { + log.debug(e.getMessage(), e); + valid = false; + } + + return valid; + } + + /** + * All the Redirect Uris must match to return true. + */ + private boolean checkWhiteListRedirectUris(List redirectUris) { + boolean valid = true; + List whiteList = appConfiguration.getClientWhiteList(); + boolean wildcardSupported = isTrue(appConfiguration.getAllowWildcardRedirectUri()); + URLPatternList urlPatternList = new URLPatternList(whiteList, wildcardSupported); + + for (String redirectUri : redirectUris) { + valid &= urlPatternList.isUrlListed(redirectUri); + } + + return valid; + } + + /** + * None of the Redirect Uris must match to return true. + */ + private boolean checkBlackListRedirectUris(List redirectUris) { + boolean valid = true; + List blackList = appConfiguration.getClientBlackList(); + boolean wildcardSupported = isTrue(appConfiguration.getAllowWildcardRedirectUri()); + URLPatternList urlPatternList = new URLPatternList(blackList, wildcardSupported); + + for (String redirectUri : redirectUris) { + valid &= !urlPatternList.isUrlListed(redirectUri); + } + + return valid; + } + + public void validateLogoutUri(List logoutUris, List redirectUris, ErrorResponseFactory errorResponseFactory) { + if (logoutUris == null || logoutUris.isEmpty()) { // logout uri is optional so null or empty list is valid + return; + } + for (String logoutUri : logoutUris) { + validateLogoutUri(logoutUri, redirectUris, errorResponseFactory); + } + } + + public void validateLogoutUri(String logoutUri, List redirectUris, ErrorResponseFactory errorResponseFactory) { + if (Util.isNullOrEmpty(logoutUri)) { // logout uri is optional so null or empty string is valid + return; + } + + // preconditions + if (redirectUris == null || redirectUris.isEmpty()) { + log.debug("Preconditions of logout uri validation are failed."); + throwInvalidLogoutUri(errorResponseFactory); + return; + } + + try { + Set redirectUriHosts = collectUriHosts(redirectUris); + + URI uri = new URI(logoutUri); + + if (!redirectUriHosts.contains(uri.getHost())) { + log.debug("logout uri host is not within redirect_uris, logout_uri: {}, redirect_uris: {}", logoutUri, redirectUris); + throwInvalidLogoutUri(errorResponseFactory); + return; + } + + if (!HTTPS.equalsIgnoreCase(uri.getScheme())) { + log.debug("logout uri schema is not https, logout_uri: {}", logoutUri); + throwInvalidLogoutUri(errorResponseFactory); + } + } catch (Exception e) { + log.debug(e.getMessage(), e); + throwInvalidLogoutUri(errorResponseFactory); + } + } + + private void throwInvalidLogoutUri(ErrorResponseFactory errorResponseFactory) throws WebApplicationException { + throw new WebApplicationException( + Response.status(Response.Status.BAD_REQUEST.getStatusCode()). + type(MediaType.APPLICATION_JSON_TYPE). + entity(errorResponseFactory.errorAsJson(RegisterErrorResponseType.INVALID_LOGOUT_URI, "Failed to valide logout uri.")). + cacheControl(ServerUtil.cacheControl(true, false)). + header("Pragma", "no-cache"). + build()); + } + + private static Set collectUriHosts(List uriList) throws URISyntaxException { + Set hosts = new HashSet(); + + for (String redirectUri : uriList) { + URI uri = new URI(redirectUri); + hosts.add(uri.getHost()); + } + return hosts; + } + + /** + * Check if exists a Password Grant Type in the list of Grant Types. + * @param grantTypes List of Grant Types. + * @return True if Password Grant Type exists in the list, otherwise false + */ + public boolean checkIfThereIsPasswordGrantType(List grantTypes) { + if (grantTypes != null) + return grantTypes.stream().anyMatch(grantType -> grantType == GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + return false; + } + +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/session/SessionClient.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/session/SessionClient.java new file mode 100644 index 00000000..7ffbecbc --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/session/SessionClient.java @@ -0,0 +1,47 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.session; + +import java.util.GregorianCalendar; +import java.util.TimeZone; + +import javax.inject.Named; + +import org.gluu.oxauth.model.registration.Client; + +/** + * @author Javier Rojas Blum Date: 03.20.2012 + */ +@Named +public class SessionClient { + + private Client client; + private Long authenticationTime; + + + public Client getClient() { + return client; + } + + public void setClient(Client client) { + this.client = client; + long authTime = -1L; + if (client != null) { + GregorianCalendar c = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + authTime = c.getTimeInMillis(); + } + setAuthenticationTime(authTime); + } + + public Long getAuthenticationTime() { + return authenticationTime; + } + + public void setAuthenticationTime(Long authenticationTime) { + this.authenticationTime = authenticationTime; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/ClientAssertion.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/ClientAssertion.java new file mode 100644 index 00000000..23593657 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/ClientAssertion.java @@ -0,0 +1,146 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.token; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.util.ServerUtil; +import org.json.JSONObject; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.crypto.AbstractCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.AlgorithmFamily; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.exception.InvalidJwtException; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.jwt.JwtType; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.util.JwtUtil; +import org.gluu.oxauth.service.ClientService; +import org.gluu.service.cdi.util.CdiUtil; +import org.gluu.util.security.StringEncrypter; + +import java.util.Date; +import java.util.List; + +/** + * @author Javier Rojas Blum + * @version February 12, 2019 + */ +public class ClientAssertion { + + private Jwt jwt; + private String clientSecret; + + public ClientAssertion(AppConfiguration appConfiguration, AbstractCryptoProvider cryptoProvider, String clientId, ClientAssertionType clientAssertionType, String encodedAssertion) + throws InvalidJwtException { + try { + if (!load(appConfiguration, cryptoProvider, clientId, clientAssertionType, encodedAssertion)) { + throw new InvalidJwtException("Cannot load the JWT"); + } + } catch (StringEncrypter.EncryptionException e) { + throw new InvalidJwtException(e.getMessage(), e); + } catch (Exception e) { + throw new InvalidJwtException("Cannot verify the JWT", e); + } + } + + public String getSubjectIdentifier() { + return jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER); + } + + public String getClientSecret() { + return clientSecret; + } + + private boolean load(AppConfiguration appConfiguration, AbstractCryptoProvider cryptoProvider, String clientId, ClientAssertionType clientAssertionType, String encodedAssertion) + throws Exception { + boolean result; + + if (clientAssertionType == ClientAssertionType.JWT_BEARER) { + if (StringUtils.isNotBlank(encodedAssertion)) { + jwt = Jwt.parse(encodedAssertion); + + // TODO: Store jti this value to check for duplicates + + // Validate clientId + String issuer = jwt.getClaims().getClaimAsString(JwtClaimName.ISSUER); + String subject = jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER); + List audience = jwt.getClaims().getClaimAsStringList(JwtClaimName.AUDIENCE); + Date expirationTime = jwt.getClaims().getClaimAsDate(JwtClaimName.EXPIRATION_TIME); + //SignatureAlgorithm algorithm = SignatureAlgorithm.fromName(jwt.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM)); + if ((clientId == null && StringUtils.isNotBlank(issuer) && StringUtils.isNotBlank(subject) && issuer.equals(subject)) + || (StringUtils.isNotBlank(clientId) && StringUtils.isNotBlank(issuer) + && StringUtils.isNotBlank(subject) && clientId.equals(issuer) && issuer.equals(subject))) { + + // Validate audience + String tokenUrl = appConfiguration.getTokenEndpoint(); + String cibaAuthUrl = appConfiguration.getBackchannelAuthenticationEndpoint(); + if (audience != null && (audience.contains(appConfiguration.getIssuer()) || audience.contains(tokenUrl) || audience.contains(cibaAuthUrl))) { + + // Validate expiration + if (expirationTime.after(new Date())) { + ClientService clientService = CdiUtil.bean(ClientService.class); + Client client = clientService.getClient(subject); + + // Validate client + if (client != null) { + JwtType jwtType = JwtType.fromString(jwt.getHeader().getClaimAsString(JwtHeaderName.TYPE)); + AuthenticationMethod authenticationMethod = client.getAuthenticationMethod(); + SignatureAlgorithm signatureAlgorithm = jwt.getHeader().getSignatureAlgorithm(); + + if (jwtType == null && signatureAlgorithm != null) { + jwtType = signatureAlgorithm.getJwtType(); + } + + if (jwtType != null && signatureAlgorithm != null && signatureAlgorithm.getFamily() != null && + ((authenticationMethod == AuthenticationMethod.CLIENT_SECRET_JWT && AlgorithmFamily.HMAC.equals(signatureAlgorithm.getFamily())) + || (authenticationMethod == AuthenticationMethod.PRIVATE_KEY_JWT && (AlgorithmFamily.RSA.equals(signatureAlgorithm.getFamily()) || AlgorithmFamily.EC.equals(signatureAlgorithm.getFamily()))))) { + if (client.getTokenEndpointAuthSigningAlg() == null || SignatureAlgorithm.fromString(client.getTokenEndpointAuthSigningAlg()).equals(signatureAlgorithm)) { + clientSecret = clientService.decryptSecret(client.getClientSecret()); + + // Validate the crypto segment + String keyId = jwt.getHeader().getKeyId(); + JSONObject jwks = ServerUtil.getJwks(client); + String sharedSecret = clientService.decryptSecret(client.getClientSecret()); + boolean validSignature = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), + keyId, jwks, sharedSecret, signatureAlgorithm); + + if (validSignature) { + result = true; + } else { + throw new InvalidJwtException("Invalid cryptographic segment"); + } + } else { + throw new InvalidJwtException("Invalid signing algorithm"); + } + } else { + throw new InvalidJwtException("Invalid authentication method"); + } + } else { + throw new InvalidJwtException("Invalid client"); + } + } else { + throw new InvalidJwtException("JWT has expired"); + } + } else { + throw new InvalidJwtException("Invalid audience: " + audience); + } + } else { + throw new InvalidJwtException("Invalid clientId"); + } + } else { + throw new InvalidJwtException("The Client Assertion is null or empty"); + } + } else { + throw new InvalidJwtException("Invalid Client Assertion Type"); + } + + return result; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/HandleTokenFactory.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/HandleTokenFactory.java new file mode 100644 index 00000000..abd83182 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/HandleTokenFactory.java @@ -0,0 +1,44 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.token; + +import java.util.UUID; + +/** + * Handle (or artifact) a reference to some internal data structure within the + * authorization server, the internal data structure contains the attributes of + * the token, such as user id, scope, etc. Handles typically require a + * communication between resource server and token server in order to validate + * the token and obtain token- bound data. Handles enable simple revocation and + * do not require cryptographic mechanisms to protected token content from being + * modified. As a disadvantage, they require additional resource/ token server + * communication impacting on performance and scalability. An authorization code + * is an example of a 'handle' token. An access token may also be implemented as + * a handle token. A 'handle' token is often referred to as an 'opaque' token + * because the resource server does not need to be able to interpret the token + * directly, it simply uses the token. + * + * @author Javier Rojas Date: 10.31.2011 + * + */ +public class HandleTokenFactory { + + /** + * When creating token handles, the authorization server MUST include a + * reasonable level of entropy in order to mitigate the risk of guessing + * attacks. The token value MUST be constructed from a cryptographically + * strong random or pseudo-random number sequence [RFC1750] generated by the + * Authorization Server. The probability of any two Authorization Code + * values being identical MUST be less than or equal to 2^(-128) and SHOULD + * be less than or equal to 2^(-160). + * + * @return The generated handle token. + */ + public static String generateHandleToken() { + return UUID.randomUUID().toString(); + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/HttpAuthTokenType.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/HttpAuthTokenType.java new file mode 100644 index 00000000..d80014a1 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/HttpAuthTokenType.java @@ -0,0 +1,19 @@ +package org.gluu.oxauth.model.token; + +public enum HttpAuthTokenType { + Basic("Basic "), + Bearer("Bearer "), + AccessToken("AccessToken "), + Negotiate("Negotiate "); + + private final String prefix; + + private HttpAuthTokenType(String prefix) { + this.prefix = prefix; + } + + public String getPrefix() { + + return this.prefix; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/IdTokenFactory.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/IdTokenFactory.java new file mode 100644 index 00000000..f5667f6e --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/IdTokenFactory.java @@ -0,0 +1,364 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.token; + +import com.google.common.base.Function; +import com.google.common.collect.Lists; +import org.apache.commons.lang.StringUtils; +import org.apache.logging.log4j.util.Strings; +import org.gluu.model.GluuAttribute; +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.model.custom.script.type.auth.PersonAuthenticationType; +import org.gluu.oxauth.claims.Audience; +import org.gluu.oxauth.model.authorize.Claim; +import org.gluu.oxauth.model.authorize.JwtAuthorizationRequest; +import org.gluu.oxauth.model.common.*; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.exception.InvalidClaimException; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtSubClaimObject; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.service.AttributeService; +import org.gluu.oxauth.service.ScopeService; +import org.gluu.oxauth.service.SessionIdService; +import org.gluu.oxauth.service.date.DateFormatterService; +import org.gluu.oxauth.service.external.ExternalAuthenticationService; +import org.gluu.oxauth.service.external.ExternalDynamicScopeService; +import org.gluu.oxauth.service.external.ExternalUpdateTokenService; +import org.gluu.oxauth.service.external.context.DynamicScopeExternalContext; +import org.gluu.oxauth.service.external.context.ExternalUpdateTokenContext; +import org.json.JSONObject; +import org.oxauth.persistence.model.Scope; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import java.io.Serializable; +import java.util.*; + +import static org.gluu.oxauth.model.common.ScopeType.DYNAMIC; + +/** + * JSON Web Token (JWT) is a compact token format intended for space constrained + * environments such as HTTP Authorization headers and URI query parameters. + * JWTs encode claims to be transmitted as a JSON object (as defined in RFC + * 4627) that is base64url encoded and digitally signed. Signing is accomplished + * using a JSON Web Signature (JWS). JWTs may also be optionally encrypted using + * JSON Web Encryption (JWE). + * + * @author Javier Rojas Blum + * @author Yuriy Movchan + * @author Yuriy Zabrovarnyy + * @version 12 Feb, 2020 + */ +@ApplicationScoped +public class IdTokenFactory { + + @Inject + private Logger log; + + @Inject + private ExternalDynamicScopeService externalDynamicScopeService; + + @Inject + private ExternalAuthenticationService externalAuthenticationService; + + @Inject + private ScopeService scopeService; + + @Inject + private AttributeService attributeService; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private JwrService jwrService; + + @Inject + private SessionIdService sessionIdService; + + @Inject + private DateFormatterService dateFormatterService; + + @Inject + private ExternalUpdateTokenService externalUpdateTokenService; + + private void setAmrClaim(JsonWebResponse jwt, String acrValues) { + List amrList = Lists.newArrayList(); + + CustomScriptConfiguration script = externalAuthenticationService.getCustomScriptConfigurationByName(acrValues); + if (script != null) { + amrList.add(Integer.toString(script.getLevel())); + + PersonAuthenticationType externalAuthenticator = (PersonAuthenticationType) script.getExternalType(); + int apiVersion = externalAuthenticator.getApiVersion(); + + if (apiVersion > 3) { + Map authenticationMethodClaimsOrNull = externalAuthenticator.getAuthenticationMethodClaims(script.getConfigurationAttributes()); + if (authenticationMethodClaimsOrNull != null) { + for (String key : authenticationMethodClaimsOrNull.keySet()) { + amrList.add(key + ":" + authenticationMethodClaimsOrNull.get(key)); + } + } + } + } + + jwt.getClaims().setClaim(JwtClaimName.AUTHENTICATION_METHOD_REFERENCES, amrList); + } + + private void fillClaims(JsonWebResponse jwr, + IAuthorizationGrant authorizationGrant, String nonce, + AuthorizationCode authorizationCode, AccessToken accessToken, RefreshToken refreshToken, + String state, Set scopes, boolean includeIdTokenClaims, + Function preProcessing, Function postProcessing, + ExecutionContext executionContext) throws Exception { + + final Client client = authorizationGrant.getClient(); + jwr.getClaims().setIssuer(appConfiguration.getIssuer()); + Audience.setAudience(jwr.getClaims(), client); + + int lifeTime = appConfiguration.getIdTokenLifetime(); + if (client.getAttributes().getIdTokenLifetime() != null) { + lifeTime = client.getAttributes().getIdTokenLifetime(); + log.trace("Override id token lifetime with value from client: {}", client.getClientId()); + } + + int lifetimeFromScript = externalUpdateTokenService.getIdTokenLifetimeInSeconds(ExternalUpdateTokenContext.of(executionContext)); + if (lifetimeFromScript > 0) { + lifeTime = lifetimeFromScript; + log.trace("Override id token lifetime with value from script: {}", lifetimeFromScript); + } + + Calendar calendar = Calendar.getInstance(); + Date issuedAt = calendar.getTime(); + calendar.add(Calendar.SECOND, lifeTime); + Date expiration = calendar.getTime(); + + jwr.getClaims().setExpirationTime(expiration); + jwr.getClaims().setIssuedAt(issuedAt); + jwr.setClaim("code", UUID.randomUUID().toString()); + + if (preProcessing != null) { + preProcessing.apply(jwr); + } + final SessionId session = sessionIdService.getSessionByDn(authorizationGrant.getSessionDn(), true); + if (session != null) { + jwr.setClaim("sid", session.getOutsideSid()); + } + + if (authorizationGrant.getAcrValues() != null) { + jwr.setClaim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, authorizationGrant.getAcrValues()); + setAmrClaim(jwr, authorizationGrant.getAcrValues()); + } + if (StringUtils.isNotBlank(nonce)) { + jwr.setClaim(JwtClaimName.NONCE, nonce); + } + if (authorizationGrant.getAuthenticationTime() != null) { + jwr.getClaims().setClaim(JwtClaimName.AUTHENTICATION_TIME, authorizationGrant.getAuthenticationTime()); + } + if (authorizationCode != null) { + String codeHash = AbstractToken.getHash(authorizationCode.getCode(), jwr.getHeader().getSignatureAlgorithm()); + jwr.setClaim(JwtClaimName.CODE_HASH, codeHash); + } + if (accessToken != null) { + String accessTokenHash = AbstractToken.getHash(accessToken.getCode(), jwr.getHeader().getSignatureAlgorithm()); + jwr.setClaim(JwtClaimName.ACCESS_TOKEN_HASH, accessTokenHash); + } + if (Strings.isNotBlank(state)) { + String stateHash = AbstractToken.getHash(state, jwr.getHeader().getSignatureAlgorithm()); + jwr.setClaim(JwtClaimName.STATE_HASH, stateHash); + } + if (authorizationGrant.getGrantType() != null) { + jwr.setClaim("grant", authorizationGrant.getGrantType().getValue()); + } + jwr.setClaim(JwtClaimName.OX_OPENID_CONNECT_VERSION, appConfiguration.getOxOpenIdConnectVersion()); + + User user = authorizationGrant.getUser(); + List dynamicScopes = new ArrayList<>(); + if (includeIdTokenClaims && client.isIncludeClaimsInIdToken()) { + for (String scopeName : scopes) { + Scope scope = scopeService.getScopeById(scopeName); + if (scope == null) { + continue; + } + + if (DYNAMIC == scope.getScopeType()) { + dynamicScopes.add(scope); + continue; + } + + Map claims = scopeService.getClaims(user, scope); + + if (Boolean.TRUE.equals(scope.isOxAuthGroupClaims())) { + JwtSubClaimObject groupClaim = new JwtSubClaimObject(); + groupClaim.setName(scope.getId()); + for (Map.Entry entry : claims.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + if (value instanceof List) { + groupClaim.setClaim(key, (List) value); + } else { + groupClaim.setClaim(key, (String) value); + } + } + + jwr.getClaims().setClaim(scope.getId(), groupClaim); + } else { + for (Map.Entry entry : claims.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + log.info("IdToken Factory called: {}", value); + + if (value instanceof List) { + jwr.getClaims().setClaim(key, (List) value); + } else if (value instanceof Boolean) { + jwr.getClaims().setClaim(key, (Boolean) value); + } else if (value instanceof Date) { + Serializable formattedValue = dateFormatterService.formatClaim((Date) value, key); + jwr.getClaims().setClaimObject(key, formattedValue, true); + } else { + jwr.setClaim(key, (String) value); + } + } + } + + jwr.getClaims().setSubjectIdentifier(authorizationGrant.getUser().getAttribute("inum")); + } + } + + setClaimsFromJwtAuthorizationRequest(jwr, authorizationGrant, scopes); + setClaimsFromRequestedClaims(((AuthorizationGrant) authorizationGrant).getClaims(), jwr, user); + jwrService.setSubjectIdentifier(jwr, authorizationGrant); + + if ((dynamicScopes.size() > 0) && externalDynamicScopeService.isEnabled()) { + final UnmodifiableAuthorizationGrant unmodifiableAuthorizationGrant = new UnmodifiableAuthorizationGrant(authorizationGrant); + DynamicScopeExternalContext dynamicScopeContext = new DynamicScopeExternalContext(dynamicScopes, jwr, unmodifiableAuthorizationGrant); + externalDynamicScopeService.executeExternalUpdateMethods(dynamicScopeContext); + } + + processCiba(jwr, authorizationGrant, refreshToken); + + if (postProcessing != null) { + postProcessing.apply(jwr); + } + } + + private void setClaimsFromRequestedClaims(String requestedClaims, JsonWebResponse jwr, User user) + throws InvalidClaimException { + if (requestedClaims != null) { + JSONObject claimsObj = new JSONObject(requestedClaims); + if (claimsObj.has("id_token")) { + JSONObject idTokenObj = claimsObj.getJSONObject("id_token"); + for (Iterator it = idTokenObj.keys(); it.hasNext(); ) { + String claimName = it.next(); + GluuAttribute gluuAttribute = attributeService.getByClaimName(claimName); + + if (gluuAttribute != null) { + String ldapClaimName = gluuAttribute.getName(); + + Object attribute = user.getAttribute(ldapClaimName, false, gluuAttribute.getOxMultiValuedAttribute()); + + if (attribute instanceof List) { + jwr.getClaims().setClaim(claimName, (List) attribute); + } else if (attribute instanceof Boolean) { + jwr.getClaims().setClaim(claimName, (Boolean) attribute); + } else if (attribute instanceof Date) { + Serializable formattedValue = dateFormatterService.formatClaim((Date) attribute, claimName); + jwr.getClaims().setClaimObject(claimName, formattedValue, true); + } else { + jwr.setClaim(claimName, (String) attribute); + } + } + } + } + } + } + + private void processCiba(JsonWebResponse jwr, IAuthorizationGrant authorizationGrant, RefreshToken refreshToken) { + if (!(authorizationGrant instanceof CIBAGrant)) { + return; + } + + String refreshTokenHash = AbstractToken.getHash(refreshToken.getCode(), null); + jwr.setClaim(JwtClaimName.REFRESH_TOKEN_HASH, refreshTokenHash); + + CIBAGrant cibaGrant = (CIBAGrant) authorizationGrant; + jwr.setClaim(JwtClaimName.AUTH_REQ_ID, cibaGrant.getAuthReqId()); + } + + private void setClaimsFromJwtAuthorizationRequest(JsonWebResponse jwr, IAuthorizationGrant authorizationGrant, Set scopes) throws InvalidClaimException { + final JwtAuthorizationRequest requestObject = authorizationGrant.getJwtAuthorizationRequest(); + if (requestObject == null || requestObject.getIdTokenMember() == null) { + return; + } + + for (Claim claim : requestObject.getIdTokenMember().getClaims()) { + boolean optional = true; // ClaimValueType.OPTIONAL.equals(claim.getClaimValue().getClaimValueType()); + GluuAttribute gluuAttribute = attributeService.getByClaimName(claim.getName()); + + if (gluuAttribute == null) { + continue; + } + + Client client = authorizationGrant.getClient(); + + if (validateRequesteClaim(gluuAttribute, client.getClaims(), scopes)) { + String ldapClaimName = gluuAttribute.getName(); + Object attribute = authorizationGrant.getUser().getAttribute(ldapClaimName, optional, gluuAttribute.getOxMultiValuedAttribute()); + jwr.getClaims().setClaimFromJsonObject(claim.getName(), attribute); + } + } + } + + public JsonWebResponse createJwr( + IAuthorizationGrant grant, String nonce, + AuthorizationCode authorizationCode, AccessToken accessToken, RefreshToken refreshToken, + String state, Set scopes, boolean includeIdTokenClaims, + Function preProcessing, Function postProcessing, + ExecutionContext executionContext) throws Exception { + + final Client client = grant.getClient(); + + JsonWebResponse jwr = jwrService.createJwr(client); + fillClaims(jwr, grant, nonce, authorizationCode, accessToken, refreshToken, state, scopes, includeIdTokenClaims, preProcessing, postProcessing, executionContext); + + if (log.isTraceEnabled()) + log.trace("Created claims for id_token, claims: " + jwr.getClaims().toJsonString()); + + return jwrService.encode(jwr, client); + } + + private boolean validateRequesteClaim(GluuAttribute gluuAttribute, String[] clientAllowedClaims, Collection scopes) { + if (gluuAttribute == null) { + return false; + } + + if (clientAllowedClaims != null) { + for (String clientAllowedClaim : clientAllowedClaims) { + if (gluuAttribute.getDn().equals(clientAllowedClaim)) { + return true; + } + } + } + + for (String scopeName : scopes) { + Scope scope = scopeService.getScopeById(scopeName); + + if (scope != null && scope.getOxAuthClaims() != null) { + for (String claimDn : scope.getOxAuthClaims()) { + if (gluuAttribute.getDisplayName().equals(attributeService.getAttributeByDn(claimDn).getDisplayName())) { + return true; + } + } + } + } + return false; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/JwrService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/JwrService.java new file mode 100644 index 00000000..14879eff --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/JwrService.java @@ -0,0 +1,163 @@ +package org.gluu.oxauth.model.token; + +import com.google.common.base.Function; + +import javax.enterprise.context.ApplicationScoped; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.common.IAuthorizationGrant; +import org.gluu.oxauth.model.config.WebKeysConfiguration; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.crypto.AbstractCryptoProvider; +import org.gluu.oxauth.model.crypto.encryption.BlockEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.encryption.KeyEncryptionAlgorithm; +import org.gluu.oxauth.model.exception.InvalidJweException; +import org.gluu.oxauth.model.jwe.Jwe; +import org.gluu.oxauth.model.jwe.JweEncrypter; +import org.gluu.oxauth.model.jwe.JweEncrypterImpl; +import org.gluu.oxauth.model.jwk.Algorithm; +import org.gluu.oxauth.model.jwk.JSONWebKeySet; +import org.gluu.oxauth.model.jwk.Use; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtType; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.service.ClientService; +import org.gluu.oxauth.service.SectorIdentifierService; +import org.gluu.oxauth.service.ServerCryptoProvider; +import org.gluu.oxauth.util.ServerUtil; +import org.json.JSONObject; +import org.slf4j.Logger; + +import javax.inject.Inject; +import java.nio.charset.StandardCharsets; +import java.security.PublicKey; + +import static org.gluu.oxauth.model.jwt.JwtHeaderName.ALGORITHM; + +/** + * @author Yuriy Zabrovarnyy + * @version April 10, 2020 + */ +@ApplicationScoped +public class JwrService { + + @Inject + private Logger log; + + @Inject + private AbstractCryptoProvider cryptoProvider; + + @Inject + private ClientService clientService; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private WebKeysConfiguration webKeysConfiguration; + + @Inject + private SectorIdentifierService sectorIdentifierService; + + /** + * Encode means encrypt for Jwe and sign for Jwt, means it's implementaiton specific but we want to abstract it. + * + * @return encoded Jwr + */ + public JsonWebResponse encode(JsonWebResponse jwr, Client client) throws Exception { + if (jwr instanceof Jwe) { + return encryptJwe((Jwe) jwr, client); + } + if (jwr instanceof Jwt) { + return signJwt((Jwt) jwr, client); + } + + throw new IllegalArgumentException("Unknown Jwr instance."); + } + + private Jwt signJwt(Jwt jwt, Client client) throws Exception { + JwtSigner jwtSigner = JwtSigner.newJwtSigner(appConfiguration, webKeysConfiguration, client); + jwtSigner.setJwt(jwt); + jwtSigner.sign(); + return jwt; + } + + private Jwe encryptJwe(Jwe jwe, Client client) throws Exception { + + if (appConfiguration.getUseNestedJwtDuringEncryption()) { + JwtSigner jwtSigner = JwtSigner.newJwtSigner(appConfiguration, webKeysConfiguration, client); + Jwt jwt = jwtSigner.newJwt(); + jwt.setClaims(jwe.getClaims()); + jwe.setSignedJWTPayload(signJwt(jwt, client)); + } + + KeyEncryptionAlgorithm keyEncryptionAlgorithm = KeyEncryptionAlgorithm.fromName(jwe.getHeader().getClaimAsString(ALGORITHM)); + final BlockEncryptionAlgorithm encryptionMethod = jwe.getHeader().getEncryptionMethod(); + + if (keyEncryptionAlgorithm == KeyEncryptionAlgorithm.RSA_OAEP || keyEncryptionAlgorithm == KeyEncryptionAlgorithm.RSA1_5) { + JSONObject jsonWebKeys = ServerUtil.getJwks(client); + String keyId = new ServerCryptoProvider(cryptoProvider).getKeyId(JSONWebKeySet.fromJSONObject(jsonWebKeys), + Algorithm.fromString(keyEncryptionAlgorithm.getName()), + Use.ENCRYPTION); + PublicKey publicKey = cryptoProvider.getPublicKey(keyId, jsonWebKeys, null); + jwe.getHeader().setKeyId(keyId); + + if (publicKey == null) { + throw new InvalidJweException("The public key is not valid"); + } + + JweEncrypter jweEncrypter = new JweEncrypterImpl(keyEncryptionAlgorithm, encryptionMethod, publicKey); + return jweEncrypter.encrypt(jwe); + } + if (keyEncryptionAlgorithm == KeyEncryptionAlgorithm.A128KW || keyEncryptionAlgorithm == KeyEncryptionAlgorithm.A256KW) { + byte[] sharedSymmetricKey = clientService.decryptSecret(client.getClientSecret()).getBytes(StandardCharsets.UTF_8); + JweEncrypter jweEncrypter = new JweEncrypterImpl(keyEncryptionAlgorithm, encryptionMethod, sharedSymmetricKey); + return jweEncrypter.encrypt(jwe); + } + + throw new IllegalArgumentException("Unsupported encryption algorithm: " + keyEncryptionAlgorithm); + } + + public JsonWebResponse createJwr(Client client) { + try { + if (client.getIdTokenEncryptedResponseAlg() != null + && client.getIdTokenEncryptedResponseEnc() != null) { + Jwe jwe = new Jwe(); + + // Header + KeyEncryptionAlgorithm keyEncryptionAlgorithm = KeyEncryptionAlgorithm.fromName(client.getIdTokenEncryptedResponseAlg()); + BlockEncryptionAlgorithm blockEncryptionAlgorithm = BlockEncryptionAlgorithm.fromName(client.getIdTokenEncryptedResponseEnc()); + jwe.getHeader().setType(JwtType.JWT); + jwe.getHeader().setAlgorithm(keyEncryptionAlgorithm); + jwe.getHeader().setEncryptionMethod(blockEncryptionAlgorithm); + return jwe; + } else { + JwtSigner jwtSigner = JwtSigner.newJwtSigner(appConfiguration, webKeysConfiguration, client); + return jwtSigner.newJwt(); + } + } catch (Exception e) { + log.error("Failed to create logout_token.", e); + return null; + } + } + + public void setSubjectIdentifier(JsonWebResponse jwr, IAuthorizationGrant authorizationGrant) { + jwr.getClaims().setSubjectIdentifier(authorizationGrant.getSub()); + } + + public static Function wrapWithSidFunction(Function input, String outsideSid) { + return jwr -> { + if (jwr == null) { + return null; + } + if (input != null) { + input.apply(jwr); + } + if (StringUtils.isNotEmpty(outsideSid)) { + jwr.setClaim("sid", outsideSid); + } + return null; + }; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/JwtSigner.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/JwtSigner.java new file mode 100644 index 00000000..9ad6153b --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/JwtSigner.java @@ -0,0 +1,112 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.token; + +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.crypto.AbstractCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jwk.Algorithm; +import org.gluu.oxauth.model.jwk.JSONWebKeySet; +import org.gluu.oxauth.model.jwk.Use; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtType; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.service.ClientService; +import org.gluu.oxauth.service.ServerCryptoProvider; +import org.gluu.service.cdi.util.CdiUtil; + +import com.google.common.base.Preconditions; + +/** + * @author Yuriy Zabrovarnyy + * @author Javier Rojas Blum + * @version February 12, 2019 + */ + +public class JwtSigner { + + private AbstractCryptoProvider cryptoProvider; + private SignatureAlgorithm signatureAlgorithm; + private String audience; + private String hmacSharedSecret; + + private AppConfiguration appConfiguration; + private JSONWebKeySet webKeys; + + private Jwt jwt; + + public JwtSigner(AppConfiguration appConfiguration, JSONWebKeySet webKeys, SignatureAlgorithm signatureAlgorithm, String audience) { + this(appConfiguration, webKeys, signatureAlgorithm, audience, null); + } + + public JwtSigner(AppConfiguration appConfiguration, JSONWebKeySet webKeys, SignatureAlgorithm signatureAlgorithm, String audience, String hmacSharedSecret) { + this(appConfiguration, webKeys, signatureAlgorithm, audience, hmacSharedSecret,null); + } + + public JwtSigner(AppConfiguration appConfiguration, JSONWebKeySet webKeys, SignatureAlgorithm signatureAlgorithm, String audience, String hmacSharedSecret, AbstractCryptoProvider cryptoProvider) { + this.appConfiguration = appConfiguration; + this.webKeys = webKeys; + this.signatureAlgorithm = signatureAlgorithm; + this.audience = audience; + this.hmacSharedSecret = hmacSharedSecret; + + this.cryptoProvider = cryptoProvider != null ? cryptoProvider : new ServerCryptoProvider( CdiUtil.bean(AbstractCryptoProvider.class)); + } + + public static JwtSigner newJwtSigner(AppConfiguration appConfiguration, JSONWebKeySet webKeys, Client client) throws Exception { + Preconditions.checkNotNull(client); + + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.fromString(appConfiguration.getDefaultSignatureAlgorithm()); + if (client.getIdTokenSignedResponseAlg() != null) { + signatureAlgorithm = SignatureAlgorithm.fromString(client.getIdTokenSignedResponseAlg()); + } + + ClientService clientService = CdiUtil.bean(ClientService.class); + return new JwtSigner(appConfiguration, webKeys, signatureAlgorithm, client.getClientId(), clientService.decryptSecret(client.getClientSecret())); + } + + public Jwt newJwt() throws Exception { + jwt = new Jwt(); + + // Header + String keyId = cryptoProvider.getKeyId(webKeys, Algorithm.fromString(signatureAlgorithm.getName()), Use.SIGNATURE); + if (keyId != null) { + jwt.getHeader().setKeyId(keyId); + } + jwt.getHeader().setType(JwtType.JWT); + jwt.getHeader().setAlgorithm(signatureAlgorithm); + + // Claims + jwt.getClaims().setIssuer(appConfiguration.getIssuer()); + jwt.getClaims().setAudience(audience); + return jwt; + } + + public Jwt sign() throws Exception { + // Signature + String signature = cryptoProvider.sign(jwt.getSigningInput(), jwt.getHeader().getKeyId(), hmacSharedSecret, signatureAlgorithm); + jwt.setEncodedSignature(signature); + + return jwt; + } + + public Jwt getJwt() { + return jwt; + } + + public void setJwt(Jwt jwt) { + this.jwt = jwt; + } + + public SignatureAlgorithm getSignatureAlgorithm() { + return signatureAlgorithm; + } + + public void setCryptoProvider(AbstractCryptoProvider cryptoProvider) { + this.cryptoProvider = cryptoProvider; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/PersistentJwt.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/PersistentJwt.java new file mode 100644 index 00000000..fc8f9ae2 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/PersistentJwt.java @@ -0,0 +1,316 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.token; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import org.apache.commons.lang.StringUtils; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.gluu.oxauth.model.common.AccessToken; +import org.gluu.oxauth.model.common.AuthorizationGrantType; +import org.gluu.oxauth.model.common.IdToken; +import org.gluu.oxauth.model.common.RefreshToken; +import org.gluu.oxauth.model.util.Util; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Javier Rojas Blum Date: 05.22.2012 + */ +public class PersistentJwt { + + private final static Logger log = LoggerFactory.getLogger(PersistentJwt.class); + + private String userId; + private String clientId; + private AuthorizationGrantType authorizationGrantType; + private Date authenticationTime; + private List scopes; + private List accessTokens; + private List refreshTokens; + private AccessToken longLivedAccessToken; + private IdToken idToken; + + public PersistentJwt() { + } + + public PersistentJwt(String jwt) { + try { + load(jwt); + } catch (JSONException e) { + log.error(e.getMessage(), e); + } + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public AuthorizationGrantType getAuthorizationGrantType() { + return authorizationGrantType; + } + + public void setAuthorizationGrantType(AuthorizationGrantType authorizationGrantType) { + this.authorizationGrantType = authorizationGrantType; + } + + public Date getAuthenticationTime() { + return authenticationTime; + } + + public List getScopes() { + return scopes; + } + + public void setScopes(List scopes) { + this.scopes = scopes; + } + + public void setAuthenticationTime(Date authenticationTime) { + this.authenticationTime = authenticationTime; + } + + public List getAccessTokens() { + return accessTokens; + } + + public void setAccessTokens(List accessTokens) { + this.accessTokens = accessTokens; + } + + public List getRefreshTokens() { + return refreshTokens; + } + + public void setRefreshTokens(List refreshTokens) { + this.refreshTokens = refreshTokens; + } + + public AccessToken getLongLivedAccessToken() { + return longLivedAccessToken; + } + + public void setLongLivedAccessToken(AccessToken longLivedAccessToken) { + this.longLivedAccessToken = longLivedAccessToken; + } + + public IdToken getIdToken() { + return idToken; + } + + public void setIdToken(IdToken idToken) { + this.idToken = idToken; + } + + @Override + public String toString() { + JSONObject jsonObject = new JSONObject(); + + try { + if (StringUtils.isNotBlank(userId)) { + jsonObject.put("user_id", userId); + } + if (StringUtils.isNotBlank(clientId)) { + jsonObject.put("client_id", clientId); + } + if (authorizationGrantType != null) { + jsonObject.put("authorization_grant_type", authorizationGrantType); + } + if (authenticationTime != null) { + jsonObject.put("authentication_time", authenticationTime.getTime()); + } + if (scopes != null) { + JSONArray scopesJsonArray = new JSONArray(); + for (String scope : scopes) { + scopesJsonArray.put(scope); + } + jsonObject.put("scopes", scopesJsonArray); + } + if (accessTokens != null) { + JSONArray accessTokensJsonArray = new JSONArray(); + + for (AccessToken accessToken : accessTokens) { + JSONObject accessTokenJsonObject = new JSONObject(); + + if (accessToken.getCode() != null && !accessToken.getCode().isEmpty()) { + accessTokenJsonObject.put("code", accessToken.getCode()); + } + if (accessToken.getCreationDate() != null) { + accessTokenJsonObject.put("creation_date", accessToken.getCreationDate().getTime()); + } + if (accessToken.getExpirationDate() != null) { + accessTokenJsonObject.put("expiration_date", accessToken.getExpirationDate().getTime()); + } + + accessTokensJsonArray.put(accessTokenJsonObject); + } + + jsonObject.put("access_tokens", accessTokensJsonArray); + } + if (refreshTokens != null) { + JSONArray refreshTokensJsonArray = new JSONArray(); + + for (RefreshToken refreshToken : refreshTokens) { + JSONObject refreshTokenJsonObject = new JSONObject(); + + if (refreshToken.getCode() != null && !refreshToken.getCode().isEmpty()) { + refreshTokenJsonObject.put("code", refreshToken.getCode()); + } + if (refreshToken.getCreationDate() != null) { + refreshTokenJsonObject.put("creation_date", refreshToken.getCreationDate().getTime()); + } + if (refreshToken.getExpirationDate() != null) { + refreshTokenJsonObject.put("expiration_date", refreshToken.getExpirationDate().getTime()); + } + } + + jsonObject.put("refresh_tokens", refreshTokensJsonArray); + } + if (longLivedAccessToken != null) { + JSONObject longLivedAccessTokenJsonObject = new JSONObject(); + + if (longLivedAccessToken.getCode() != null && !longLivedAccessToken.getCode().isEmpty()) { + longLivedAccessTokenJsonObject.put("code", longLivedAccessToken.getCode()); + } + if (longLivedAccessToken.getCreationDate() != null) { + longLivedAccessTokenJsonObject.put("creation_date", longLivedAccessToken.getCreationDate().getTime()); + } + if (longLivedAccessToken.getExpirationDate() != null) { + longLivedAccessTokenJsonObject.put("expiration_date", longLivedAccessToken.getExpirationDate().getTime()); + } + + jsonObject.put("long_lived_access_token", longLivedAccessTokenJsonObject); + } + if (idToken != null) { + JSONObject idTokenJsonObject = new JSONObject(); + + if (idToken.getCode() != null && !idToken.getCode().isEmpty()) { + idTokenJsonObject.put("code", idToken.getCode()); + } + if (idToken.getCreationDate() != null) { + idTokenJsonObject.put("creation_date", idToken.getCreationDate().getTime()); + } + if (idToken.getExpirationDate() != null) { + idTokenJsonObject.put("expiration_date", idToken.getExpirationDate().getTime()); + } + + jsonObject.put("id_token", idTokenJsonObject); + } + + + } catch (JSONException e) { + log.error(e.getMessage(), e); + } + + return jsonObject.toString(); + } + + private boolean load(String jwt) throws JSONException { + boolean result = false; + + JSONObject jsonObject = new JSONObject(jwt); + + if (jsonObject.has("user_id")) { + userId = jsonObject.getString("user_id"); + } + if (jsonObject.has("client_id")) { + clientId = jsonObject.getString("client_id"); + } + if (jsonObject.has("authorization_grant_type")) { + authorizationGrantType = AuthorizationGrantType.fromString(jsonObject.getString("authorization_grant_type")); + } + if (jsonObject.has("authentication_time")) { + authenticationTime = new Date(jsonObject.getLong("authentication_time")); + } + if (jsonObject.has("scopes")) { + JSONArray jsonArray = jsonObject.getJSONArray("scopes"); + scopes = Util.asList(jsonArray); + } + if (jsonObject.has("access_tokens")) { + JSONArray accessTokensJsonArray = jsonObject.getJSONArray("access_tokens"); + accessTokens = new ArrayList(); + + for (int i = 0; i < accessTokensJsonArray.length(); i++) { + JSONObject accessTokenJsonObject = accessTokensJsonArray.getJSONObject(i); + + if (accessTokenJsonObject.has("code") + && accessTokenJsonObject.has("creation_date") + && accessTokenJsonObject.has("expiration_date")) { + String tokenCode = accessTokenJsonObject.getString("code"); + Date creationDate = new Date(accessTokenJsonObject.getLong("creation_date")); + Date expirationDate = new Date(accessTokenJsonObject.getLong("expiration_date")); + + AccessToken accessToken = new AccessToken(tokenCode, creationDate, expirationDate); + accessTokens.add(accessToken); + } + } + } + if (jsonObject.has("refresh_tokens")) { + JSONArray refreshTokensJsonArray = jsonObject.getJSONArray("refresh_tokens"); + refreshTokens = new ArrayList(); + + for (int i = 0; i < refreshTokensJsonArray.length(); i++) { + JSONObject refreshTokenJsonObject = refreshTokensJsonArray.getJSONObject(i); + + if (refreshTokenJsonObject.has("code") + && refreshTokenJsonObject.has("creation_date") + && refreshTokenJsonObject.has("expiration_date")) { + String tokenCode = refreshTokenJsonObject.getString("code"); + Date creationDate = new Date(refreshTokenJsonObject.getLong("creation_date")); + Date expirationDate = new Date(refreshTokenJsonObject.getLong("expiration_date")); + + RefreshToken refreshToken = new RefreshToken(tokenCode, creationDate, expirationDate); + refreshTokens.add(refreshToken); + } + } + } + if (jsonObject.has("long_lived_access_token")) { + JSONObject longLivedAccessTokenJsonObject = jsonObject.getJSONObject("long_lived_access_token"); + + if (longLivedAccessTokenJsonObject.has("code") + && longLivedAccessTokenJsonObject.has("creation_date") + && longLivedAccessTokenJsonObject.has("expiration_date")) { + String tokenCode = longLivedAccessTokenJsonObject.getString("code"); + Date creationDate = new Date(longLivedAccessTokenJsonObject.getLong("creation_date")); + Date expirationDate = new Date(longLivedAccessTokenJsonObject.getLong("expiration_date")); + + longLivedAccessToken = new AccessToken(tokenCode, creationDate, expirationDate); + } + } + if (jsonObject.has("id_token")) { + JSONObject idTokenJsonObject = jsonObject.getJSONObject("id_token"); + + if (idTokenJsonObject.has("code") + && idTokenJsonObject.has("creation_date") + && idTokenJsonObject.has("expiration_date")) { + String tokenCode = idTokenJsonObject.getString("code"); + Date creationDate = new Date(idTokenJsonObject.getLong("creation_date")); + Date expirationDate = new Date(idTokenJsonObject.getLong("expiration_date")); + + idToken = new IdToken(tokenCode, creationDate, expirationDate); + } + } + + return result; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/TokenParamsValidator.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/TokenParamsValidator.java new file mode 100644 index 00000000..34b7e0c7 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/TokenParamsValidator.java @@ -0,0 +1,84 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.token; + +import org.gluu.oxauth.model.common.GrantType; + +import java.net.URI; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +/** + * Validates the parameters received for the token web service. + * + * @author Javier Rojas Blum + * @version February 25, 2020 + */ +public class TokenParamsValidator { + + /** + * Validates the parameters for a token request. + * + * @param grantType The grant type. This parameter is mandatory. Value must be set + * to: authorization_code, password, + * client_credentials, refresh_token, + * or a valid {@link URI}. + * @param code The authorization code. + * @param redirectUri + * @param username + * @param password + * @param scope + * @param assertion + * @param refreshToken + * @return Returns true when all the parameters are valid. + */ + public static boolean validateParams(String grantType, String code, + String redirectUri, String username, String password, String scope, + String assertion, String refreshToken) { + boolean result = false; + if (grantType == null || grantType.isEmpty()) { + return false; + } + + GrantType gt = GrantType.fromString(grantType); + + switch (gt) { + case AUTHORIZATION_CODE: + result = code != null && !code.isEmpty() && redirectUri != null && !redirectUri.isEmpty(); + break; + case RESOURCE_OWNER_PASSWORD_CREDENTIALS: + result = true; + break; + case CLIENT_CREDENTIALS: + result = true; + break; + case REFRESH_TOKEN: + result = refreshToken != null && !refreshToken.isEmpty(); + break; + case CIBA: + result = true; + break; + case DEVICE_CODE: + result = true; + break; + } + + return result; + } + + public static boolean validateParams(String clientId, String clientSecret) { + return clientId != null && !clientId.isEmpty() + && clientSecret != null && !clientSecret.isEmpty(); + } + + public static boolean validateGrantType(GrantType requestedGrantType, GrantType[] clientGrantTypesArray, Set grantTypesSupported) { + List clientGrantTypes = Arrays.asList(clientGrantTypesArray); + + return clientGrantTypes.contains(requestedGrantType) && grantTypesSupported.contains(requestedGrantType); + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/ValidateTokenParamsValidator.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/ValidateTokenParamsValidator.java new file mode 100644 index 00000000..7540f1c3 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/token/ValidateTokenParamsValidator.java @@ -0,0 +1,26 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.token; + +/** + * Validates the parameters received for the validate token web service. + * + * @author Javier Rojas Blum Date: 10.27.2011 + */ +public class ValidateTokenParamsValidator { + + /** + * Validates the parameters for a validate token request. + * + * @param accessToken + * The access token issued by the authorization server. + * @return Returns true when all the parameters are valid. + */ + public static boolean validateParams(String accessToken) { + return accessToken != null && !accessToken.isEmpty(); + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/model/userinfo/UserInfoParamsValidator.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/userinfo/UserInfoParamsValidator.java new file mode 100644 index 00000000..c4bae63f --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/model/userinfo/UserInfoParamsValidator.java @@ -0,0 +1,25 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.model.userinfo; + +/** + * Validates the parameters received for the user info web service. + * + * @author Javier Rojas Blum Date: 12.30.2011 + */ +public class UserInfoParamsValidator { + + /** + * Validates the parameters for an user info request. + * + * @param accessToken + * @return Returns true when all the parameters are valid. + */ + public static boolean validateParams(String accessToken) { + return accessToken != null && !accessToken.isEmpty(); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/register/ws/rs/RegisterRestWebService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/register/ws/rs/RegisterRestWebService.java new file mode 100644 index 00000000..83bf704a --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/register/ws/rs/RegisterRestWebService.java @@ -0,0 +1,106 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.register.ws.rs; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; + +/** + * Provides interface for register REST web services. + * + * @author Javier Rojas Blum + * @author Yuriy Zabrovarnyy + * @version 0.1, 01.11.2012 + */ +public interface RegisterRestWebService { + + /** + * In order for an OpenID Connect client to utilize OpenID services for a user, the client needs to register with + * the OpenID Provider to acquire a client ID and shared secret. + * + * @param requestParams request parameters + * @param httpRequest http request object + * @param securityContext An injectable interface that provides access to security related information. + * @return response + */ + @POST + @Path("/register") + @Produces({MediaType.APPLICATION_JSON}) + Response requestRegister( + String requestParams, + @Context HttpServletRequest httpRequest, + @Context SecurityContext securityContext); + + /** + * This operation updates the Client Metadata for a previously registered client. + * + * @param requestParams request parameters + * @param clientId client id + * @param authorization Access Token that is used at the Client Configuration Endpoint + * @param httpRequest http request object + * @param securityContext An injectable interface that provides access to security related information. + * @return response + */ + + @PUT + @Path("register") + @Produces({MediaType.APPLICATION_JSON}) + Response requestClientUpdate( + String requestParams, + @QueryParam("client_id") + String clientId, + @HeaderParam("Authorization") String authorization, + @Context HttpServletRequest httpRequest, + @Context SecurityContext securityContext); + + /** + * This operation retrieves the Client Metadata for a previously registered client. + * + * @param clientId Unique Client identifier. + * @param securityContext An injectable interface that provides access to security related information. + * @return response + */ + @GET + @Path("/register") + @Produces({MediaType.APPLICATION_JSON}) + Response requestClientRead( + @QueryParam("client_id") + String clientId, + @HeaderParam("Authorization") String authorization, + @Context HttpServletRequest httpRequest, + @Context SecurityContext securityContext); + + /** + * This operation removes the Client Metadata for a previously registered client. + * + * @param clientId Unique Client identifier. + * @param securityContext An injectable interface that provides access to security related information. + * @return If a client has been successfully deprovisioned, the authorization + * server responds with an HTTP 204 No Content message. + *

+ * If the registration access token used to make this request is not + * valid, the server responds with HTTP 401 Unauthorized. + *

+ * If the client does not exist on this server, the server responds + * with HTTP 401 Unauthorized. + *

+ * If the client is not allowed to delete itself, the server + * responds with HTTP 403 Forbidden. + */ + @DELETE + @Path("/register") + @Produces({MediaType.APPLICATION_JSON}) + Response delete( + @QueryParam("client_id") String clientId, + @HeaderParam("Authorization") String authorization, + @Context HttpServletRequest httpRequest, + @Context SecurityContext securityContext); +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/register/ws/rs/RegisterRestWebServiceImpl.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/register/ws/rs/RegisterRestWebServiceImpl.java new file mode 100644 index 00000000..5d955376 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/register/ws/rs/RegisterRestWebServiceImpl.java @@ -0,0 +1,1092 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.register.ws.rs; + +import com.google.common.base.Strings; +import com.google.common.collect.Lists; +import org.apache.commons.lang.BooleanUtils; +import org.apache.commons.lang.StringUtils; +import org.gluu.model.GluuAttribute; +import org.gluu.model.metric.MetricType; +import org.gluu.oxauth.audit.ApplicationAuditLogger; +import org.gluu.oxauth.ciba.CIBARegisterClientMetadataService; +import org.gluu.oxauth.ciba.CIBARegisterClientResponseService; +import org.gluu.oxauth.ciba.CIBARegisterParamsValidatorService; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.model.audit.Action; +import org.gluu.oxauth.model.audit.OAuth2AuditLog; +import org.gluu.oxauth.model.common.*; +import org.gluu.oxauth.model.config.StaticConfiguration; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.crypto.AbstractCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.AlgorithmFamily; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.exception.InvalidJwtException; +import org.gluu.oxauth.model.json.JsonApplier; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.register.RegisterErrorResponseType; +import org.gluu.oxauth.model.register.RegisterResponseParam; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.registration.RegisterParamsValidator; +import org.gluu.oxauth.model.token.HandleTokenFactory; +import org.gluu.oxauth.model.util.JwtUtil; +import org.gluu.oxauth.model.util.Pair; +import org.gluu.oxauth.model.util.Util; +import org.gluu.oxauth.service.AttributeService; +import org.gluu.oxauth.service.ClientService; +import org.gluu.oxauth.service.MetricService; +import org.gluu.oxauth.service.ScopeService; +import org.gluu.oxauth.service.common.InumService; +import org.gluu.oxauth.service.external.ExternalDynamicClientRegistrationService; +import org.gluu.oxauth.service.token.TokenService; +import org.gluu.oxauth.util.ServerUtil; +import org.gluu.persist.model.base.CustomAttribute; +import org.gluu.util.StringHelper; +import org.gluu.util.security.StringEncrypter; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.oxauth.persistence.model.Scope; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.Path; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; +import java.net.URI; +import java.util.*; + +import static org.apache.commons.lang3.BooleanUtils.isTrue; +import static org.gluu.oxauth.model.register.RegisterRequestParam.*; +import static org.gluu.oxauth.model.register.RegisterResponseParam.*; +import static org.gluu.oxauth.model.util.StringUtils.implode; +import static org.gluu.oxauth.model.util.StringUtils.toList; + +/** + * Implementation for register REST web services. + * + * @author Javier Rojas Blum + * @author Yuriy Zabrovarnyy + * @author Yuriy Movchan + * @version May 20, 2020 + */ +@Path("/") +public class RegisterRestWebServiceImpl implements RegisterRestWebService { + + @Inject + private Logger log; + @Inject + private ApplicationAuditLogger applicationAuditLogger; + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private ScopeService scopeService; + + @Inject + private AttributeService attributeService; + + @Inject + private InumService inumService; + @Inject + private ClientService clientService; + @Inject + private TokenService tokenService; + + @Inject + private MetricService metricService; + + @Inject + private ExternalDynamicClientRegistrationService externalDynamicClientRegistrationService; + + @Inject + private RegisterParamsValidator registerParamsValidator; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private StaticConfiguration staticConfiguration; + + @Inject + private AbstractCryptoProvider cryptoProvider; + + @Inject + private CIBARegisterParamsValidatorService cibaRegisterParamsValidatorService; + + @Inject + private CIBARegisterClientMetadataService cibaRegisterClientMetadataService; + + @Inject + private CIBARegisterClientResponseService cibaRegisterClientResponseService; + + @Inject + private AuthorizationGrantList authorizationGrantList; + + @Override + public Response requestRegister(String requestParams, HttpServletRequest httpRequest, SecurityContext securityContext) { + com.codahale.metrics.Timer.Context timerContext = metricService.getTimer(MetricType.DYNAMIC_CLIENT_REGISTRATION_RATE).time(); + try { + return registerClientImpl(requestParams, httpRequest, securityContext); + } finally { + timerContext.stop(); + } + } + + private Response registerClientImpl(String requestParams, HttpServletRequest httpRequest, SecurityContext securityContext) { + Response.ResponseBuilder builder = Response.status(Response.Status.CREATED); + if (appConfiguration.getReturn200OnClientRegistration()) { + builder = Response.ok(); + } + + OAuth2AuditLog oAuth2AuditLog = new OAuth2AuditLog(ServerUtil.getIpAddress(httpRequest), Action.CLIENT_REGISTRATION); + try { + final JSONObject requestObject = new JSONObject(requestParams); + final JSONObject softwareStatement = validateSoftwareStatement(httpRequest, requestObject); + if (softwareStatement != null) { + log.trace("Override request parameters by software_statement"); + for (String key : softwareStatement.keySet()) { + requestObject.putOpt(key, softwareStatement.get(key)); + } + } + + final RegisterRequest r = RegisterRequest.fromJson(requestObject, appConfiguration.getLegacyDynamicRegistrationScopeParam()); + if (requestObject.has(SOFTWARE_STATEMENT.toString())) { + r.setSoftwareStatement(requestObject.getString(SOFTWARE_STATEMENT.toString())); + } + + log.info("Attempting to register client: applicationType = {}, clientName = {}, redirectUris = {}, isSecure = {}, sectorIdentifierUri = {}, defaultAcrValues = {}", + r.getApplicationType(), r.getClientName(), r.getRedirectUris(), securityContext.isSecure(), r.getSectorIdentifierUri(), r.getDefaultAcrValues()); + log.trace("Registration request = {}", requestParams); + + if (!appConfiguration.getDynamicRegistrationEnabled()) { + log.info("Dynamic client registration is disabled."); + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, RegisterErrorResponseType.ACCESS_DENIED, "Dynamic client registration is disabled."); + } + + if (!appConfiguration.getDynamicRegistrationPasswordGrantTypeEnabled() + && registerParamsValidator.checkIfThereIsPasswordGrantType(r.getGrantTypes())) { + log.info("Password Grant Type is not allowed for Dynamic Client Registration."); + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, RegisterErrorResponseType.ACCESS_DENIED, "Password Grant Type is not allowed for Dynamic Client Registration."); + } + + if (r.getSubjectType() == null) { + SubjectType defaultSubjectType = SubjectType.fromString(appConfiguration.getDefaultSubjectType()); + if (defaultSubjectType != null) { + r.setSubjectType(defaultSubjectType); + } else if (appConfiguration.getSubjectTypesSupported().contains(SubjectType.PUBLIC.toString())) { + r.setSubjectType(SubjectType.PUBLIC); + } else if (appConfiguration.getSubjectTypesSupported().contains(SubjectType.PAIRWISE.toString())) { + r.setSubjectType(SubjectType.PAIRWISE); + } + } + + registerParamsValidator.validateAlgorithms(r); // Throws a WebApplicationException whether a validation doesn't pass + + if (r.getIdTokenSignedResponseAlg() == null) { + r.setIdTokenSignedResponseAlg(SignatureAlgorithm.fromString(appConfiguration.getDefaultSignatureAlgorithm())); + } + if (r.getAccessTokenSigningAlg() == null) { + r.setAccessTokenSigningAlg(SignatureAlgorithm.fromString(appConfiguration.getDefaultSignatureAlgorithm())); + } + + if (r.getClaimsRedirectUris() != null && !r.getClaimsRedirectUris().isEmpty()) { + if (!registerParamsValidator.validateRedirectUris(r.getGrantTypes(), r.getResponseTypes(), + r.getApplicationType(), r.getSubjectType(), r.getClaimsRedirectUris(), r.getSectorIdentifierUri())) { + log.debug("Value of one or more claims_redirect_uris is invalid, claims_redirect_uris: " + r.getClaimsRedirectUris()); + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, RegisterErrorResponseType.INVALID_CLAIMS_REDIRECT_URI, "Value of one or more claims_redirect_uris is invalid"); + } + } + + if (!Strings.isNullOrEmpty(r.getInitiateLoginUri())) { + if (!registerParamsValidator.validateInitiateLoginUri(r.getInitiateLoginUri())) { + log.debug("The Initiate Login Uri is invalid. The initiate_login_uri must use the https schema: " + r.getInitiateLoginUri()); + throw errorResponseFactory.createWebApplicationException( + Response.Status.BAD_REQUEST, + RegisterErrorResponseType.INVALID_CLIENT_METADATA, + "The Initiate Login Uri is invalid. The initiate_login_uri must use the https schema."); + } + } + + final Pair validateResult = registerParamsValidator.validateParamsClientRegister( + r.getApplicationType(), r.getSubjectType(), + r.getGrantTypes(), r.getResponseTypes(), + r.getRedirectUris()); + if (!validateResult.getFirst()) { + log.trace("Client parameters are invalid, returns invalid_request error. Reason: " + validateResult.getSecond()); + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, RegisterErrorResponseType.INVALID_CLIENT_METADATA, validateResult.getSecond()); + } + + if (!registerParamsValidator.validateRedirectUris( + r.getGrantTypes(), r.getResponseTypes(), + r.getApplicationType(), r.getSubjectType(), + r.getRedirectUris(), r.getSectorIdentifierUri())) { + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, RegisterErrorResponseType.INVALID_REDIRECT_URI, "Failed to validate redirect uris."); + } + + if (!cibaRegisterParamsValidatorService.validateParams( + r.getBackchannelTokenDeliveryMode(), + r.getBackchannelClientNotificationEndpoint(), + r.getBackchannelAuthenticationRequestSigningAlg(), + r.getBackchannelUserCodeParameter(), + r.getGrantTypes(), + r.getSubjectType(), + r.getSectorIdentifierUri(), + r.getJwks(), + r.getJwksUri() + )) { // CIBA + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, RegisterErrorResponseType.INVALID_CLIENT_METADATA, + "Invalid Client Metadata registering to use CIBA (Client Initiated Backchannel Authentication)."); + } + + + registerParamsValidator.validateLogoutUri(r.getFrontChannelLogoutUris(), r.getRedirectUris(), errorResponseFactory); + registerParamsValidator.validateLogoutUri(r.getBackchannelLogoutUris(), r.getRedirectUris(), errorResponseFactory); + + String clientsBaseDN = staticConfiguration.getBaseDn().getClients(); + + String inum = inumService.generateClientInum(); + String generatedClientSecret = UUID.randomUUID().toString(); + + final Client client = new Client(); + client.setDn("inum=" + inum + "," + clientsBaseDN); + client.setClientId(inum); + client.setDeletable(true); + client.setClientSecret(clientService.encryptSecret(generatedClientSecret)); + client.setRegistrationAccessToken(HandleTokenFactory.generateHandleToken()); + client.setIdTokenTokenBindingCnf(r.getIdTokenTokenBindingCnf()); + + final Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + client.setClientIdIssuedAt(calendar.getTime()); + + if (appConfiguration.getDynamicRegistrationExpirationTime() > 0) { // #883 : expiration can be -1, mean does not expire + calendar.add(Calendar.SECOND, appConfiguration.getDynamicRegistrationExpirationTime()); + client.setClientSecretExpiresAt(calendar.getTime()); + client.setExpirationDate(calendar.getTime()); + client.setTtl(appConfiguration.getDynamicRegistrationExpirationTime()); + } + client.setDeletable(client.getClientSecretExpiresAt() != null); + + if (StringUtils.isBlank(r.getClientName()) && r.getRedirectUris() != null && !r.getRedirectUris().isEmpty()) { + try { + URI redUri = new URI(r.getRedirectUris().get(0)); + client.setClientName(redUri.getHost()); + } catch (Exception e) { + //ignore + log.error(e.getMessage(), e); + client.setClientName("Unknown"); + } + } + + updateClientFromRequestObject(client, r, false); + + boolean registerClient = true; + if (externalDynamicClientRegistrationService.isEnabled()) { + registerClient = externalDynamicClientRegistrationService.executeExternalCreateClientMethods(r, client); + } + + if (!registerClient) { + log.trace("Client parameters are invalid, returns invalid_request error. External registration script returned false."); + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, RegisterErrorResponseType.INVALID_CLIENT_METADATA, "External registration script returned false."); + } + + Date currentTime = Calendar.getInstance().getTime(); + client.setLastAccessTime(currentTime); + client.setLastLogonTime(currentTime); + + Boolean persistClientAuthorizations = appConfiguration.getDynamicRegistrationPersistClientAuthorizations(); + client.setPersistClientAuthorizations(persistClientAuthorizations != null ? persistClientAuthorizations : false); + + clientService.persist(client); + + JSONObject jsonObject = getJSONObject(client); + builder.entity(jsonObject.toString(4).replace("\\/", "/")); + + log.info("Client registered: clientId = {}, applicationType = {}, clientName = {}, redirectUris = {}, sectorIdentifierUri = {}", + client.getClientId(), client.getApplicationType(), client.getClientName(), client.getRedirectUris(), client.getSectorIdentifierUri()); + + oAuth2AuditLog.setClientId(client.getClientId()); + oAuth2AuditLog.setScope(clientScopesToString(client)); + oAuth2AuditLog.setSuccess(true); + } catch (StringEncrypter.EncryptionException e) { + builder = internalErrorResponse("Encryption exception occured."); + log.error(e.getMessage(), e); + } catch (JSONException e) { + builder = internalErrorResponse("Failed to parse JSON."); + log.error(e.getMessage(), e); + } catch (WebApplicationException e) { + log.error(e.getMessage(), e); + throw e; + } catch (Exception e) { + builder = internalErrorResponse("Unknown."); + log.error(e.getMessage(), e); + } + + builder.cacheControl(ServerUtil.cacheControl(true, false)); + builder.header("Pragma", "no-cache"); + builder.type(MediaType.APPLICATION_JSON_TYPE); + applicationAuditLogger.sendMessage(oAuth2AuditLog); + return builder.build(); + } + + private JSONObject validateSoftwareStatement(HttpServletRequest httpServletRequest, JSONObject requestObject) { + if (!requestObject.has(SOFTWARE_STATEMENT.toString())) { + return null; + } + + try { + Jwt softwareStatement = Jwt.parse(requestObject.getString(SOFTWARE_STATEMENT.toString())); + final SignatureAlgorithm signatureAlgorithm = softwareStatement.getHeader().getSignatureAlgorithm(); + + final SoftwareStatementValidationType validationType = SoftwareStatementValidationType.fromString(appConfiguration.getSoftwareStatementValidationType()); + if (validationType == SoftwareStatementValidationType.NONE) { + log.trace("software_statement validation was skipped due to `softwareStatementValidationType` configuration property set to none. (Not recommended.)"); + return softwareStatement.getClaims().toJsonObject(); + } + + if (validationType == SoftwareStatementValidationType.SCRIPT) { + if (!externalDynamicClientRegistrationService.isEnabled()) { + log.error("Server is mis-configured. softwareStatementValidationType=script but there is no any Dynamic Client Registration script enabled."); + return null; + } + + if (AlgorithmFamily.HMAC.equals(signatureAlgorithm.getFamily())) { + + final String hmacSecret = externalDynamicClientRegistrationService.getSoftwareStatementHmacSecret(httpServletRequest, requestObject, softwareStatement); + if (StringUtils.isBlank(hmacSecret)) { + log.error("No hmacSecret provided in Dynamic Client Registration script (method getSoftwareStatementHmacSecret didn't return actual secret). "); + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, RegisterErrorResponseType.INVALID_SOFTWARE_STATEMENT, ""); + } + + if (!cryptoProvider.verifySignature(softwareStatement.getSigningInput(), softwareStatement.getEncodedSignature(), null, null, hmacSecret, signatureAlgorithm)) { + throw new InvalidJwtException("Invalid signature in the software statement"); + } + + return softwareStatement.getClaims().toJsonObject(); + } + + final JSONObject softwareStatementJwks = externalDynamicClientRegistrationService.getSoftwareStatementJwks(httpServletRequest, requestObject, softwareStatement); + if (softwareStatementJwks == null) { + log.error("No jwks provided in Dynamic Client Registration script (method getSoftwareStatementJwks didn't return actual jwks). "); + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, RegisterErrorResponseType.INVALID_SOFTWARE_STATEMENT, ""); + } + + if (!cryptoProvider.verifySignature(softwareStatement.getSigningInput(), softwareStatement.getEncodedSignature(), softwareStatement.getHeader().getKeyId(), softwareStatementJwks, null, signatureAlgorithm)) { + throw new InvalidJwtException("Invalid signature in the software statement"); + } + + return softwareStatement.getClaims().toJsonObject(); + } + + if ((validationType == SoftwareStatementValidationType.JWKS_URI || + validationType == SoftwareStatementValidationType.JWKS) && + StringUtils.isBlank(appConfiguration.getSoftwareStatementValidationClaimName())) { + log.error("softwareStatementValidationClaimName configuration property is not specified. Please specify claim name from software_statement which points to jwks (or jwks_uri)."); + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, RegisterErrorResponseType.INVALID_SOFTWARE_STATEMENT, "Failed to validate software statement"); + } + + String jwksUriClaim = null; + if (validationType == SoftwareStatementValidationType.JWKS_URI) { + jwksUriClaim = softwareStatement.getClaims().getClaimAsString(appConfiguration.getSoftwareStatementValidationClaimName()); + } + + String jwksClaim = null; + if (validationType == SoftwareStatementValidationType.JWKS) { + jwksClaim = softwareStatement.getClaims().getClaimAsString(appConfiguration.getSoftwareStatementValidationClaimName()); + } + + if (StringUtils.isBlank(jwksUriClaim) && StringUtils.isBlank(jwksClaim)) { + final String msg = String.format("software_statement does not contain `%s` claim and thus is considered as invalid.", appConfiguration.getSoftwareStatementValidationClaimName()); + log.error(msg); + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, RegisterErrorResponseType.INVALID_SOFTWARE_STATEMENT, msg); + } + + JSONObject jwks = Strings.isNullOrEmpty(jwksUriClaim) ? + new JSONObject(jwksClaim) : + JwtUtil.getJSONWebKeys(jwksUriClaim); + + boolean validSignature = cryptoProvider.verifySignature(softwareStatement.getSigningInput(), + softwareStatement.getEncodedSignature(), + softwareStatement.getHeader().getKeyId(), jwks, null, signatureAlgorithm); + + if (!validSignature) { + throw new InvalidJwtException("Invalid cryptographic segment in the software statement"); + } + + return softwareStatement.getClaims().toJsonObject(); + } catch (Exception e) { + final String msg = "Invalid software_statement."; + log.error(msg, e); + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, RegisterErrorResponseType.INVALID_SOFTWARE_STATEMENT, msg); + } + } + + private Response.ResponseBuilder internalErrorResponse(String reason) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .type(MediaType.APPLICATION_JSON_TYPE) + .entity(errorResponseFactory.errorAsJson(RegisterErrorResponseType.INVALID_CLIENT_METADATA, reason)); + } + + // yuriyz - ATTENTION : this method is used for both registration and update client metadata cases, therefore any logic here + // will be applied for both cases. + private void updateClientFromRequestObject(Client p_client, RegisterRequest requestObject, boolean update) throws JSONException { + + JsonApplier.getInstance().transfer(requestObject, p_client); + JsonApplier.getInstance().transfer(requestObject, p_client.getAttributes()); + + List redirectUris = requestObject.getRedirectUris(); + if (redirectUris != null && !redirectUris.isEmpty()) { + redirectUris = new ArrayList<>(new HashSet<>(redirectUris)); // Remove repeated elements + p_client.setRedirectUris(redirectUris.toArray(new String[redirectUris.size()])); + } + List claimsRedirectUris = requestObject.getClaimsRedirectUris(); + if (claimsRedirectUris != null && !claimsRedirectUris.isEmpty()) { + claimsRedirectUris = new ArrayList<>(new HashSet<>(claimsRedirectUris)); // Remove repeated elements + p_client.setClaimRedirectUris(claimsRedirectUris.toArray(new String[claimsRedirectUris.size()])); + } + if (requestObject.getApplicationType() != null) { + p_client.setApplicationType(requestObject.getApplicationType().toString()); + } + if (StringUtils.isNotBlank(requestObject.getClientName())) { + p_client.setClientName(requestObject.getClientName()); + } + if (StringUtils.isNotBlank(requestObject.getSectorIdentifierUri())) { + p_client.setSectorIdentifierUri(requestObject.getSectorIdentifierUri()); + } + + Set responseTypeSet = new HashSet<>(); + responseTypeSet.addAll(requestObject.getResponseTypes()); + + Set grantTypeSet = new HashSet<>(); + grantTypeSet.addAll(requestObject.getGrantTypes()); + + if (isTrue(appConfiguration.getGrantTypesAndResponseTypesAutofixEnabled())) { + if (appConfiguration.getClientRegDefaultToCodeFlowWithRefresh()) { + if (responseTypeSet.size() == 0 && grantTypeSet.size() == 0) { + responseTypeSet.add(ResponseType.CODE); + } + if (responseTypeSet.contains(ResponseType.CODE)) { + grantTypeSet.add(GrantType.AUTHORIZATION_CODE); + grantTypeSet.add(GrantType.REFRESH_TOKEN); + } + if (grantTypeSet.contains(GrantType.AUTHORIZATION_CODE)) { + responseTypeSet.add(ResponseType.CODE); + grantTypeSet.add(GrantType.REFRESH_TOKEN); + } + } + if (responseTypeSet.contains(ResponseType.TOKEN) || responseTypeSet.contains(ResponseType.ID_TOKEN)) { + grantTypeSet.add(GrantType.IMPLICIT); + } + if (grantTypeSet.contains(GrantType.IMPLICIT)) { + responseTypeSet.add(ResponseType.TOKEN); + } + } + + Set> responseTypesSupported = appConfiguration.getResponseTypesSupported(); + Set grantTypesSupported = appConfiguration.getGrantTypesSupported(); + + if (!responseTypesSupported.contains(responseTypeSet)) { + responseTypeSet.clear(); + } + + grantTypeSet.retainAll(grantTypesSupported); + + Set dynamicGrantTypeDefault = appConfiguration.getDynamicGrantTypeDefault(); + grantTypeSet.retainAll(dynamicGrantTypeDefault); + + if (!update || requestObject.getResponseTypes().size() > 0) { + p_client.setResponseTypes(responseTypeSet.toArray(new ResponseType[responseTypeSet.size()])); + } + if (!update) { + p_client.setGrantTypes(grantTypeSet.toArray(new GrantType[grantTypeSet.size()])); + } else if (appConfiguration.getEnableClientGrantTypeUpdate() && requestObject.getGrantTypes().size() > 0) { + p_client.setGrantTypes(grantTypeSet.toArray(new GrantType[grantTypeSet.size()])); + } + + List contacts = requestObject.getContacts(); + if (contacts != null && !contacts.isEmpty()) { + contacts = new ArrayList<>(new HashSet<>(contacts)); // Remove repeated elements + p_client.setContacts(contacts.toArray(new String[contacts.size()])); + } + if (StringUtils.isNotBlank(requestObject.getLogoUri())) { + p_client.setLogoUri(requestObject.getLogoUri()); + } + if (StringUtils.isNotBlank(requestObject.getClientUri())) { + p_client.setClientUri(requestObject.getClientUri()); + } + if (StringUtils.isNotBlank(requestObject.getPolicyUri())) { + p_client.setPolicyUri(requestObject.getPolicyUri()); + } + if (StringUtils.isNotBlank(requestObject.getTosUri())) { + p_client.setTosUri(requestObject.getTosUri()); + } + if (StringUtils.isNotBlank(requestObject.getJwksUri())) { + p_client.setJwksUri(requestObject.getJwksUri()); + } + if (StringUtils.isNotBlank(requestObject.getJwks())) { + p_client.setJwks(requestObject.getJwks()); + } + if (requestObject.getSubjectType() != null) { + p_client.setSubjectType(requestObject.getSubjectType().toString()); + } + if (requestObject.getRptAsJwt() != null) { + p_client.setRptAsJwt(requestObject.getRptAsJwt()); + } + if (requestObject.getAccessTokenAsJwt() != null) { + p_client.setAccessTokenAsJwt(requestObject.getAccessTokenAsJwt()); + } + if (requestObject.getTlsClientAuthSubjectDn() != null) { + p_client.getAttributes().setTlsClientAuthSubjectDn(requestObject.getTlsClientAuthSubjectDn()); + } + if (requestObject.getAllowSpontaneousScopes() != null) { + p_client.getAttributes().setAllowSpontaneousScopes(requestObject.getAllowSpontaneousScopes()); + } + if (requestObject.getSpontaneousScopes() != null) { + p_client.getAttributes().setSpontaneousScopes(requestObject.getSpontaneousScopes()); + } + if (requestObject.getRunIntrospectionScriptBeforeAccessTokenAsJwtCreationAndIncludeClaims() != null) { + p_client.getAttributes().setRunIntrospectionScriptBeforeAccessTokenAsJwtCreationAndIncludeClaims(requestObject.getRunIntrospectionScriptBeforeAccessTokenAsJwtCreationAndIncludeClaims()); + } + if (requestObject.getKeepClientAuthorizationAfterExpiration() != null) { + p_client.getAttributes().setKeepClientAuthorizationAfterExpiration(requestObject.getKeepClientAuthorizationAfterExpiration()); + } + if (requestObject.getAccessTokenSigningAlg() != null) { + p_client.setAccessTokenSigningAlg(requestObject.getAccessTokenSigningAlg().toString()); + } + if (requestObject.getIdTokenSignedResponseAlg() != null) { + p_client.setIdTokenSignedResponseAlg(requestObject.getIdTokenSignedResponseAlg().toString()); + } + if (requestObject.getIdTokenEncryptedResponseAlg() != null) { + p_client.setIdTokenEncryptedResponseAlg(requestObject.getIdTokenEncryptedResponseAlg().toString()); + } + if (requestObject.getIdTokenEncryptedResponseEnc() != null) { + p_client.setIdTokenEncryptedResponseEnc(requestObject.getIdTokenEncryptedResponseEnc().toString()); + } + if (requestObject.getUserInfoSignedResponseAlg() != null) { + p_client.setUserInfoSignedResponseAlg(requestObject.getUserInfoSignedResponseAlg().toString()); + } + if (requestObject.getUserInfoEncryptedResponseAlg() != null) { + p_client.setUserInfoEncryptedResponseAlg(requestObject.getUserInfoEncryptedResponseAlg().toString()); + } + if (requestObject.getUserInfoEncryptedResponseEnc() != null) { + p_client.setUserInfoEncryptedResponseEnc(requestObject.getUserInfoEncryptedResponseEnc().toString()); + } + if (requestObject.getRequestObjectSigningAlg() != null) { + p_client.setRequestObjectSigningAlg(requestObject.getRequestObjectSigningAlg().toString()); + } + if (requestObject.getRequestObjectEncryptionAlg() != null) { + p_client.setRequestObjectEncryptionAlg(requestObject.getRequestObjectEncryptionAlg().toString()); + } + if (requestObject.getRequestObjectEncryptionEnc() != null) { + p_client.setRequestObjectEncryptionEnc(requestObject.getRequestObjectEncryptionEnc().toString()); + } + if (requestObject.getTokenEndpointAuthMethod() != null) { + p_client.setTokenEndpointAuthMethod(requestObject.getTokenEndpointAuthMethod().toString()); + } else { // If omitted, the default is client_secret_basic + p_client.setTokenEndpointAuthMethod(AuthenticationMethod.CLIENT_SECRET_BASIC.toString()); + } + if (requestObject.getTokenEndpointAuthSigningAlg() != null) { + p_client.setTokenEndpointAuthSigningAlg(requestObject.getTokenEndpointAuthSigningAlg().toString()); + } + if (requestObject.getDefaultMaxAge() != null) { + p_client.setDefaultMaxAge(requestObject.getDefaultMaxAge()); + } + if (requestObject.getRequireAuthTime() != null) { + p_client.setRequireAuthTime(requestObject.getRequireAuthTime()); + } + List defaultAcrValues = requestObject.getDefaultAcrValues(); + if (defaultAcrValues != null && !defaultAcrValues.isEmpty()) { + defaultAcrValues = new ArrayList<>(new HashSet<>(defaultAcrValues)); // Remove repeated elements + p_client.setDefaultAcrValues(defaultAcrValues.toArray(new String[defaultAcrValues.size()])); + } + if (StringUtils.isNotBlank(requestObject.getInitiateLoginUri())) { + p_client.setInitiateLoginUri(requestObject.getInitiateLoginUri()); + } + List postLogoutRedirectUris = requestObject.getPostLogoutRedirectUris(); + if (postLogoutRedirectUris != null && !postLogoutRedirectUris.isEmpty()) { + postLogoutRedirectUris = new ArrayList<>(new HashSet<>(postLogoutRedirectUris)); // Remove repeated elements + p_client.setPostLogoutRedirectUris(postLogoutRedirectUris.toArray(new String[postLogoutRedirectUris.size()])); + } + + if (requestObject.getFrontChannelLogoutUris() != null && !requestObject.getFrontChannelLogoutUris().isEmpty()) { + p_client.setFrontChannelLogoutUri(requestObject.getFrontChannelLogoutUris().toArray(new String[requestObject.getFrontChannelLogoutUris().size()])); + } + p_client.setFrontChannelLogoutSessionRequired(requestObject.getFrontChannelLogoutSessionRequired()); + + if (requestObject.getBackchannelLogoutUris() != null && !requestObject.getBackchannelLogoutUris().isEmpty()) { + p_client.getAttributes().setBackchannelLogoutUri(requestObject.getBackchannelLogoutUris()); + } + p_client.getAttributes().setBackchannelLogoutSessionRequired(requestObject.getBackchannelLogoutSessionRequired()); + + List requestUris = requestObject.getRequestUris(); + if (requestUris != null && !requestUris.isEmpty()) { + requestUris = new ArrayList<>(new HashSet<>(requestUris)); // Remove repeated elements + p_client.setRequestUris(requestUris.toArray(new String[requestUris.size()])); + } + + List authorizedOrigins = requestObject.getAuthorizedOrigins(); + if (authorizedOrigins != null && !authorizedOrigins.isEmpty()) { + authorizedOrigins = new ArrayList<>(new HashSet<>(authorizedOrigins)); // Remove repeated elements + p_client.setAuthorizedOrigins(authorizedOrigins.toArray(new String[authorizedOrigins.size()])); + } + + List scopes = requestObject.getScope(); + if (grantTypeSet.contains(GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS) && !appConfiguration.getDynamicRegistrationAllowedPasswordGrantScopes().isEmpty()) { + scopes = Lists.newArrayList(scopes); + scopes.retainAll(appConfiguration.getDynamicRegistrationAllowedPasswordGrantScopes()); + } + List scopesDn; + if (scopes != null && !scopes.isEmpty() + && appConfiguration.getDynamicRegistrationScopesParamEnabled() != null + && appConfiguration.getDynamicRegistrationScopesParamEnabled()) { + List defaultScopes = scopeService.getDefaultScopesDn(); + List requestedScopes = scopeService.getScopesDn(scopes); + Set allowedScopes = new HashSet<>(); + + for (String requestedScope : requestedScopes) { + if (defaultScopes.contains(requestedScope)) { + allowedScopes.add(requestedScope); + } + } + + scopesDn = new ArrayList<>(allowedScopes); + p_client.setScopes(scopesDn.toArray(new String[scopesDn.size()])); + } else if (BooleanUtils.isFalse(appConfiguration.getDynamicRegistrationDisableFallbackScopesAssigning())) { + scopesDn = scopeService.getDefaultScopesDn(); + p_client.setScopes(scopesDn.toArray(new String[scopesDn.size()])); + } + + List claims = requestObject.getClaims(); + if (claims != null && !claims.isEmpty()) { + List claimsDn = attributeService.getAttributesDn(claims); + p_client.setClaims(claimsDn.toArray(new String[claimsDn.size()])); + } + + if (requestObject.getJsonObject() != null) { + // Custom params + putCustomStuffIntoObject(p_client, requestObject.getJsonObject()); + } + + if (requestObject.getAccessTokenLifetime() != null) { + p_client.setAccessTokenLifetime(requestObject.getAccessTokenLifetime()); + } + + if (StringUtils.isNotBlank(requestObject.getSoftwareId())) { + p_client.setSoftwareId(requestObject.getSoftwareId()); + } + if (StringUtils.isNotBlank(requestObject.getSoftwareVersion())) { + p_client.setSoftwareVersion(requestObject.getSoftwareVersion()); + } + if (StringUtils.isNotBlank(requestObject.getSoftwareStatement())) { + p_client.setSoftwareStatement(requestObject.getSoftwareStatement()); + } + + cibaRegisterClientMetadataService.updateClient(p_client, requestObject.getBackchannelTokenDeliveryMode(), + requestObject.getBackchannelClientNotificationEndpoint(), requestObject.getBackchannelAuthenticationRequestSigningAlg(), + requestObject.getBackchannelUserCodeParameter()); + } + + @Override + public Response requestClientUpdate(String requestParams, String clientId, @HeaderParam("Authorization") String authorization, @Context HttpServletRequest httpRequest, @Context SecurityContext securityContext) { + OAuth2AuditLog oAuth2AuditLog = new OAuth2AuditLog(ServerUtil.getIpAddress(httpRequest), Action.CLIENT_UPDATE); + oAuth2AuditLog.setClientId(clientId); + try { + log.debug("Attempting to UPDATE client, client_id: {}, requestParams = {}, isSecure = {}", + clientId, requestParams, securityContext.isSecure()); + final String accessToken = tokenService.getToken(authorization); + + if (StringUtils.isNotBlank(accessToken) && StringUtils.isNotBlank(clientId) && StringUtils.isNotBlank(requestParams)) { + JSONObject requestObject = new JSONObject(requestParams); + final JSONObject softwareStatement = validateSoftwareStatement(httpRequest, requestObject); + if (softwareStatement != null) { + log.trace("Override request parameters by software_statement"); + for (String key : softwareStatement.keySet()) { + requestObject.putOpt(key, softwareStatement.get(key)); + } + } + + final RegisterRequest request = RegisterRequest.fromJson(requestObject, appConfiguration.getLegacyDynamicRegistrationScopeParam()); + if (request != null) { + boolean redirectUrisValidated = true; + if (request.getRedirectUris() != null && !request.getRedirectUris().isEmpty()) { + redirectUrisValidated = registerParamsValidator.validateRedirectUris( + request.getGrantTypes(), request.getResponseTypes(), + request.getApplicationType(), request.getSubjectType(), + request.getRedirectUris(), request.getSectorIdentifierUri()); + } + + if (redirectUrisValidated) { + if (!cibaRegisterParamsValidatorService.validateParams( + request.getBackchannelTokenDeliveryMode(), + request.getBackchannelClientNotificationEndpoint(), + request.getBackchannelAuthenticationRequestSigningAlg(), + request.getBackchannelUserCodeParameter(), + request.getGrantTypes(), + request.getSubjectType(), + request.getSectorIdentifierUri(), + request.getJwks(), + request.getJwksUri() + )) { + return Response.status(Response.Status.BAD_REQUEST). + entity(errorResponseFactory.errorAsJson(RegisterErrorResponseType.INVALID_CLIENT_METADATA, + "Invalid Client Metadata registering to use CIBA.")).build(); + } + + if (request.getSubjectType() != null + && !appConfiguration.getSubjectTypesSupported().contains(request.getSubjectType().toString())) { + log.debug("Client UPDATE : parameter subject_type is invalid. Returns BAD_REQUEST response."); + applicationAuditLogger.sendMessage(oAuth2AuditLog); + return Response.status(Response.Status.BAD_REQUEST). + entity(errorResponseFactory.errorAsJson(RegisterErrorResponseType.INVALID_CLIENT_METADATA, "subject_type is invalid.")).build(); + } + + final Client client = clientService.getClient(clientId, accessToken); + if (client != null) { + updateClientFromRequestObject(client, request, true); + + boolean updateClient = true; + if (externalDynamicClientRegistrationService.isEnabled()) { + updateClient = externalDynamicClientRegistrationService.executeExternalUpdateClientMethods(request, client); + } + + if (updateClient) { + clientService.merge(client); + + oAuth2AuditLog.setScope(clientScopesToString(client)); + oAuth2AuditLog.setSuccess(true); + applicationAuditLogger.sendMessage(oAuth2AuditLog); + return Response.status(Response.Status.OK).entity(clientAsEntity(client)).build(); + } else { + log.trace("The Access Token is not valid for the Client ID, returns invalid_token error."); + applicationAuditLogger.sendMessage(oAuth2AuditLog); + return Response.status(Response.Status.BAD_REQUEST). + type(MediaType.APPLICATION_JSON_TYPE). + entity(errorResponseFactory.errorAsJson(RegisterErrorResponseType.INVALID_TOKEN, "External registration script returned false.")).build(); + } + } else { + log.trace("The Access Token is not valid for the Client ID, returns invalid_token error."); + applicationAuditLogger.sendMessage(oAuth2AuditLog); + return Response.status(Response.Status.BAD_REQUEST). + type(MediaType.APPLICATION_JSON_TYPE). + entity(errorResponseFactory.errorAsJson(RegisterErrorResponseType.INVALID_TOKEN, "The Access Token is not valid for the Client ID.")).build(); + } + } + } + } + + log.debug("Client UPDATE : parameters are invalid. Returns BAD_REQUEST response."); + applicationAuditLogger.sendMessage(oAuth2AuditLog); + return Response.status(Response.Status.BAD_REQUEST). + entity(errorResponseFactory.errorAsJson(RegisterErrorResponseType.INVALID_CLIENT_METADATA, "Unknown.")).build(); + + } catch (WebApplicationException e) { + log.error(e.getMessage(), e); + throw e; + } catch (Exception e) { + log.error(e.getMessage(), e); + } + applicationAuditLogger.sendMessage(oAuth2AuditLog); + return internalErrorResponse("Unknown.").build(); + } + + @Override + public Response requestClientRead(String clientId, String authorization, HttpServletRequest httpRequest, + SecurityContext securityContext) { + String accessToken = tokenService.getToken(authorization); + log.debug("Attempting to read client: clientId = {}, registrationAccessToken = {} isSecure = {}", + clientId, accessToken, securityContext.isSecure()); + Response.ResponseBuilder builder = Response.ok(); + + OAuth2AuditLog oAuth2AuditLog = new OAuth2AuditLog(ServerUtil.getIpAddress(httpRequest), Action.CLIENT_READ); + oAuth2AuditLog.setClientId(clientId); + try { + if (appConfiguration.getDynamicRegistrationEnabled()) { + if (registerParamsValidator.validateParamsClientRead(clientId, accessToken)) { + Client client = clientService.getClient(clientId, accessToken); + if (client != null) { + oAuth2AuditLog.setScope(clientScopesToString(client)); + oAuth2AuditLog.setSuccess(true); + builder.entity(clientAsEntity(client)); + } else { + log.trace("The Access Token is not valid for the Client ID, returns invalid_token error."); + builder = Response.status(Response.Status.BAD_REQUEST.getStatusCode()).type(MediaType.APPLICATION_JSON_TYPE); + builder.entity(errorResponseFactory.errorAsJson(RegisterErrorResponseType.INVALID_TOKEN, "The Access Token is not valid for the Client")); + } + } else { + log.trace("Client ID or Access Token is not valid."); + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, RegisterErrorResponseType.INVALID_CLIENT_METADATA, "Client ID or Access Token is not valid."); + } + } else { + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, RegisterErrorResponseType.ACCESS_DENIED, "Dynamic registration is disabled."); + } + } catch (JSONException e) { + log.error(e.getMessage(), e); + throw errorResponseFactory.createWebApplicationException(Response.Status.INTERNAL_SERVER_ERROR, RegisterErrorResponseType.INVALID_CLIENT_METADATA, "Failed to parse json."); + } catch (StringEncrypter.EncryptionException e) { + log.error(e.getMessage(), e); + throw errorResponseFactory.createWebApplicationException(Response.Status.INTERNAL_SERVER_ERROR, RegisterErrorResponseType.INVALID_CLIENT_METADATA, "Encryption exception occurred."); + } + + builder.cacheControl(ServerUtil.cacheControl(true, false)); + builder.header("Pragma", "no-cache"); + applicationAuditLogger.sendMessage(oAuth2AuditLog); + return builder.build(); + } + + private String clientAsEntity(Client p_client) throws JSONException, StringEncrypter.EncryptionException { + final JSONObject jsonObject = getJSONObject(p_client); + return jsonObject.toString(4).replace("\\/", "/"); + } + + private JSONObject getJSONObject(Client client) throws JSONException, StringEncrypter.EncryptionException { + JSONObject responseJsonObject = new JSONObject(); + + JsonApplier.getInstance().apply(client, responseJsonObject); + JsonApplier.getInstance().apply(client.getAttributes(), responseJsonObject); + + Util.addToJSONObjectIfNotNull(responseJsonObject, RegisterResponseParam.CLIENT_ID.toString(), client.getClientId()); + if (appConfiguration.getReturnClientSecretOnRead()) { + Util.addToJSONObjectIfNotNull(responseJsonObject, CLIENT_SECRET.toString(), clientService.decryptSecret(client.getClientSecret())); + } + Util.addToJSONObjectIfNotNull(responseJsonObject, RegisterResponseParam.REGISTRATION_ACCESS_TOKEN.toString(), client.getRegistrationAccessToken()); + Util.addToJSONObjectIfNotNull(responseJsonObject, REGISTRATION_CLIENT_URI.toString(), + appConfiguration.getRegistrationEndpoint() + "?" + + RegisterResponseParam.CLIENT_ID.toString() + "=" + client.getClientId()); + responseJsonObject.put(CLIENT_ID_ISSUED_AT.toString(), client.getClientIdIssuedAt().getTime() / 1000); + responseJsonObject.put(CLIENT_SECRET_EXPIRES_AT.toString(), client.getClientSecretExpiresAt() != null && client.getClientSecretExpiresAt().getTime() > 0 ? + client.getClientSecretExpiresAt().getTime() / 1000 : 0); + + Util.addToJSONObjectIfNotNull(responseJsonObject, REDIRECT_URIS.toString(), client.getRedirectUris()); + Util.addToJSONObjectIfNotNull(responseJsonObject, CLAIMS_REDIRECT_URIS.toString(), client.getClaimRedirectUris()); + Util.addToJSONObjectIfNotNull(responseJsonObject, RESPONSE_TYPES.toString(), ResponseType.toStringArray(client.getResponseTypes())); + Util.addToJSONObjectIfNotNull(responseJsonObject, GRANT_TYPES.toString(), GrantType.toStringArray(client.getGrantTypes())); + Util.addToJSONObjectIfNotNull(responseJsonObject, APPLICATION_TYPE.toString(), client.getApplicationType()); + Util.addToJSONObjectIfNotNull(responseJsonObject, CONTACTS.toString(), client.getContacts()); + Util.addToJSONObjectIfNotNull(responseJsonObject, CLIENT_NAME.toString(), client.getClientName()); + Util.addToJSONObjectIfNotNull(responseJsonObject, LOGO_URI.toString(), client.getLogoUri()); + Util.addToJSONObjectIfNotNull(responseJsonObject, CLIENT_URI.toString(), client.getClientUri()); + Util.addToJSONObjectIfNotNull(responseJsonObject, POLICY_URI.toString(), client.getPolicyUri()); + Util.addToJSONObjectIfNotNull(responseJsonObject, TOS_URI.toString(), client.getTosUri()); + Util.addToJSONObjectIfNotNull(responseJsonObject, JWKS_URI.toString(), client.getJwksUri()); + Util.addToJSONObjectIfNotNull(responseJsonObject, SECTOR_IDENTIFIER_URI.toString(), client.getSectorIdentifierUri()); + Util.addToJSONObjectIfNotNull(responseJsonObject, SUBJECT_TYPE.toString(), client.getSubjectType()); + Util.addToJSONObjectIfNotNull(responseJsonObject, ID_TOKEN_SIGNED_RESPONSE_ALG.toString(), client.getIdTokenSignedResponseAlg()); + Util.addToJSONObjectIfNotNull(responseJsonObject, ID_TOKEN_ENCRYPTED_RESPONSE_ALG.toString(), client.getIdTokenEncryptedResponseAlg()); + Util.addToJSONObjectIfNotNull(responseJsonObject, ID_TOKEN_ENCRYPTED_RESPONSE_ENC.toString(), client.getIdTokenEncryptedResponseEnc()); + Util.addToJSONObjectIfNotNull(responseJsonObject, USERINFO_SIGNED_RESPONSE_ALG.toString(), client.getUserInfoSignedResponseAlg()); + Util.addToJSONObjectIfNotNull(responseJsonObject, USERINFO_ENCRYPTED_RESPONSE_ALG.toString(), client.getUserInfoEncryptedResponseAlg()); + Util.addToJSONObjectIfNotNull(responseJsonObject, USERINFO_ENCRYPTED_RESPONSE_ENC.toString(), client.getUserInfoEncryptedResponseEnc()); + Util.addToJSONObjectIfNotNull(responseJsonObject, REQUEST_OBJECT_SIGNING_ALG.toString(), client.getRequestObjectSigningAlg()); + Util.addToJSONObjectIfNotNull(responseJsonObject, REQUEST_OBJECT_ENCRYPTION_ALG.toString(), client.getRequestObjectEncryptionAlg()); + Util.addToJSONObjectIfNotNull(responseJsonObject, REQUEST_OBJECT_ENCRYPTION_ENC.toString(), client.getRequestObjectEncryptionEnc()); + Util.addToJSONObjectIfNotNull(responseJsonObject, TOKEN_ENDPOINT_AUTH_METHOD.toString(), client.getTokenEndpointAuthMethod()); + Util.addToJSONObjectIfNotNull(responseJsonObject, TOKEN_ENDPOINT_AUTH_SIGNING_ALG.toString(), client.getTokenEndpointAuthSigningAlg()); + Util.addToJSONObjectIfNotNull(responseJsonObject, DEFAULT_MAX_AGE.toString(), client.getDefaultMaxAge()); + Util.addToJSONObjectIfNotNull(responseJsonObject, REQUIRE_AUTH_TIME.toString(), client.getRequireAuthTime()); + Util.addToJSONObjectIfNotNull(responseJsonObject, DEFAULT_ACR_VALUES.toString(), client.getDefaultAcrValues()); + Util.addToJSONObjectIfNotNull(responseJsonObject, INITIATE_LOGIN_URI.toString(), client.getInitiateLoginUri()); + Util.addToJSONObjectIfNotNull(responseJsonObject, POST_LOGOUT_REDIRECT_URIS.toString(), client.getPostLogoutRedirectUris()); + Util.addToJSONObjectIfNotNull(responseJsonObject, REQUEST_URIS.toString(), client.getRequestUris()); + Util.addToJSONObjectIfNotNull(responseJsonObject, AUTHORIZED_ORIGINS.toString(), client.getAuthorizedOrigins()); + Util.addToJSONObjectIfNotNull(responseJsonObject, RPT_AS_JWT.toString(), client.isRptAsJwt()); + Util.addToJSONObjectIfNotNull(responseJsonObject, TLS_CLIENT_AUTH_SUBJECT_DN.toString(), client.getAttributes().getTlsClientAuthSubjectDn()); + Util.addToJSONObjectIfNotNull(responseJsonObject, ALLOW_SPONTANEOUS_SCOPES.toString(), client.getAttributes().getAllowSpontaneousScopes()); + Util.addToJSONObjectIfNotNull(responseJsonObject, SPONTANEOUS_SCOPES.toString(), client.getAttributes().getSpontaneousScopes()); + Util.addToJSONObjectIfNotNull(responseJsonObject, RUN_INTROSPECTION_SCRIPT_BEFORE_ACCESS_TOKEN_CREATION_AS_JWT_AND_INCLUDE_CLAIMS.toString(), client.getAttributes().getRunIntrospectionScriptBeforeAccessTokenAsJwtCreationAndIncludeClaims()); + Util.addToJSONObjectIfNotNull(responseJsonObject, KEEP_CLIENT_AUTHORIZATION_AFTER_EXPIRATION.toString(), client.getAttributes().getKeepClientAuthorizationAfterExpiration()); + Util.addToJSONObjectIfNotNull(responseJsonObject, ACCESS_TOKEN_AS_JWT.toString(), client.isAccessTokenAsJwt()); + Util.addToJSONObjectIfNotNull(responseJsonObject, ACCESS_TOKEN_SIGNING_ALG.toString(), client.getAccessTokenSigningAlg()); + Util.addToJSONObjectIfNotNull(responseJsonObject, ACCESS_TOKEN_LIFETIME.toString(), client.getAccessTokenLifetime()); + Util.addToJSONObjectIfNotNull(responseJsonObject, SOFTWARE_ID.toString(), client.getSoftwareId()); + Util.addToJSONObjectIfNotNull(responseJsonObject, SOFTWARE_VERSION.toString(), client.getSoftwareVersion()); + Util.addToJSONObjectIfNotNull(responseJsonObject, SOFTWARE_STATEMENT.toString(), client.getSoftwareStatement()); + + if (!Util.isNullOrEmpty(client.getJwks())) { + Util.addToJSONObjectIfNotNull(responseJsonObject, JWKS.toString(), new JSONObject(client.getJwks())); + } + + // Logout params + Util.addToJSONObjectIfNotNull(responseJsonObject, FRONT_CHANNEL_LOGOUT_URI.toString(), client.getFrontChannelLogoutUri()); + Util.addToJSONObjectIfNotNull(responseJsonObject, FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED.toString(), client.getFrontChannelLogoutSessionRequired()); + Util.addToJSONObjectIfNotNull(responseJsonObject, BACKCHANNEL_LOGOUT_URI.toString(), client.getAttributes().getBackchannelLogoutUri()); + Util.addToJSONObjectIfNotNull(responseJsonObject, BACKCHANNEL_LOGOUT_SESSION_REQUIRED.toString(), client.getAttributes().getBackchannelLogoutSessionRequired()); + + // Custom Params + String[] scopeNames = null; + String[] scopeDns = client.getScopes(); + if (scopeDns != null) { + scopeNames = new String[scopeDns.length]; + for (int i = 0; i < scopeDns.length; i++) { + Scope scope = scopeService.getScopeByDn(scopeDns[i]); + scopeNames[i] = scope.getId(); + } + } + + if (appConfiguration.getLegacyDynamicRegistrationScopeParam()) { + Util.addToJSONObjectIfNotNull(responseJsonObject, SCOPES.toString(), scopeNames); + } else { + Util.addToJSONObjectIfNotNull(responseJsonObject, SCOPE.toString(), implode(scopeNames, " ")); + } + + String[] claimNames = null; + String[] claimDns = client.getClaims(); + if (claimDns != null) { + claimNames = new String[claimDns.length]; + for (int i = 0; i < claimDns.length; i++) { + GluuAttribute gluuAttribute = attributeService.getAttributeByDn(claimDns[i]); + claimNames[i] = gluuAttribute.getOxAuthClaimName(); + } + } + + putCustomAttributesInResponse(client, responseJsonObject); + + if (claimNames != null && claimNames.length > 0) { + Util.addToJSONObjectIfNotNull(responseJsonObject, CLAIMS.toString(), implode(claimNames, " ")); + } + + cibaRegisterClientResponseService.updateResponse(responseJsonObject, client); + + return responseJsonObject; + } + + private void putCustomAttributesInResponse(Client client, JSONObject responseJsonObject) { + final List allowedCustomAttributeNames = appConfiguration.getDynamicRegistrationCustomAttributes(); + final List customAttributes = client.getCustomAttributes(); + if (allowedCustomAttributeNames == null || allowedCustomAttributeNames.isEmpty() || customAttributes == null) { + return; + } + + for (CustomAttribute attribute : customAttributes) { + if (!allowedCustomAttributeNames.contains(attribute.getName())) + continue; + + if (attribute.isMultiValued()) { + Util.addToJSONObjectIfNotNull(responseJsonObject, attribute.getName(), attribute.getValues()); + } else { + Util.addToJSONObjectIfNotNull(responseJsonObject, attribute.getName(), attribute.getValue()); + } + } + } + + /** + * Puts custom object class and custom attributes in client object for persistence. + * + * @param p_client client object + * @param p_requestObject request object + */ + private void putCustomStuffIntoObject(Client p_client, JSONObject p_requestObject) throws JSONException { + // custom object class + final String customOC = appConfiguration.getDynamicRegistrationCustomObjectClass(); + if (StringUtils.isNotBlank(customOC)) { + p_client.setCustomObjectClasses(new String[]{customOC}); + } + + // custom attributes (custom attributes must be in custom object class) + final List attrList = appConfiguration.getDynamicRegistrationCustomAttributes(); + if (attrList != null && !attrList.isEmpty()) { + for (String attr : attrList) { + if (p_requestObject.has(attr)) { + final JSONArray parameterValuesJsonArray = p_requestObject.optJSONArray(attr); + final List parameterValues = parameterValuesJsonArray != null ? + toList(parameterValuesJsonArray) : + Arrays.asList(p_requestObject.getString(attr)); + if (parameterValues != null && !parameterValues.isEmpty()) { + try { + boolean processed = processApplicationAttributes(p_client, attr, parameterValues); + if (!processed) { + p_client.getCustomAttributes().add(new CustomAttribute(attr, parameterValues)); + } + } catch (Exception e) { + log.debug(e.getMessage(), e); + } + } + } + } + } + } + + private boolean processApplicationAttributes(Client p_client, String attr, final List parameterValues) { + if (StringHelper.equalsIgnoreCase("oxAuthTrustedClient", attr)) { + boolean trustedClient = StringHelper.toBoolean(parameterValues.get(0), false); + p_client.setTrustedClient(trustedClient); + + return true; + } else if (StringHelper.equalsIgnoreCase("oxIncludeClaimsInIdToken", attr)) { + boolean includeClaimsInIdToken = StringHelper.toBoolean(parameterValues.get(0), false); + p_client.setIncludeClaimsInIdToken(includeClaimsInIdToken); + + return true; + } + + return false; + } + + private String clientScopesToString(Client client) { + String[] scopeDns = client.getScopes(); + if (scopeDns != null) { + String[] scopeNames = new String[scopeDns.length]; + for (int i = 0; i < scopeDns.length; i++) { + Scope scope = scopeService.getScopeByDn(scopeDns[i]); + scopeNames[i] = scope.getId(); + } + return StringUtils.join(scopeNames, " "); + } + return null; + } + + @Override + public Response delete(String clientId, String authorization, HttpServletRequest httpRequest, SecurityContext securityContext) { + OAuth2AuditLog auditLog = new OAuth2AuditLog(ServerUtil.getIpAddress(httpRequest), Action.CLIENT_DELETE); + auditLog.setClientId(clientId); + + try { + String accessToken = tokenService.getToken(authorization); + + log.debug("Attempting to delete client: clientId = {0}, registrationAccessToken = {1} isSecure = {2}", + clientId, accessToken, securityContext.isSecure()); + + if (!appConfiguration.getDynamicRegistrationEnabled()) { + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, RegisterErrorResponseType.ACCESS_DENIED, "Dynamic registration is disabled."); + } + + if (!registerParamsValidator.validateParamsClientRead(clientId, accessToken)) { + log.trace("Client parameters are invalid."); + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, RegisterErrorResponseType.INVALID_CLIENT_METADATA, ""); + } + + Client client = clientService.getClient(clientId, accessToken); + if (client == null) { + throw errorResponseFactory.createWebApplicationException(Response.Status.UNAUTHORIZED, RegisterErrorResponseType.INVALID_TOKEN, ""); + } + + clientService.remove(client); + auditLog.setSuccess(true); + + return Response + .status(Response.Status.NO_CONTENT) + .cacheControl(ServerUtil.cacheControl(true, false)) + .header("Pragma", "no-cache").build(); + } catch (WebApplicationException e) { + if (e.getResponse() != null) { + return e.getResponse(); + } + throw e; + } catch (Exception e) { + log.error(e.getMessage(), e); + throw errorResponseFactory.createWebApplicationException(Response.Status.INTERNAL_SERVER_ERROR, RegisterErrorResponseType.INVALID_CLIENT_METADATA, "Failed to process request."); + } finally { + applicationAuditLogger.sendMessage(auditLog); + } + } + +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/revoke/RevokeRestWebService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/revoke/RevokeRestWebService.java new file mode 100644 index 00000000..325bb5af --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/revoke/RevokeRestWebService.java @@ -0,0 +1,46 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.revoke; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.FormParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; + +/** + * Provides interface for token revocation REST web services. + *

+ * The oxAuth authorization server's revocation policy acts as follows: + * The revocation of a particular token cause the revocation of related + * tokens and the underlying authorization grant. If the particular + * token is a refresh token, then the authorization server will also + * invalidate all access tokens based on the same authorization grant. + * If the token passed to the request is an access token, the server will + * revoke the respective refresh token as well. + * + * @author Javier Rojas Blum + * @version January 16, 2019 + */ +public interface RevokeRestWebService { + + @POST + @Path("/revoke") + @Produces({MediaType.APPLICATION_JSON}) + Response requestAccessToken( + @FormParam("token") String token, + @FormParam("token_type_hint") String tokenTypeHint, + @FormParam("client_id") String clientId, + @Context HttpServletRequest request, + @Context HttpServletResponse response, + @Context SecurityContext sec); +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/revoke/RevokeRestWebServiceImpl.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/revoke/RevokeRestWebServiceImpl.java new file mode 100644 index 00000000..e929483d --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/revoke/RevokeRestWebServiceImpl.java @@ -0,0 +1,152 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.revoke; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.audit.ApplicationAuditLogger; +import org.gluu.oxauth.model.audit.Action; +import org.gluu.oxauth.model.audit.OAuth2AuditLog; +import org.gluu.oxauth.model.common.AuthorizationGrant; +import org.gluu.oxauth.model.common.AuthorizationGrantList; +import org.gluu.oxauth.model.common.TokenTypeHint; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.session.SessionClient; +import org.gluu.oxauth.model.token.TokenRevocationErrorResponseType; +import org.gluu.oxauth.security.Identity; +import org.gluu.oxauth.service.ClientService; +import org.gluu.oxauth.service.GrantService; +import org.gluu.oxauth.service.external.ExternalRevokeTokenService; +import org.gluu.oxauth.service.external.context.RevokeTokenContext; +import org.gluu.oxauth.util.ServerUtil; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.Path; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; + +/** + * Provides interface for token revocation REST web services + * + * @author Javier Rojas Blum + * @author Yuriy Zabrovarnyy + */ +@Path("/") +public class RevokeRestWebServiceImpl implements RevokeRestWebService { + + @Inject + private Logger log; + + @Inject + private ApplicationAuditLogger applicationAuditLogger; + + @Inject + private Identity identity; + + @Inject + private AuthorizationGrantList authorizationGrantList; + + @Inject + private GrantService grantService; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private ClientService clientService; + + @Inject + private ExternalRevokeTokenService externalRevokeTokenService; + + @Override + public Response requestAccessToken(String token, String tokenTypeHint, String clientId, + HttpServletRequest request, HttpServletResponse response, SecurityContext sec) { + log.debug("Attempting to revoke token: token = {}, tokenTypeHint = {}, isSecure = {}", token, tokenTypeHint, sec.isSecure()); + OAuth2AuditLog oAuth2AuditLog = new OAuth2AuditLog(ServerUtil.getIpAddress(request), Action.TOKEN_REVOCATION); + + validateToken(token); + + Response.ResponseBuilder builder = Response.ok(); + SessionClient sessionClient = identity.getSessionClient(); + + Client client = sessionClient != null ? sessionClient.getClient() : null; + if (client == null) { + client = clientService.getClient(clientId); + if (!clientService.isPublic(client)) { + log.trace("Client is not public and not authenticated. Skip revoking."); + return response(builder, oAuth2AuditLog); + } + } + if (client == null) { + log.trace("Client is not unknown. Skip revoking."); + return response(builder, oAuth2AuditLog); + } + + oAuth2AuditLog.setClientId(client.getClientId()); + + TokenTypeHint tth = TokenTypeHint.getByValue(tokenTypeHint); + AuthorizationGrant authorizationGrant = null; + + if (tth == TokenTypeHint.ACCESS_TOKEN) { + authorizationGrant = authorizationGrantList.getAuthorizationGrantByAccessToken(token); + } else if (tth == TokenTypeHint.REFRESH_TOKEN) { + authorizationGrant = authorizationGrantList.getAuthorizationGrantByRefreshToken(client.getClientId(), token); + } else { + // Since the hint about the type of the token submitted for revocation is optional. oxAuth will + // search it as Access Token then as Refresh Token. + authorizationGrant = authorizationGrantList.getAuthorizationGrantByAccessToken(token); + if (authorizationGrant == null) { + authorizationGrant = authorizationGrantList.getAuthorizationGrantByRefreshToken(client.getClientId(), token); + } + } + + if (authorizationGrant == null) { + log.trace("Unable to find token."); + return response(builder, oAuth2AuditLog); + } + if (!authorizationGrant.getClientId().equals(client.getClientId())) { + log.trace("Token was issued with client {} but revoke is requested with client {}. Skip revoking.", authorizationGrant.getClientId(), client.getClientId()); + return response(builder, oAuth2AuditLog); + } + + RevokeTokenContext revokeTokenContext = new RevokeTokenContext(request, client, authorizationGrant, builder); + final boolean scriptResult = externalRevokeTokenService.revokeTokenMethods(revokeTokenContext); + if (!scriptResult) { + log.trace("Revoke is forbidden by 'Revoke Token' custom script (method returned false). Exit without revoking."); + return response(builder, oAuth2AuditLog); + } + + grantService.removeAllByGrantId(authorizationGrant.getGrantId()); + log.trace("Revoked successfully."); + + return response(builder, oAuth2AuditLog); + } + + private void validateToken(String token) { + if (StringUtils.isBlank(token)) { + throw new WebApplicationException(Response + .status(Response.Status.BAD_REQUEST.getStatusCode()) + .type(MediaType.APPLICATION_JSON_TYPE) + .entity(errorResponseFactory.errorAsJson(TokenRevocationErrorResponseType.INVALID_REQUEST, "Failed to validate token.")) + .build()); + } + } + + private Response response(Response.ResponseBuilder builder, OAuth2AuditLog oAuth2AuditLog) { + builder.cacheControl(ServerUtil.cacheControl(true, false)); + builder.header("Pragma", "no-cache"); + + applicationAuditLogger.sendMessage(oAuth2AuditLog); + + return builder.build(); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/revoke/RevokeSessionRestWebService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/revoke/RevokeSessionRestWebService.java new file mode 100644 index 00000000..fbc33a7b --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/revoke/RevokeSessionRestWebService.java @@ -0,0 +1,113 @@ +package org.gluu.oxauth.revoke; + +import org.apache.commons.lang.ArrayUtils; +import org.gluu.oxauth.model.common.User; +import org.gluu.oxauth.model.config.Constants; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.session.EndSessionErrorResponseType; +import org.gluu.oxauth.model.session.SessionClient; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.model.session.SessionIdState; +import org.gluu.oxauth.security.Identity; +import org.gluu.oxauth.service.ScopeService; +import org.gluu.oxauth.service.SessionIdService; +import org.gluu.oxauth.service.UserService; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + + +/** + * @author Yuriy Zabrovarnyy + */ +@Path("/") +public class RevokeSessionRestWebService { + + @Inject + private Logger log; + + @Inject + private UserService userService; + + @Inject + private SessionIdService sessionIdService; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private Identity identity; + + @Inject + private ScopeService scopeService; + + @POST + @Path("/revoke_session") + @Produces({MediaType.APPLICATION_JSON}) + public Response requestRevokeSession( + @FormParam("user_criterion_key") String userCriterionKey, + @FormParam("user_criterion_value") String userCriterionValue, + @Context HttpServletRequest request, + @Context HttpServletResponse response, + @Context SecurityContext sec) { + try { + log.debug("Attempting to revoke session: userCriterionKey = {}, userCriterionValue = {}, isSecure = {}", + userCriterionKey, userCriterionValue, sec.isSecure()); + + validateAccess(); + + final User user = userService.getUserByAttribute(userCriterionKey, userCriterionValue); + if (user == null) { + log.trace("Unable to find user by {}={}", userCriterionKey, userCriterionValue); + return Response.ok().build(); // no error because we don't want to disclose internal AS info about users + } + + List sessionIdList = sessionIdService.findByUser(user.getDn()); + if (sessionIdList == null || sessionIdList.isEmpty()) { + log.trace("No sessions found for user uid: {}, dn: {}", user.getUserId(), user.getDn()); + return Response.ok().build(); + } + + final List authenticatedSessions = sessionIdList.stream().filter(sessionId -> sessionId.getState() == SessionIdState.AUTHENTICATED).collect(Collectors.toList()); + sessionIdService.remove(authenticatedSessions); + log.debug("Revoked {} user's sessions (user: {})", authenticatedSessions.size(), user.getUserId()); + + return Response.ok().build(); + } catch (WebApplicationException e) { + throw e; + } catch (Exception e) { + log.error(e.getMessage(), e); + return Response.status(500).build(); + } + } + + private void validateAccess() { + SessionClient sessionClient = identity.getSessionClient(); + if (sessionClient == null || sessionClient.getClient() == null || ArrayUtils.isEmpty(sessionClient.getClient().getScopes())) { + log.debug("Client failed to authenticate."); + throw new WebApplicationException( + Response.status(Response.Status.UNAUTHORIZED.getStatusCode()) + .entity(errorResponseFactory.getErrorAsJson(EndSessionErrorResponseType.INVALID_REQUEST)) + .build()); + } + + List scopesAllowedIds = scopeService.getScopeIdsByDns(Arrays.asList(sessionClient.getClient().getScopes())); + + if (!scopesAllowedIds.contains(Constants.REVOKE_SESSION_SCOPE)) { + log.debug("Client does not have required revoke_session scope."); + throw new WebApplicationException(Response.status(Response.Status.UNAUTHORIZED.getStatusCode()) + .entity(errorResponseFactory.getErrorAsJson(EndSessionErrorResponseType.INVALID_REQUEST)) + .build()); + } + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/security/Identity.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/security/Identity.java new file mode 100644 index 00000000..fa4a4cb4 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/security/Identity.java @@ -0,0 +1,59 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.security; + +import javax.annotation.Priority; +import javax.enterprise.context.RequestScoped; +import javax.enterprise.inject.Alternative; +import javax.inject.Named; +import javax.interceptor.Interceptor; + +import org.gluu.oxauth.model.session.SessionClient; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.model.common.User; + +/** + * @version August 9, 2017 + */ +@Alternative +@Priority(Interceptor.Priority.APPLICATION + 20) +@RequestScoped +@Named +public class Identity extends org.gluu.model.security.Identity { + + private static final long serialVersionUID = 2751659008033189259L; + + private SessionId sessionId; + + private User user; + private SessionClient sessionClient; + + public SessionId getSessionId() { + return sessionId; + } + + public void setSessionId(SessionId sessionId) { + this.sessionId = sessionId; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public void setSessionClient(SessionClient sessionClient) { + this.sessionClient = sessionClient; + } + + public SessionClient getSessionClient() { + return sessionClient; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/AppInitializer.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/AppInitializer.java new file mode 100644 index 00000000..63c16f84 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/AppInitializer.java @@ -0,0 +1,715 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service; + +import com.google.common.collect.Lists; +import org.gluu.exception.ConfigurationException; +import org.gluu.model.AuthenticationScriptUsageType; +import org.gluu.model.SimpleProperty; +import org.gluu.model.custom.script.CustomScriptType; +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.model.ldap.GluuLdapConfiguration; +import org.gluu.oxauth.model.auth.AuthenticationMode; +import org.gluu.oxauth.model.config.ConfigurationFactory; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.service.cdi.event.AuthConfigurationEvent; +import org.gluu.oxauth.service.cdi.event.ReloadAuthScript; +import org.gluu.oxauth.service.ciba.CibaRequestsProcessorJob; +import org.gluu.oxauth.service.common.ApplicationFactory; +import org.gluu.oxauth.service.common.EncryptionService; +import org.gluu.oxauth.service.expiration.ExpirationNotificatorTimer; +import org.gluu.oxauth.service.external.ExternalAuthenticationService; +import org.gluu.oxauth.service.logger.LoggerService; +import org.gluu.oxauth.service.stat.StatService; +import org.gluu.oxauth.service.stat.StatTimer; +import org.gluu.oxauth.service.status.ldap.LdapStatusTimer; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.persist.PersistenceEntryManagerFactory; +import org.gluu.persist.exception.BasePersistenceException; +import org.gluu.persist.ldap.impl.LdapEntryManagerFactory; +import org.gluu.persist.model.PersistenceConfiguration; +import org.gluu.service.PythonService; +import org.gluu.service.cdi.async.Asynchronous; +import org.gluu.service.cdi.event.ApplicationInitialized; +import org.gluu.service.cdi.event.ApplicationInitializedEvent; +import org.gluu.service.cdi.event.LdapConfigurationReload; +import org.gluu.service.cdi.event.Scheduled; +import org.gluu.service.cdi.util.CdiUtil; +import org.gluu.service.custom.lib.CustomLibrariesLoader; +import org.gluu.service.custom.script.CustomScriptManager; +import org.gluu.service.external.ExternalPersistenceExtensionService; +import org.gluu.service.metric.inject.ReportMetric; +import org.gluu.service.timer.QuartzSchedulerManager; +import org.gluu.service.timer.event.TimerEvent; +import org.gluu.service.timer.schedule.TimerSchedule; +import org.gluu.util.OxConstants; +import org.gluu.util.StringHelper; +import org.gluu.orm.util.properties.FileConfiguration; +import org.gluu.util.security.SecurityProviderUtility; +import org.gluu.util.security.StringEncrypter; +import org.gluu.util.security.StringEncrypter.EncryptionException; +import org.jboss.weld.util.reflection.ParameterizedTypeImpl; +import org.oxauth.persistence.model.configuration.GluuConfiguration; +import org.oxauth.persistence.model.configuration.oxIDPAuthConf; +import org.slf4j.Logger; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.BeforeDestroyed; +import javax.enterprise.context.Initialized; +import javax.enterprise.event.Event; +import javax.enterprise.event.Observes; +import javax.enterprise.inject.Instance; +import javax.enterprise.inject.Produces; +import javax.enterprise.inject.spi.BeanManager; +import javax.inject.Inject; +import javax.inject.Named; +import javax.servlet.ServletContext; +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * @author Javier Rojas Blum + * @author Yuriy Movchan + * @author Yuriy Zabrovarnyy + * @version 0.1, 24/10/2011 + */ +@ApplicationScoped +@Named +public class AppInitializer { + + private final static int DEFAULT_INTERVAL = 30; // 30 seconds + + @Inject + private Logger log; + + @Inject + private BeanManager beanManager; + + @Inject + private Event event; + + @Inject + private Event eventApplicationInitialized; + + @Inject + private Event timerEvent; + + @Inject + @Named(ApplicationFactory.PERSISTENCE_ENTRY_MANAGER_NAME) + private Instance persistenceEntryManagerInstance; + + @Inject + @Named(ApplicationFactory.PERSISTENCE_METRIC_ENTRY_MANAGER_NAME) + @ReportMetric + private Instance persistenceMetricEntryManagerInstance; + + @Inject + @Named(ApplicationFactory.PERSISTENCE_AUTH_ENTRY_MANAGER_NAME) + private Instance> persistenceAuthEntryManagerInstance; + + @Inject + @Named(ApplicationFactory.PERSISTENCE_AUTH_CONFIG_NAME) + private Instance> persistenceAuthConfigInstance; + + @Inject + private ApplicationFactory applicationFactory; + + @Inject + private Instance authenticationModeInstance; + + @Inject + private Instance encryptionServiceInstance; + + @Inject + private PythonService pythonService; + + @Inject + private MetricService metricService; + + @Inject + private CustomScriptManager customScriptManager; + + @Inject + private ExternalPersistenceExtensionService externalPersistenceExtensionService; + + @Inject + private ConfigurationFactory configurationFactory; + + @Inject + private CleanerTimer cleanerTimer; + + @Inject + private KeyGeneratorTimer keyGeneratorTimer; + + @Inject + private StatService statService; + + @Inject + private StatTimer statTimer; + + @Inject + private ExpirationNotificatorTimer expirationNotificatorTimer; + + @Inject + private CustomLibrariesLoader customLibrariesLoader; + + @Inject + private LdapStatusTimer ldapStatusTimer; + + @Inject + private QuartzSchedulerManager quartzSchedulerManager; + + @Inject + private LoggerService loggerService; + + @Inject + private ExternalAuthenticationService externalAuthenticationService; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private CibaRequestsProcessorJob cibaRequestsProcessorJob; + + private AtomicBoolean isActive; + private long lastFinishedTime; + private AuthenticationMode authenticationMode; + + private List persistenceAuthConfigs; + + @PostConstruct + public void createApplicationComponents() { + SecurityProviderUtility.installBCProvider(); + } + + public void applicationInitialized(@Observes @Initialized(ApplicationScoped.class) Object init) { + log.debug("Initializing application services"); + + configurationFactory.create(); + + PersistenceEntryManager localPersistenceEntryManager = persistenceEntryManagerInstance.get(); + log.trace("Attempting to use {}: {}", ApplicationFactory.PERSISTENCE_ENTRY_MANAGER_NAME, localPersistenceEntryManager.getOperationService()); + + GluuConfiguration newConfiguration = loadConfiguration(localPersistenceEntryManager, "oxIDPAuthentication", "oxAuthenticationMode"); + + this.persistenceAuthConfigs = loadPersistenceAuthConfigs(newConfiguration); + + // Initialize python interpreter + pythonService.initPythonInterpreter(configurationFactory.getBaseConfiguration() + .getString("pythonModulesDir", null)); + + // Initialize script manager + List supportedCustomScriptTypes = Lists.newArrayList(CustomScriptType.values()); + + supportedCustomScriptTypes.remove(CustomScriptType.CACHE_REFRESH); + supportedCustomScriptTypes.remove(CustomScriptType.UPDATE_USER); + supportedCustomScriptTypes.remove(CustomScriptType.USER_REGISTRATION); + supportedCustomScriptTypes.remove(CustomScriptType.SCIM); + supportedCustomScriptTypes.remove(CustomScriptType.IDP); + + statService.init(); + + // Start timer + initSchedulerService(); + + // Schedule timer tasks + metricService.initTimer(); + configurationFactory.initTimer(); + loggerService.initTimer(); + ldapStatusTimer.initTimer(); + cleanerTimer.initTimer(); + customScriptManager.initTimer(supportedCustomScriptTypes); + keyGeneratorTimer.initTimer(); + statTimer.initTimer(); + expirationNotificatorTimer.initTimer(); + initTimer(); + initCibaRequestsProcessor(); + + // Set default authentication method after + setDefaultAuthenticationMethod(newConfiguration); + + // Notify plugins about finish application initialization + eventApplicationInitialized.select(ApplicationInitialized.Literal.APPLICATION) + .fire(new ApplicationInitializedEvent()); + } + + protected void initSchedulerService() { + quartzSchedulerManager.start(); + + String disableScheduler = System.getProperties().getProperty("gluu.disable.scheduler"); + if ((disableScheduler != null) && Boolean.valueOf(disableScheduler)) { + this.log.warn("Suspending Quartz Scheduler Service..."); + quartzSchedulerManager.standby(); + return; + } + } + + @Produces + @ApplicationScoped + public StringEncrypter getStringEncrypter() { + String encodeSalt = configurationFactory.getCryptoConfigurationSalt(); + + if (StringHelper.isEmpty(encodeSalt)) { + throw new ConfigurationException("Encode salt isn't defined"); + } + + try { + StringEncrypter stringEncrypter = StringEncrypter.instance(encodeSalt); + + return stringEncrypter; + } catch (EncryptionException ex) { + throw new ConfigurationException("Failed to create StringEncrypter instance"); + } + } + + public void initTimer() { + this.isActive = new AtomicBoolean(false); + this.setLastFinishedTime(System.currentTimeMillis()); + + timerEvent.fire(new TimerEvent(new TimerSchedule(60, DEFAULT_INTERVAL), new AuthConfigurationEvent(), + Scheduled.Literal.INSTANCE)); + } + + @Asynchronous + public void reloadConfigurationTimerEvent(@Observes @Scheduled AuthConfigurationEvent authConfigurationEvent) { + if (this.isActive.get()) { + return; + } + + if (!this.isActive.compareAndSet(false, true)) { + return; + } + + try { + reloadConfiguration(); + } catch (Throwable ex) { + log.error("Exception happened while reloading application configuration", ex); + } finally { + this.isActive.set(false); + this.setLastFinishedTime(System.currentTimeMillis()); + } + } + + private void reloadConfiguration() { + PersistenceEntryManager localPersistenceEntryManager = persistenceEntryManagerInstance.get(); + log.trace("Attempting to use {}: {}", ApplicationFactory.PERSISTENCE_ENTRY_MANAGER_NAME, localPersistenceEntryManager.getOperationService()); + + GluuConfiguration newConfiguration = loadConfiguration(localPersistenceEntryManager, "oxIDPAuthentication", "oxAuthenticationMode"); + + List newPersistenceAuthConfigs = loadPersistenceAuthConfigs(newConfiguration); + + if (!this.persistenceAuthConfigs.equals(newPersistenceAuthConfigs)) { + recreatePersistenceAuthEntryManagers(newPersistenceAuthConfigs); + this.persistenceAuthConfigs = newPersistenceAuthConfigs; + + event.select(ReloadAuthScript.Literal.INSTANCE) + .fire(ExternalAuthenticationService.MODIFIED_INTERNAL_TYPES_EVENT_TYPE); + } + + setDefaultAuthenticationMethod(newConfiguration); + } + + /* + * Utility method which can be used in custom scripts + */ + public PersistenceEntryManager createPersistenceAuthEntryManager(GluuLdapConfiguration persistenceAuthConfig) { + PersistenceEntryManagerFactory persistenceEntryManagerFactory = applicationFactory.getPersistenceEntryManagerFactory(); + Properties persistenceConnectionProperties = prepareAuthConnectionProperties(persistenceAuthConfig, persistenceEntryManagerFactory.getPersistenceType()); + + PersistenceEntryManager persistenceAuthEntryManager = + persistenceEntryManagerFactory.createEntryManager(persistenceConnectionProperties); + log.debug("Created custom authentication PersistenceEntryManager: {}", persistenceAuthEntryManager); + + externalPersistenceExtensionService.executePersistenceExtensionAfterCreate(persistenceConnectionProperties, persistenceAuthEntryManager); + + return persistenceAuthEntryManager; + } + + protected Properties preparePersistanceProperties() { + PersistenceConfiguration persistenceConfiguration = this.configurationFactory.getPersistenceConfiguration(); + FileConfiguration persistenceConfig = persistenceConfiguration.getConfiguration(); + Properties connectionProperties = (Properties) persistenceConfig.getProperties(); + + EncryptionService securityService = encryptionServiceInstance.get(); + Properties decryptedConnectionProperties = securityService.decryptAllProperties(connectionProperties); + return decryptedConnectionProperties; + } + + protected Properties prepareCustomPersistanceProperties(String configId) { + Properties connectionProperties = preparePersistanceProperties(); + if (StringHelper.isNotEmpty(configId)) { + // Replace properties names 'configId.xyz' to 'configId.xyz' in order to + // override default values + connectionProperties = (Properties) connectionProperties.clone(); + + String baseGroup = configId + "."; + for (Object key : connectionProperties.keySet()) { + String propertyName = (String) key; + if (propertyName.startsWith(baseGroup)) { + propertyName = propertyName.substring(baseGroup.length()); + + Object value = connectionProperties.get(key); + connectionProperties.put(propertyName, value); + } + } + } + + return connectionProperties; + } + + @Produces + @ApplicationScoped + @Named(ApplicationFactory.PERSISTENCE_ENTRY_MANAGER_NAME) + public PersistenceEntryManager createPersistenceEntryManager() { + Properties connectionProperties = preparePersistanceProperties(); + + PersistenceEntryManager persistenceEntryManager = applicationFactory.getPersistenceEntryManagerFactory() + .createEntryManager(connectionProperties); + log.info("Created {}: {} with operation service: {}", + new Object[] { ApplicationFactory.PERSISTENCE_ENTRY_MANAGER_NAME, persistenceEntryManager, + persistenceEntryManager.getOperationService() }); + + externalPersistenceExtensionService.executePersistenceExtensionAfterCreate(connectionProperties, persistenceEntryManager); + + return persistenceEntryManager; + } + + @Produces + @ApplicationScoped + @Named(ApplicationFactory.PERSISTENCE_METRIC_ENTRY_MANAGER_NAME) + @ReportMetric + public PersistenceEntryManager createMetricPersistenceEntryManager() { + Properties connectionProperties = prepareCustomPersistanceProperties( + ApplicationFactory.PERSISTENCE_METRIC_CONFIG_GROUP_NAME); + + PersistenceEntryManager persistenceEntryManager = applicationFactory.getPersistenceEntryManagerFactory() + .createEntryManager(connectionProperties); + log.info("Created {}: {} with operation service: {}", + new Object[] { ApplicationFactory.PERSISTENCE_METRIC_ENTRY_MANAGER_NAME, persistenceEntryManager, + persistenceEntryManager.getOperationService() }); + + externalPersistenceExtensionService.executePersistenceExtensionAfterCreate(connectionProperties, persistenceEntryManager); + + return persistenceEntryManager; + } + + @Produces + @ApplicationScoped + @Named(ApplicationFactory.PERSISTENCE_AUTH_CONFIG_NAME) + public List createPersistenceAuthConfigs() { + return persistenceAuthConfigs; + } + + @Produces + @ApplicationScoped + @Named(ApplicationFactory.PERSISTENCE_AUTH_ENTRY_MANAGER_NAME) + public List createPersistenceAuthEntryManager() { + List persistenceAuthEntryManagers = new ArrayList(); + if (this.persistenceAuthConfigs.size() == 0) { + return persistenceAuthEntryManagers; + } + + PersistenceEntryManagerFactory persistenceEntryManagerFactory = applicationFactory.getPersistenceEntryManagerFactory(LdapEntryManagerFactory.class); + + List persistenceAuthProperties = prepareAuthConnectionProperties(this.persistenceAuthConfigs, persistenceEntryManagerFactory.getPersistenceType()); + log.trace("Attempting to create LDAP auth PersistenceEntryManager with properties: {}", persistenceAuthProperties); + + for (int i = 0; i < persistenceAuthProperties.size(); i++) { + PersistenceEntryManager persistenceAuthEntryManager = + persistenceEntryManagerFactory.createEntryManager(persistenceAuthProperties.get(i)); + log.debug("Created {}#{}: {}", new Object[] { ApplicationFactory.PERSISTENCE_AUTH_ENTRY_MANAGER_NAME, i, + persistenceAuthEntryManager }); + + persistenceAuthEntryManagers.add(persistenceAuthEntryManager); + + externalPersistenceExtensionService.executePersistenceExtensionAfterCreate(persistenceAuthProperties.get(i), persistenceAuthEntryManager); + } + + return persistenceAuthEntryManagers; + } + + public void recreatePersistenceEntryManager(@Observes @LdapConfigurationReload String event) { + recreatePersistanceEntryManagerImpl(persistenceEntryManagerInstance, + ApplicationFactory.PERSISTENCE_ENTRY_MANAGER_NAME); + + recreatePersistanceEntryManagerImpl(persistenceEntryManagerInstance, + ApplicationFactory.PERSISTENCE_METRIC_ENTRY_MANAGER_NAME, ReportMetric.Literal.INSTANCE); + } + + protected void recreatePersistanceEntryManagerImpl(Instance instance, + String persistenceEntryManagerName, Annotation... qualifiers) { + // Get existing application scoped instance + PersistenceEntryManager oldPersistenceEntryManager = CdiUtil.getContextBean(beanManager, + PersistenceEntryManager.class, persistenceEntryManagerName); + + // Close existing connections + closePersistenceEntryManager(oldPersistenceEntryManager, persistenceEntryManagerName); + + // Force to create new bean + PersistenceEntryManager persistenceEntryManager = instance.get(); + instance.destroy(persistenceEntryManager); + log.info("Recreated instance {}: {} with operation service: {}", persistenceEntryManagerName, + persistenceEntryManager, persistenceEntryManager.getOperationService()); + } + + private void closePersistenceEntryManager(PersistenceEntryManager oldPersistenceEntryManager, + String persistenceEntryManagerName) { + // Close existing connections + if ((oldPersistenceEntryManager != null) && (oldPersistenceEntryManager.getOperationService() != null)) { + log.debug("Attempting to destroy {}:{} with operation service: {}", persistenceEntryManagerName, + oldPersistenceEntryManager, oldPersistenceEntryManager.getOperationService()); + oldPersistenceEntryManager.destroy(); + log.debug("Destroyed {}:{} with operation service: {}", persistenceEntryManagerName, + oldPersistenceEntryManager, oldPersistenceEntryManager.getOperationService()); + + externalPersistenceExtensionService.executePersistenceExtensionAfterDestroy(oldPersistenceEntryManager); + } + } + + private void closePersistenceEntryManagers(List oldPersistenceEntryManagers) { + // Close existing connections + for (PersistenceEntryManager oldPersistenceEntryManager : oldPersistenceEntryManagers) { + log.debug("Attempting to destroy {}: {}", ApplicationFactory.PERSISTENCE_AUTH_ENTRY_MANAGER_NAME, + oldPersistenceEntryManager); + oldPersistenceEntryManager.destroy(); + log.debug("Destroyed {}: {}", ApplicationFactory.PERSISTENCE_AUTH_ENTRY_MANAGER_NAME, + oldPersistenceEntryManager); + + externalPersistenceExtensionService.executePersistenceExtensionAfterDestroy(oldPersistenceEntryManager); + } + } + + public void recreatePersistenceAuthEntryManagers(List newPersistenceAuthConfigs) { + // Get existing application scoped instance + List oldPersistenceAuthEntryManagers = CdiUtil.getContextBean(beanManager, + new ParameterizedTypeImpl(List.class, PersistenceEntryManager.class), + ApplicationFactory.PERSISTENCE_AUTH_ENTRY_MANAGER_NAME); + + // Recreate components + this.persistenceAuthConfigs = newPersistenceAuthConfigs; + + // Close existing connections + closePersistenceEntryManagers(oldPersistenceAuthEntryManagers); + + // Destroy old Ldap auth entry managers + for (PersistenceEntryManager oldPersistenceAuthEntryManager : oldPersistenceAuthEntryManagers) { + log.debug("Attempting to destroy {}: {}", ApplicationFactory.PERSISTENCE_AUTH_ENTRY_MANAGER_NAME, + oldPersistenceAuthEntryManager); + oldPersistenceAuthEntryManager.destroy(); + log.debug("Destroyed {}: {}", ApplicationFactory.PERSISTENCE_AUTH_ENTRY_MANAGER_NAME, + oldPersistenceAuthEntryManager); + + externalPersistenceExtensionService.executePersistenceExtensionAfterDestroy(oldPersistenceAuthEntryManager); + } + + // Force to create new Ldap auth entry managers bean + List persistenceAuthEntryManagers = persistenceAuthEntryManagerInstance.get(); + persistenceAuthEntryManagerInstance.destroy(persistenceAuthEntryManagers); + log.info("Recreated instance {}: {}", ApplicationFactory.PERSISTENCE_AUTH_ENTRY_MANAGER_NAME, + persistenceAuthEntryManagers); + + // Force to create new auth configuration bean + List oldPersistenceAuthConfigs = persistenceAuthConfigInstance.get(); + persistenceAuthConfigInstance.destroy(oldPersistenceAuthConfigs); + } + + private List prepareAuthConnectionProperties(List persistenceAuthConfigs, String persistenceType) { + List result = new ArrayList(); + + // Prepare connection providers per LDAP authentication configuration + for (GluuLdapConfiguration persistenceAuthConfig : persistenceAuthConfigs) { + Properties decrypytedConnectionProperties = prepareAuthConnectionProperties(persistenceAuthConfig, persistenceType); + + result.add(decrypytedConnectionProperties); + } + + return result; + } + + private Properties prepareAuthConnectionProperties(GluuLdapConfiguration persistenceAuthConfig, String persistenceType) { + String prefix = persistenceType + "#"; + FileConfiguration configuration = configurationFactory.getPersistenceConfiguration().getConfiguration(); + + Properties properties = (Properties) configuration.getProperties().clone(); + if (persistenceAuthConfig != null) { + properties.setProperty(prefix + "servers", buildServersString(persistenceAuthConfig.getServers())); + + String bindDn = persistenceAuthConfig.getBindDN(); + if (StringHelper.isNotEmpty(bindDn)) { + properties.setProperty(prefix + "bindDN", bindDn); + properties.setProperty(prefix + "bindPassword", persistenceAuthConfig.getBindPassword()); + } + properties.setProperty(prefix + "useSSL", Boolean.toString(persistenceAuthConfig.isUseSSL())); + properties.setProperty(prefix + "maxconnections", Integer.toString(persistenceAuthConfig.getMaxConnections())); + + // Remove internal DB trustStoreFile property + properties.remove(prefix + "ssl.trustStoreFile"); + properties.remove(prefix + "ssl.trustStorePin"); + properties.remove(prefix + "ssl.trustStoreFormat"); + } + + EncryptionService securityService = encryptionServiceInstance.get(); + Properties decrypytedProperties = securityService.decryptAllProperties(properties); + + return decrypytedProperties; + } + + private String buildServersString(List servers) { + StringBuilder sb = new StringBuilder(); + + if (servers == null) { + return sb.toString(); + } + + boolean first = true; + for (Object server : servers) { + if (first) { + first = false; + } else { + sb.append(","); + } + + if (server instanceof SimpleProperty) { + sb.append(((SimpleProperty) server).getValue()); + } else { + sb.append(server); + } + } + + return sb.toString(); + } + + private void setDefaultAuthenticationMethod(GluuConfiguration configuration) { + String currentAuthMethod = null; + if (this.authenticationMode != null) { + currentAuthMethod = this.authenticationMode.getName(); + } + + String actualAuthMethod = getActualDefaultAuthenticationMethod(configuration); + + if (!StringHelper.equals(currentAuthMethod, actualAuthMethod)) { + authenticationMode = null; + if (actualAuthMethod != null) { + this.authenticationMode = new AuthenticationMode(actualAuthMethod); + } + + authenticationModeInstance.destroy(authenticationModeInstance.get()); + } + } + + private String getActualDefaultAuthenticationMethod(GluuConfiguration configuration) { + if ((configuration != null) && (configuration.getAuthenticationMode() != null)) { + return configuration.getAuthenticationMode(); + } + + CustomScriptConfiguration defaultExternalAuthenticator = externalAuthenticationService.getDefaultExternalAuthenticator(AuthenticationScriptUsageType.INTERACTIVE); + if (defaultExternalAuthenticator != null) { + return defaultExternalAuthenticator.getName(); + } + + return OxConstants.SCRIPT_TYPE_INTERNAL_RESERVED_NAME; + } + + @Produces + @ApplicationScoped + public AuthenticationMode getDefaultAuthenticationMode() { + return authenticationMode; + } + + private GluuConfiguration loadConfiguration(PersistenceEntryManager localPersistenceEntryManager, + String... persistenceReturnAttributes) { + String configurationDn = configurationFactory.getBaseDn().getConfiguration(); + if (StringHelper.isEmpty(configurationDn)) { + return null; + } + + GluuConfiguration configuration = null; + try { + configuration = localPersistenceEntryManager.find(configurationDn, GluuConfiguration.class, + persistenceReturnAttributes); + } catch (BasePersistenceException ex) { + log.error("Failed to load global configuration entry from Ldap", ex); + return null; + } + + return configuration; + } + + private List loadPersistenceAuthConfigs(GluuConfiguration configuration) { + List persistenceAuthConfigs = new ArrayList(); + + List persistenceIdpAuthConfigs = loadLdapIdpAuthConfigs(configuration); + if (persistenceIdpAuthConfigs == null) { + return persistenceAuthConfigs; + } + + for (oxIDPAuthConf persistenceIdpAuthConfig : persistenceIdpAuthConfigs) { + GluuLdapConfiguration persistenceAuthConfig = persistenceIdpAuthConfig.getConfig(); + if ((persistenceAuthConfig != null) && persistenceAuthConfig.isEnabled()) { + persistenceAuthConfigs.add(persistenceAuthConfig); + } + } + + return persistenceAuthConfigs; + } + + private List loadLdapIdpAuthConfigs(GluuConfiguration configuration) { + if ((configuration == null) || (configuration.getOxIDPAuthentication() == null)) { + return null; + } + + List configurations = new ArrayList(); + for (oxIDPAuthConf authConf : configuration.getOxIDPAuthentication()) { + if (authConf.getType().equalsIgnoreCase("ldap") || authConf.getType().equalsIgnoreCase("auth")) { + configurations.add(authConf); + } + } + + return configurations; + } + + public void destroy(@Observes @BeforeDestroyed(ApplicationScoped.class) ServletContext init) { + log.info("Stopping services and closing DB connections at server shutdown..."); + log.debug("Checking who intiated destory", new Throwable()); + + metricService.close(); + + PersistenceEntryManager persistenceEntryManager = persistenceEntryManagerInstance.get(); + closePersistenceEntryManager(persistenceEntryManager, ApplicationFactory.PERSISTENCE_ENTRY_MANAGER_NAME); + + List persistenceAuthEntryManagers = persistenceAuthEntryManagerInstance.get(); + closePersistenceEntryManagers(persistenceAuthEntryManagers); + } + + public long getLastFinishedTime() { + return lastFinishedTime; + } + + public void setLastFinishedTime(long lastFinishedTime) { + this.lastFinishedTime = lastFinishedTime; + } + + /** + * Method to initialize CIBA requests processor job according to a json property which + * should be more than 0 seconds of interval + */ + private void initCibaRequestsProcessor() { + if (appConfiguration.getCibaEnabled() && appConfiguration.getBackchannelRequestsProcessorJobIntervalSec() > 0) { + if (cibaRequestsProcessorJob != null) { + cibaRequestsProcessorJob.initTimer(); + } + } else { + log.warn("Ciba requests processor hasn't been started because the interval is not valid to run or this is disabled, value: {}", + appConfiguration.getBackchannelRequestsProcessorJobIntervalSec()); + } + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/AttributeService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/AttributeService.java new file mode 100644 index 00000000..862cc94c --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/AttributeService.java @@ -0,0 +1,114 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service; + +import java.util.ArrayList; +import java.util.List; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.gluu.model.GluuAttribute; +import org.gluu.oxauth.model.config.StaticConfiguration; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.service.BaseCacheService; +import org.gluu.util.StringHelper; +import org.slf4j.Logger; + +/** + * @author Javier Rojas Blum + * @version May 30, 2018 + */ +@ApplicationScoped +public class AttributeService extends org.gluu.service.AttributeService { + + /** + * + */ + private static final long serialVersionUID = -990409035168814270L; + + @Inject + private Logger log; + + @Inject + private StaticConfiguration staticConfiguration; + + @Inject + private AppConfiguration appConfiguration; + + /** + * returns GluuAttribute by Dn + * + * @return GluuAttribute + */ + public GluuAttribute getAttributeByDn(String dn) { + BaseCacheService usedCacheService = getCacheService(); + + return usedCacheService.getWithPut(dn, () -> persistenceEntryManager.find(GluuAttribute.class, dn), 60); + } + + public GluuAttribute getByLdapName(String name) { + List gluuAttributes = getAttributesByAttribute("gluuAttributeName", name, staticConfiguration.getBaseDn().getAttributes()); + if (gluuAttributes.size() > 0) { + for (GluuAttribute gluuAttribute : gluuAttributes) { + if (gluuAttribute.getName() != null && gluuAttribute.getName().equals(name)) { + return gluuAttribute; + } + } + } + + return null; + } + + public GluuAttribute getByClaimName(String name) { + List gluuAttributes = getAttributesByAttribute("oxAuthClaimName", name, staticConfiguration.getBaseDn().getAttributes()); + if (gluuAttributes.size() > 0) { + for (GluuAttribute gluuAttribute : gluuAttributes) { + if (gluuAttribute.getOxAuthClaimName() != null && gluuAttribute.getOxAuthClaimName().equals(name)) { + return gluuAttribute; + } + } + } + + return null; + } + + public List getAllAttributes() { + return getAllAttributes(staticConfiguration.getBaseDn().getAttributes()); + } + + public String getDnForAttribute(String inum) { + String attributesDn = staticConfiguration.getBaseDn().getAttributes(); + if (StringHelper.isEmpty(inum)) { + return attributesDn; + } + + return String.format("inum=%s,%s", inum, attributesDn); + } + + public List getAttributesDn(List claimNames) { + List claims = new ArrayList(); + + for (String claimName : claimNames) { + GluuAttribute gluuAttribute = getByClaimName(claimName); + if (gluuAttribute != null) { + claims.add(gluuAttribute.getDn()); + } + } + + return claims; + } + + protected BaseCacheService getCacheService() { + if (appConfiguration.getUseLocalCache()) { + return localCacheService; + } + + return cacheService; + } + +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/AuthenticationFilterService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/AuthenticationFilterService.java new file mode 100644 index 00000000..bb845ff6 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/AuthenticationFilterService.java @@ -0,0 +1,76 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service; + +import java.util.Map; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.common.User; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.persist.exception.AuthenticationException; +import org.gluu.persist.exception.operation.SearchException; +import org.gluu.util.StringHelper; + +/** + * Provides operations with authentication filters + * + * @author Yuriy Movchan Date: 07.20.2012 + */ +@ApplicationScoped +public class AuthenticationFilterService extends BaseAuthFilterService { + + @Inject + private PersistenceEntryManager ldapEntryManager; + + @Inject + private AppConfiguration appConfiguration; + + @PostConstruct + public void init() { + super.init(appConfiguration.getAuthenticationFilters(), Boolean.TRUE.equals(appConfiguration.getAuthenticationFiltersEnabled()), true); + } + + public String processAuthenticationFilter(AuthenticationFilterWithParameters authenticationFilterWithParameters, Map attributeValues) throws SearchException { + if (attributeValues == null) { + return null; + } + final Map normalizedAttributeValues = normalizeAttributeMap(attributeValues); + final String resultDn = loadEntryDN(ldapEntryManager, User.class, authenticationFilterWithParameters, normalizedAttributeValues); + if (StringUtils.isBlank(resultDn)) { + return null; + } + + if (!Boolean.TRUE.equals(authenticationFilterWithParameters.getAuthenticationFilter().getBind())) { + return resultDn; + } + + String bindPasswordAttribute = authenticationFilterWithParameters.getAuthenticationFilter().getBindPasswordAttribute(); + if (StringHelper.isEmpty(bindPasswordAttribute)) { + log.error("Skipping authentication filter:\n '{}'\n. It should contains not empty bind-password-attribute attribute. ", authenticationFilterWithParameters.getAuthenticationFilter()); + return null; + } + + bindPasswordAttribute = StringHelper.toLowerCase(bindPasswordAttribute); + + try { + boolean authenticated = ldapEntryManager.authenticate(resultDn, User.class, normalizedAttributeValues.get(bindPasswordAttribute)); + if (authenticated) { + return resultDn; + } + } catch (AuthenticationException ex) { + log.error("Invalid password", ex); + } + + return null; + } + +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/AuthenticationProtectionService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/AuthenticationProtectionService.java new file mode 100644 index 00000000..1f4fe32d --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/AuthenticationProtectionService.java @@ -0,0 +1,64 @@ +package org.gluu.oxauth.service; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Observes; +import javax.inject.Inject; +import javax.inject.Named; + +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.configuration.AuthenticationProtectionConfiguration; +import org.gluu.service.cdi.event.ConfigurationUpdate; + +/** + * Brute Force authentication protection service implementation + * + * @author Yuriy Movchan Date: 08/21/2018 + */ +@ApplicationScoped +@Named +public class AuthenticationProtectionService extends org.gluu.service.security.protect.AuthenticationProtectionService { + + private static final int DEFAULT_ATTEMPT_EXPIRATION = 15; // 15 seconds + + private static final int DEFAULT_MAXIMUM_ALLOWED_ATTEMPTS_WITHOUT_DELAY = 4; // 4 attempts + + private static final int DEFAULT_DELAY_TIME = 2; // 5 seconds + + private static final String DEFAULT_KEY_PREFIX = "user"; + + @Inject + private AppConfiguration appConfiguration; + + @Override + protected void init() { + updateConfiguration(appConfiguration); + } + + public void updateConfiguration(@Observes @ConfigurationUpdate AppConfiguration appConfiguration) { + AuthenticationProtectionConfiguration authenticationProtectionConfiguration = appConfiguration.getAuthenticationProtectionConfiguration(); + if (authenticationProtectionConfiguration == null) { + this.attemptExpiration = DEFAULT_ATTEMPT_EXPIRATION; + this.maximumAllowedAttemptsWithoutDelay = DEFAULT_MAXIMUM_ALLOWED_ATTEMPTS_WITHOUT_DELAY; + + this.delayTime = DEFAULT_DELAY_TIME; + } else { + this.attemptExpiration = authenticationProtectionConfiguration.getAttemptExpiration(); + this.maximumAllowedAttemptsWithoutDelay = authenticationProtectionConfiguration.getMaximumAllowedAttemptsWithoutDelay(); + + this.delayTime = authenticationProtectionConfiguration.getDelayTime(); + } + } + + @Override + protected String getKeyPrefix() { + return DEFAULT_KEY_PREFIX; + } + + public boolean isEnabled() { + AuthenticationProtectionConfiguration authenticationProtectionConfiguration = appConfiguration.getAuthenticationProtectionConfiguration(); + + return (authenticationProtectionConfiguration != null) && (authenticationProtectionConfiguration.getBruteForceProtectionEnabled()); + + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/AuthenticationService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/AuthenticationService.java new file mode 100644 index 00000000..ae89a8e3 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/AuthenticationService.java @@ -0,0 +1,868 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service; + +import static org.gluu.oxauth.model.authorize.AuthorizeResponseParam.SESSION_ID; +import static org.gluu.oxauth.model.authorize.AuthorizeResponseParam.SID; + +import java.io.UnsupportedEncodingException; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; + +import javax.enterprise.context.RequestScoped; +import javax.faces.context.ExternalContext; +import javax.inject.Inject; +import javax.inject.Named; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang.StringUtils; +import org.gluu.jsf2.service.FacesService; +import org.gluu.model.GluuStatus; +import org.gluu.model.SimpleProperty; +import org.gluu.model.ldap.GluuLdapConfiguration; +import org.gluu.model.metric.MetricType; +import org.gluu.model.security.Credentials; +import org.gluu.model.security.SimplePrincipal; +import org.gluu.oxauth.model.common.SimpleUser; +import org.gluu.oxauth.model.common.User; +import org.gluu.oxauth.model.config.Constants; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.session.SessionClient; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.model.util.Util; +import org.gluu.oxauth.security.Identity; +import org.gluu.oxauth.service.common.ApplicationFactory; +import org.gluu.oxauth.service.common.UserService; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.persist.exception.AuthenticationException; +import org.gluu.persist.exception.EntryPersistenceException; +import org.gluu.persist.model.base.CustomAttribute; +import org.gluu.persist.model.base.CustomEntry; +import org.gluu.persist.model.base.CustomObjectAttribute; +import org.gluu.util.ArrayHelper; +import org.gluu.util.Pair; +import org.gluu.util.StringHelper; +import org.json.JSONException; +import org.slf4j.Logger; + +/** + * Authentication service methods + * + * @author Yuriy Movchan + * @author Javier Rojas Blum + * @version November 23, 2017 + */ +@RequestScoped +public class AuthenticationService { + + private static final String AUTH_EXTERNAL_ATTRIBUTES = "auth_external_attributes"; + + @Inject + private Logger log; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private Identity identity; + + @Inject + private Credentials credentials; + + @Inject + @Named(ApplicationFactory.PERSISTENCE_AUTH_CONFIG_NAME) + private List ldapAuthConfigs; + + @Inject + private PersistenceEntryManager ldapEntryManager; + + @Inject + @Named(ApplicationFactory.PERSISTENCE_AUTH_ENTRY_MANAGER_NAME) + private List ldapAuthEntryManagers; + + @Inject + private UserService userService; + + @Inject + private ClientService clientService; + + @Inject + private SessionIdService sessionIdService; + + @Inject + private MetricService metricService; + + @Inject + private ExternalContext externalContext; + + @Inject + private FacesService facesService; + + @Inject + private RequestParameterService requestParameterService; + + @Inject + private AuthenticationProtectionService authenticationProtectionService; + + /** + * Authenticate user. + * + * @param userName + * The username. + * @param password + * The user's password. + * @return true if success, otherwise false. + */ + public boolean authenticate(String userName, String password) { + log.debug("Authenticating user with LDAP: username: '{}', credentials: '{}'", userName, + System.identityHashCode(credentials)); + + boolean authenticated = false; + boolean protectionServiceEnabled = authenticationProtectionService.isEnabled(); + + com.codahale.metrics.Timer.Context timerContext = null; + timerContext = metricService + .getTimer(MetricType.OXAUTH_USER_AUTHENTICATION_RATE).time(); + try { + if ((this.ldapAuthConfigs == null) || (this.ldapAuthConfigs.size() == 0)) { + authenticated = localAuthenticate(userName, password); + } else { + authenticated = externalAuthenticate(userName, password); + } + } finally { + timerContext.stop(); + } + + String userId = userName; + if ((identity.getUser() != null) && StringHelper.isNotEmpty(identity.getUser().getUserId())) { + userId = identity.getUser().getUserId(); + } + setAuthenticatedUserSessionAttribute(userId, authenticated); + + MetricType metricType; + if (authenticated) { + metricType = MetricType.OXAUTH_USER_AUTHENTICATION_SUCCESS; + } else { + metricType = MetricType.OXAUTH_USER_AUTHENTICATION_FAILURES; + } + + metricService.incCounter(metricType); + + if (protectionServiceEnabled) { + authenticationProtectionService.storeAttempt(userId, authenticated); + authenticationProtectionService.doDelayIfNeeded(userId); + } + + return authenticated; + } + + /** + * Authenticate user. + * + * @param nameValue + * The name value to find user + * @param password + * The user's password. + * @param nameAttributes + * List of attribute to search. + * @return true if success, otherwise false. + */ + public boolean authenticate(String nameValue, String password, String ... nameAttributes) { + log.debug("Authenticating user with LDAP: nameValue: '{}', nameAttributes: '{}', credentials: '{}'", nameValue, + ArrayHelper.toString(nameAttributes), + System.identityHashCode(credentials)); + + Pair authenticatedPair = null; + boolean authenticated = false; + boolean protectionServiceEnabled = authenticationProtectionService.isEnabled(); + + com.codahale.metrics.Timer.Context timerContext = metricService + .getTimer(MetricType.OXAUTH_USER_AUTHENTICATION_RATE).time(); + try { + authenticatedPair = localAuthenticate(nameValue, password, nameAttributes); + } finally { + timerContext.stop(); + } + + String userId = null; + if ((authenticatedPair != null) && (authenticatedPair.getSecond() != null)) { + authenticated = authenticatedPair.getFirst(); + userId = authenticatedPair.getSecond().getUserId(); + } + setAuthenticatedUserSessionAttribute(userId, authenticated); + + MetricType metricType; + if (authenticated) { + metricType = MetricType.OXAUTH_USER_AUTHENTICATION_SUCCESS; + } else { + metricType = MetricType.OXAUTH_USER_AUTHENTICATION_FAILURES; + } + + metricService.incCounter(metricType); + + if (protectionServiceEnabled) { + authenticationProtectionService.storeAttempt(userId, authenticated); + authenticationProtectionService.doDelayIfNeeded(userId); + } + + return authenticated; + } + + private void setAuthenticatedUserSessionAttribute(String userName, boolean authenticated) { + SessionId sessionId = sessionIdService.getSessionId(); + if (sessionId != null) { + Map sessionIdAttributes = sessionId.getSessionAttributes(); + if (authenticated) { + sessionIdAttributes.put(Constants.AUTHENTICATED_USER, userName); + } + sessionIdService.updateSessionIdIfNeeded(sessionId, authenticated); + } + } + + private boolean localAuthenticate(String userName, String password) { + User user = userService.getUser(userName); + if (user != null) { + if (!checkUserStatus(user)) { + return false; + } + + // Use local LDAP server for user authentication + boolean authenticated = false; + try { + authenticated = ldapEntryManager.authenticate(user.getDn(), User.class, password); + } catch (AuthenticationException ex) { + log.error("Authentication failed: " + ex.getMessage()); + if (log.isDebugEnabled()) { + log.debug("Authentication failed:", ex); + } + } + if (authenticated) { + configureAuthenticatedUser(user); + updateLastLogonUserTime(user); + + log.trace("Authenticate: credentials: '{}', credentials.userName: '{}', authenticatedUser.userId: '{}'", + System.identityHashCode(credentials), credentials.getUsername(), getAuthenticatedUserId()); + } + + return authenticated; + } + + return false; + } + + private Pair localAuthenticate(String nameValue, String password, String ... nameAttributes) { + String lowerNameValue = StringHelper.toString(nameValue); + User user = userService.getUserByAttributes(lowerNameValue, nameAttributes, new String[] {"uid", "gluuStatus"}); + if (user != null) { + if (!checkUserStatus(user)) { + return new Pair(false, user); + } + + // Use local LDAP server for user authentication + boolean authenticated = ldapEntryManager.authenticate(user.getDn(), User.class, password); + if (authenticated) { + configureAuthenticatedUser(user); + updateLastLogonUserTime(user); + + log.trace("Authenticate: credentials: '{}', credentials.userName: '{}', authenticatedUser.userId: '{}'", + System.identityHashCode(credentials), credentials.getUsername(), getAuthenticatedUserId()); + } + + return new Pair(authenticated, user); + } + + return new Pair(false, null); + } + + private boolean externalAuthenticate(String keyValue, String password) { + for (int i = 0; i < this.ldapAuthConfigs.size(); i++) { + GluuLdapConfiguration ldapAuthConfig = this.ldapAuthConfigs.get(i); + PersistenceEntryManager ldapAuthEntryManager = this.ldapAuthEntryManagers.get(i); + + String primaryKey = "uid"; + if (StringHelper.isNotEmpty(ldapAuthConfig.getPrimaryKey())) { + primaryKey = ldapAuthConfig.getPrimaryKey(); + } + + String localPrimaryKey = "uid"; + if (StringHelper.isNotEmpty(ldapAuthConfig.getLocalPrimaryKey())) { + localPrimaryKey = ldapAuthConfig.getLocalPrimaryKey(); + } + + boolean authenticated = authenticate(ldapAuthConfig, ldapAuthEntryManager, keyValue, password, primaryKey, + localPrimaryKey, false); + if (authenticated) { + return authenticated; + } + } + + return false; + } + + public boolean authenticate(String keyValue, String password, String primaryKey, String localPrimaryKey) { + if (this.ldapAuthConfigs == null) { + return authenticate(null, ldapEntryManager, keyValue, password, primaryKey, localPrimaryKey); + } + + boolean authenticated = false; + boolean protectionServiceEnabled = authenticationProtectionService.isEnabled(); + + com.codahale.metrics.Timer.Context timerContext = metricService + .getTimer(MetricType.OXAUTH_USER_AUTHENTICATION_RATE).time(); + try { + for (int i = 0; i < this.ldapAuthConfigs.size(); i++) { + GluuLdapConfiguration ldapAuthConfig = this.ldapAuthConfigs.get(i); + PersistenceEntryManager ldapAuthEntryManager = this.ldapAuthEntryManagers.get(i); + + authenticated = authenticate(ldapAuthConfig, ldapAuthEntryManager, keyValue, password, primaryKey, + localPrimaryKey, false); + if (authenticated) { + break; + } + } + } finally { + timerContext.stop(); + } + String userId = null; + if ((identity.getUser() != null) && StringHelper.isNotEmpty(identity.getUser().getUserId())) { + userId = identity.getUser().getUserId(); + } + setAuthenticatedUserSessionAttribute(userId, authenticated); + + MetricType metricType; + if (authenticated) { + metricType = MetricType.OXAUTH_USER_AUTHENTICATION_SUCCESS; + } else { + metricType = MetricType.OXAUTH_USER_AUTHENTICATION_FAILURES; + } + + metricService.incCounter(metricType); + + if (protectionServiceEnabled) { + authenticationProtectionService.storeAttempt(keyValue, authenticated); + authenticationProtectionService.doDelayIfNeeded(keyValue); + } + + return authenticated; + } + + /* + * Utility method which can be used in custom scripts + */ + public boolean authenticate(GluuLdapConfiguration ldapAuthConfig, PersistenceEntryManager ldapAuthEntryManager, + String keyValue, String password, String primaryKey, String localPrimaryKey) { + + return authenticate(ldapAuthConfig, ldapAuthEntryManager, keyValue, password, primaryKey, localPrimaryKey, true); + } + + /* + * Utility method which can be used in custom scripts + */ + public boolean authenticate(GluuLdapConfiguration ldapAuthConfig, PersistenceEntryManager ldapAuthEntryManager, + String keyValue, String password, String primaryKey, String localPrimaryKey, boolean updateMetrics) { + boolean authenticated = false; + boolean protectionServiceEnabled = authenticationProtectionService.isEnabled(); + + com.codahale.metrics.Timer.Context timerContext = null; + + if (updateMetrics) { + timerContext = metricService.getTimer(MetricType.OXAUTH_USER_AUTHENTICATION_RATE).time(); + } + + try { + authenticated = authenticateImpl(ldapAuthConfig, ldapAuthEntryManager, keyValue, password, primaryKey, localPrimaryKey); + } finally { + if (updateMetrics) { + timerContext.stop(); + } + } + + String userId = keyValue; + if ((identity.getUser() != null) && StringHelper.isNotEmpty(identity.getUser().getUserId())) { + userId = identity.getUser().getUserId(); + } + setAuthenticatedUserSessionAttribute(userId, authenticated); + + if (updateMetrics) { + MetricType metricType; + if (authenticated) { + metricType = MetricType.OXAUTH_USER_AUTHENTICATION_SUCCESS; + } else { + metricType = MetricType.OXAUTH_USER_AUTHENTICATION_FAILURES; + } + + metricService.incCounter(metricType); + } + + if (protectionServiceEnabled) { + authenticationProtectionService.storeAttempt(userId, authenticated); + authenticationProtectionService.doDelayIfNeeded(userId); + } + + return authenticated; + } + + private boolean authenticateImpl(GluuLdapConfiguration ldapAuthConfig, PersistenceEntryManager ldapAuthEntryManager, + String keyValue, String password, String primaryKey, String localPrimaryKey) { + log.debug("Attempting to find userDN by primary key: '{}' and key value: '{}', credentials: '{}'", primaryKey, + keyValue, System.identityHashCode(credentials)); + + try { + List baseDNs; + if (ldapAuthConfig == null) { + baseDNs = Arrays.asList(userService.getDnForUser(null)); + } else { + baseDNs = ldapAuthConfig.getBaseDNs(); + } + + if (baseDNs != null && !baseDNs.isEmpty()) { + for (Object baseDnProperty : baseDNs) { + String baseDn; + if (baseDnProperty instanceof SimpleProperty) { + baseDn = ((SimpleProperty) baseDnProperty).getValue(); + } else { + baseDn = baseDnProperty.toString(); + } + + User user = getUserByAttribute(ldapAuthEntryManager, baseDn, primaryKey, keyValue); + if (user != null) { + String userDn = user.getDn(); + log.debug("Attempting to authenticate userDN: {}", userDn); + if (ldapAuthEntryManager.authenticate(userDn, User.class, password)) { + log.debug("User authenticated: {}", userDn); + + log.debug("Attempting to find userDN by local primary key: {}", localPrimaryKey); + User localUser = userService.getUserByAttribute(localPrimaryKey, keyValue); + if (localUser != null) { + if (!checkUserStatus(localUser)) { + return false; + } + + configureAuthenticatedUser(localUser); + updateLastLogonUserTime(localUser); + + log.trace( + "authenticate_external: credentials: '{}', credentials.userName: '{}', authenticatedUser.userId: '{}'", + System.identityHashCode(credentials), credentials.getUsername(), + getAuthenticatedUserId()); + + return true; + } + } + } + } + } else { + log.error("There are no baseDns specified in authentication configuration."); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + + return false; + } + + public boolean authenticate(String userName) { + log.debug("Authenticating user with LDAP: username: '{}', credentials: '{}'", userName, + System.identityHashCode(credentials)); + + boolean authenticated = false; + boolean protectionServiceEnabled = authenticationProtectionService.isEnabled(); + + com.codahale.metrics.Timer.Context timerContext = metricService + .getTimer(MetricType.OXAUTH_USER_AUTHENTICATION_RATE).time(); + try { + User user = userService.getUser(userName); + if ((user != null) && checkUserStatus(user)) { + credentials.setUsername(user.getUserId()); + configureAuthenticatedUser(user); + updateLastLogonUserTime(user); + + log.trace("Authenticate: credentials: '{}', credentials.userName: '{}', authenticatedUser.userId: '{}'", + System.identityHashCode(credentials), credentials.getUsername(), getAuthenticatedUserId()); + + authenticated = true; + } + } finally { + timerContext.stop(); + } + + setAuthenticatedUserSessionAttribute(userName, authenticated); + + MetricType metricType; + if (authenticated) { + metricType = MetricType.OXAUTH_USER_AUTHENTICATION_SUCCESS; + } else { + metricType = MetricType.OXAUTH_USER_AUTHENTICATION_FAILURES; + } + + metricService.incCounter(metricType); + + if (protectionServiceEnabled) { + authenticationProtectionService.storeAttempt(userName, authenticated); + authenticationProtectionService.doDelayIfNeeded(userName); + } + + return authenticated; + } + + private User getUserByAttribute(PersistenceEntryManager ldapAuthEntryManager, String baseDn, String attributeName, + String attributeValue) { + log.debug("Getting user information from LDAP: attributeName = '{}', attributeValue = '{}'", attributeName, + attributeValue); + + if (StringHelper.isEmpty(attributeValue)) { + return null; + } + + SimpleUser sampleUser = new SimpleUser(); + sampleUser.setDn(baseDn); + + List customAttributes = new ArrayList(); + customAttributes.add(new CustomObjectAttribute(attributeName, attributeValue)); + + sampleUser.setCustomAttributes(customAttributes); + + log.debug("Searching user by attributes: '{}', baseDn: '{}'", customAttributes, baseDn); + List entries = ldapAuthEntryManager.findEntries(sampleUser, 1); + log.debug("Found '{}' entries", entries.size()); + + if (entries.size() > 0) { + SimpleUser foundUser = entries.get(0); + + return ldapAuthEntryManager.find(User.class, foundUser.getDn()); + } else { + return null; + } + } + + private boolean checkUserStatus(User user) { + CustomObjectAttribute userStatus = userService.getCustomAttribute(user, "gluuStatus"); + + if ((userStatus != null) && GluuStatus.ACTIVE.equals(GluuStatus.getByValue(StringHelper.toString(userStatus.getValue())))) { + return true; + } + + log.warn("User '{}' was disabled", user.getUserId()); + return false; + } + + private void updateLastLogonUserTime(User user) { + if (!appConfiguration.getUpdateUserLastLogonTime()) { + return; + } + + CustomEntry customEntry = new CustomEntry(); + customEntry.setDn(user.getDn()); + + List personCustomObjectClassList = appConfiguration.getPersonCustomObjectClassList(); + if ((personCustomObjectClassList != null) && !personCustomObjectClassList.isEmpty()) { + // Combine object classes from LDAP and configuration in one list + Set customPersonCustomObjectClassList = new HashSet(); + customPersonCustomObjectClassList.add("gluuPerson"); + customPersonCustomObjectClassList.addAll(personCustomObjectClassList); + if (user.getCustomObjectClasses() != null) { + customPersonCustomObjectClassList.addAll(Arrays.asList(user.getCustomObjectClasses())); + } + + customEntry.setCustomObjectClasses( + customPersonCustomObjectClassList.toArray(new String[customPersonCustomObjectClassList.size()])); + } else { + customEntry.setCustomObjectClasses(UserService.USER_OBJECT_CLASSES); + } + + Date now = new GregorianCalendar(TimeZone.getTimeZone("UTC")).getTime(); + String nowDateString = ldapEntryManager.encodeTime(customEntry.getDn(), now); + CustomAttribute customAttribute = new CustomAttribute("oxLastLogonTime", nowDateString); + customEntry.getCustomAttributes().add(customAttribute); + + try { + ldapEntryManager.merge(customEntry); + } catch (EntryPersistenceException epe) { + log.error("Failed to update oxLastLogonTime of user '{}'", user.getUserId()); + } + } + + public SessionId configureSessionUser(SessionId sessionId, Map sessionIdAttributes) { + log.trace("configureSessionUser: credentials: '{}', sessionId: '{}', credentials.userName: '{}', authenticatedUser.userId: '{}'", + System.identityHashCode(credentials), sessionId, credentials.getUsername(), getAuthenticatedUserId()); + + User user = getAuthenticatedUser(); + + String sessionAuthUser = sessionIdAttributes.get(Constants.AUTHENTICATED_USER); + log.trace("configureSessionUser sessionId: '{}', sessionId.auth_user: '{}'", sessionId, sessionAuthUser); + + SessionId newSessionId = sessionIdService.setSessionIdStateAuthenticated(getHttpRequest(), getHttpResponse(), sessionId, user.getDn()); + + identity.setSessionId(sessionId); + newSessionId.setUser(user); + + return newSessionId; + } + + public SessionId configureEventUser() { + User user = getAuthenticatedUser(); + if (user == null) { + return null; + } + + log.debug("ConfigureEventUser: username: '{}', credentials: '{}'", user.getUserId(), + System.identityHashCode(credentials)); + + SessionId sessionId = sessionIdService.generateAuthenticatedSessionId(getHttpRequest(), user.getDn()); + + identity.setSessionId(sessionId); + + return sessionId; + } + + private HttpServletRequest getHttpRequest() { + if (externalContext == null) { + return null; + } + return (HttpServletRequest) externalContext.getRequest(); + } + + private HttpServletResponse getHttpResponse() { + if (externalContext == null) { + return null; + } + return (HttpServletResponse) externalContext.getResponse(); + } + + public void configureEventUser(SessionId sessionId) { + sessionIdService.updateSessionId(sessionId); + + identity.setSessionId(sessionId); + } + + public void quietLogin(String userName) { + Principal principal = new SimplePrincipal(userName); + identity.acceptExternallyAuthenticatedPrincipal(principal); + identity.quietLogin(); + } + + private void configureAuthenticatedUser(User user) { + identity.setUser(user); + } + + public User getAuthenticatedUser() { + if (identity.getUser() != null) { + return identity.getUser(); + } else { + SessionId sessionId = sessionIdService.getSessionId(); + if (sessionId != null) { + Map sessionIdAttributes = sessionId.getSessionAttributes(); + String userId = sessionIdAttributes.get(Constants.AUTHENTICATED_USER); + if (StringHelper.isNotEmpty(userId)) { + User user = userService.getUser(userId); + identity.setUser(user); + + return user; + } + } + } + + return null; + } + + public String getAuthenticatedUserId() { + User authenticatedUser = getAuthenticatedUser(); + if (authenticatedUser != null) { + return authenticatedUser.getUserId(); + } + + return null; + } + + public Client configureSessionClient() { + String clientInum = credentials.getUsername(); + log.debug("ConfigureSessionClient: username: '{}', credentials: '{}'", clientInum, + System.identityHashCode(credentials)); + + Client client = clientService.getClient(clientInum); + configureSessionClient(client); + return client; + } + + public void configureSessionClient(Client client) { + SessionClient sessionClient = new SessionClient(); + sessionClient.setClient(client); + + identity.setSessionClient(sessionClient); + + clientService.updateAccessTime(client, true); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void onSuccessfulLogin(SessionId sessionUser) { + log.info("Attempting to redirect user: SessionUser: {}", sessionUser != null ? sessionUser.getId() : ""); + + if ((sessionUser == null) || StringUtils.isBlank(sessionUser.getUserDn())) { + return; + } + + User user = sessionIdService.getUser(sessionUser); + + log.info("Attempting to redirect user: User: {}", user); + if (user == null) { + log.error("Failed to identify logged in user for session: {}", sessionUser); + return; + } + + final Map result = sessionUser.getSessionAttributes(); + result.put(SESSION_ID, sessionUser.getId()); // parameters must be filled before filtering + result.put(SID, sessionUser.getOutsideSid()); // parameters must be filled before filtering + + Map allowedParameters = requestParameterService.getAllowedParameters(result); + + log.trace("Logged in successfully! User: {}, page: /authorize.xhtml, map: {}", user, allowedParameters); + facesService.redirect("/authorize.xhtml", (Map) allowedParameters); + } + + public User getUserOrRemoveSession(SessionId p_sessionId) { + if (p_sessionId != null) { + try { + if (StringUtils.isNotBlank(p_sessionId.getUserDn())) { + final User user = sessionIdService.getUser(p_sessionId); + if (user != null) { + return user; + } else { // if there is no user than session is invalid + sessionIdService.remove(p_sessionId); + } + } else { // if there is no user than session is invalid + sessionIdService.remove(p_sessionId); + } + } catch (Exception e) { + log.trace(e.getMessage(), e); + } + } + return null; + } + + public String parametersAsString() throws UnsupportedEncodingException { + final Map parameterMap = getParametersMap(null); + + return requestParameterService.parametersAsString(parameterMap); + } + + public Map getParametersMap(List extraParameters) { + final Map parameterMap = new HashMap(externalContext.getRequestParameterMap()); + + return requestParameterService.getParametersMap(extraParameters, parameterMap); + } + + public boolean isParameterExists(String p_name) { + return identity.isSetWorkingParameter(p_name); + } + + public void updateExtraParameters(Map sessionIdAttributes, List extraParameters) { + // Load extra parameters set + Map authExternalAttributes = getExternalScriptExtraParameters(sessionIdAttributes); + + if (extraParameters != null) { + log.trace("Attempting to store extraParameters: {}", extraParameters); + for (String extraParameter : extraParameters) { + if (isParameterExists(extraParameter)) { + Pair extraParameterValueWithType = requestParameterService + .getParameterValueWithType(extraParameter); + String extraParameterValue = extraParameterValueWithType.getFirst(); + String extraParameterType = extraParameterValueWithType.getSecond(); + + // Store parameter name and value + sessionIdAttributes.put(extraParameter, extraParameterValue); + + // Store parameter name and type + authExternalAttributes.put(extraParameter, extraParameterType); + } + } + } + + // Store identity working parameters in session + setExternalScriptExtraParameters(sessionIdAttributes, authExternalAttributes); + log.trace("Storing sessionIdAttributes: {}", sessionIdAttributes); + log.trace("Storing authExternalAttributes: {}", authExternalAttributes); + } + + public Map getExternalScriptExtraParameters(Map sessionIdAttributes) { + String authExternalAttributesString = sessionIdAttributes.get(AUTH_EXTERNAL_ATTRIBUTES); + Map authExternalAttributes = new HashMap(); + try { + authExternalAttributes = Util.jsonObjectArrayStringAsMap(authExternalAttributesString); + } catch (JSONException ex) { + log.error("Failed to convert JSON array of auth_external_attributes to Map"); + } + + return authExternalAttributes; + } + + public void setExternalScriptExtraParameters(Map sessionIdAttributes, + Map authExternalAttributes) { + String authExternalAttributesString = null; + try { + authExternalAttributesString = Util.mapAsString(authExternalAttributes); + } catch (JSONException ex) { + log.error("Failed to convert Map of auth_external_attributes to JSON array"); + } + + sessionIdAttributes.put(AUTH_EXTERNAL_ATTRIBUTES, authExternalAttributesString); + } + + public void clearExternalScriptExtraParameters(Map sessionIdAttributes) { + Map authExternalAttributes = getExternalScriptExtraParameters(sessionIdAttributes); + + for (String authExternalAttribute : authExternalAttributes.keySet()) { + sessionIdAttributes.remove(authExternalAttribute); + } + + sessionIdAttributes.remove(AUTH_EXTERNAL_ATTRIBUTES); + } + + public void copyAuthenticatorExternalAttributes(SessionId oldSession, SessionId newSession) { + if ((oldSession != null) && (oldSession.getSessionAttributes() != null) && + (newSession != null) && (newSession.getSessionAttributes() != null)) { + + Map newSessionIdAttributes = newSession.getSessionAttributes(); + Map oldSessionIdAttributes = oldSession.getSessionAttributes(); + + Map authExternalAttributes = getExternalScriptExtraParameters(oldSession.getSessionAttributes()); + + if (authExternalAttributes != null) { + log.trace("Attempting to copy extraParameters into new session: {}", authExternalAttributes); + for (String authExternalAttributeName : authExternalAttributes.keySet()) { + if (oldSessionIdAttributes.containsKey(authExternalAttributeName)) { + String authExternalAttributeValue = oldSessionIdAttributes.get(authExternalAttributeName); + + // Store in new session + newSessionIdAttributes.put(authExternalAttributeName, authExternalAttributeValue); + } + } + } + + setExternalScriptExtraParameters(newSessionIdAttributes, authExternalAttributes); + } + } + + public List getLdapAuthConfigs() { + return ldapAuthConfigs; + } + + public List getLdapAuthEntryManagers() { + return ldapAuthEntryManagers; + } + +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/AuthorizeService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/AuthorizeService.java new file mode 100644 index 00000000..343e947a --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/AuthorizeService.java @@ -0,0 +1,327 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service; + +import com.google.common.collect.Sets; +import org.apache.commons.lang.StringUtils; +import org.gluu.jsf2.message.FacesMessages; +import org.gluu.jsf2.service.FacesService; +import org.gluu.oxauth.auth.Authenticator; +import org.gluu.oxauth.ciba.CIBAPingCallbackService; +import org.gluu.oxauth.ciba.CIBAPushErrorService; +import org.gluu.oxauth.model.authorize.AuthorizeErrorResponseType; +import org.gluu.oxauth.model.authorize.AuthorizeRequestParam; +import org.gluu.oxauth.model.ciba.PushErrorResponseType; +import org.gluu.oxauth.model.common.*; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.security.Identity; +import org.gluu.oxauth.service.ciba.CibaRequestService; +import org.gluu.oxauth.util.RedirectUri; +import org.gluu.oxauth.util.ServerUtil; +import org.oxauth.persistence.model.Scope; +import org.slf4j.Logger; + +import javax.enterprise.context.RequestScoped; +import javax.faces.application.FacesMessage; +import javax.faces.context.ExternalContext; +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.gluu.oxauth.model.util.StringUtils.spaceSeparatedToList; + +/** + * @author Yuriy Movchan + * @author Javier Rojas Blum + * @version May 9, 2020 + */ +@RequestScoped +public class AuthorizeService { + + @Inject + private Logger log; + + @Inject + private ClientService clientService; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private SessionIdService sessionIdService; + + @Inject + private CookieService cookieService; + + @Inject + private ClientAuthorizationsService clientAuthorizationsService; + + @Inject + private Identity identity; + + @Inject + private Authenticator authenticator; + + @Inject + private FacesService facesService; + + @Inject + private FacesMessages facesMessages; + + @Inject + private ExternalContext externalContext; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private ScopeService scopeService; + + @Inject + private RequestParameterService requestParameterService; + + @Inject + private AuthorizationGrantList authorizationGrantList; + + @Inject + private CIBAPingCallbackService cibaPingCallbackService; + + @Inject + private CIBAPushErrorService cibaPushErrorService; + + @Inject + private CibaRequestService cibaRequestService; + + @Inject + private DeviceAuthorizationService deviceAuthorizationService; + + public SessionId getSession() { + return getSession(null); + } + + public SessionId getSession(String sessionId) { + if (StringUtils.isBlank(sessionId)) { + sessionId = cookieService.getSessionIdFromCookie(); + if (StringUtils.isBlank(sessionId)) { + return null; + } + } + + if (!identity.isLoggedIn()) { + authenticator.authenticateBySessionId(sessionId); + } + + SessionId ldapSessionId = sessionIdService.getSessionId(sessionId); + if (ldapSessionId == null) { + identity.logout(); + } + + return ldapSessionId; + } + + public void permissionGranted(HttpServletRequest httpRequest, final SessionId session) { + log.trace("permissionGranted"); + try { + final User user = sessionIdService.getUser(session); + if (user == null) { + log.debug("Permission denied. Failed to find session user: userDn = " + session.getUserDn() + "."); + permissionDenied(session); + return; + } + + String clientId = session.getSessionAttributes().get(AuthorizeRequestParam.CLIENT_ID); + final Client client = clientService.getClient(clientId); + + String scope = session.getSessionAttributes().get(AuthorizeRequestParam.SCOPE); + Set scopeSet = Sets.newHashSet(spaceSeparatedToList(scope)); + String responseType = session.getSessionAttributes().get(AuthorizeRequestParam.RESPONSE_TYPE); + + boolean persistDuringImplicitFlow = ServerUtil.isFalse(appConfiguration.getUseCacheForAllImplicitFlowObjects()) || !ResponseType.isImplicitFlow(responseType); + if (!client.getTrustedClient() && persistDuringImplicitFlow && client.getPersistClientAuthorizations()) { + + clientAuthorizationsService.add(user.getAttribute("inum"), client.getClientId(), scopeSet); + } + session.addPermission(clientId, true, scopeSet); + sessionIdService.updateSessionId(session); + identity.setSessionId(session); + + // OXAUTH-297 - set session_id cookie + if (!appConfiguration.getInvalidateSessionCookiesAfterAuthorizationFlow()) { + cookieService.createSessionIdCookie(session, false); + } + Map sessionAttribute = requestParameterService.getAllowedParameters(session.getSessionAttributes()); + + if (sessionAttribute.containsKey(AuthorizeRequestParam.PROMPT)) { + List prompts = Prompt.fromString(sessionAttribute.get(AuthorizeRequestParam.PROMPT), " "); + prompts.remove(Prompt.CONSENT); + sessionAttribute.put(AuthorizeRequestParam.PROMPT, org.gluu.oxauth.model.util.StringUtils.implodeEnum(prompts, " ")); + } + + final String parametersAsString = requestParameterService.parametersAsString(sessionAttribute); + String uri = httpRequest.getContextPath() + "/restv1/authorize?" + parametersAsString; + log.trace("permissionGranted, redirectTo: {}", uri); + + if (invalidateSessionCookiesIfNeeded()) { + if (!uri.contains(AuthorizeRequestParam.SESSION_ID) && appConfiguration.getSessionIdRequestParameterEnabled()) { + uri += "&session_id=" + session.getId(); + } + } + facesService.redirectToExternalURL(uri); + } catch (Exception e) { + log.error("Unable to perform grant permission", e); + showErrorPage("login.failedToGrantPermission"); + } + } + + public void permissionDenied(final SessionId session) { + try { + permissionDeniedInternal(session); + } catch (Exception e) { + log.error("Unable to perform permission deny", e); + showErrorPage("login.failedToDeny"); + } + } + + public void permissionDeniedInternal(final SessionId session) { + log.trace("permissionDenied"); + invalidateSessionCookiesIfNeeded(); + + if (session == null) { + authenticationFailedSessionInvalid(); + return; + } + + String baseRedirectUri = session.getSessionAttributes().get(AuthorizeRequestParam.REDIRECT_URI); + String state = session.getSessionAttributes().get(AuthorizeRequestParam.STATE); + ResponseMode responseMode = ResponseMode.fromString(session.getSessionAttributes().get(AuthorizeRequestParam.RESPONSE_MODE)); + List responseType = ResponseType.fromString(session.getSessionAttributes().get(AuthorizeRequestParam.RESPONSE_TYPE), " "); + + RedirectUri redirectUri = new RedirectUri(baseRedirectUri, responseType, responseMode); + redirectUri.parseQueryString(errorResponseFactory.getErrorAsQueryString(AuthorizeErrorResponseType.ACCESS_DENIED, state)); + + // CIBA + Map sessionAttribute = requestParameterService.getAllowedParameters(session.getSessionAttributes()); + if (sessionAttribute.containsKey(AuthorizeRequestParam.AUTH_REQ_ID)) { + String authReqId = sessionAttribute.get(AuthorizeRequestParam.AUTH_REQ_ID); + CibaRequestCacheControl request = cibaRequestService.getCibaRequest(authReqId); + + if (request != null && request.getClient() != null) { + if (request.getStatus() == CibaRequestStatus.PENDING) { + cibaRequestService.removeCibaRequest(authReqId); + } + switch (request.getClient().getBackchannelTokenDeliveryMode()) { + case POLL: + request.setStatus(CibaRequestStatus.DENIED); + request.setTokensDelivered(false); + cibaRequestService.update(request); + break; + case PING: + request.setStatus(CibaRequestStatus.DENIED); + request.setTokensDelivered(false); + cibaRequestService.update(request); + + cibaPingCallbackService.pingCallback( + request.getAuthReqId(), + request.getClient().getBackchannelClientNotificationEndpoint(), + request.getClientNotificationToken() + ); + break; + case PUSH: + cibaPushErrorService.pushError( + request.getAuthReqId(), + request.getClient().getBackchannelClientNotificationEndpoint(), + request.getClientNotificationToken(), + PushErrorResponseType.ACCESS_DENIED, + "The end-user denied the authorization request."); + break; + } + } + } + if (sessionAttribute.containsKey(DeviceAuthorizationService.SESSION_USER_CODE)) { + processDeviceAuthDeniedResponse(sessionAttribute); + } + + facesService.redirectToExternalURL(redirectUri.toString()); + } + + private void authenticationFailedSessionInvalid() { + showErrorPage("login.errorSessionInvalidMessage"); + } + + private void showErrorPage(String errorCode) { + log.debug("Redirect to /error.xhtml page with {} error code.", errorCode); + facesMessages.add(FacesMessage.SEVERITY_ERROR, errorCode); + facesService.redirect("/error.xhtml"); + } + + public List getScopes() { + SessionId session = getSession(); + String scope = session.getSessionAttributes().get("scope"); + + return getScopes(scope); + + } + + public List getScopes(String scopes) { + List result = new ArrayList(); + + if (scopes != null && !scopes.isEmpty()) { + String[] scopesName = scopes.split(" "); + for (String scopeName : scopesName) { + org.oxauth.persistence.model.Scope s = scopeService.getScopeById(scopeName); + if (s != null && s.getDescription() != null) { + result.add(s); + } + } + } + + return result; + } + + private boolean invalidateSessionCookiesIfNeeded() { + if (appConfiguration.getInvalidateSessionCookiesAfterAuthorizationFlow()) { + return invalidateSessionCookies(); + } + return false; + } + + private boolean invalidateSessionCookies() { + try { + if (externalContext.getResponse() instanceof HttpServletResponse) { + final HttpServletResponse httpResponse = (HttpServletResponse) externalContext.getResponse(); + + log.trace("Invalidated {} cookie.", CookieService.SESSION_ID_COOKIE_NAME); + httpResponse.addHeader("Set-Cookie", CookieService.SESSION_ID_COOKIE_NAME + "=deleted; Path=/; Secure; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:01 GMT;"); + + log.trace("Invalidated {} cookie.", CookieService.CONSENT_SESSION_ID_COOKIE_NAME); + httpResponse.addHeader("Set-Cookie", CookieService.CONSENT_SESSION_ID_COOKIE_NAME + "=deleted; Path=/; Secure; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:01 GMT;"); + return true; + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return false; + } + + private void processDeviceAuthDeniedResponse(Map sessionAttribute) { + String userCode = sessionAttribute.get(DeviceAuthorizationService.SESSION_USER_CODE); + DeviceAuthorizationCacheControl cacheData = deviceAuthorizationService.getDeviceAuthzByUserCode(userCode); + + if (cacheData != null && cacheData.getStatus() == DeviceAuthorizationStatus.PENDING) { + cacheData.setStatus(DeviceAuthorizationStatus.DENIED); + deviceAuthorizationService.saveInCache(cacheData, true, false); + deviceAuthorizationService.removeDeviceAuthRequestInCache(userCode, null); + } + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/BaseAuthFilterService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/BaseAuthFilterService.java new file mode 100644 index 00000000..e3d10a34 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/BaseAuthFilterService.java @@ -0,0 +1,283 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.inject.Inject; + +import org.gluu.oxauth.model.configuration.BaseFilter; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.persist.exception.operation.SearchException; +import org.gluu.persist.ldap.impl.LdapFilterConverter; +import org.gluu.persist.model.base.BaseEntry; +import org.gluu.search.filter.Filter; +import org.gluu.util.ArrayHelper; +import org.gluu.util.StringHelper; +import org.slf4j.Logger; + +/** + * @author Yuriy Movchan + * @author Yuriy Zabrovarnyy + * @author Javier Rojas Blum + * @version March 4, 2016 + */ + +public abstract class BaseAuthFilterService { + + @Inject + protected Logger log; + + @Inject + protected LdapFilterConverter ldapFilterConverter; + + public static final Pattern PARAM_VALUE_PATTERN = Pattern.compile("([\\w]+)[\\s]*\\=[\\*\\s]*(\\{[\\s]*[\\d]+[\\s]*\\})[\\*\\s]*"); + + private boolean enabled; + private boolean filterAttributes = true; + + private List filterWithParameters; + + public static class AuthenticationFilterWithParameters { + + private BaseFilter authenticationFilter; + private List variableNames; + private List indexedVariables; + + public AuthenticationFilterWithParameters(BaseFilter authenticationFilter, List variableNames, List indexedVariables) { + this.authenticationFilter = authenticationFilter; + this.variableNames = variableNames; + this.indexedVariables = indexedVariables; + } + + public BaseFilter getAuthenticationFilter() { + return authenticationFilter; + } + + public void setAuthenticationFilter(BaseFilter authenticationFilter) { + this.authenticationFilter = authenticationFilter; + } + + public List getVariableNames() { + return variableNames; + } + + public void setVariableNames(List variableNames) { + this.variableNames = variableNames; + } + + public List getIndexedVariables() { + return indexedVariables; + } + + public void setIndexedVariables(List indexedVariables) { + this.indexedVariables = indexedVariables; + } + + public String toString() { + return String.format("AutheticationFilterWithParameters [authenticationFilter=%s, variableNames=%s, indexedVariables=%s]", + authenticationFilter, variableNames, indexedVariables); + } + + } + + public static class IndexedParameter { + + private String paramName; + private String paramIndex; + + public IndexedParameter(String paramName, String paramIndex) { + this.paramName = paramName; + this.paramIndex = paramIndex; + } + + public String getParamName() { + return paramName; + } + + public void setParamName(String paramName) { + this.paramName = paramName; + } + + public String getParamIndex() { + return paramIndex; + } + + public void setParamIndex(String paramIndex) { + this.paramIndex = paramIndex; + } + + public String toString() { + return String.format("IndexedParameter [paramName=%s, paramIndex=%s]", paramName, paramIndex); + } + } + + public void init(List p_filterList, boolean p_enabled, boolean p_filterAttributes) { + this.enabled = p_enabled; + this.filterWithParameters = prepareAuthenticationFilterWithParameters(p_filterList); + this.filterAttributes = p_filterAttributes; + } + + private List prepareAuthenticationFilterWithParameters(List p_filterList) { + final List tmpAuthenticationFilterWithParameters = new ArrayList(); + + if (!this.enabled || p_filterList == null) { + return tmpAuthenticationFilterWithParameters; + } + + for (BaseFilter authenticationFilter : p_filterList) { + if (Boolean.TRUE.equals(authenticationFilter.getBind()) && StringHelper.isEmpty(authenticationFilter.getBindPasswordAttribute())) { + log.error("Skipping authentication filter:\n '{}'\n. It should contains not empty bind-password-attribute attribute. ", authenticationFilter); + continue; + } + + List variableNames = new ArrayList(); + List indexedParameters = new ArrayList(); + + Matcher matcher = BaseAuthFilterService.PARAM_VALUE_PATTERN.matcher(authenticationFilter.getFilter()); + while (matcher.find()) { + String paramName = normalizeAttributeName(matcher.group(1)); + String paramIndex = matcher.group(2); + + variableNames.add(paramName); + indexedParameters.add(new BaseAuthFilterService.IndexedParameter(paramName, paramIndex)); + } + + AuthenticationFilterWithParameters tmpAutheticationFilterWithParameter = new AuthenticationFilterWithParameters(authenticationFilter, variableNames, indexedParameters); + tmpAuthenticationFilterWithParameters.add(tmpAutheticationFilterWithParameter); + + log.debug("Authentication filter with parameters: '{}'. ", tmpAutheticationFilterWithParameter); + } + + return tmpAuthenticationFilterWithParameters; + } + + public static List getAllowedAuthenticationFilters(Collection attributeNames, List p_filterList) { + List tmpAuthenticationFilterWithParameters = new ArrayList(); + if (attributeNames == null) { + return tmpAuthenticationFilterWithParameters; + } + + Set normalizedAttributeNames = new HashSet(); + for (Object attributeName : attributeNames) { + normalizedAttributeNames.add(normalizeAttributeName(attributeName.toString())); + } + + for (AuthenticationFilterWithParameters autheticationFilterWithParameters : p_filterList) { + if (normalizedAttributeNames.containsAll(autheticationFilterWithParameters.getVariableNames())) { + tmpAuthenticationFilterWithParameters.add(autheticationFilterWithParameters); + } + } + + return tmpAuthenticationFilterWithParameters; + } + + public static Map normalizeAttributeMap(Map attributeValues) { + Map normalizedAttributeValues = new HashMap(); + for (Map.Entry attributeValueEntry : attributeValues.entrySet()) { + String attributeValue = null; + + Object attributeValueEntryValue = attributeValueEntry.getValue(); + if (attributeValueEntryValue instanceof String[]) { + if (ArrayHelper.isNotEmpty((String[]) attributeValueEntryValue)) { + attributeValue = ((String[]) attributeValueEntryValue)[0]; + } + } else if (attributeValueEntryValue instanceof String) { + attributeValue = (String) attributeValueEntryValue; + } else if (attributeValueEntryValue != null) { + attributeValue = attributeValueEntryValue.toString(); + } + + if (attributeValue != null) { + normalizedAttributeValues.put(normalizeAttributeName(attributeValueEntry.getKey().toString()), attributeValue); + } + } + return normalizedAttributeValues; + } + + public static String buildFilter(AuthenticationFilterWithParameters authenticationFilterWithParameters, Map p_normalizedAttributeValues) { + String filter = authenticationFilterWithParameters.getAuthenticationFilter().getFilter(); + for (IndexedParameter indexedParameter : authenticationFilterWithParameters.getIndexedVariables()) { + String attributeValue = p_normalizedAttributeValues.get(indexedParameter.getParamName()); + if (attributeValue != null) { + filter = filter.replace(indexedParameter.getParamIndex(), attributeValue); + } + } + return filter; + } + + public String loadEntryDN(PersistenceEntryManager p_manager, Class entryClass, AuthenticationFilterWithParameters authenticationFilterWithParameters, Map normalizedAttributeValues) throws SearchException { + final String filter = buildFilter(authenticationFilterWithParameters, normalizedAttributeValues); + + Filter ldapFilter = ldapFilterConverter.convertRawLdapFilterToFilter(filter).multiValued(false); + List foundEntries = p_manager.findEntries(authenticationFilterWithParameters.getAuthenticationFilter().getBaseDn(), entryClass, ldapFilter, new String[0]); + + if (foundEntries.size() > 1) { + log.error("Found more than one entry by filter: '{}'. Entries:\n", ldapFilter, foundEntries); + return null; + } + + if (!(foundEntries.size() == 1)) { + return null; + } + + return ((BaseEntry) foundEntries.get(0)).getDn(); + } + + public String processAuthenticationFilters(Map attributeValues) throws SearchException { + if (attributeValues == null) { + return null; + } + + final List allowedList = filterAttributes ? + getAllowedAuthenticationFilters(attributeValues.keySet(), getFilterWithParameters()) : + getFilterWithParameters(); + + for (AuthenticationFilterWithParameters allowed : allowedList) { + String resultDn = processAuthenticationFilter(allowed, attributeValues); + if (StringHelper.isNotEmpty(resultDn)) { + return resultDn; + } + } + + return null; + } + + public abstract String processAuthenticationFilter(AuthenticationFilterWithParameters p_allowed, Map p_attributeValues) throws SearchException; + + public List getFilterWithParameters() { + return filterWithParameters; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean p_enabled) { + enabled = p_enabled; + } + + public boolean isFilterAttributes() { + return filterAttributes; + } + + public void setFilterAttributes(boolean p_filterAttributes) { + filterAttributes = p_filterAttributes; + } + + public static String normalizeAttributeName(String attributeName) { + return StringHelper.toLowerCase(attributeName.trim()); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/CleanerTimer.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/CleanerTimer.java new file mode 100644 index 00000000..d2322d92 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/CleanerTimer.java @@ -0,0 +1,230 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service; + +import java.util.Date; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Event; +import javax.enterprise.event.Observes; +import javax.inject.Inject; +import javax.inject.Named; + +import org.gluu.model.ApplicationType; +import org.gluu.model.metric.ldap.MetricEntry; +import org.gluu.oxauth.model.config.StaticConfiguration; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.fido.u2f.DeviceRegistration; +import org.gluu.oxauth.model.fido.u2f.RegisterRequestMessageLdap; +import org.gluu.oxauth.model.ldap.ClientAuthorization; +import org.gluu.oxauth.model.ldap.TokenLdap; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.model.uma.persistence.UmaResource; +import org.gluu.oxauth.service.fido.u2f.RequestService; +import org.gluu.oxauth.uma.authorization.UmaPCT; +import org.gluu.oxauth.uma.service.UmaPctService; +import org.gluu.oxauth.uma.service.UmaResourceService; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.search.filter.Filter; +import org.gluu.service.cache.CacheProvider; +import org.gluu.service.cdi.async.Asynchronous; +import org.gluu.service.cdi.event.CleanerEvent; +import org.gluu.service.cdi.event.Scheduled; +import org.gluu.service.timer.event.TimerEvent; +import org.gluu.service.timer.schedule.TimerSchedule; +import org.oxauth.persistence.model.Scope; +import org.slf4j.Logger; + +import com.google.common.base.Stopwatch; +import com.google.common.collect.Maps; + +/** + * @author Yuriy Zabrovarnyy + * @author Javier Rojas Blum + * @version August 9, 2017 + */ +@ApplicationScoped +public class CleanerTimer { + + public final static int BATCH_SIZE = 1000; + private final static int DEFAULT_INTERVAL = 30; // 30 seconds + + @Inject + private Logger log; + + @Inject + private PersistenceEntryManager entryManager; + + @Inject + private UmaPctService umaPctService; + + @Inject + private UmaResourceService umaResourceService; + + @Inject + private CacheProvider cacheProvider; + + @Inject + @Named("u2fRequestService") + private RequestService u2fRequestService; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private StaticConfiguration staticConfiguration; + + @Inject + private Event cleanerEvent; + + @Inject + private MetricService metricService; + + private long lastFinishedTime; + + private AtomicBoolean isActive; + + public void initTimer() { + log.debug("Initializing Cleaner Timer"); + this.isActive = new AtomicBoolean(false); + + // Schedule to start cleaner every 30 seconds + cleanerEvent.fire( + new TimerEvent(new TimerSchedule(DEFAULT_INTERVAL, DEFAULT_INTERVAL), new CleanerEvent(), Scheduled.Literal.INSTANCE)); + + this.lastFinishedTime = System.currentTimeMillis(); + } + + @Asynchronous + public void process(@Observes @Scheduled CleanerEvent cleanerEvent) { + if (this.isActive.get()) { + return; + } + + if (!this.isActive.compareAndSet(false, true)) { + return; + } + + try { + processImpl(); + } finally { + this.isActive.set(false); + } + } + + private boolean isStartProcess() { + int interval = appConfiguration.getCleanServiceInterval(); + if (interval < 0) { + log.info("Cleaner Timer is disabled."); + log.warn("Cleaner Timer Interval (cleanServiceInterval in oxauth configuration) is negative which turns OFF internal clean up by the server. Please set it to positive value if you wish internal clean up timer run."); + return false; + } + + long cleaningInterval = interval * 1000; + + long timeDiffrence = System.currentTimeMillis() - this.lastFinishedTime; + + return timeDiffrence >= cleaningInterval; + } + + public void processImpl() { + try { + if (!isStartProcess()) { + log.trace("Starting conditions aren't reached"); + return; + } + + int chunkSize = appConfiguration.getCleanServiceBatchChunkSize(); + if (chunkSize <= 0) + chunkSize = BATCH_SIZE; + + Date now = new Date(); + + final Set processedBaseDns = new HashSet<>(); + for (Map.Entry> baseDn : createCleanServiceBaseDns().entrySet()) { + try { + if (entryManager.hasExpirationSupport(baseDn.getKey())) { + continue; + } + + String processedBaseDn = baseDn.getKey() + "_" + (baseDn.getValue() == null ? "" : baseDn.getValue().getSimpleName()); + if (processedBaseDns.contains(processedBaseDn)) { + log.warn("baseDn: {}, already processed. Please fix cleaner configuration! Skipping second run...", baseDn); + continue; + } + + processedBaseDns.add(processedBaseDn); + + log.debug("Start clean up for baseDn: " + baseDn.getValue() + ", class: " + baseDn.getValue()); + final Stopwatch started = Stopwatch.createStarted(); + + int removed = cleanup(baseDn, now, chunkSize); + + log.debug("Finished clean up for baseDn: {}, takes: {}ms, removed items: {}", baseDn, started.elapsed(TimeUnit.MILLISECONDS), removed); + } catch (Exception e) { + log.error("Failed to process clean up for baseDn: " + baseDn + ", class: " + baseDn.getValue(), e); + } + } + + processCache(now); + + this.lastFinishedTime = System.currentTimeMillis(); + } catch (Exception e) { + log.error("Failed to process clean up.", e); + } + } + + private Map> createCleanServiceBaseDns() { + final String u2fBase = staticConfiguration.getBaseDn().getU2fBase(); + + final Map> cleanServiceBaseDns = Maps.newHashMap(); + + cleanServiceBaseDns.put(staticConfiguration.getBaseDn().getClients(), Client.class); + cleanServiceBaseDns.put(umaPctService.branchBaseDn(), UmaPCT.class); + cleanServiceBaseDns.put(umaResourceService.getBaseDnForResource(), UmaResource.class); + cleanServiceBaseDns.put(String.format("ou=registration_requests,%s", u2fBase), RegisterRequestMessageLdap.class); + cleanServiceBaseDns.put(String.format("ou=registered_devices,%s", u2fBase), DeviceRegistration.class); + // cleanServiceBaseDns.put(staticConfiguration.getBaseDn().getPeople(), User.class); + cleanServiceBaseDns.put(metricService.buildDn(null, null, ApplicationType.OX_AUTH), MetricEntry.class); + cleanServiceBaseDns.put(staticConfiguration.getBaseDn().getTokens(), TokenLdap.class); + cleanServiceBaseDns.put(staticConfiguration.getBaseDn().getAuthorizations(), ClientAuthorization.class); + cleanServiceBaseDns.put(staticConfiguration.getBaseDn().getScopes(), Scope.class); + cleanServiceBaseDns.put(staticConfiguration.getBaseDn().getSessions(), SessionId.class); + + return cleanServiceBaseDns; + } + + public int cleanup(final Map.Entry> baseDn, final Date now, final int batchSize) { + try { + Filter filter = Filter.createANDFilter( + Filter.createEqualityFilter("del", true), + Filter.createLessOrEqualFilter("exp", entryManager.encodeTime(baseDn.getKey(), now))); + + int removedCount = entryManager.remove(baseDn.getKey(), baseDn.getValue(), filter, batchSize); + log.trace("Removed " + removedCount + " entries from " + baseDn.getKey()); + return removedCount; + } catch (Exception e) { + log.error("Failed to perform clean up.", e); + } + + return 0; + } + + private void processCache(Date now) { + try { + cacheProvider.cleanup(now); + } catch (Exception e) { + log.error("Failed to clean up cache.", e); + } + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/ClientAuthorizationsService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/ClientAuthorizationsService.java new file mode 100644 index 00000000..4ab4316d --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/ClientAuthorizationsService.java @@ -0,0 +1,173 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service; + +import org.apache.commons.lang3.ArrayUtils; +import org.gluu.oxauth.model.config.StaticConfiguration; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.ldap.ClientAuthorization; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.persist.exception.EntryPersistenceException; +import org.gluu.persist.model.base.SimpleBranch; +import org.gluu.search.filter.Filter; +import org.gluu.util.StringHelper; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.inject.Named; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * @author Javier Rojas Blum + * @version March 4, 2020 + */ +@Named +public class ClientAuthorizationsService { + + @Inject + private Logger log; + + @Inject + private PersistenceEntryManager ldapEntryManager; + + @Inject + private ClientService clientService; + + @Inject + private StaticConfiguration staticConfiguration; + + @Inject + private AppConfiguration appConfiguration; + + public void addBranch() { + SimpleBranch branch = new SimpleBranch(); + branch.setOrganizationalUnitName("authorizations"); + branch.setDn(createDn(null)); + + ldapEntryManager.persist(branch); + } + + public boolean containsBranch() { + return ldapEntryManager.contains(createDn(null), SimpleBranch.class); + } + + public void prepareBranch() { + String baseDn = createDn(null); + if (!ldapEntryManager.hasBranchesSupport(baseDn)) { + return; + } + + // Create client authorizations branch if needed + if (!containsBranch()) { + addBranch(); + } + } + + public ClientAuthorization find(String userInum, String clientId) { + prepareBranch(); + + final String id = createId(userInum, clientId); + try { + if (appConfiguration.getClientAuthorizationBackwardCompatibility()) { + return findToRemoveIn50(userInum, clientId); + } + return ldapEntryManager.find(ClientAuthorization.class, createDn(createId(userInum, clientId))); + } catch (EntryPersistenceException e) { + log.trace("Unable to find client persistence for {}", id); + return null; + } catch (Exception e) { + log.error(e.getMessage(), e); + return null; + } + } + + // old version should should be removed in 5.0 version. (We have to fetch entry by key instead of query to improve performance) + public ClientAuthorization findToRemoveIn50(String userInum, String clientId) { + Filter filter = Filter.createANDFilter( + Filter.createEqualityFilter("oxAuthClientId", clientId), + Filter.createEqualityFilter("oxAuthUserId", userInum) + ); + + List entries = ldapEntryManager.findEntries(staticConfiguration.getBaseDn().getAuthorizations(), ClientAuthorization.class, filter); + if (entries != null && !entries.isEmpty()) { + if (entries.size() > 1) { + for (ClientAuthorization entry : entries) { + if (entry.getId().equals(createId(entry.getUserId(), entry.getClientId()))) { + return entry; // return entry where id fits to "userId + _ + clientId" pattern + } + } + } + return entries.get(0); + } + + return null; + } + + public void clearAuthorizations(ClientAuthorization clientAuthorization, boolean persistInPersistence) { + if (clientAuthorization == null) { + return; + } + + if (persistInPersistence) { + ldapEntryManager.remove(clientAuthorization); + } + } + + public void add(String userInum, String clientId, Set scopes) { + log.trace("Attempting to add client authorization, scopes:" + scopes + ", clientId: " + clientId + ", userInum: " + userInum); + Client client = clientService.getClient(clientId); + + + // oxAuth #441 Pre-Authorization + Persist Authorizations... don't write anything + // If a client has pre-authorization=true, there is no point to create the entry under + // ou=clientAuthorizations it will negatively impact performance, grow the size of the + // ldap database, and serve no purpose. + prepareBranch(); + + ClientAuthorization clientAuthorization = find(userInum, clientId); + + if (clientAuthorization == null) { + final String id = createId(userInum, clientId); + + clientAuthorization = new ClientAuthorization(); + clientAuthorization.setId(id); + clientAuthorization.setDn(createDn(id)); + clientAuthorization.setClientId(clientId); + clientAuthorization.setUserId(userInum); + clientAuthorization.setScopes(scopes.toArray(new String[scopes.size()])); + clientAuthorization.setDeletable(!client.getAttributes().getKeepClientAuthorizationAfterExpiration()); + clientAuthorization.setExpirationDate(client.getExpirationDate()); + clientAuthorization.setTtl(appConfiguration.getDynamicRegistrationExpirationTime()); + + ldapEntryManager.persist(clientAuthorization); + } else if (ArrayUtils.isNotEmpty(clientAuthorization.getScopes())) { + Set set = new HashSet<>(scopes); + set.addAll(Arrays.asList(clientAuthorization.getScopes())); + + if (set.size() != clientAuthorization.getScopes().length) { + clientAuthorization.setScopes(set.toArray(new String[set.size()])); + ldapEntryManager.merge(clientAuthorization); + } + } + } + + public static String createId(String userId, String clientId) { + return userId + "_" + clientId; + } + + public String createDn(String oxId) { + String baseDn = staticConfiguration.getBaseDn().getAuthorizations(); + if (StringHelper.isEmpty(oxId)) { + return baseDn; + } + return String.format("oxId=%s,%s", oxId, baseDn); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/ClientFilterService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/ClientFilterService.java new file mode 100644 index 00000000..e38b90e7 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/ClientFilterService.java @@ -0,0 +1,53 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service; + +import java.util.Map; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.persist.exception.operation.SearchException; + +/** + * @author Yuriy Zabrovarnyy + * @author Javier Rojas Blum + * @version March 4, 2016 + */ +@ApplicationScoped +public class ClientFilterService extends BaseAuthFilterService { + + @Inject + private PersistenceEntryManager ldapEntryManager; + + @Inject + private AppConfiguration appConfiguration; + + @PostConstruct + public void init() { + super.init(appConfiguration.getClientAuthenticationFilters(), Boolean.TRUE.equals(appConfiguration.getClientAuthenticationFiltersEnabled()), false); + } + + public String processAuthenticationFilter(AuthenticationFilterWithParameters authenticationFilterWithParameters, Map attributeValues) throws SearchException { + if (attributeValues == null) { + return null; + } + final Map normalizedAttributeValues = normalizeAttributeMap(attributeValues); + final String resultDn = loadEntryDN(ldapEntryManager, Client.class, authenticationFilterWithParameters, normalizedAttributeValues); + if (StringUtils.isBlank(resultDn)) { + return null; + } + + return resultDn; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/ClientService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/ClientService.java new file mode 100644 index 00000000..4ba71dac --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/ClientService.java @@ -0,0 +1,355 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Sets; +import org.gluu.oxauth.model.common.AuthenticationMethod; +import org.gluu.oxauth.model.config.StaticConfiguration; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.exception.InvalidClaimException; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.service.common.EncryptionService; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.persist.exception.EntryPersistenceException; +import org.gluu.persist.model.base.CustomAttribute; +import org.gluu.persist.model.base.CustomEntry; +import org.gluu.service.BaseCacheService; +import org.gluu.service.CacheService; +import org.gluu.service.LocalCacheService; +import org.gluu.util.StringHelper; +import org.gluu.util.security.StringEncrypter; +import org.gluu.util.security.StringEncrypter.EncryptionException; +import org.json.JSONArray; +import org.oxauth.persistence.model.Scope; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import java.util.*; + +/** + * Provides operations with clients. + * + * @author Javier Rojas Blum + * @author Yuriy Movchan Date: 04/15/2014 + * @version October 22, 2016 + */ +@ApplicationScoped +public class ClientService { + + public static final String[] CLIENT_OBJECT_CLASSES = new String[] { "oxAuthClient" }; + + @Inject + private Logger log; + + @Inject + private PersistenceEntryManager ldapEntryManager; + + @Inject + private CacheService cacheService; + + @Inject + private LocalCacheService localCacheService; + + @Inject + private ScopeService scopeService; + + @Inject + private EncryptionService encryptionService; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private StaticConfiguration staticConfiguration; + + public void persist(Client client) { + ldapEntryManager.persist(client); + } + + public void merge(Client client) { + ldapEntryManager.merge(client); + removeFromCache(client); + } + + /** + * Authenticate client. + * + * @param clientId + * Client inum. + * @param password + * Client password. + * @return true if success, otherwise false. + */ + public boolean authenticate(String clientId, String password) { + log.debug("Authenticating Client with LDAP: clientId = {}", clientId); + boolean authenticated = false; + + try { + Client client = getClient(clientId); + if (client == null) { + log.debug("Failed to find client = {}", clientId); + return authenticated; + } + String decryptedClientSecret = decryptSecret(client.getClientSecret()); + authenticated = client != null && decryptedClientSecret != null && decryptedClientSecret.equals(password); + } catch (StringEncrypter.EncryptionException e) { + log.error(e.getMessage(), e); + } + + return authenticated; + } + + public Set getClient(Collection clientIds, boolean silent) { + Set set = Sets.newHashSet(); + + if (clientIds == null) { + return set; + } + + for (String clientId : clientIds) { + try { + Client client = getClient(clientId); + if (client != null) { + set.add(client); + } + } catch (RuntimeException e) { + if (!silent) { + throw e; + } + } + } + return set; + } + + public Client getClient(String clientId) { + if (clientId != null && !clientId.isEmpty()) { + Client result = getClientByDn(buildClientDn(clientId)); + log.debug("Found {} entries for client id = {}", result != null ? 1 : 0, clientId); + + return result; + } + return null; + } + + public boolean isPublic(String clientId) { + return isPublic(getClient(clientId)); + } + + public boolean isPublic(Client client) { + return client != null && client.getAuthenticationMethod() == AuthenticationMethod.NONE; + } + + public Client getClient(String clientId, String registrationAccessToken) { + final Client client = getClient(clientId); + if (client != null && registrationAccessToken != null && registrationAccessToken.equals(client.getRegistrationAccessToken())) { + return client; + } + return null; + } + + public Set getClientsByDns(Collection dnList) { + return getClientsByDns(dnList, true); + } + + public Set getClientsByDns(Collection dnList, boolean silently) { + Preconditions.checkNotNull(dnList); + + final Set result = Sets.newHashSet(); + for (String clientDn : dnList) { + try { + result.add(getClientByDn(clientDn)); + } catch (RuntimeException e) { + if (!silently) { + throw e; + } + } + } + return result; + } + + /** + * Returns client by DN. + * + * @param dn + * dn of client + * @return Client + */ + public Client getClientByDn(String dn) { + BaseCacheService usedCacheService = getCacheService(); + try { + return usedCacheService.getWithPut(dn, () -> ldapEntryManager.find(Client.class, dn), 60); + } catch (Exception e) { + log.trace(e.getMessage(), e); + return null; + } + } + + public org.gluu.persist.model.base.CustomAttribute getCustomAttribute(Client client, String attributeName) { + for (org.gluu.persist.model.base.CustomAttribute customAttribute : client.getCustomAttributes()) { + if (StringHelper.equalsIgnoreCase(attributeName, customAttribute.getName())) { + return customAttribute; + } + } + + return null; + } + + public void setCustomAttribute(Client client, String attributeName, String attributeValue) { + org.gluu.persist.model.base.CustomAttribute customAttribute = getCustomAttribute(client, attributeName); + + if (customAttribute == null) { + customAttribute = new org.gluu.persist.model.base.CustomAttribute(attributeName); + client.getCustomAttributes().add(customAttribute); + } + + customAttribute.setValue(attributeValue); + } + + public List getAllClients(String[] returnAttributes) { + String baseDn = staticConfiguration.getBaseDn().getClients(); + + List result = ldapEntryManager.findEntries(baseDn, Client.class, null, returnAttributes); + + return result; + } + + public List getAllClients(String[] returnAttributes, int size) { + String baseDn = staticConfiguration.getBaseDn().getClients(); + + List result = ldapEntryManager.findEntries(baseDn, Client.class, null, returnAttributes, size); + + return result; + } + + public String buildClientDn(String p_clientId) { + final StringBuilder dn = new StringBuilder(); + dn.append(String.format("inum=%s,", p_clientId)); + dn.append(staticConfiguration.getBaseDn().getClients()); // ou=clients,o=gluu + return dn.toString(); + } + + public void remove(Client client) { + if (client != null) { + removeFromCache(client); + + String clientDn = client.getDn(); + ldapEntryManager.removeRecursively(clientDn, Client.class); + } + } + + private void removeFromCache(Client client) { + BaseCacheService usedCacheService = getCacheService(); + try { + usedCacheService.remove(client.getDn()); + } catch (Exception e) { + log.error("Failed to remove client from cache." + client.getDn(), e); + } + } + + public void updateAccessTime(Client client, boolean isUpdateLogonTime) { + if (!appConfiguration.getUpdateClientAccessTime()) { + return; + } + + String clientDn = client.getDn(); + + CustomEntry customEntry = new CustomEntry(); + customEntry.setDn(clientDn); + customEntry.setCustomObjectClasses(CLIENT_OBJECT_CLASSES); + + Date now = new GregorianCalendar(TimeZone.getTimeZone("UTC")).getTime(); + String nowDateString = ldapEntryManager.encodeTime(customEntry.getDn(), now); + + CustomAttribute customAttributeLastAccessTime = new CustomAttribute("oxLastAccessTime", nowDateString); + customEntry.getCustomAttributes().add(customAttributeLastAccessTime); + + if (isUpdateLogonTime) { + CustomAttribute customAttributeLastLogonTime = new CustomAttribute("oxLastLogonTime", nowDateString); + customEntry.getCustomAttributes().add(customAttributeLastLogonTime); + } + + try { + ldapEntryManager.merge(customEntry); + } catch (EntryPersistenceException epe) { + log.error("Failed to update oxLastAccessTime and oxLastLogonTime of client '{}'", clientDn); + } + + removeFromCache(client); + } + + public Object getAttribute(Client client, String clientAttribute) throws InvalidClaimException { + Object attribute = null; + + if (clientAttribute != null) { + if (clientAttribute.equals("displayName")) { + attribute = client.getClientName(); + } else if (clientAttribute.equals("inum")) { + attribute = client.getClientId(); + } else if (clientAttribute.equals("oxAuthAppType")) { + attribute = client.getApplicationType(); + } else if (clientAttribute.equals("oxAuthIdTokenSignedResponseAlg")) { + attribute = client.getIdTokenSignedResponseAlg(); + } else if (clientAttribute.equals("oxAuthRedirectURI") && client.getRedirectUris() != null) { + JSONArray array = new JSONArray(); + for (String redirectUri : client.getRedirectUris()) { + array.put(redirectUri); + } + attribute = array; + } else if (clientAttribute.equals("oxAuthScope") && client.getScopes() != null) { + JSONArray array = new JSONArray(); + for (String scopeDN : client.getScopes()) { + Scope s = scopeService.getScopeByDn(scopeDN); + if (s != null) { + String scopeName = s.getId(); + array.put(scopeName); + } + } + attribute = array; + } else { + for (CustomAttribute customAttribute : client.getCustomAttributes()) { + if (customAttribute.getName().equals(clientAttribute)) { + List values = customAttribute.getValues(); + if (values != null) { + if (values.size() == 1) { + attribute = values.get(0); + } else { + JSONArray array = new JSONArray(); + for (String v : values) { + array.put(v); + } + attribute = array; + } + } + + break; + } + } + } + } + + return attribute; + } + + public String decryptSecret(String encryptedClientSecret) throws EncryptionException { + return encryptionService.decrypt(encryptedClientSecret); + } + + public String encryptSecret(String clientSecret) throws EncryptionException { + return encryptionService.encrypt(clientSecret); + } + + private BaseCacheService getCacheService() { + if (appConfiguration.getUseLocalCache()) { + return localCacheService; + } + + return cacheService; + } + +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/CookieService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/CookieService.java new file mode 100644 index 00000000..77322303 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/CookieService.java @@ -0,0 +1,333 @@ +package org.gluu.oxauth.service; + +import com.google.common.collect.Sets; +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.config.ConfigurationFactory; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.model.session.SessionIdState; +import org.gluu.persist.exception.EntryPersistenceException; +import org.gluu.service.cdi.util.CdiUtil; +import org.json.JSONArray; +import org.json.JSONException; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.RequestScoped; +import javax.faces.context.ExternalContext; +import javax.faces.context.FacesContext; +import javax.inject.Inject; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Set; + +import static org.gluu.oxauth.model.util.StringUtils.toList; + +/** + * @author Yuriy Zabrovarnyy + */ +@RequestScoped +public class CookieService { + + private static final String SESSION_STATE_COOKIE_NAME = "session_state"; + public static final String OP_BROWSER_STATE = "opbs"; + public static final String SESSION_ID_COOKIE_NAME = "session_id"; + private static final String RP_ORIGIN_ID_COOKIE_NAME = "rp_origin_id"; + private static final String UMA_SESSION_ID_COOKIE_NAME = "uma_session_id"; + public static final String CONSENT_SESSION_ID_COOKIE_NAME = "consent_session_id"; + public static final String CURRENT_SESSIONS_COOKIE_NAME = "current_sessions"; + + @Inject + private Logger log; + + @Inject + private FacesContext facesContext; + + @Inject + private ExternalContext externalContext; + + @Inject + private ConfigurationFactory configurationFactory; + + @Inject + private AppConfiguration appConfiguration; + + public String getSessionIdFromCookie(HttpServletRequest request) { + return getValueFromCookie(request, SESSION_ID_COOKIE_NAME); + } + + public String getUmaSessionIdFromCookie(HttpServletRequest request) { + return getValueFromCookie(request, UMA_SESSION_ID_COOKIE_NAME); + } + + public String getConsentSessionIdFromCookie(HttpServletRequest request) { + return getValueFromCookie(request, CONSENT_SESSION_ID_COOKIE_NAME); + } + + public String getSessionStateFromCookie(HttpServletRequest request) { + return getValueFromCookie(request, SESSION_STATE_COOKIE_NAME); + } + + public Set getCurrentSessions() { + try { + if (facesContext == null) { + return null; + } + final HttpServletRequest request = (HttpServletRequest) externalContext.getRequest(); + if (request != null) { + return getCurrentSessions(request); + } else { + log.trace("Faces context returns null for http request object."); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + + return null; + } + + public Set getCurrentSessions(HttpServletRequest request) { + final String valueFromCookie = getValueFromCookie(request, CURRENT_SESSIONS_COOKIE_NAME); + if (StringUtils.isBlank(valueFromCookie)) { + return Sets.newHashSet(); + } + + try { + return Sets.newHashSet(toList(new JSONArray(valueFromCookie))); + } catch (JSONException e) { + log.error("Failed to parse current_sessions, value: " + valueFromCookie, e); + return Sets.newHashSet(); + } + } + + public void addCurrentSessionCookie(SessionId sessionId, HttpServletRequest request, HttpServletResponse httpResponse) { + final Set currentSessions = getCurrentSessions(request); + removeOutdatedCurrentSessions(currentSessions, sessionId); + currentSessions.add(sessionId.getId()); + + String header = CURRENT_SESSIONS_COOKIE_NAME + "=" + new JSONArray(currentSessions).toString(); + header += "; Path=/"; + header += "; Secure"; + header += "; HttpOnly"; + + createCookie(header, httpResponse); + } + + private void removeOutdatedCurrentSessions(Set currentSessions, SessionId session) { + if (session != null) { + final String oldSessionId = session.getSessionAttributes().get(SessionId.OLD_SESSION_ID_ATTR_KEY); + if (StringUtils.isNotBlank(oldSessionId)) { + currentSessions.remove(oldSessionId); + } + } + + if (currentSessions.isEmpty()) { + return; + } + + SessionIdService sessionIdService = CdiUtil.bean(SessionIdService.class); // avoid cycle dependency + + Set toRemove = Sets.newHashSet(); + for (String sessionId : currentSessions) { + SessionId sessionIdObject = null; + try { + sessionIdObject = sessionIdService.getSessionId(sessionId, true); + } catch (EntryPersistenceException e) { + // ignore - valid case if session is outdated + } + if (sessionIdObject == null) { + toRemove.add(sessionId); + } + } + currentSessions.removeAll(toRemove); + } + + public String getValueFromCookie(HttpServletRequest request, String cookieName) { + try { + final Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if (cookie.getName().equals(cookieName) /*&& cookie.getSecure()*/) { + log.trace("Found cookie: '{}'", cookie.getValue()); + return cookie.getValue(); + } + } + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return ""; + } + + + public String getRpOriginIdCookie() { + return getValueFromCookie(RP_ORIGIN_ID_COOKIE_NAME); + } + + public String getValueFromCookie(String cookieName) { + try { + if (facesContext == null) { + return null; + } + final HttpServletRequest request = (HttpServletRequest) externalContext.getRequest(); + if (request != null) { + return getValueFromCookie(request, cookieName); + } else { + log.trace("Faces context returns null for http request object."); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + + return null; + } + + public String getSessionIdFromCookie() { + try { + if (facesContext == null) { + return null; + } + final HttpServletRequest request = (HttpServletRequest) externalContext.getRequest(); + if (request != null) { + return getSessionIdFromCookie(request); + } else { + log.trace("Faces context returns null for http request object."); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + + return null; + } + + public void creatRpOriginIdCookie(String rpOriginId) { + try { + final Object response = externalContext.getResponse(); + if (response instanceof HttpServletResponse) { + final HttpServletResponse httpResponse = (HttpServletResponse) response; + + creatRpOriginIdCookie(rpOriginId, httpResponse); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + + public void creatRpOriginIdCookie(String rpOriginId, HttpServletResponse httpResponse) { + String header = RP_ORIGIN_ID_COOKIE_NAME + "=" + rpOriginId; + header += "; Path=" + configurationFactory.getContextPath(); + header += "; Secure"; + header += "; HttpOnly"; + + createCookie(header, httpResponse); + } + + public void createCookieWithState(String sessionId, String sessionState, String opbs, HttpServletRequest request, HttpServletResponse httpResponse, String cookieName) { + String header = cookieName + "=" + sessionId; + header += "; Path=/"; + header += "; Secure"; + header += "; HttpOnly"; + + createCookie(header, httpResponse); + + createSessionStateCookie(sessionState, httpResponse); + createOPBrowserStateCookie(opbs, httpResponse); + } + + public void createSessionIdCookie(SessionId sessionId, HttpServletRequest request, HttpServletResponse httpResponse, boolean isUma) { + String cookieName = isUma ? UMA_SESSION_ID_COOKIE_NAME : SESSION_ID_COOKIE_NAME; + if (!isUma && sessionId.getState() == SessionIdState.AUTHENTICATED) { + addCurrentSessionCookie(sessionId, request, httpResponse); + } + createCookieWithState(sessionId.getId(), sessionId.getSessionState(), sessionId.getOPBrowserState(), request, httpResponse, cookieName); + } + + public void createSessionIdCookie(SessionId sessionId, boolean isUma) { + try { + final Object response = externalContext.getResponse(); + final Object request = externalContext.getRequest(); + if (response instanceof HttpServletResponse && request instanceof HttpServletRequest) { + final HttpServletResponse httpResponse = (HttpServletResponse) response; + final HttpServletRequest httpRequest = (HttpServletRequest) request; + + createSessionIdCookie(sessionId, httpRequest, httpResponse, isUma); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + + public void createSessionStateCookie(String sessionState, HttpServletResponse httpResponse) { + // Create the special cookie header with secure flag but not HttpOnly because the session_state + // needs to be read from the OP iframe using JavaScript + String header = SESSION_STATE_COOKIE_NAME + "=" + sessionState; + header += "; Path=/"; + header += "; Secure"; + + createCookie(header, httpResponse); + } + + public void createOPBrowserStateCookie(String opbs, HttpServletResponse httpResponse) { + // Create the special cookie header with secure flag but not HttpOnly because the opbs + // needs to be read from the OP iframe using JavaScript + String header = OP_BROWSER_STATE + "=" + opbs; + header += "; Path=/"; + header += "; Secure"; + Integer sessionStateLifetime = appConfiguration.getSessionIdLifetime(); + if (sessionStateLifetime != null && sessionStateLifetime > 0) { + DateFormat formatter = new SimpleDateFormat("E, dd MMM yyyy HH:mm:ss Z"); + Calendar expirationDate = Calendar.getInstance(); + expirationDate.add(Calendar.SECOND, sessionStateLifetime); + header += "; Expires=" + formatter.format(expirationDate.getTime()) + ";"; + if (StringUtils.isNotBlank(appConfiguration.getCookieDomain())) { + header += "Domain=" + appConfiguration.getCookieDomain() + ";"; + } + } + httpResponse.addHeader("Set-Cookie", header); + } + + protected void createCookie(String header, HttpServletResponse httpResponse) { + Integer sessionStateLifetime = appConfiguration.getSessionIdLifetime(); + if (sessionStateLifetime != null && sessionStateLifetime > 0) { + DateFormat formatter = new SimpleDateFormat("E, dd MMM yyyy HH:mm:ss Z"); + Calendar expirationDate = Calendar.getInstance(); + expirationDate.add(Calendar.SECOND, sessionStateLifetime); + header += "; Expires=" + formatter.format(expirationDate.getTime()) + ";"; + if (StringUtils.isNotBlank(appConfiguration.getCookieDomain())) { + header += "Domain=" + appConfiguration.getCookieDomain() + ";"; + } + } + + httpResponse.addHeader("Set-Cookie", header); + } + + public void removeSessionIdCookie(HttpServletResponse httpResponse) { + removeCookie(SESSION_ID_COOKIE_NAME, httpResponse); + } + + public void removeOPBrowserStateCookie(HttpServletResponse httpResponse) { + removeCookie(OP_BROWSER_STATE, httpResponse); + } + + public void removeUmaSessionIdCookie(HttpServletResponse httpResponse) { + removeCookie(UMA_SESSION_ID_COOKIE_NAME, httpResponse); + } + + public void removeConsentSessionIdCookie(HttpServletResponse httpResponse) { + removeCookie(CONSENT_SESSION_ID_COOKIE_NAME, httpResponse); + } + + public void removeCookie(String cookieName, HttpServletResponse httpResponse) { + final Cookie cookie = new Cookie(cookieName, null); // Not necessary, but saves bandwidth. + cookie.setPath("/"); + cookie.setMaxAge(0); // Don't set to -1 or it will become a session cookie! + if (StringUtils.isNotBlank(appConfiguration.getCookieDomain())) { + cookie.setDomain(appConfiguration.getCookieDomain()); + } + httpResponse.addCookie(cookie); + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/CryptoProviderProviderFactory.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/CryptoProviderProviderFactory.java new file mode 100644 index 00000000..bb9b325d --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/CryptoProviderProviderFactory.java @@ -0,0 +1,48 @@ +package org.gluu.oxauth.service; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Produces; +import javax.inject.Inject; +import javax.inject.Named; + +import org.gluu.oxauth.model.common.WebKeyStorage; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.crypto.AbstractCryptoProvider; +import org.slf4j.Logger; + +/** + * Crypto Provider + * + * @author Yuriy Movchan + * @version 11/02/2018 + */ +@ApplicationScoped +@Named +public class CryptoProviderProviderFactory { + + @Inject + private Logger log; + + @Inject + private AppConfiguration appConfiguration; + + @Produces + @ApplicationScoped + public AbstractCryptoProvider getCryptoProvider() throws Exception { + log.debug("Started to create crypto provider"); + + WebKeyStorage webKeyStorage = appConfiguration.getWebKeysStorage(); + if (webKeyStorage == null) { + throw new RuntimeException("Failed to initialize cryptoProvider, cryptoProviderType is not specified!"); + } + + AbstractCryptoProvider cryptoProvider = org.gluu.oxauth.model.crypto.CryptoProviderFactory.getCryptoProvider(appConfiguration); + + if (cryptoProvider == null) { + throw new RuntimeException("Failed to initialize cryptoProvider, cryptoProviderType is unsupported: " + webKeyStorage); + } + + return cryptoProvider; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/DeviceAuthorizationService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/DeviceAuthorizationService.java new file mode 100644 index 00000000..f9e652e6 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/DeviceAuthorizationService.java @@ -0,0 +1,183 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.authorize.AuthorizeErrorResponseType; +import org.gluu.oxauth.model.common.DeviceAuthorizationCacheControl; +import org.gluu.oxauth.model.common.DeviceAuthorizationStatus; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.service.CacheService; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import java.io.Serializable; +import java.net.URI; +import java.util.Map; + +/** + * Service used to process data related to device code grant type. + */ +@ApplicationScoped +public class DeviceAuthorizationService implements Serializable { + + public static final String SESSION_ATTEMPTS = "attemps"; + public static final String SESSION_LAST_ATTEMPT = "last_attempt"; + public static final String SESSION_USER_CODE = "user_code"; + + @Inject + private Logger log; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private CacheService cacheService; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private SessionIdService sessionIdService; + + /** + * Saves data in cache, it could be saved with two identifiers used by Token endpoint or device_authorization page. + * @param data Data to be saved. + * @param saveDeviceCode Defines whether data should be saved using device code. + * @param saveUserCode Defines whether data should be saved using user code. + */ + public void saveInCache(DeviceAuthorizationCacheControl data, boolean saveDeviceCode, boolean saveUserCode) { + if (saveDeviceCode) { + cacheService.put(data.getExpiresIn(), data.getDeviceCode(), data); + } + if (saveUserCode) { + cacheService.put(data.getExpiresIn(), data.getUserCode(), data); + } + log.trace("Device request saved in cache, userCode: {}, deviceCode: {}, clientId: {}", data.getUserCode(), data.getDeviceCode(), data.getClient().getClientId()); + } + + /** + * Returns cache data related to the device authz request using device_code as cache key. + */ + public DeviceAuthorizationCacheControl getDeviceAuthzByUserCode(String userCode) { + Object cachedObject = cacheService.get(userCode); + if (cachedObject == null) { + // retry one time : sometimes during high load cache client may be not fast enough + cachedObject = cacheService.get(userCode); + log.trace("Failed to fetch DeviceAuthorizationCacheControl request from cache, cacheKey: {}", userCode); + } + return cachedObject instanceof DeviceAuthorizationCacheControl ? (DeviceAuthorizationCacheControl) cachedObject : null; + } + + /** + * Returns cache data related to the device authz request using user_code as cache key. + */ + public DeviceAuthorizationCacheControl getDeviceAuthzByDeviceCode(String deviceCode) { + Object cachedObject = cacheService.get(deviceCode); + if (cachedObject == null) { + // retry one time : sometimes during high load cache client may be not fast enough + cachedObject = cacheService.get(deviceCode); + log.trace("Failed to fetch DeviceAuthorizationCacheControl request from cache, cacheKey: {}", deviceCode); + } + return cachedObject instanceof DeviceAuthorizationCacheControl ? (DeviceAuthorizationCacheControl) cachedObject : null; + } + + /** + * Verifies whether a specific client has Device Code grant type compatibility. + * + * @param client Client to check. + */ + public boolean hasDeviceCodeCompatibility(Client client) { + for (GrantType gt : client.getGrantTypes()) { + if (gt.getValue().equals(GrantType.DEVICE_CODE.getValue())) { + return true; + } + } + return false; + } + + /** + * Validates data related to the cache, status and client in order to return correct redirection + * used to process device authorizations. + * + * @param deviceAuthorizationCacheControl Cache data related to the device code request. + * @param client Client in process. + * @param state State of the authorization request. + * @param servletRequest HttpServletRequest + */ + public String getDeviceAuthorizationPage(DeviceAuthorizationCacheControl deviceAuthorizationCacheControl, Client client, + String state, HttpServletRequest servletRequest) { + if (deviceAuthorizationCacheControl == null) { + throw new WebApplicationException(Response + .status(Response.Status.BAD_REQUEST) + .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.INVALID_REQUEST, state, "Request not processed.")) + .type(MediaType.APPLICATION_JSON_TYPE) + .build()); + } + if (deviceAuthorizationCacheControl.getStatus() != DeviceAuthorizationStatus.PENDING) { + throw new WebApplicationException(Response + .status(Response.Status.BAD_REQUEST) + .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.INVALID_REQUEST, state, "Request already processed.")) + .type(MediaType.APPLICATION_JSON_TYPE) + .build()); + } + if (!deviceAuthorizationCacheControl.getClient().getClientId().equals(client.getClientId())) { + throw new WebApplicationException(Response + .status(Response.Status.BAD_REQUEST) + .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.UNAUTHORIZED_CLIENT, state, "Client doesn't match.")) + .type(MediaType.APPLICATION_JSON_TYPE) + .build()); + } + final URI uri = UriBuilder.fromPath(appConfiguration.getIssuer()).path(servletRequest.getContextPath()) + .path("/device_authorization.htm").build(); + return uri.toString(); + } + + /** + * Removes device request data from cache using user_code and device_code. + * @param userCode User code used as key in cache. + * @param deviceCode Device code used as key in cache. + */ + public void removeDeviceAuthRequestInCache(String userCode, String deviceCode) { + try { + if (StringUtils.isNotBlank(userCode)) { + cacheService.remove(userCode); + } + if (StringUtils.isNotBlank(deviceCode)) { + cacheService.remove(deviceCode); + } + log.debug("Removed from cache device authorization using user_code: {}, device_code: {}", userCode, deviceCode); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + + /** + * Uses an HttpServletRequest, process it and return userCode in the session whether it exists. + * @param httpRequest Request received from an user agent. + */ + public String getUserCodeFromSession(HttpServletRequest httpRequest) { + SessionId sessionId = sessionIdService.getSessionId(httpRequest); + if (sessionId != null) { + final Map sessionAttributes = sessionId.getSessionAttributes(); + if (sessionAttributes.containsKey(SESSION_USER_CODE)) { + return sessionAttributes.get(SESSION_USER_CODE); + } + } + return null; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/ErrorHandlerService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/ErrorHandlerService.java new file mode 100644 index 00000000..d8351951 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/ErrorHandlerService.java @@ -0,0 +1,95 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service; + +import org.gluu.jsf2.message.FacesMessages; +import org.gluu.jsf2.service.FacesService; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.error.ErrorHandlingMethod; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.error.IErrorType; +import org.gluu.oxauth.util.RedirectUri; +import org.gluu.util.StringHelper; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.faces.application.FacesMessage; +import javax.faces.application.FacesMessage.Severity; +import javax.inject.Inject; +import javax.inject.Named; + +/** + * Helper service to generate either error response or local error based on application settings + * + * @author Yuriy Movchan Date: 12/07/2018 + */ +@ApplicationScoped +@Named +public class ErrorHandlerService { + + @Inject + private Logger log; + + @Inject + private SessionIdService sessionIdService; + + @Inject + private CookieService cookieService; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private FacesService facesService; + + @Inject + private FacesMessages facesMessages; + + public void handleError(String facesMessageId, IErrorType errorType, String hint) { + if (ErrorHandlingMethod.REMOTE == appConfiguration.getErrorHandlingMethod()) { + handleRemoteError(facesMessageId, errorType, hint); + } else { + handleLocalError(facesMessageId); + } + } + + private void addMessage(Severity severity, String facesMessageId) { + if (StringHelper.isNotEmpty(facesMessageId)) { + facesMessages.add(FacesMessage.SEVERITY_ERROR, String.format("#{msgs['%s']}", facesMessageId)); + } + } + + private void handleLocalError(String facesMessageId) { + addMessage(FacesMessage.SEVERITY_ERROR, facesMessageId); + facesService.redirect("/error.xhtml"); + } + + private void handleRemoteError(String facesMessageId, IErrorType errorType, String hint) { + String redirectUri = cookieService.getRpOriginIdCookie(); + + if (StringHelper.isEmpty(redirectUri)) { + log.error("Failed to get redirect_uri from cookie"); + handleLocalError(facesMessageId); + return; + } + + RedirectUri redirectUriResponse = new RedirectUri(redirectUri, null, null); + redirectUriResponse.parseQueryString(errorResponseFactory.getErrorAsQueryString( + errorType, null)); + if (StringHelper.isNotEmpty(hint)) { + redirectUriResponse.addResponseParameter("hint", "Create authorization request to start new authentication session."); + } + final String redirectTo = redirectUriResponse.toString(); + log.debug("Redirect to {}", redirectTo); + facesService.redirectToExternalURL(redirectTo); + + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/GrantService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/GrantService.java new file mode 100644 index 00000000..1eb04572 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/GrantService.java @@ -0,0 +1,374 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service; + +import com.google.common.collect.Lists; +import org.apache.commons.lang.BooleanUtils; +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.common.AuthorizationGrant; +import org.gluu.oxauth.model.common.CacheGrant; +import org.gluu.oxauth.model.common.ClientTokens; +import org.gluu.oxauth.model.common.SessionTokens; +import org.gluu.oxauth.model.config.StaticConfiguration; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.ldap.TokenLdap; +import org.gluu.oxauth.model.ldap.TokenType; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.util.TokenHashUtil; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.search.filter.Filter; +import org.gluu.service.CacheService; +import org.gluu.service.cache.CacheConfiguration; +import org.gluu.service.cache.CacheProviderType; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import java.util.*; + +import static org.gluu.oxauth.util.ServerUtil.isTrue; + +/** + * @author Yuriy Zabrovarnyy + * @author Javier Rojas Blum + * @version November 28, 2018 + */ +@ApplicationScoped +public class GrantService { + + @Inject + private Logger log; + + @Inject + private PersistenceEntryManager ldapEntryManager; + + @Inject + private ClientService clientService; + + @Inject + private CacheService cacheService; + + @Inject + private StaticConfiguration staticConfiguration; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private CacheConfiguration cacheConfiguration; + + public static String generateGrantId() { + return UUID.randomUUID().toString(); + } + + public String buildDn(String p_hashedToken) { + return String.format("tknCde=%s,", p_hashedToken) + tokenBaseDn(); + } + + private String tokenBaseDn() { + return staticConfiguration.getBaseDn().getTokens(); // ou=tokens,o=gluu + } + + public void merge(TokenLdap p_token) { + ldapEntryManager.merge(p_token); + } + + public void mergeSilently(TokenLdap p_token) { + try { + ldapEntryManager.merge(p_token); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + + private boolean shouldPutInCache(TokenType tokenType, boolean isImplicitFlow) { + if (cacheConfiguration.getCacheProviderType() == CacheProviderType.NATIVE_PERSISTENCE) { + return false; + } + + if (isImplicitFlow && BooleanUtils.isTrue(appConfiguration.getUseCacheForAllImplicitFlowObjects())) { + return true; + } + + switch (tokenType) { + case ID_TOKEN: + if (!isTrue(appConfiguration.getPersistIdTokenInLdap())) { + return true; + } + case REFRESH_TOKEN: + if (!isTrue(appConfiguration.getPersistRefreshTokenInLdap())) { + return true; + } + } + return false; + } + + public void persist(TokenLdap token) { + if (shouldPutInCache(token.getTokenTypeEnum(), token.isImplicitFlow())) { + ClientTokens clientTokens = getCacheClientTokens(token.getClientId()); + clientTokens.getTokenHashes().add(token.getTokenCode()); + + int expiration = appConfiguration.getDynamicRegistrationExpirationTime(); // fallback to client's lifetime + switch (token.getTokenTypeEnum()) { + case ID_TOKEN: + expiration = appConfiguration.getIdTokenLifetime(); + break; + case REFRESH_TOKEN: + expiration = appConfiguration.getRefreshTokenLifetime(); + break; + case ACCESS_TOKEN: + case LONG_LIVED_ACCESS_TOKEN: + int lifetime = appConfiguration.getAccessTokenLifetime(); + Client client = clientService.getClient(token.getClientId()); + // oxAuth #830 Client-specific access token expiration + if (client != null && client.getAccessTokenLifetime() != null && client.getAccessTokenLifetime() > 0) { + lifetime = client.getAccessTokenLifetime(); + } + expiration = lifetime; + break; + case AUTHORIZATION_CODE: + expiration = appConfiguration.getAuthorizationCodeLifetime(); + break; + } + + token.setIsFromCache(true); + cacheService.put(expiration, token.getTokenCode(), token); + cacheService.put(expiration, clientTokens.cacheKey(), clientTokens); + + if (StringUtils.isNotBlank(token.getSessionDn())) { + SessionTokens sessionTokens = getCacheSessionTokens(token.getSessionDn()); + sessionTokens.getTokenHashes().add(token.getTokenCode()); + + cacheService.put(expiration, sessionTokens.cacheKey(), sessionTokens); + } + return; + } + + ldapEntryManager.persist(token); + } + + public ClientTokens getCacheClientTokens(String clientId) { + ClientTokens clientTokens = new ClientTokens(clientId); + Object o = cacheService.get(clientTokens.cacheKey()); + if (o instanceof ClientTokens) { + return (ClientTokens) o; + } else { + return clientTokens; + } + } + + public SessionTokens getCacheSessionTokens(String sessionDn) { + SessionTokens sessionTokens = new SessionTokens(sessionDn); + Object o = cacheService.get(sessionTokens.cacheKey()); + if (o instanceof SessionTokens) { + return (SessionTokens) o; + } else { + return sessionTokens; + } + } + + public void remove(TokenLdap p_token) { + if (p_token.isFromCache()) { + cacheService.remove(p_token.getTokenCode()); + log.trace("Removed token from cache, code: " + p_token.getTokenCode()); + } else { + ldapEntryManager.remove(p_token); + log.trace("Removed token from LDAP, code: " + p_token.getTokenCode()); + } + } + + public void removeSilently(TokenLdap token) { + try { + remove(token); + + if (StringUtils.isNotBlank(token.getAuthorizationCode())) { + cacheService.remove(CacheGrant.cacheKey(token.getAuthorizationCode(), token.getGrantId())); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + + public void remove(List p_entries) { + if (p_entries != null && !p_entries.isEmpty()) { + for (TokenLdap t : p_entries) { + try { + remove(t); + } catch (Exception e) { + log.error("Failed to remove entry", e); + } + } + } + } + + public void removeSilently(List p_entries) { + if (p_entries != null && !p_entries.isEmpty()) { + for (TokenLdap t : p_entries) { + removeSilently(t); + } + } + } + + public void remove(AuthorizationGrant p_grant) { + if (p_grant != null && p_grant.getTokenLdap() != null) { + try { + remove(p_grant.getTokenLdap()); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + } + + public List getGrantsOfClient(String p_clientId) { + try { + final String baseDn = clientService.buildClientDn(p_clientId); + return ldapEntryManager.findEntries(baseDn, TokenLdap.class, Filter.createPresenceFilter("tknCde")); + } catch (Exception e) { + logException(e); + } + return Collections.emptyList(); + } + + public TokenLdap getGrantByCode(String p_code) { + Object grant = cacheService.get(TokenHashUtil.hash(p_code)); + if (grant instanceof TokenLdap) { + return (TokenLdap) grant; + } else { + return load(buildDn(TokenHashUtil.hash(p_code))); + } + } + + private TokenLdap load(String p_tokenDn) { + try { + final TokenLdap entry = ldapEntryManager.find(TokenLdap.class, p_tokenDn); + return entry; + } catch (Exception e) { + logException(e); + } + return null; + } + + public List getGrantsByGrantId(String p_grantId) { + try { + return ldapEntryManager.findEntries(tokenBaseDn(), TokenLdap.class, Filter.createEqualityFilter("grtId", p_grantId)); + } catch (Exception e) { + logException(e); + } + return Collections.emptyList(); + } + + public List getGrantsByAuthorizationCode(String p_authorizationCode) { + try { + return ldapEntryManager.findEntries(tokenBaseDn(), TokenLdap.class, Filter.createEqualityFilter("authzCode", TokenHashUtil.hash(p_authorizationCode))); + } catch (Exception e) { + logException(e); + } + return Collections.emptyList(); + } + + public List getGrantsBySessionDn(String sessionDn) { + List grants = new ArrayList<>(); + try { + List ldapGrants = ldapEntryManager.findEntries(tokenBaseDn(), TokenLdap.class, Filter.createEqualityFilter("ssnId", sessionDn)); + if (ldapGrants != null) { + grants.addAll(ldapGrants); + } + grants.addAll(getGrantsFromCacheBySessionDn(sessionDn)); + } catch (Exception e) { + logException(e); + } + return grants; + } + + private void logException(Exception e) { + if (BooleanUtils.isTrue(appConfiguration.getLogNotFoundEntityAsError())) { + log.error(e.getMessage(), e); + } else { + log.trace(e.getMessage(), e); + } + } + + public List getGrantsFromCacheBySessionDn(String sessionDn) { + if (StringUtils.isBlank(sessionDn)) { + return Collections.emptyList(); + } + return getCacheTokensEntries(getCacheSessionTokens(sessionDn).getTokenHashes()); + } + + public List getCacheClientTokensEntries(String clientId) { + if (cacheConfiguration.getCacheProviderType() == CacheProviderType.NATIVE_PERSISTENCE) { + return Collections.emptyList(); + } + Object o = cacheService.get(new ClientTokens(clientId).cacheKey()); + if (o instanceof ClientTokens) { + return getCacheTokensEntries(((ClientTokens) o).getTokenHashes()); + } + return Collections.emptyList(); + } + + public List getCacheTokensEntries(Set tokenHashes) { + List tokens = new ArrayList<>(); + + for (String tokenHash : tokenHashes) { + Object o1 = cacheService.get(tokenHash); + if (o1 instanceof TokenLdap) { + TokenLdap token = (TokenLdap) o1; + token.setIsFromCache(true); + tokens.add(token); + } + } + return tokens; + } + + public void logout(String sessionDn) { + final List tokens = getGrantsBySessionDn(sessionDn); + if (!appConfiguration.getRemoveRefreshTokensForClientOnLogout()) { + List refreshTokens = Lists.newArrayList(); + for (TokenLdap token : tokens) { + if (token.getTokenTypeEnum() == TokenType.REFRESH_TOKEN) { + refreshTokens.add(token); + } + } + if (!refreshTokens.isEmpty()) { + log.trace("Refresh tokens are not removed on logout (because removeRefreshTokensForClientOnLogout configuration property is false)"); + tokens.removeAll(refreshTokens); + } + } + removeSilently(tokens); + } + + public void removeAllTokensBySession(String sessionDn, boolean logout) { + removeSilently(getGrantsBySessionDn(sessionDn)); + } + + /** + * Removes grant with particular code. + * + * @param p_code code + */ + public void removeByCode(String p_code) { + final TokenLdap t = getGrantByCode(p_code); + if (t != null) { + removeSilently(t); + } + cacheService.remove(CacheGrant.cacheKey(p_code, null)); + } + + // authorization code is saved only in cache + public void removeAuthorizationCode(String code) { + cacheService.remove(CacheGrant.cacheKey(code, null)); + } + + public void removeAllByAuthorizationCode(String p_authorizationCode) { + removeSilently(getGrantsByAuthorizationCode(p_authorizationCode)); + } + + public void removeAllByGrantId(String p_grantId) { + removeSilently(getGrantsByGrantId(p_grantId)); + } + +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/KeyGeneratorTimer.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/KeyGeneratorTimer.java new file mode 100644 index 00000000..0d1cdb01 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/KeyGeneratorTimer.java @@ -0,0 +1,180 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service; + +import org.gluu.oxauth.model.config.Conf; +import org.gluu.oxauth.model.config.ConfigurationFactory; +import org.gluu.oxauth.model.config.WebKeysConfiguration; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.crypto.AbstractCryptoProvider; +import org.gluu.oxauth.model.jwk.JSONWebKey; +import org.gluu.oxauth.service.cdi.event.KeyGenerationEvent; +import org.gluu.oxauth.util.ServerUtil; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.service.cdi.async.Asynchronous; +import org.gluu.service.cdi.event.Scheduled; +import org.gluu.service.timer.event.TimerEvent; +import org.gluu.service.timer.schedule.TimerSchedule; +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Event; +import javax.enterprise.event.Observes; +import javax.inject.Inject; +import javax.inject.Named; +import java.util.GregorianCalendar; +import java.util.TimeZone; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +import static org.gluu.oxauth.model.jwk.JWKParameter.*; + +/** + * @author Javier Rojas Blum + * @author Yuriy Zabrovarnyy + */ +@ApplicationScoped +@Named +public class KeyGeneratorTimer { + + private static final int DEFAULT_INTERVAL = 60; + + @Inject + private Logger log; + + @Inject + private Event timerEvent; + + @Inject + private ConfigurationFactory configurationFactory; + + @Inject + private PersistenceEntryManager ldapEntryManager; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private AbstractCryptoProvider cryptoProvider; + + private AtomicBoolean isActive; + private long lastFinishedTime; + + public void initTimer() { + log.info("Initializing Key Generator Timer"); + this.isActive = new AtomicBoolean(false); + + timerEvent.fire(new TimerEvent(new TimerSchedule(DEFAULT_INTERVAL, DEFAULT_INTERVAL), new KeyGenerationEvent(), + Scheduled.Literal.INSTANCE)); + + this.lastFinishedTime = System.currentTimeMillis(); + log.info("Initialized Key Generator Timer"); + } + + @Asynchronous + public void process(@Observes @Scheduled KeyGenerationEvent keyGenerationEvent) { + if (!appConfiguration.getKeyRegenerationEnabled()) { + return; + } + + if (this.isActive.get()) { + return; + } + + if (!this.isActive.compareAndSet(false, true)) { + return; + } + + try { + updateKeys(); + } catch (Exception ex) { + log.error("Exception happened while executing keys update", ex); + } finally { + this.isActive.set(false); + } + } + + private void updateKeys() throws Exception { + if (!isStartUpdateKeys()) { + return; + } + + updateKeysImpl(); + this.lastFinishedTime = System.currentTimeMillis(); + } + + private boolean isStartUpdateKeys() { + long poolingInterval = appConfiguration.getKeyRegenerationInterval(); + if (poolingInterval <= 0) { + poolingInterval = DEFAULT_INTERVAL; + } + + poolingInterval = poolingInterval * 3600 * 1000L; + + long timeDifference = System.currentTimeMillis() - this.lastFinishedTime; + + return timeDifference >= poolingInterval; + } + + private void updateKeysImpl() throws Exception { + log.info("Updating JWKS keys ..."); + String dn = configurationFactory.getBaseConfiguration().getString("oxauth_ConfigurationEntryDN"); + Conf conf = ldapEntryManager.find(Conf.class, dn); + + JSONObject jwks = conf.getWebKeys().toJSONObject(); + JSONObject updatedJwks = updateKeys(jwks); + + conf.setWebKeys(ServerUtil.createJsonMapper().readValue(updatedJwks.toString(), WebKeysConfiguration.class)); + + long nextRevision = conf.getRevision() + 1; + conf.setRevision(nextRevision); + ldapEntryManager.merge(conf); + + log.info("Updated JWKS successfully"); + log.trace("JWKS keys: " + conf.getWebKeys().getKeys().stream().map(JSONWebKey::getKid).collect(Collectors.toList())); + log.trace("KeyStore keys: " + cryptoProvider.getKeys()); + } + + private JSONObject updateKeys(JSONObject jwks) throws Exception { + JSONObject jsonObject = AbstractCryptoProvider.generateJwks(cryptoProvider, appConfiguration); + + JSONArray keys = jwks.getJSONArray(JSON_WEB_KEY_SET); + for (int i = 0; i < keys.length(); i++) { + JSONObject key = keys.getJSONObject(i); + + if (key.has(EXPIRATION_TIME) && !key.isNull(EXPIRATION_TIME)) { + GregorianCalendar now = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + GregorianCalendar expirationDate = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + expirationDate.setTimeInMillis(key.getLong(EXPIRATION_TIME)); + + if (expirationDate.before(now)) { + // The expired key is not added to the array of keys + log.trace("Removing JWK: {}, Expiration date: {}", key.getString(KEY_ID), + key.getLong(EXPIRATION_TIME)); + cryptoProvider.deleteKey(key.getString(KEY_ID)); + } else if (cryptoProvider.containsKey(key.getString(KEY_ID))) { + log.trace("Contains kid: {}", key.getString(KEY_ID)); + jsonObject.getJSONArray(JSON_WEB_KEY_SET).put(key); + } + } else if (cryptoProvider.containsKey(key.getString(KEY_ID))) { + GregorianCalendar expirationTime = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + expirationTime.add(GregorianCalendar.HOUR, appConfiguration.getKeyRegenerationInterval()); + expirationTime.add(GregorianCalendar.SECOND, appConfiguration.getIdTokenLifetime()); + key.put(EXPIRATION_TIME, expirationTime.getTimeInMillis()); + + log.trace("Contains kid {} without exp {}", key.getString(KEY_ID), expirationTime); + + jsonObject.getJSONArray(JSON_WEB_KEY_SET).put(key); + } + } + + return jsonObject; + } + +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/LdapCustomAuthenticationConfigurationService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/LdapCustomAuthenticationConfigurationService.java new file mode 100644 index 00000000..aaf2d5f1 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/LdapCustomAuthenticationConfigurationService.java @@ -0,0 +1,112 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.gluu.model.AuthenticationScriptUsageType; +import org.gluu.model.SimpleCustomProperty; +import org.gluu.model.config.CustomAuthenticationConfiguration; +import org.gluu.oxauth.service.common.ConfigurationService; +import org.gluu.util.StringHelper; +import org.oxauth.persistence.model.configuration.CustomProperty; +import org.oxauth.persistence.model.configuration.GluuConfiguration; +import org.oxauth.persistence.model.configuration.oxIDPAuthConf; +import org.slf4j.Logger; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Provides service methods methods with LDAP configuration + * + * @author Yuriy Movchan Date: 08.27.2012 + */ +@ApplicationScoped +public class LdapCustomAuthenticationConfigurationService implements Serializable { + + private static final long serialVersionUID = -2225890597520443390L; + + private static final String CUSTOM_AUTHENTICATION_SCRIPT_PROPERTY_NAME = "script.__$__customAuthenticationScript__$__"; + private static final String CUSTOM_AUTHENTICATION_PROPERTY_PREFIX = "property."; + private static final String CUSTOM_AUTHENTICATION_SCRIPT_USAGE_TYPE = "usage."; + + @Inject + private Logger log; + + @Inject + private ConfigurationService configurationService; + + public List getCustomAuthenticationConfigurations() { + GluuConfiguration gluuConfiguration = configurationService.getConfiguration(); + List authConfigurations = gluuConfiguration.getOxIDPAuthentication(); + + List customAuthenticationConfigurations = new ArrayList(); + + if (authConfigurations == null) { + return customAuthenticationConfigurations; + } + + for (oxIDPAuthConf authConfiguration : authConfigurations) { + if (authConfiguration.getEnabled() && authConfiguration.getType().equalsIgnoreCase("customAuthentication")) { + CustomAuthenticationConfiguration customAuthenticationConfiguration = mapCustomAuthentication(authConfiguration); + customAuthenticationConfigurations.add(customAuthenticationConfiguration); + } + } + + return customAuthenticationConfigurations; + } + + private CustomAuthenticationConfiguration mapCustomAuthentication(oxIDPAuthConf oneConf) { + CustomAuthenticationConfiguration customAuthenticationConfig = new CustomAuthenticationConfiguration(); + customAuthenticationConfig.setName(oneConf.getName()); + customAuthenticationConfig.setLevel(oneConf.getLevel()); + customAuthenticationConfig.setPriority(oneConf.getPriority()); + customAuthenticationConfig.setEnabled(oneConf.getEnabled()); + customAuthenticationConfig.setVersion(oneConf.getVersion()); + + for (CustomProperty customProperty : oneConf.getFields()) { + if ((customProperty.getValues() == null) || (customProperty.getValues().size() == 0)) { + continue; + } + + String attrName = StringHelper.toLowerCase(customProperty.getName()); + + if (StringHelper.isEmpty(attrName)) { + continue; + } + + String value = customProperty.getValues().get(0); + + if (attrName.startsWith(CUSTOM_AUTHENTICATION_PROPERTY_PREFIX)) { + String key = customProperty.getName().substring(CUSTOM_AUTHENTICATION_PROPERTY_PREFIX.length()); + SimpleCustomProperty property = new SimpleCustomProperty(key, value); + customAuthenticationConfig.getCustomAuthenticationAttributes().add(property); + } else if (StringHelper.equalsIgnoreCase(attrName, CUSTOM_AUTHENTICATION_SCRIPT_PROPERTY_NAME)) { + customAuthenticationConfig.setCustomAuthenticationScript(value); + } else if (StringHelper.equalsIgnoreCase(attrName, CUSTOM_AUTHENTICATION_SCRIPT_USAGE_TYPE)) { + if (StringHelper.isNotEmpty(value)) { + AuthenticationScriptUsageType authenticationScriptUsageType = AuthenticationScriptUsageType.getByValue(value); + customAuthenticationConfig.setUsageType(authenticationScriptUsageType); + } + } + } + + return customAuthenticationConfig; + } + + private Object jsonToObject(String json, Class clazz) throws Exception { + ObjectMapper mapper = new ObjectMapper(); + Object clazzObject = mapper.readValue(json, clazz); + return clazzObject; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/LocalResponseCache.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/LocalResponseCache.java new file mode 100644 index 00000000..4a41131d --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/LocalResponseCache.java @@ -0,0 +1,99 @@ +package org.gluu.oxauth.service; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.service.cdi.event.AuthConfigurationEvent; +import org.gluu.service.cdi.async.Asynchronous; +import org.gluu.service.cdi.event.Scheduled; +import org.json.JSONObject; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Observes; +import javax.inject.Inject; +import javax.inject.Named; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * @author Yuriy Zabrovarnyy + */ +@ApplicationScoped +@Named +public class LocalResponseCache { + + public static final int DEFAULT_DISCOVERY_LIFETIME = 60; + public static final int DEFAULT_SECTOR_IDENTIFIER_LIFETIME = 1440; // 1 day + + private static final String DISCOVERY_CACHE_KEY = "DISCOVERY_CACHE_KEY"; + + @Inject + private AppConfiguration appConfiguration; + @Inject + private Logger log; + + private final AtomicBoolean rebuilding = new AtomicBoolean(false); + + private Cache discoveryCache = CacheBuilder.newBuilder() + .expireAfterWrite(DEFAULT_DISCOVERY_LIFETIME, TimeUnit.MINUTES).build(); + private Cache> sectorIdentifierCache = CacheBuilder.newBuilder() + .expireAfterWrite(DEFAULT_SECTOR_IDENTIFIER_LIFETIME, TimeUnit.MINUTES).build(); + + + private int currentDiscoveryLifetime = DEFAULT_DISCOVERY_LIFETIME; + private int currentSectorIdentifierLifetime = DEFAULT_SECTOR_IDENTIFIER_LIFETIME; + + @Asynchronous + public void reloadConfigurationTimerEvent(@Observes @Scheduled AuthConfigurationEvent authConfigurationEvent) { + try { + if (rebuilding.get()) + return; + + rebuilding.set(true); + + if (currentDiscoveryLifetime != appConfiguration.getDiscoveryCacheLifetimeInMinutes()) { + currentDiscoveryLifetime = appConfiguration.getDiscoveryCacheLifetimeInMinutes(); + discoveryCache = CacheBuilder.newBuilder() + .expireAfterWrite(appConfiguration.getDiscoveryCacheLifetimeInMinutes(), TimeUnit.MINUTES).build(); + log.trace("Re-created discovery cache with lifetime: " + appConfiguration.getDiscoveryCacheLifetimeInMinutes()); + } + if (currentSectorIdentifierLifetime != appConfiguration.getSectorIdentifierCacheLifetimeInMinutes()) { + currentSectorIdentifierLifetime = appConfiguration.getSectorIdentifierCacheLifetimeInMinutes(); + sectorIdentifierCache = CacheBuilder.newBuilder() + .expireAfterWrite(appConfiguration.getSectorIdentifierCacheLifetimeInMinutes(), TimeUnit.MINUTES).build(); + log.trace("Re-created sector identifier cache with lifetime: " + appConfiguration.getSectorIdentifierCacheLifetimeInMinutes()); + } + } finally { + rebuilding.set(false); + } + + } + + public List getSectorRedirectUris(String sectorIdentifierUri) { + if (sectorIdentifierCache == null || rebuilding.get()) + return null; + return sectorIdentifierCache.getIfPresent(sectorIdentifierUri); + } + + public void putSectorRedirectUris(String sectorIdentifierUri, List redirectUris) { + if (sectorIdentifierCache == null || rebuilding.get()) + return; + + sectorIdentifierCache.put(sectorIdentifierUri, redirectUris); + } + + public JSONObject getDiscoveryResponse() { + if (discoveryCache == null || rebuilding.get()) + return null; + return discoveryCache.getIfPresent(DISCOVERY_CACHE_KEY); + } + + public void putDiscoveryResponse(JSONObject response) { + if (discoveryCache == null || rebuilding.get()) + return; + + discoveryCache.put(DISCOVERY_CACHE_KEY, response); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/MetricService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/MetricService.java new file mode 100644 index 00000000..8834a52f --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/MetricService.java @@ -0,0 +1,93 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service; + +import org.gluu.model.ApplicationType; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.service.common.ApplicationFactory; +import org.gluu.oxauth.service.common.ConfigurationService; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.service.metric.inject.ReportMetric; +import org.gluu.service.net.NetworkService; +import org.gluu.oxauth.model.config.StaticConfiguration; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Instance; +import javax.inject.Inject; +import javax.inject.Named; + +/** + * Store and retrieve metric + * + * @author Yuriy Movchan Date: 07/30/2015 + */ +@ApplicationScoped +@Named(MetricService.METRIC_SERVICE_COMPONENT_NAME) +public class MetricService extends org.gluu.service.metric.MetricService { + + public static final String METRIC_SERVICE_COMPONENT_NAME = "metricService"; + + private static final long serialVersionUID = 7875838160379126796L; + + @Inject + private Instance instance; + + @Inject + private ConfigurationService configurationService; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private StaticConfiguration staticConfiguration; + + @Inject + private NetworkService networkService; + + @Inject + @Named(ApplicationFactory.PERSISTENCE_METRIC_ENTRY_MANAGER_NAME) + @ReportMetric + private PersistenceEntryManager ldapEntryManager; + + public void initTimer() { + initTimer(this.appConfiguration.getMetricReporterInterval(), this.appConfiguration.getMetricReporterKeepDataDays()); + } + + @Override + public String baseDn() { + return staticConfiguration.getBaseDn().getMetric(); + } + + public org.gluu.service.metric.MetricService getMetricServiceInstance() { + return instance.get(); + } + + @Override + public boolean isMetricReporterEnabled() { + if (this.appConfiguration.getMetricReporterEnabled() == null) { + return false; + } + + return this.appConfiguration.getMetricReporterEnabled(); + } + + @Override + public ApplicationType getApplicationType() { + return ApplicationType.OX_AUTH; + } + + @Override + public PersistenceEntryManager getEntryManager() { + return ldapEntryManager; + } + + @Override + public String getNodeIndetifier() { + return networkService.getMacAdress(); + } + +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/OrganizationService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/OrganizationService.java new file mode 100644 index 00000000..0f6dc665 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/OrganizationService.java @@ -0,0 +1,65 @@ +package org.gluu.oxauth.service; + +import org.gluu.model.ApplicationType; +import org.gluu.oxauth.model.GluuOrganization; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.service.BaseCacheService; +import org.gluu.service.CacheService; +import org.gluu.service.LocalCacheService; +import org.gluu.util.OxConstants; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +@ApplicationScoped +public class OrganizationService extends org.gluu.service.OrganizationService { + + private static final long serialVersionUID = -8966940469789981584L; + public static final int ONE_MINUTE_IN_SECONDS = 60; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private PersistenceEntryManager ldapEntryManager; + + @Inject + private CacheService cacheService; + + @Inject + private LocalCacheService localCacheService; + + /** + * Update organization entry + * + * @param organization + * Organization + */ + public void updateOrganization(GluuOrganization organization) { + ldapEntryManager.merge(organization); + } + + public GluuOrganization getOrganization() { + BaseCacheService usedCacheService = getCacheService(); + return usedCacheService.getWithPut(OxConstants.CACHE_ORGANIZATION_KEY + "_" + getApplicationType(), () -> ldapEntryManager.find(GluuOrganization.class, getDnForOrganization()), ONE_MINUTE_IN_SECONDS); + } + + public String getDnForOrganization() { + return "o=gluu"; + } + + private BaseCacheService getCacheService() { + if (appConfiguration.getUseLocalCache()) { + return localCacheService; + } + + return cacheService; + } + + @Override + public ApplicationType getApplicationType() { + return ApplicationType.OX_AUTH; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/OxAuthConfigurationService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/OxAuthConfigurationService.java new file mode 100644 index 00000000..8e95997f --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/OxAuthConfigurationService.java @@ -0,0 +1,65 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service; + +import javax.enterprise.context.ApplicationScoped; +import javax.faces.context.FacesContext; +import javax.inject.Inject; +import javax.inject.Named; +import javax.servlet.ServletContext; + +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.util.StringHelper; + +/** + * OxAuthConfigurationService + * + * @author Oleksiy Tataryn Date: 08.07.2014 + */ +@ApplicationScoped +@Named +@Deprecated //TODO: We don't need this class +public class OxAuthConfigurationService { + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private ServletContext context; + + public String getCssLocation() { + if (StringHelper.isEmpty(appConfiguration.getCssLocation())) { + FacesContext ctx = FacesContext.getCurrentInstance(); + if (ctx == null) { + return ""; + } + String contextPath = ctx.getExternalContext().getRequestContextPath(); + return contextPath + "/stylesheet"; + } else { + return appConfiguration.getCssLocation(); + } + } + + public String getJsLocation() { + if (StringHelper.isEmpty(appConfiguration.getJsLocation())) { + String contextPath = context.getContextPath(); + return contextPath + "/js"; + } else { + return appConfiguration.getJsLocation(); + } + } + + public String getImgLocation() { + if (StringHelper.isEmpty(appConfiguration.getImgLocation())) { + String contextPath = context.getContextPath(); + return contextPath + "/img"; + } else { + return appConfiguration.getImgLocation(); + } + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/PairwiseIdentifierService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/PairwiseIdentifierService.java new file mode 100644 index 00000000..d50f8e4d --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/PairwiseIdentifierService.java @@ -0,0 +1,130 @@ +package org.gluu.oxauth.service; + +import org.gluu.oxauth.model.common.PairwiseIdType; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.util.SubjectIdentifierGenerator; +import org.gluu.oxauth.service.common.UserService; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.persist.model.base.SimpleBranch; +import org.gluu.search.filter.Filter; +import org.gluu.util.StringHelper; +import org.oxauth.persistence.model.PairwiseIdentifier; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import java.util.List; + +/** + * @author Javier Rojas Blum + * @version May 7, 2019 + */ +@ApplicationScoped +public class PairwiseIdentifierService { + + @Inject + private Logger log; + + @Inject + private PersistenceEntryManager ldapEntryManager; + + @Inject + private UserService userService; + + @Inject + private AppConfiguration appConfiguration; + + public void addBranch(final String userInum) { + SimpleBranch branch = new SimpleBranch(); + branch.setOrganizationalUnitName("pairwiseIdentifiers"); + branch.setDn(getBaseDnForPairwiseIdentifiers(userInum)); + + ldapEntryManager.persist(branch); + } + + public boolean containsBranch(final String userInum) { + return ldapEntryManager.contains(getBaseDnForPairwiseIdentifiers(userInum), SimpleBranch.class); + } + + public void prepareBranch(final String userInum) { + if (!ldapEntryManager.hasBranchesSupport(userService.getDnForUser(userInum))) { + return; + } + + // Create pairwise identifier branch if needed + if (!containsBranch(userInum)) { + addBranch(userInum); + } + } + + public PairwiseIdentifier findPairWiseIdentifier(String userInum, String sectorIdentifier, String clientId) throws Exception { + PairwiseIdType pairwiseIdType = PairwiseIdType.fromString(appConfiguration.getPairwiseIdType()); + + if (PairwiseIdType.PERSISTENT == pairwiseIdType) { + prepareBranch(userInum); + + String baseDnForPairwiseIdentifiers = getBaseDnForPairwiseIdentifiers(userInum); + + final Filter filter; + if (appConfiguration.isShareSubjectIdBetweenClientsWithSameSectorId()) { + Filter sectorIdentifierFilter = Filter.createEqualityFilter("oxSectorIdentifier", sectorIdentifier); + Filter userInumFilter = Filter.createEqualityFilter("oxAuthUserId", userInum); + + filter = Filter.createANDFilter(sectorIdentifierFilter, userInumFilter); + } else { + Filter sectorIdentifierFilter = Filter.createEqualityFilter("oxSectorIdentifier", sectorIdentifier); + Filter clientIdFilter = Filter.createEqualityFilter("oxAuthClientId", clientId); + Filter userInumFilter = Filter.createEqualityFilter("oxAuthUserId", userInum); + + filter = Filter.createANDFilter(sectorIdentifierFilter, clientIdFilter, userInumFilter); + } + + List entries = ldapEntryManager.findEntries(baseDnForPairwiseIdentifiers, PairwiseIdentifier.class, filter); + if (entries != null && !entries.isEmpty()) { + // if more then one entry then it's problem, non-deterministic behavior, id must be unique + if (entries.size() > 1) { + log.error("Found more then one pairwise identifier by sector identifier: {}" + sectorIdentifier); + for (PairwiseIdentifier pairwiseIdentifier : entries) { + log.error("PairwiseIdentifier: {}", pairwiseIdentifier); + } + } + return entries.get(0); + } + } else { // PairwiseIdType.ALGORITHMIC + String key = appConfiguration.getPairwiseCalculationKey(); + String salt = appConfiguration.getPairwiseCalculationSalt(); + String localAccountId = appConfiguration.isShareSubjectIdBetweenClientsWithSameSectorId() ? + userInum : userInum + clientId; + + String calculatedSub = SubjectIdentifierGenerator.generatePairwiseSubjectIdentifier(sectorIdentifier, localAccountId, key, salt, appConfiguration); + + PairwiseIdentifier pairwiseIdentifier = new PairwiseIdentifier(sectorIdentifier, clientId, userInum); + pairwiseIdentifier.setId(calculatedSub); + + return pairwiseIdentifier; + } + + return null; + } + + public void addPairwiseIdentifier(String userInum, PairwiseIdentifier pairwiseIdentifier) { + prepareBranch(userInum); + userService.addUserAttributeByUserInum(userInum, "oxPPID", pairwiseIdentifier.getId()); + + ldapEntryManager.persist(pairwiseIdentifier); + } + + public String getDnForPairwiseIdentifier(String oxId, String userInum) { + String baseDn = getBaseDnForPairwiseIdentifiers(userInum); + if (StringHelper.isEmpty(oxId)) { + return baseDn; + } + return String.format("oxId=%s,%s", oxId, baseDn); + } + + public String getBaseDnForPairwiseIdentifiers(String userInum) { + final String userBaseDn = userService.getDnForUser(userInum); // "ou=pairwiseIdentifiers,inum=1234,ou=people,o=gluu" + return String.format("ou=pairwiseIdentifiers,%s", userBaseDn); + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/RedirectUriResponse.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/RedirectUriResponse.java new file mode 100644 index 00000000..6dca548a --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/RedirectUriResponse.java @@ -0,0 +1,71 @@ +package org.gluu.oxauth.service; + +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.error.IErrorType; +import org.gluu.oxauth.util.RedirectUri; +import org.gluu.oxauth.util.RedirectUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; + +/** + * @author Yuriy Zabrovarnyy + */ +public class RedirectUriResponse { + + private final static Logger log = LoggerFactory.getLogger(RedirectUriResponse.class); + + private RedirectUri redirectUri; + private String state; + private HttpServletRequest httpRequest; + private ErrorResponseFactory errorFactory; + private boolean fapiCompatible = false; + + public RedirectUriResponse(RedirectUri redirectUri, String state, HttpServletRequest httpRequest, ErrorResponseFactory errorFactory) { + this.redirectUri = redirectUri; + this.state = state; + this.httpRequest = httpRequest; + this.errorFactory = errorFactory; + } + + public WebApplicationException createWebException(IErrorType errorType) { + return createWebException(errorType, null); + } + + public WebApplicationException createWebException(IErrorType errorType, String reason) { + if (fapiCompatible) { + log.trace("Reason: " + reason); // print reason and set it to null since FAPI does not allow unknown fields in response + reason = null; + } + redirectUri.parseQueryString(errorFactory.getErrorAsQueryString(errorType, state, reason)); + return new WebApplicationException(RedirectUtil.getRedirectResponseBuilder(redirectUri, httpRequest).build()); + } + + public void setState(String state) { + this.state = state; + } + + public String getState() { + return state; + } + + public Response.ResponseBuilder createErrorBuilder(IErrorType errorType) { + redirectUri.parseQueryString(errorFactory.getErrorAsQueryString(errorType, state)); + return RedirectUtil.getRedirectResponseBuilder(redirectUri, httpRequest); + } + + public RedirectUri getRedirectUri() { + return redirectUri; + } + + public boolean isFapiCompatible() { + return fapiCompatible; + } + + public void setFapiCompatible(boolean fapiCompatible) { + this.fapiCompatible = fapiCompatible; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/RedirectionUriService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/RedirectionUriService.java new file mode 100644 index 00000000..3cda6846 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/RedirectionUriService.java @@ -0,0 +1,262 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service; + +import com.google.common.base.Joiner; +import com.google.common.base.Strings; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.client.QueryStringDecoder; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.session.EndSessionErrorResponseType; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.model.util.URLPatternList; +import org.gluu.oxauth.model.util.Util; +import org.jetbrains.annotations.NotNull; +import org.json.JSONArray; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.Response; +import java.util.*; + +import static org.apache.commons.lang.BooleanUtils.isTrue; + +/** + * @author Javier Rojas Blum + * @version August 9, 2017 + */ +@ApplicationScoped +public class RedirectionUriService { + + private static final Logger log = LoggerFactory.getLogger(RedirectionUriService.class); + + @Inject + private ClientService clientService; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private LocalResponseCache localResponseCache; + + public String validateRedirectionUri(String clientIdentifier, String redirectionUri) { + Client client = clientService.getClient(clientIdentifier); + if (client == null) { + return null; + } + return validateRedirectionUri(client, redirectionUri); + } + + public List getSectorRedirectUris(String sectorIdentiferUri) throws Exception { + List result = Lists.newArrayList(); + if (StringUtils.isBlank(sectorIdentiferUri)) { + return result; + } + + final List sectorRedirectUris = localResponseCache.getSectorRedirectUris(sectorIdentiferUri); + if (sectorRedirectUris != null) { + return sectorRedirectUris; + } + + javax.ws.rs.client.Client clientRequest = ClientBuilder.newClient(); + String entity = null; + try { + Response clientResponse = clientRequest.target(sectorIdentiferUri).request().buildGet().invoke(); + + int status = clientResponse.getStatus(); + if (status != 200) { + return result; + } + + entity = clientResponse.readEntity(String.class); + } finally { + clientRequest.close(); + } + + JSONArray sectorIdentifierJsonArray = new JSONArray(entity); + + for (int i = 0; i < sectorIdentifierJsonArray.length(); i++) { + result.add(sectorIdentifierJsonArray.getString(i)); + } + localResponseCache.putSectorRedirectUris(sectorIdentiferUri, result); + return result; + } + + public String validateRedirectionUri(@NotNull Client client, String redirectionUri) { + try { + String sectorIdentifierUri = client.getSectorIdentifierUri(); + String[] redirectUris = client.getRedirectUris(); + + if (StringUtils.isNotBlank(sectorIdentifierUri)) { + redirectUris = getSectorRedirectUris(sectorIdentifierUri).toArray(new String[0]); + } + + if (StringUtils.isNotBlank(redirectionUri) && redirectUris != null) { + log.debug("Validating redirection URI: clientIdentifier = {}, redirectionUri = {}, found = {}", + client.getClientId(), redirectionUri, redirectUris.length); + + if (isUriEqual(redirectionUri, redirectUris)) { + return redirectionUri; + } + } else { + // Accept Request Without redirect_uri when One Registered + if (redirectUris != null && redirectUris.length == 1) { + return redirectUris[0]; + } + } + + if (isTrue(appConfiguration.getAllowWildcardRedirectUri()) && redirectUris != null && redirectUris.length > 0) { + URLPatternList urlPatternList = new URLPatternList(Arrays.asList(redirectUris), true); + boolean valid = urlPatternList.isUrlListed(redirectionUri); + if (valid) { + log.trace("Allowed by wildcard redirect_uris: {}", Joiner.on(",").join(redirectUris)); + return redirectionUri; + } + } + } catch (Exception e) { + return null; + } + return null; + } + + public static boolean isUriEqual(String redirectionUri, String[] redirectUris) { + final String redirectUriWithoutParams = uriWithoutParams(redirectionUri); + + for (String uri : redirectUris) { + log.debug("Comparing {} == {}", uri, redirectionUri); + if (uri.equals(redirectionUri)) { // compare complete uri + return true; + } + + String uriWithoutParams = uriWithoutParams(uri); + final Map params = getParams(uri); + + if ((uriWithoutParams.equals(redirectUriWithoutParams) && params.size() == 0 && getParams(redirectionUri).size() == 0) || + uriWithoutParams.equals(redirectUriWithoutParams) && params.size() > 0 && compareParams(redirectionUri, uri)) { + return true; + } + } + return false; + } + + + public String validatePostLogoutRedirectUri(String clientId, String postLogoutRedirectUri) { + + boolean isBlank = Util.isNullOrEmpty(postLogoutRedirectUri); + + Client client = clientService.getClient(clientId); + + if (client != null) { + String[] postLogoutRedirectUris = client.getPostLogoutRedirectUris(); + log.debug("Validating post logout redirect URI: clientId = {}, postLogoutRedirectUri = {}", clientId, postLogoutRedirectUri); + + return validatePostLogoutRedirectUri(postLogoutRedirectUri, postLogoutRedirectUris); + } + + if (!isBlank) { + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, EndSessionErrorResponseType.POST_LOGOUT_URI_NOT_ASSOCIATED_WITH_CLIENT, "`post_logout_redirect_uri` is not added to associated client."); + } + + return null; + } + + public String validatePostLogoutRedirectUri(SessionId sessionId, String postLogoutRedirectUri) { + if (sessionId == null) { + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, EndSessionErrorResponseType.SESSION_NOT_PASSED, "Session object is not found."); + } + if (Strings.isNullOrEmpty(postLogoutRedirectUri)) { + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, EndSessionErrorResponseType.POST_LOGOUT_URI_NOT_PASSED, "`post_logout_redirect_uri` is empty."); + } + + final Set clientsByDns = sessionId.getPermissionGrantedMap() != null + ? clientService.getClient(sessionId.getPermissionGrantedMap().getClientIds(true), true) + : Sets.newHashSet(); + + log.trace("Validating post logout redirect URI: postLogoutRedirectUri = {}", postLogoutRedirectUri); + + for (Client client : clientsByDns) { + String[] postLogoutRedirectUris = client.getPostLogoutRedirectUris(); + + String validatedUri = validatePostLogoutRedirectUri(postLogoutRedirectUri, postLogoutRedirectUris); + + if (StringUtils.isNotBlank(validatedUri)) { + return validatedUri; + } + } + + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, EndSessionErrorResponseType.POST_LOGOUT_URI_NOT_ASSOCIATED_WITH_CLIENT, "Unable to validate `post_logout_redirect_uri`"); + } + + public boolean isUrlWhiteListed(String url) { + final boolean result = new URLPatternList(appConfiguration.getClientWhiteList()).isUrlListed(url); + log.trace("White listed result: {}, url: {}", result, url); + return result; + } + + public String validatePostLogoutRedirectUri(String postLogoutRedirectUri, String[] allowedPostLogoutRedirectUris) { + if (appConfiguration.getAllowPostLogoutRedirectWithoutValidation() && isUrlWhiteListed(postLogoutRedirectUri)) { + log.trace("PostLogoutRedirectUri {} is whitelisted by 'clientWhiteList' configuration property.", postLogoutRedirectUri); + return postLogoutRedirectUri; + } + + if (allowedPostLogoutRedirectUris != null && StringUtils.isNotBlank(postLogoutRedirectUri)) { + if (isUriEqual(postLogoutRedirectUri, allowedPostLogoutRedirectUris)) { + return postLogoutRedirectUri; + } + } else { + // Accept Request Without post_logout_redirect_uri when One Registered + if (allowedPostLogoutRedirectUris != null && allowedPostLogoutRedirectUris.length == 1) { + return allowedPostLogoutRedirectUris[0]; + } + } + return ""; + } + + public static Map getParams(String uri) { + Map params = new HashMap(); + + if (uri != null) { + int paramsIndex = uri.indexOf("?"); + if (paramsIndex != -1) { + String queryString = uri.substring(paramsIndex + 1); + params = QueryStringDecoder.decode(queryString); + } + } + return params; + } + + public static String uriWithoutParams(String uri) { + if (uri != null) { + int paramsIndex = uri.indexOf("?"); + if (paramsIndex != -1) { + return uri.substring(0, paramsIndex); + } + } + return uri; + } + + public static boolean compareParams(String uri1, String uri2) { + if (StringUtils.isBlank(uri1) || StringUtils.isBlank(uri2)) { + return false; + } + + Map params1 = getParams(uri1); + Map params2 = getParams(uri2); + + return params1.equals(params2); + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/RequestParameterService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/RequestParameterService.java new file mode 100644 index 00000000..eba05563 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/RequestParameterService.java @@ -0,0 +1,281 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service; + +import com.google.common.collect.Lists; +import org.apache.commons.lang.StringUtils; +import org.gluu.model.security.Identity; +import org.gluu.oxauth.model.authorize.AuthorizeRequestParam; +import org.gluu.oxauth.model.authorize.JwtAuthorizationRequest; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.model.util.Util; +import org.gluu.util.Pair; +import org.gluu.util.StringHelper; +import org.json.JSONObject; +import org.slf4j.Logger; + +import javax.annotation.Nonnull; +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.*; +import java.util.Map.Entry; + +/** + * @author Yuriy Movchan + * @author Javier Rojas Blum + * + * @version October 7, 2019 + */ +@ApplicationScoped +public class RequestParameterService { + + // use only "acr" instead of "acr_values" #334 + private static final List ALLOWED_PARAMETER = Collections.unmodifiableList(Arrays.asList( + AuthorizeRequestParam.SCOPE, + AuthorizeRequestParam.RESPONSE_TYPE, + AuthorizeRequestParam.CLIENT_ID, + AuthorizeRequestParam.REDIRECT_URI, + AuthorizeRequestParam.STATE, + AuthorizeRequestParam.RESPONSE_MODE, + AuthorizeRequestParam.NONCE, + AuthorizeRequestParam.DISPLAY, + AuthorizeRequestParam.PROMPT, + AuthorizeRequestParam.MAX_AGE, + AuthorizeRequestParam.UI_LOCALES, + AuthorizeRequestParam.ID_TOKEN_HINT, + AuthorizeRequestParam.LOGIN_HINT, + AuthorizeRequestParam.ACR_VALUES, + AuthorizeRequestParam.REQUEST, + AuthorizeRequestParam.REQUEST_URI, + AuthorizeRequestParam.ORIGIN_HEADERS, + AuthorizeRequestParam.CODE_CHALLENGE, + AuthorizeRequestParam.CODE_CHALLENGE_METHOD, + AuthorizeRequestParam.CUSTOM_RESPONSE_HEADERS, + AuthorizeRequestParam.CLAIMS, + AuthorizeRequestParam.AUTH_REQ_ID, + AuthorizeRequestParam.SID, + DeviceAuthorizationService.SESSION_USER_CODE)); + + @Inject + private Logger log; + + @Inject + private Identity identity; + + @Inject + private AppConfiguration appConfiguration; + + private List getAllAllowedParameters() { + List allowedParameters = Lists.newArrayList(ALLOWED_PARAMETER); + if (appConfiguration.getSessionIdRequestParameterEnabled()) { + allowedParameters.add(AuthorizeRequestParam.SESSION_ID); + } + return allowedParameters; + } + + public Map getAllowedParameters(@Nonnull final Map requestParameterMap) { + Set authorizationRequestCustomAllowedParameters = appConfiguration.getAuthorizationRequestCustomAllowedParameters(); + if (authorizationRequestCustomAllowedParameters == null) { + authorizationRequestCustomAllowedParameters = new HashSet(0); + } + + final Map result = new HashMap(); + if (requestParameterMap.isEmpty()) { + return result; + } + + final List allAllowed = getAllAllowedParameters(); + final Set> set = requestParameterMap.entrySet(); + for (Map.Entry entry : set) { + if (allAllowed.contains(entry.getKey()) || authorizationRequestCustomAllowedParameters.contains(entry.getKey())) { + result.put(entry.getKey(), entry.getValue()); + } + } + return result; + } + + public Map getCustomParameters(@Nonnull final Map requestParameterMap) { + Set authorizationRequestCustomAllowedParameters = appConfiguration.getAuthorizationRequestCustomAllowedParameters(); + + final Map result = new HashMap(); + if (authorizationRequestCustomAllowedParameters == null) { + return result; + } + + if (!requestParameterMap.isEmpty()) { + final Set> set = requestParameterMap.entrySet(); + for (Map.Entry entry : set) { + if (authorizationRequestCustomAllowedParameters.contains(entry.getKey())) { + result.put(entry.getKey(), entry.getValue()); + } + } + } + + return result; + } + + public String parametersAsString(final Map parameterMap) throws UnsupportedEncodingException { + final StringBuilder sb = new StringBuilder(); + final Set> set = parameterMap.entrySet(); + for (Map.Entry entry : set) { + final String value = (String) entry.getValue(); + if (StringUtils.isNotBlank(value)) { + sb.append(entry.getKey()).append("=").append(URLEncoder.encode(value, Util.UTF8_STRING_ENCODING)).append("&"); + } + } + + String result = sb.toString(); + if (result.endsWith("&")) { + result = result.substring(0, result.length() - 1); + } + return result; + } + + public Map getParametersMap(List extraParameters, final Map parameterMap) { + final List allowedParameters = getAllAllowedParameters(); + + if (extraParameters != null) { + for (String extraParameter : extraParameters) { + putInMap(parameterMap, extraParameter); + } + + allowedParameters.addAll(extraParameters); + } + + parameterMap.entrySet().removeIf(entry -> !allowedParameters.contains(entry.getKey())); + return parameterMap; + } + + private void putInMap(Map map, String p_name) { + if (map == null) { + return; + } + + String value = getParameterValue(p_name); + + map.put(p_name, value); + } + + public String getParameterValue(String p_name) { + Pair valueWithType = getParameterValueWithType(p_name); + if (valueWithType == null) { + return null; + } + + return valueWithType.getFirst(); + } + + public Pair getParameterValueWithType(String p_name) { + String value = null; + String clazz = null; + final Object o = identity.getWorkingParameter(p_name); + if (o instanceof String) { + final String s = (String) o; + value = s; + clazz = String.class.getName(); + } else if (o instanceof Integer) { + final Integer i = (Integer) o; + value = i.toString(); + clazz = Integer.class.getName(); + } else if (o instanceof Boolean) { + final Boolean b = (Boolean) o; + value = b.toString(); + clazz = Boolean.class.getName(); + } + + return new Pair(value, clazz); + } + + public Object getTypedValue(String stringValue, String type) { + if (StringHelper.equals(Boolean.class.getName(), type)) { + return Boolean.valueOf(stringValue); + } else if (StringHelper.equals(Integer.class.getName(), type)) { + return Integer.valueOf(stringValue); + } + + return stringValue; + } + + /** + * Process a JWT Request instance and update Custom Parameters according to custom parameters sent. + * @param jwtRequest JWT processing + * @param customParameters Custom parameters used in the authorization flow. + */ + public void getCustomParameters(JwtAuthorizationRequest jwtRequest, Map customParameters) { + Set authorizationRequestCustomAllowedParameters = appConfiguration + .getAuthorizationRequestCustomAllowedParameters(); + + if (authorizationRequestCustomAllowedParameters == null) { + return; + } + + JSONObject jsonPayload = new JSONObject(jwtRequest.getPayload()); + for (String customParam : authorizationRequestCustomAllowedParameters) { + if (jsonPayload.has( customParam )) { + customParameters.put(customParam, jsonPayload.getString(customParam)); + } + } + } + + public Map getCustomParameters(HttpServletRequest request) { + Map customParameters = new HashMap<>(); + addCustomParameters(request, customParameters); + return customParameters; + } + + public void addCustomParameters(HttpServletRequest request, Map customParameters) { + Set authorizationRequestCustomAllowedParameters = appConfiguration + .getAuthorizationRequestCustomAllowedParameters(); + + if (authorizationRequestCustomAllowedParameters == null) { + log.trace("Skipped custom parameters because 'authorizationRequestCustomAllowedParameters' AS configuration is not set."); + return; + } + + final Enumeration parameterNames = request.getParameterNames(); + while (parameterNames.hasMoreElements()) { + final String parameterName = parameterNames.nextElement(); + if (!authorizationRequestCustomAllowedParameters.contains(parameterName)) { + log.trace("Skipped '{}' as custom parameter (not defined in 'authorizationRequestCustomAllowedParameters')", parameterName); + continue; + } + + final String parameterValue = request.getParameter(parameterName); + if (StringUtils.isNotBlank(parameterValue)) { + customParameters.put(parameterName, parameterValue); + } + } + + log.trace("Custom parameters: {}", customParameters); + } + + public void putCustomParametersIntoSession(SessionId sessionId, HttpServletRequest httpRequest) { + putCustomParametersIntoSession(sessionId, getCustomParameters(httpRequest)); + } + + public void putCustomParametersIntoSession(SessionId sessionId, Map customParameters) { + if (sessionId == null || customParameters == null) { + return; + } + + putCustomParametersIntoSession(sessionId.getSessionAttributes(), customParameters); + } + + public void putCustomParametersIntoSession(Map sessionAttributes, Map customParameters) { + if (sessionAttributes == null || customParameters == null) { + return; + } + + for (Map.Entry entry : customParameters.entrySet()) { + sessionAttributes.put("custom_" + entry.getKey(), entry.getValue()); + } + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/ResteasyInitializer.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/ResteasyInitializer.java new file mode 100644 index 00000000..fc003ed8 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/ResteasyInitializer.java @@ -0,0 +1,14 @@ +package org.gluu.oxauth.service; + +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; + +/** + * Integration with Resteasy + * + * @author Yuriy Movchan + * @version 0.1, 03/21/2017 + */ +@ApplicationPath("/restv1") +public class ResteasyInitializer extends Application { +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/ScopeService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/ScopeService.java new file mode 100644 index 00000000..7224bf02 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/ScopeService.java @@ -0,0 +1,327 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service; + +import com.google.common.collect.Lists; +import org.apache.commons.lang.StringUtils; +import org.gluu.model.GluuAttribute; +import org.gluu.model.attribute.AttributeDataType; +import org.gluu.oxauth.model.common.User; +import org.gluu.oxauth.model.config.StaticConfiguration; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.exception.InvalidClaimException; +import org.gluu.oxauth.model.json.JsonApplier; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.search.filter.Filter; +import org.gluu.service.BaseCacheService; +import org.gluu.service.CacheService; +import org.gluu.service.LocalCacheService; +import org.gluu.util.StringHelper; +import org.json.JSONArray; +import org.oxauth.persistence.model.Scope; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.*; + +/** + * @author Javier Rojas Blum Date: 07.05.2012 + * @author Yuriy Movchan Date: 2016/04/26 + */ +@ApplicationScoped +public class ScopeService { + + @Inject + private Logger log; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private CacheService cacheService; + + @Inject + private LocalCacheService localCacheService; + + @Inject + private PersistenceEntryManager ldapEntryManager; + + @Inject + private StaticConfiguration staticConfiguration; + + @Inject + private AttributeService attributeService; + + /** + * returns a list of all scopes + * + * @return list of scopes + */ + public List getAllScopesList() { + String scopesBaseDN = staticConfiguration.getBaseDn().getScopes(); + + return ldapEntryManager.findEntries(scopesBaseDN, + Scope.class, + Filter.createPresenceFilter("inum")); + } + + public List getDefaultScopesDn() { + List defaultScopes = new ArrayList<>(); + + for (Scope scope : getAllScopesList()) { + if (Boolean.TRUE.equals(scope.isDefaultScope())) { + defaultScopes.add(scope.getDn()); + } + } + + return defaultScopes; + } + + public List getScopesDn(List scopeNames) { + List scopes = new ArrayList<>(); + + for (String scopeName : scopeNames) { + Scope scope = getScopeById(scopeName); + if (scope != null) { + scopes.add(scope.getDn()); + } + } + + return scopes; + } + + public List getScopeIdsByDns(List dns) { + List names = Lists.newArrayList(); + if (dns == null || dns.isEmpty()) { + return dns; + } + + for (String dn : dns) { + Scope scope = getScopeByDnSilently(dn); + if (scope != null && StringUtils.isNotBlank(scope.getId())) { + names.add(scope.getId()); + } + } + return names; + } + + /** + * returns Scope by Dn + * + * @return Scope + */ + public Scope getScopeByDn(String dn) { + BaseCacheService usedCacheService = getCacheService(); + final Scope scope = usedCacheService.getWithPut(dn, () -> ldapEntryManager.find(Scope.class, dn), 60); + if (scope != null && StringUtils.isNotBlank(scope.getId())) { + usedCacheService.put(scope.getId(), scope); // put also by id, since we call it by id and dn + } + return scope; + } + + /** + * returns Scope by Dn + * + * @return Scope + */ + public Scope getScopeByDnSilently(String dn) { + try { + return getScopeByDn(dn); + } catch (Exception e) { + log.trace(e.getMessage(), e); + return null; + } + } + + /** + * Get scope by DisplayName + * + * @param id + * @return scope + */ + public Scope getScopeById(String id) { + BaseCacheService usedCacheService = getCacheService(); + + final Object cached = usedCacheService.get(id); + if (cached != null) + return (Scope) cached; + + try { + List scopes = ldapEntryManager.findEntries( + staticConfiguration.getBaseDn().getScopes(), Scope.class, Filter.createEqualityFilter("oxId", id)); + if ((scopes != null) && (scopes.size() > 0)) { + final Scope scope = scopes.get(0); + usedCacheService.put(id, scope); + usedCacheService.put(scope.getDn(), scope); + return scope; + } + } catch (Exception e) { + log.error("Failed to find scope with id: " + id, e); + } + return null; + } + + /** + * Get scope by oxAuthClaims + * + * @param claimDn + * @return List of scope + */ + public List getScopeByClaim(String claimDn) { + List scopes = fromCacheByClaimDn(claimDn); + if (scopes == null) { + Filter filter = Filter.createEqualityFilter("oxAuthClaim", claimDn); + + String scopesBaseDN = staticConfiguration.getBaseDn().getScopes(); + scopes = ldapEntryManager.findEntries(scopesBaseDN, Scope.class, filter); + + putInCache(claimDn, scopes); + } + + return scopes; + } + + public List getScopesByClaim(List scopes, String claimDn) { + List result = new ArrayList<>(); + for (Scope scope : scopes) { + List claims = scope.getOxAuthClaims(); + if ((claims != null) && claims.contains(claimDn)) { + result.add(scope); + } + } + + return result; + } + + private void putInCache(String claimDn, List scopes) { + if (scopes == null) { + return; + } + + BaseCacheService usedCacheService = getCacheService(); + try { + String key = getClaimDnCacheKey(claimDn); + usedCacheService.put(key, scopes); + } catch (Exception ex) { + log.error("Failed to put scopes in cache, claimDn: '{}'", claimDn, ex); + } + } + + @SuppressWarnings("unchecked") + private List fromCacheByClaimDn(String claimDn) { + BaseCacheService usedCacheService = getCacheService(); + try { + String key = getClaimDnCacheKey(claimDn); + return (List) usedCacheService.get(key); + } catch (Exception ex) { + log.error("Failed to get scopes from cache, claimDn: '{}'", claimDn, ex); + return null; + } + } + + private static String getClaimDnCacheKey(String claimDn) { + return "claim_dn" + StringHelper.toLowerCase(claimDn); + } + + public void persist(Scope scope) { + ldapEntryManager.persist(scope); + } + + private BaseCacheService getCacheService() { + if (appConfiguration.getUseLocalCache()) { + return localCacheService; + } + + return cacheService; + } + + public Map getClaims(User user, Scope scope) throws InvalidClaimException { + Map claims = new HashMap<>(); + + if (scope == null) { + log.trace("Scope is null."); + return claims; + } + + final List scopeClaims = scope.getOxAuthClaims(); + if (scopeClaims == null) { + log.trace("No claims set for scope: {}", scope.getId()); + return claims; + } + + fillClaims(claims, scopeClaims, user); + + return claims; + } + + private void fillClaims(Map claims, List scopeClaims, User user) throws InvalidClaimException { + for (String claimDn : scopeClaims) { + GluuAttribute gluuAttribute = attributeService.getAttributeByDn(claimDn); + + String claimName = gluuAttribute.getOxAuthClaimName(); + String ldapName = gluuAttribute.getName(); + + if (StringUtils.isBlank(claimName)) { + log.error("Failed to get claim because claim name is not set for attribute, id: {}", gluuAttribute.getDn()); + continue; + } + if (StringUtils.isBlank(ldapName)) { + log.error("Failed to get claim because name is not set for attribute, id: {}", gluuAttribute.getDn()); + continue; + } + + setClaimField(ldapName, claimName, user, gluuAttribute, claims); + } + } + + private void setClaimField(String ldapName, String claimName, User user, GluuAttribute gluuAttribute, + Map claims) throws InvalidClaimException { + Object attribute = null; + if (ldapName.equals("uid")) { + attribute = user.getUserId(); + } else if (ldapName.equals("updatedAt")) { + attribute = user.getUpdatedAt(); + } else if (ldapName.equals("createdAt")) { + attribute = user.getCreatedAt(); + } else if (AttributeDataType.BOOLEAN.equals(gluuAttribute.getDataType())) { + final Object value = user.getAttribute(gluuAttribute.getName(), true, gluuAttribute.getOxMultiValuedAttribute()); + if (value instanceof String) { + attribute = Boolean.parseBoolean(String.valueOf(value)); + } else { + attribute = value; + } + } else if (AttributeDataType.DATE.equals(gluuAttribute.getDataType())) { + final Object value = user.getAttribute(gluuAttribute.getName(), true, gluuAttribute.getOxMultiValuedAttribute()); + if (value instanceof Date) { + attribute = value; + } else if (value != null) { + attribute = decodeTime(user.getDn(), value.toString()); + } + } else { + attribute = user.getAttribute(gluuAttribute.getName(), true, gluuAttribute.getOxMultiValuedAttribute()); + } + + if (attribute != null) { + claims.put(claimName, attribute instanceof JSONArray ? JsonApplier.getStringList((JSONArray) attribute) : attribute); + } + } + + private Date decodeTime(String userDn, String value) { + Date date = ldapEntryManager.decodeTime(userDn, value); + if (date == null) { + try { + return new Date(value); + } catch (Exception e) { + log.error("Error on parse date: {}, input: {}", e.getMessage(), value); + return null; + } + } + return date; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/SectorIdentifierService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/SectorIdentifierService.java new file mode 100644 index 00000000..18cd0eb3 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/SectorIdentifierService.java @@ -0,0 +1,150 @@ +package org.gluu.oxauth.service; + +import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.common.*; +import org.gluu.oxauth.model.config.StaticConfiguration; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.util.StringHelper; +import org.oxauth.persistence.model.PairwiseIdentifier; +import org.oxauth.persistence.model.SectorIdentifier; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import java.net.URI; +import java.util.UUID; + +/** + * @author Javier Rojas Blum + * @version April 10, 2020 + */ +@ApplicationScoped +public class SectorIdentifierService { + + @Inject + private Logger log; + + @Inject + private PersistenceEntryManager ldapEntryManager; + + @Inject + private StaticConfiguration staticConfiguration; + + @Inject + private PairwiseIdentifierService pairwiseIdentifierService; + + @Inject + protected AppConfiguration appConfiguration; + + /** + * Get sector identifier by oxId + * + * @param oxId Sector identifier oxId + * @return Sector identifier + */ + public SectorIdentifier getSectorIdentifierById(String oxId) { + SectorIdentifier result = null; + try { + result = ldapEntryManager.find(SectorIdentifier.class, getDnForSectorIdentifier(oxId)); + } catch (Exception e) { + log.error("Failed to find sector identifier by oxId " + oxId, e); + } + return result; + } + + /** + * Build DN string for sector identifier + * + * @param oxId Sector Identifier oxId + * @return DN string for specified sector identifier or DN for sector identifiers branch if oxId is null + * @throws Exception + */ + public String getDnForSectorIdentifier(String oxId) { + String sectorIdentifierDn = staticConfiguration.getBaseDn().getSectorIdentifiers(); + if (StringHelper.isEmpty(oxId)) { + return sectorIdentifierDn; + } + + return String.format("oxId=%s,%s", oxId, sectorIdentifierDn); + } + + public String getSub(IAuthorizationGrant grant) { + Client client = grant.getClient(); + User user = grant.getUser(); + + if (user == null) { + log.trace("User is null, return blank sub"); + return ""; + } + if (client == null) { + log.trace("Client is null, return blank sub."); + return ""; + } + + return getSub(client, user, grant instanceof CIBAGrant); + } + + public String getSub(Client client, User user, boolean isCibaGrant) { + if (user == null) { + log.trace("User is null, return blank sub"); + return ""; + } + if (client == null) { + log.trace("Client is null, return blank sub."); + return ""; + } + + final boolean isClientPairwise = SubjectType.PAIRWISE.equals(SubjectType.fromString(client.getSubjectType())); + if (isClientPairwise) { + final String sectorIdentifierUri; + + if (StringUtils.isNotBlank(client.getSectorIdentifierUri())) { + sectorIdentifierUri = client.getSectorIdentifierUri(); + } else { + if (!isCibaGrant) { + sectorIdentifierUri = !ArrayUtils.isEmpty(client.getRedirectUris()) ? client.getRedirectUris()[0] : null; + } else { + if (client.getBackchannelTokenDeliveryMode() == BackchannelTokenDeliveryMode.PUSH) { + sectorIdentifierUri = client.getBackchannelClientNotificationEndpoint(); + } else { + sectorIdentifierUri = client.getJwksUri(); + } + } + } + + String userInum = user.getAttribute("inum"); + + try { + if (StringUtils.isNotBlank(sectorIdentifierUri)) { + String sectorIdentifier = URI.create(sectorIdentifierUri).getHost(); + if (appConfiguration.getSubjectIdentifierBasedOnWholeUriBackwardCompatibility()) // todo remove in 5.0 + sectorIdentifier = sectorIdentifierUri; + + PairwiseIdentifier pairwiseIdentifier = pairwiseIdentifierService.findPairWiseIdentifier(userInum, + sectorIdentifier, client.getClientId()); + if (pairwiseIdentifier == null) { + pairwiseIdentifier = new PairwiseIdentifier(sectorIdentifier, client.getClientId(), userInum); + pairwiseIdentifier.setId(UUID.randomUUID().toString()); + pairwiseIdentifier.setDn(pairwiseIdentifierService.getDnForPairwiseIdentifier(pairwiseIdentifier.getId(), userInum)); + pairwiseIdentifierService.addPairwiseIdentifier(userInum, pairwiseIdentifier); + } + return pairwiseIdentifier.getId(); + } else { + log.trace("Sector identifier uri is blank for client: " + client.getClientId()); + } + } catch (Exception e) { + log.error("Failed to get sub claim. PairwiseIdentifierService failed to find pair wise identifier.", e); + return ""; + } + } + + String openidSubAttribute = appConfiguration.getOpenidSubAttribute(); + if (StringHelper.equalsIgnoreCase(openidSubAttribute, "uid")) { + return user.getUserId(); + } + return user.getAttribute(openidSubAttribute); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/ServerCryptoProvider.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/ServerCryptoProvider.java new file mode 100644 index 00000000..4febaf06 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/ServerCryptoProvider.java @@ -0,0 +1,97 @@ +package org.gluu.oxauth.service; + +import org.apache.log4j.Logger; +import org.gluu.oxauth.model.config.ConfigurationFactory; +import org.gluu.oxauth.model.crypto.AbstractCryptoProvider; +import org.gluu.oxauth.model.crypto.signature.AlgorithmFamily; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jwk.Algorithm; +import org.gluu.oxauth.model.jwk.JSONWebKeySet; +import org.gluu.oxauth.model.jwk.Use; +import org.gluu.service.cdi.util.CdiUtil; +import org.json.JSONObject; +import org.msgpack.core.Preconditions; + +import java.security.KeyStoreException; +import java.security.PrivateKey; + +/** + * @author Yuriy Zabrovarnyy + */ +public class ServerCryptoProvider extends AbstractCryptoProvider { + + private static final Logger LOG = Logger.getLogger(ServerCryptoProvider.class); + + private final ConfigurationFactory configurationFactory; + private final AbstractCryptoProvider cryptoProvider; + + public ServerCryptoProvider(AbstractCryptoProvider cryptoProvider) { + this.configurationFactory = CdiUtil.bean(ConfigurationFactory.class); + this.cryptoProvider = cryptoProvider; + Preconditions.checkNotNull(configurationFactory); + Preconditions.checkNotNull(cryptoProvider); + } + + @Override + public String getKeyId(JSONWebKeySet jsonWebKeySet, Algorithm algorithm, Use use) throws Exception { + try { + if (algorithm == null || AlgorithmFamily.HMAC.equals(algorithm.getFamily())) { + return null; + } + final String kid = cryptoProvider.getKeyId(jsonWebKeySet, algorithm, use); + if (!cryptoProvider.getKeys().contains(kid) && configurationFactory.reloadConfFromLdap()) { + return cryptoProvider.getKeyId(jsonWebKeySet, algorithm, use); + } + return kid; + + } catch (KeyStoreException e) { + LOG.trace("Try to re-load configuration due to keystore exception (it can be rotated)."); + if (configurationFactory.reloadConfFromLdap()) { + return cryptoProvider.getKeyId(jsonWebKeySet, algorithm, use); + } + } + return null; + } + + @Override + public JSONObject generateKey(Algorithm algorithm, Long expirationTime, Use use) throws Exception { + return cryptoProvider.generateKey(algorithm, expirationTime, use); + } + + @Override + public JSONObject generateKey(Algorithm algorithm, Long expirationTime, Use use, int keyLength) throws Exception { + return cryptoProvider.generateKey(algorithm, expirationTime, use, keyLength); + } + + @Override + public String sign(String signingInput, String keyId, String sharedSecret, SignatureAlgorithm signatureAlgorithm) throws Exception { + if (configurationFactory.getAppConfiguration().getRejectJwtWithNoneAlg() && signatureAlgorithm == SignatureAlgorithm.NONE) { + throw new UnsupportedOperationException("None algorithm is forbidden by `rejectJwtWithNoneAlg` configuration property."); + } + return cryptoProvider.sign(signingInput, keyId, sharedSecret, signatureAlgorithm); + } + + @Override + public boolean verifySignature(String signingInput, String encodedSignature, String keyId, JSONObject jwks, String sharedSecret, SignatureAlgorithm signatureAlgorithm) throws Exception { + if (configurationFactory.getAppConfiguration().getRejectJwtWithNoneAlg() && signatureAlgorithm == SignatureAlgorithm.NONE) { + LOG.trace("None algorithm is forbidden by `rejectJwtWithNoneAlg` configuration property."); + return false; + } + return cryptoProvider.verifySignature(signingInput, encodedSignature, keyId, jwks, sharedSecret, signatureAlgorithm); + } + + @Override + public boolean deleteKey(String keyId) throws Exception { + return cryptoProvider.deleteKey(keyId); + } + + @Override + public boolean containsKey(String keyId) { + return cryptoProvider.containsKey(keyId); + } + + @Override + public PrivateKey getPrivateKey(String keyId) throws Exception { + return cryptoProvider.getPrivateKey(keyId); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/SessionIdService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/SessionIdService.java new file mode 100644 index 00000000..f2973a20 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/SessionIdService.java @@ -0,0 +1,1028 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Sets; +import com.unboundid.ldap.sdk.LDAPException; +import com.unboundid.ldap.sdk.ResultCode; +import org.apache.commons.lang.BooleanUtils; +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.audit.ApplicationAuditLogger; +import org.gluu.oxauth.model.audit.Action; +import org.gluu.oxauth.model.audit.OAuth2AuditLog; +import org.gluu.oxauth.model.authorize.AuthorizeRequestParam; +import org.gluu.oxauth.model.common.Prompt; +import org.gluu.oxauth.model.common.User; +import org.gluu.oxauth.model.config.Constants; +import org.gluu.oxauth.model.config.StaticConfiguration; +import org.gluu.oxauth.model.config.WebKeysConfiguration; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.exception.AcrChangedException; +import org.gluu.oxauth.model.exception.InvalidSessionStateException; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtSubClaimObject; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.model.session.SessionIdState; +import org.gluu.oxauth.model.token.JwtSigner; +import org.gluu.oxauth.model.util.JwtUtil; +import org.gluu.oxauth.model.util.Pair; +import org.gluu.oxauth.model.util.Util; +import org.gluu.oxauth.security.Identity; +import org.gluu.oxauth.service.common.UserService; +import org.gluu.oxauth.service.external.ExternalApplicationSessionService; +import org.gluu.oxauth.service.external.ExternalAuthenticationService; +import org.gluu.oxauth.service.external.session.SessionEvent; +import org.gluu.oxauth.service.external.session.SessionEventType; +import org.gluu.oxauth.service.stat.StatService; +import org.gluu.oxauth.util.ServerUtil; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.persist.exception.EntryPersistenceException; +import org.gluu.search.filter.Filter; +import org.gluu.service.CacheService; +import org.gluu.service.LocalCacheService; +import org.gluu.util.StringHelper; +import org.jetbrains.annotations.Nullable; +import org.json.JSONException; +import org.slf4j.Logger; + +import javax.enterprise.context.RequestScoped; +import javax.faces.context.ExternalContext; +import javax.faces.context.FacesContext; +import javax.inject.Inject; +import javax.inject.Named; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; + +/** + * @author Yuriy Zabrovarnyy + * @author Yuriy Movchan + * @author Javier Rojas Blum + * @version December 8, 2018 + */ +@RequestScoped +@Named +public class SessionIdService { + + public static final String OP_BROWSER_STATE = "opbs"; + public static final String SESSION_CUSTOM_STATE = "session_custom_state"; + private static final int MAX_MERGE_ATTEMPTS = 3; + private static final int DEFAULT_LOCAL_CACHE_EXPIRATION = 2; + + @Inject + private Logger log; + + @Inject + private ExternalAuthenticationService externalAuthenticationService; + + @Inject + private ExternalApplicationSessionService externalApplicationSessionService; + + @Inject + private ApplicationAuditLogger applicationAuditLogger; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private WebKeysConfiguration webKeysConfiguration; + + @Inject + private FacesContext facesContext; + + @Inject + private ExternalContext externalContext; + + @Inject + private RequestParameterService requestParameterService; + + @Inject + private UserService userService; + + @Inject + private PersistenceEntryManager persistenceEntryManager; + + @Inject + private StaticConfiguration staticConfiguration; + + @Inject + private CookieService cookieService; + + @Inject + private Identity identity; + + @Inject + private LocalCacheService localCacheService; + + @Inject + private CacheService cacheService; + + @Inject + private StatService statService; + + private String buildDn(String sessionId) { + return String.format("oxId=%s,%s", sessionId, staticConfiguration.getBaseDn().getSessions()); + } + + public Set getCurrentSessions() { + final Set ids = cookieService.getCurrentSessions(); + final Set sessions = Sets.newHashSet(); + for (String sessionId : ids) { + if (StringUtils.isBlank(sessionId)) { + log.error("Invalid sessionId in current_sessions: " + sessionId); + continue; + } + + final SessionId sessionIdObj = getSessionId(sessionId); + if (sessionIdObj == null) { + log.trace("Unable to find session object by id: " + sessionId + " {expired?}"); + continue; + } + + if (sessionIdObj.getState() != SessionIdState.AUTHENTICATED) { + log.error("Session is not authenticated, id: " + sessionId); + continue; + } + sessions.add(sessionIdObj); + } + return sessions; + } + + public String getAcr(SessionId session) { + if (session == null) { + return null; + } + + String acr = session.getSessionAttributes().get(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE); + if (StringUtils.isBlank(acr)) { + acr = session.getSessionAttributes().get("acr_values"); + } + return acr; + } + + // #34 - update session attributes with each request + // 1) redirect_uri change -> update session + // 2) acr change -> throw acr change exception + // 3) client_id change -> do nothing + // https://github.com/GluuFederation/oxAuth/issues/34 + public SessionId assertAuthenticatedSessionCorrespondsToNewRequest(SessionId session, String acrValuesStr) throws AcrChangedException { + if (session != null && !session.getSessionAttributes().isEmpty() && session.getState() == SessionIdState.AUTHENTICATED) { + + final Map sessionAttributes = session.getSessionAttributes(); + + String sessionAcr = getAcr(session); + + if (StringUtils.isBlank(sessionAcr)) { + log.trace("Failed to fetch acr from session, attributes: " + sessionAttributes); + return session; + } + + List acrValuesList = acrValuesList(acrValuesStr); + boolean isAcrChanged = !acrValuesList.isEmpty() && !acrValuesList.contains(sessionAcr); + if (isAcrChanged) { + Map acrToLevel = externalAuthenticationService.acrToLevelMapping(); + Integer sessionAcrLevel = acrToLevel.get(externalAuthenticationService.scriptName(sessionAcr)); + + for (String acrValue : acrValuesList) { + Integer currentAcrLevel = acrToLevel.get(externalAuthenticationService.scriptName(acrValue)); + + log.info("Acr is changed. Session acr: " + sessionAcr + "(level: " + sessionAcrLevel + "), " + + "current acr: " + acrValue + "(level: " + currentAcrLevel + ")"); + + // Requested acr method not enabled + if (currentAcrLevel == null) { + throw new AcrChangedException(false); + } + + if (sessionAcrLevel < currentAcrLevel) { + throw new AcrChangedException(); + } + } + // https://github.com/GluuFederation/oxAuth/issues/291 + return session; // we don't want to reinit login because we have stronger acr (avoid overriding) + } + + reinitLogin(session, false); + } + + return session; + } + + private static boolean shouldReinitSession(Map sessionAttributes, Map currentSessionAttributes) { + final Map copySessionAttributes = new HashMap<>(sessionAttributes); + final Map copyCurrentSessionAttributes = new HashMap<>(currentSessionAttributes); + + // it's up to RP whether to change state per request + copySessionAttributes.remove(AuthorizeRequestParam.STATE); + copyCurrentSessionAttributes.remove(AuthorizeRequestParam.STATE); + + return !copyCurrentSessionAttributes.equals(copySessionAttributes); + } + + /** + * + * @param session + * @param force + * @return returns whether session was updated + */ + public boolean reinitLogin(SessionId session, boolean force) { + final Map sessionAttributes = session.getSessionAttributes(); + final Map currentSessionAttributes = getCurrentSessionAttributes(sessionAttributes); + if (log.isTraceEnabled()) { + log.trace("sessionAttributes: {}", sessionAttributes); + log.trace("currentSessionAttributes: {}", currentSessionAttributes); + log.trace("shouldReinitSession: {}, force: {}", shouldReinitSession(sessionAttributes, currentSessionAttributes), force); + } + + if (force || shouldReinitSession(sessionAttributes, currentSessionAttributes)) { + sessionAttributes.putAll(currentSessionAttributes); + + // Reinit login + sessionAttributes.put("c", "1"); + + for (Iterator> it = currentSessionAttributes.entrySet().iterator(); it.hasNext(); ) { + Entry currentSessionAttributesEntry = it.next(); + String name = currentSessionAttributesEntry.getKey(); + if (name.startsWith("auth_step_passed_")) { + it.remove(); + } + } + + session.setSessionAttributes(currentSessionAttributes); + + if (force) { + // Reset state to unauthenticated + session.setState(SessionIdState.UNAUTHENTICATED); + externalEvent(new SessionEvent(SessionEventType.UNAUTHENTICATED, session)); + } + + boolean updateResult = updateSessionId(session, true, true, true); + if (!updateResult) { + log.debug("Failed to update session entry: '{}'", session.getId()); + } + if (log.isTraceEnabled()) { + log.trace("sessionAttributes after update: {}, ", session.getSessionAttributes()); + } + return updateResult; + } + return false; + } + + public SessionId resetToStep(SessionId session, int resetToStep) { + final Map sessionAttributes = session.getSessionAttributes(); + + int currentStep = 1; + if (sessionAttributes.containsKey("auth_step")) { + currentStep = StringHelper.toInteger(sessionAttributes.get("auth_step"), currentStep); + } + + if (resetToStep <= currentStep) { + for (int i = resetToStep; i <= currentStep; i++) { + String key = String.format("auth_step_passed_%d", i); + sessionAttributes.remove(key); + } + } else { + // Scenario when we sckip steps. In this case we need to mark all previous steps as passed + for (int i = currentStep + 1; i < resetToStep; i++) { + sessionAttributes.put(String.format("auth_step_passed_%d", i), Boolean.TRUE.toString()); + } + } + + sessionAttributes.put("auth_step", String.valueOf(resetToStep)); + + boolean updateResult = updateSessionId(session, true, true, true); + if (!updateResult) { + log.debug("Failed to update session entry: '{}'", session.getId()); + return null; + } + + return session; + } + + private Map getCurrentSessionAttributes(Map sessionAttributes) { + if (facesContext == null) { + return sessionAttributes; + } + + // Update from request + final Map currentSessionAttributes = new HashMap<>(sessionAttributes); + + Map requestParameters = externalContext.getRequestParameterMap(); + Map newRequestParameterMap = requestParameterService.getAllowedParameters(requestParameters); + for (Entry newRequestParameterMapEntry : newRequestParameterMap.entrySet()) { + String name = newRequestParameterMapEntry.getKey(); + if (!StringHelper.equalsIgnoreCase(name, "auth_step")) { + currentSessionAttributes.put(name, newRequestParameterMapEntry.getValue()); + } + } + if (!requestParameters.containsKey(AuthorizeRequestParam.CODE_CHALLENGE) || !requestParameters.containsKey(AuthorizeRequestParam.CODE_CHALLENGE_METHOD)) { + currentSessionAttributes.remove(AuthorizeRequestParam.CODE_CHALLENGE); + currentSessionAttributes.remove(AuthorizeRequestParam.CODE_CHALLENGE_METHOD); + } + + return currentSessionAttributes; + } + + public SessionId getSessionId() { + String sessionId = cookieService.getSessionIdFromCookie(); + + if (StringHelper.isEmpty(sessionId)) { + if (identity.getSessionId() != null) { + sessionId = identity.getSessionId().getId(); + } + } + + if (StringHelper.isNotEmpty(sessionId)) { + return getSessionId(sessionId); + } else { + log.trace("Session cookie not exists"); + } + + return null; + } + + public Map getSessionAttributes(SessionId sessionId) { + if (sessionId != null) { + return sessionId.getSessionAttributes(); + } + + return null; + } + + public SessionId generateAuthenticatedSessionId(HttpServletRequest httpRequest, String userDn) throws InvalidSessionStateException { + Map sessionIdAttributes = new HashMap<>(); + sessionIdAttributes.put("prompt", ""); + + return generateAuthenticatedSessionId(httpRequest, userDn, sessionIdAttributes); + } + + public SessionId generateAuthenticatedSessionId(HttpServletRequest httpRequest, String userDn, String prompt) throws InvalidSessionStateException { + Map sessionIdAttributes = new HashMap<>(); + sessionIdAttributes.put("prompt", prompt); + + return generateAuthenticatedSessionId(httpRequest, userDn, sessionIdAttributes); + } + + public SessionId generateAuthenticatedSessionId(HttpServletRequest httpRequest, String userDn, Map sessionIdAttributes) throws InvalidSessionStateException { + SessionId sessionId = generateSessionId(userDn, new Date(), SessionIdState.AUTHENTICATED, sessionIdAttributes, true); + + reportActiveUser(sessionId); + + if (externalApplicationSessionService.isEnabled()) { + String userName = sessionId.getSessionAttributes().get(Constants.AUTHENTICATED_USER); + boolean externalResult = externalApplicationSessionService.executeExternalStartSessionMethods(httpRequest, sessionId); + log.info("Start session result for '{}': '{}'", userName, "start", externalResult); + + if (!externalResult) { + reinitLogin(sessionId, true); + throw new InvalidSessionStateException("Session creation is prohibited by external session script!"); + } + + externalEvent(new SessionEvent(SessionEventType.AUTHENTICATED, sessionId).setHttpRequest(httpRequest)); + } + + return sessionId; + } + + private void reportActiveUser(SessionId sessionId) { + try { + final User user = getUser(sessionId); + if (user != null) { + statService.reportActiveUser(user.getUserId()); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + + public SessionId generateUnauthenticatedSessionId(String userDn) { + Map sessionIdAttributes = new HashMap<>(); + return generateSessionId(userDn, new Date(), SessionIdState.UNAUTHENTICATED, sessionIdAttributes, true); + } + + public SessionId generateUnauthenticatedSessionId(String userDn, Date authenticationDate, SessionIdState state, Map sessionIdAttributes, boolean persist) { + return generateSessionId(userDn, authenticationDate, state, sessionIdAttributes, persist); + } + + public String computeSessionState(SessionId sessionId, String clientId, String redirectUri) { + final boolean isSameClient = clientId.equals(sessionId.getSessionAttributes().get("client_id")) && + redirectUri.equals(sessionId.getSessionAttributes().get("redirect_uri")); + if(isSameClient) + return sessionId.getSessionState(); + final String salt = UUID.randomUUID().toString(); + final String opbs = sessionId.getOPBrowserState(); + final String sessionState = computeSessionState(clientId,redirectUri, opbs, salt); + return sessionState; + } + + private String computeSessionState(String clientId, String redirectUri, String opbs, String salt) { + try { + final String clientOrigin = getClientOrigin(redirectUri); + final String sessionState = JwtUtil.bytesToHex(JwtUtil.getMessageDigestSHA256( + clientId + " " + clientOrigin + " " + opbs + " " + salt)) + "." + salt; + return sessionState; + } catch (NoSuchProviderException | NoSuchAlgorithmException | UnsupportedEncodingException | URISyntaxException e) { + log.error("Failed generating session state! " + e.getMessage(), e); + throw new RuntimeException(e); + } + } + + private String getClientOrigin(String redirectUri) throws URISyntaxException { + if (StringHelper.isNotEmpty(redirectUri)) { + final URI uri = new URI(redirectUri); + String result = uri.getScheme() + "://" + uri.getHost(); + if(uri.getPort() > 0) + result += ":" + Integer.toString(uri.getPort()); + return result; + } else { + return appConfiguration.getIssuer(); + } + } + + private SessionId generateSessionId(String userDn, Date authenticationDate, SessionIdState state, Map sessionIdAttributes, boolean persist) { + final String internalSid = UUID.randomUUID().toString(); + final String outsideSid = UUID.randomUUID().toString(); + final String salt = UUID.randomUUID().toString(); + final String clientId = sessionIdAttributes.get("client_id"); + final String opbs = UUID.randomUUID().toString(); + final String redirectUri = sessionIdAttributes.get("redirect_uri"); + final String sessionState = computeSessionState(clientId, redirectUri, opbs, salt); + final String dn = buildDn(internalSid); + sessionIdAttributes.put(OP_BROWSER_STATE, opbs); + + Preconditions.checkNotNull(dn); + + if (SessionIdState.AUTHENTICATED == state && StringUtils.isBlank(userDn) && !sessionIdAttributes.containsKey("uma")) { + return null; + } + + final SessionId sessionId = new SessionId(); + sessionId.setId(internalSid); + sessionId.setOutsideSid(outsideSid); + sessionId.setDn(dn); + sessionId.setUserDn(userDn); + sessionId.setSessionState(sessionState); + + final Pair expiration = expirationDate(sessionId.getCreationDate(), state); + sessionId.setExpirationDate(expiration.getFirst()); + sessionId.setTtl(expiration.getSecond()); + + Boolean sessionAsJwt = appConfiguration.getSessionAsJwt(); + sessionId.setIsJwt(sessionAsJwt != null && sessionAsJwt); + + sessionId.setAuthenticationTime(authenticationDate != null ? authenticationDate : new Date()); + + if (state != null) { + sessionId.setState(state); + } + + sessionId.setSessionAttributes(sessionIdAttributes); + sessionId.setLastUsedAt(new Date()); + + if (sessionId.getIsJwt()) { + sessionId.setJwt(generateJwt(sessionId, userDn).asString()); + } + + boolean persisted = false; + if (persist) { + persisted = persistSessionId(sessionId); + } + + auditLogging(sessionId); + + log.trace("Generated new session, id = '{}', state = '{}', asJwt = '{}', persisted = '{}'", sessionId.getId(), sessionId.getState(), sessionId.getIsJwt(), persisted); + return sessionId; + } + + + private Jwt generateJwt(SessionId sessionId, String audience) { + try { + JwtSigner jwtSigner = new JwtSigner(appConfiguration, webKeysConfiguration, SignatureAlgorithm.RS512, audience); + Jwt jwt = jwtSigner.newJwt(); + + // claims + jwt.getClaims().setClaim("id", sessionId.getId()); + jwt.getClaims().setClaim("authentication_time", sessionId.getAuthenticationTime()); + jwt.getClaims().setClaim("user_dn", sessionId.getUserDn()); + jwt.getClaims().setClaim("state", sessionId.getState() != null ? + sessionId.getState().getValue() : ""); + + jwt.getClaims().setClaim("session_attributes", JwtSubClaimObject.fromMap(sessionId.getSessionAttributes())); + + jwt.getClaims().setClaim("last_used_at", sessionId.getLastUsedAt()); + jwt.getClaims().setClaim("permission_granted", sessionId.getPermissionGranted()); + jwt.getClaims().setClaim("permission_granted_map", JwtSubClaimObject.fromBooleanMap(sessionId.getPermissionGrantedMap().getPermissionGranted())); + + // sign + return jwtSigner.sign(); + } catch (Exception e) { + log.error("Failed to sign session jwt! " + e.getMessage(), e); + throw new RuntimeException(e); + } + } + + public SessionId setSessionIdStateAuthenticated(HttpServletRequest httpRequest, HttpServletResponse httpResponse, SessionId sessionId, String p_userDn) { + sessionId.setUserDn(p_userDn); + sessionId.setAuthenticationTime(new Date()); + sessionId.setState(SessionIdState.AUTHENTICATED); + + final User user = getUser(sessionId); + if (user != null) { + statService.reportActiveUser(user.getUserId()); + } + + final boolean persisted; + if (appConfiguration.getChangeSessionIdOnAuthentication() && httpResponse != null) { + final String oldSessionId = sessionId.getId(); + final String newSessionId = UUID.randomUUID().toString(); + + log.debug("Changing session id from {} to {} ...", oldSessionId, newSessionId); + remove(sessionId); + + sessionId.setId(newSessionId); + sessionId.setDn(buildDn(newSessionId)); + sessionId.getSessionAttributes().put(SessionId.OLD_SESSION_ID_ATTR_KEY, oldSessionId); + if (sessionId.getIsJwt()) { + sessionId.setJwt(generateJwt(sessionId, sessionId.getUserDn()).asString()); + } + + persisted = persistSessionId(sessionId, true); + cookieService.createSessionIdCookie(sessionId, httpRequest, httpResponse, false); + log.debug("Session identifier changed from {} to {} .", oldSessionId, newSessionId); + } else { + persisted = updateSessionId(sessionId, true, true, true); + } + + auditLogging(sessionId); + log.trace("Authenticated session, id = '{}', state = '{}', persisted = '{}'", sessionId.getId(), sessionId.getState(), persisted); + + if (externalApplicationSessionService.isEnabled()) { + String userName = sessionId.getSessionAttributes().get(Constants.AUTHENTICATED_USER); + boolean externalResult = externalApplicationSessionService.executeExternalStartSessionMethods(httpRequest, sessionId); + log.info("Start session result for '{}': '{}'", userName, "start", externalResult); + + if (!externalResult) { + reinitLogin(sessionId, true); + throw new InvalidSessionStateException("Session creation is prohibited by external session script!"); + } + externalEvent(new SessionEvent(SessionEventType.AUTHENTICATED, sessionId).setHttpRequest(httpRequest).setHttpResponse(httpResponse)); + } + + return sessionId; + } + + public boolean persistSessionId(final SessionId sessionId) { + return persistSessionId(sessionId, false); + } + + public boolean persistSessionId(final SessionId sessionId, boolean forcePersistence) { + List prompts = getPromptsFromSessionId(sessionId); + + try { + final int unusedLifetime = appConfiguration.getSessionIdUnusedLifetime(); + if ((unusedLifetime > 0 && isPersisted(prompts)) || forcePersistence) { + sessionId.setLastUsedAt(new Date()); + + final Pair expiration = expirationDate(sessionId.getCreationDate(), sessionId.getState()); + sessionId.setPersisted(true); + sessionId.setExpirationDate(expiration.getFirst()); + sessionId.setTtl(expiration.getSecond()); + log.trace("sessionIdAttributes: " + sessionId.getPermissionGrantedMap()); + if (appConfiguration.getSessionIdPersistInCache()) { + cacheService.put(expiration.getSecond(), sessionId.getDn(), sessionId); + } else { + persistenceEntryManager.persist(sessionId); + } + localCacheService.put(DEFAULT_LOCAL_CACHE_EXPIRATION, sessionId.getDn(), sessionId); + return true; + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + + return false; + } + + public boolean updateSessionId(final SessionId sessionId) { + return updateSessionId(sessionId, true); + } + + public boolean updateSessionId(final SessionId sessionId, boolean updateLastUsedAt) { + return updateSessionId(sessionId, updateLastUsedAt, false, true); + } + + public boolean updateSessionId(final SessionId sessionId, boolean updateLastUsedAt, boolean forceUpdate, boolean modified) { + List prompts = getPromptsFromSessionId(sessionId); + + try { + final int unusedLifetime = appConfiguration.getSessionIdUnusedLifetime(); + if ((unusedLifetime > 0 && isPersisted(prompts)) || forceUpdate) { + boolean update = modified; + + if (updateLastUsedAt) { + Date lastUsedAt = new Date(); + if (sessionId.getLastUsedAt() != null) { + long diff = lastUsedAt.getTime() - sessionId.getLastUsedAt().getTime(); + int unusedDiffInSeconds = (int) (diff/1000); + if (unusedDiffInSeconds > unusedLifetime) { + log.debug("Session id expired: {} by sessionIdUnusedLifetime, remove it.", sessionId.getId()); + remove(sessionId); // expired + return false; + } + + if (diff > 500) { // update only if diff is more than 500ms + update = true; + sessionId.setLastUsedAt(lastUsedAt); + } + } else { + update = true; + sessionId.setLastUsedAt(lastUsedAt); + } + } + + if (!sessionId.isPersisted()) { + update = true; + sessionId.setPersisted(true); + } + + if (isExpired(sessionId)) { + log.debug("Session id expired: {} by lifetime property, remove it.", sessionId.getId()); + remove(sessionId); // expired + update = false; + } + + if (update) { + mergeWithRetry(sessionId); + } + } + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + + return true; + } + + public boolean isExpired(SessionId sessionId) { + if (sessionId.getAuthenticationTime() == null) { + return false; + } + final long currentLifetimeInSeconds = (System.currentTimeMillis() - sessionId.getAuthenticationTime().getTime()) / 1000; + + return currentLifetimeInSeconds > getServerSessionIdLifetimeInSeconds(); + } + + public int getServerSessionIdLifetimeInSeconds() { + if (appConfiguration.getServerSessionIdLifetime() != null && appConfiguration.getServerSessionIdLifetime() > 0) { + return appConfiguration.getServerSessionIdLifetime(); + } + if (appConfiguration.getSessionIdLifetime() != null && appConfiguration.getSessionIdLifetime() > 0) { + return appConfiguration.getSessionIdLifetime(); + } + + // we don't know for how long we can put it in cache/persistence since expiration is not set, so we set it to max integer. + if (appConfiguration.getServerSessionIdLifetime() != null && appConfiguration.getSessionIdLifetime() != null && + appConfiguration.getServerSessionIdLifetime() <= 0 && appConfiguration.getSessionIdLifetime() <= 0) { + return Integer.MAX_VALUE; + } + log.debug("Session id lifetime configuration is null."); + return AppConfiguration.DEFAULT_SESSION_ID_LIFETIME; + } + + private Pair expirationDate(Date creationDate, SessionIdState state) { + int expirationInSeconds = state == SessionIdState.UNAUTHENTICATED ? + appConfiguration.getSessionIdUnauthenticatedUnusedLifetime() : + getServerSessionIdLifetimeInSeconds(); + Calendar calendar = Calendar.getInstance(); + calendar.setTime(creationDate); + calendar.add(Calendar.SECOND, expirationInSeconds); + return new Pair<>(calendar.getTime(), expirationInSeconds); + } + + private void mergeWithRetry(final SessionId sessionId) { + final Pair expiration = expirationDate(sessionId.getCreationDate(), sessionId.getState()); + sessionId.setExpirationDate(expiration.getFirst()); + sessionId.setTtl(expiration.getSecond()); + + EntryPersistenceException lastException = null; + for (int i = 1; i <= MAX_MERGE_ATTEMPTS; i++) { + try { + if (appConfiguration.getSessionIdPersistInCache()) { + cacheService.put(expiration.getSecond(), sessionId.getDn(), sessionId); + } else { + persistenceEntryManager.merge(sessionId); + } + localCacheService.put(DEFAULT_LOCAL_CACHE_EXPIRATION, sessionId.getDn(), sessionId); + externalEvent(new SessionEvent(SessionEventType.UPDATED, sessionId)); + return; + } catch (EntryPersistenceException ex) { + lastException = ex; + if (ex.getCause() instanceof LDAPException) { + LDAPException parentEx = ((LDAPException) ex.getCause()); + log.debug("LDAP exception resultCode: '{}'", parentEx.getResultCode().intValue()); + if ((parentEx.getResultCode().intValue() == ResultCode.NO_SUCH_ATTRIBUTE_INT_VALUE) || + (parentEx.getResultCode().intValue() == ResultCode.ATTRIBUTE_OR_VALUE_EXISTS_INT_VALUE)) { + log.warn("Session entry update attempt '{}' was unsuccessfull", i); + continue; + } + } + + throw ex; + } + } + + log.error("Session entry update attempt was unsuccessfull after '{}' attempts", MAX_MERGE_ATTEMPTS); + throw lastException; + } + + public void updateSessionIdIfNeeded(SessionId sessionId, boolean modified) { + updateSessionId(sessionId, true, false, modified); + } + + private boolean isPersisted(List prompts) { + if (prompts != null && prompts.contains(Prompt.NONE)) { + final Boolean persistOnPromptNone = appConfiguration.getSessionIdPersistOnPromptNone(); + return persistOnPromptNone != null && persistOnPromptNone; + } + return true; + } + + @Nullable + public SessionId getSessionById(@Nullable String sessionId, boolean silently) { + return getSessionByDn(buildDn(sessionId), silently); + } + + @Nullable + public SessionId getSessionByDn(@Nullable String dn) { + return getSessionByDn(dn, false); + } + + @Nullable + public SessionId getSessionBySid(@Nullable String sid) { + if (StringUtils.isBlank(sid)) { + return null; + } + + final List entries = persistenceEntryManager.findEntries(staticConfiguration.getBaseDn().getSessions(), SessionId.class, Filter.createEqualityFilter("sid", sid)); + if (entries == null || entries.size() != 1) { + return null; + } + return entries.get(0); + } + + @Nullable + public SessionId getSessionByDn(@Nullable String dn, boolean silently) { + if (StringUtils.isBlank(dn)) { + return null; + } + + final Object localCopy = localCacheService.get(dn); + if (localCopy instanceof SessionId) { + if (isSessionValid((SessionId) localCopy)) { + return (SessionId) localCopy; + } else { + localCacheService.remove(dn); + } + } + + try { + final SessionId sessionId; + if (appConfiguration.getSessionIdPersistInCache()) { + sessionId = (SessionId) cacheService.get(dn); + } else { + sessionId = persistenceEntryManager.find(SessionId.class, dn); + } + localCacheService.put(DEFAULT_LOCAL_CACHE_EXPIRATION, sessionId.getDn(), sessionId); + return sessionId; + } catch (Exception e) { + if (!silently) { + if (BooleanUtils.isTrue(appConfiguration.getLogNotFoundEntityAsError())) { + log.error("Failed to get session by dn: " + dn, e); + } else { + log.trace("Failed to get session by dn: " + dn, e); + } + } + } + return null; + } + + @Deprecated + public String getSessionIdFromCookie() { + return cookieService.getSessionIdFromCookie(); + } + + public SessionId getSessionId(HttpServletRequest request) { + final String sessionIdFromCookie = cookieService.getSessionIdFromCookie(request); + log.trace("SessionId from cookie: " + sessionIdFromCookie); + return getSessionId(sessionIdFromCookie); + } + + public SessionId getSessionId(String sessionId) { + return getSessionId(sessionId, false); + } + + public SessionId getSessionId(String sessionId, boolean silently) { + if (StringHelper.isEmpty(sessionId)) { + return null; + } + + try { + final SessionId entity = getSessionById(sessionId, silently); + log.trace("Try to get session by id: {} ...", sessionId); + if (entity != null) { + log.trace("Session dn: {}", entity.getDn()); + + if (isSessionValid(entity)) { + return entity; + } + } + } catch (Exception ex) { + if (!silently) { + log.trace(ex.getMessage()); + } + } + + log.trace("Failed to get session by id: {}", sessionId); + return null; + } + + public boolean remove(SessionId sessionId) { + try { + if (appConfiguration.getSessionIdPersistInCache()) { + cacheService.remove(sessionId.getDn()); + } else { + persistenceEntryManager.remove(sessionId.getDn(), SessionId.class); + } + localCacheService.remove(sessionId.getDn()); + externalEvent(new SessionEvent(SessionEventType.GONE, sessionId)); + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + public void remove(List list) { + for (SessionId id : list) { + try { + remove(id); + } catch (Exception e) { + log.error("Failed to remove entry", e); + } + } + } + + public boolean isSessionValid(SessionId sessionId) { + if (sessionId == null) { + return false; + } + + final long sessionInterval = TimeUnit.SECONDS.toMillis(appConfiguration.getSessionIdUnusedLifetime()); + final long sessionUnauthenticatedInterval = TimeUnit.SECONDS.toMillis(appConfiguration.getSessionIdUnauthenticatedUnusedLifetime()); + + final long timeSinceLastAccess = System.currentTimeMillis() - sessionId.getLastUsedAt().getTime(); + if (timeSinceLastAccess > sessionInterval && appConfiguration.getSessionIdUnusedLifetime() != -1) { + return false; + } + if (sessionId.getState() == SessionIdState.UNAUTHENTICATED && timeSinceLastAccess > sessionUnauthenticatedInterval && appConfiguration.getSessionIdUnauthenticatedUnusedLifetime() != -1) { + return false; + } + + return true; + } + + private List getPromptsFromSessionId(final SessionId sessionId) { + String promptParam = sessionId.getSessionAttributes().get("prompt"); + return Prompt.fromString(promptParam, " "); + } + + public boolean isSessionIdAuthenticated(SessionId sessionId) { + if (sessionId == null) { + return false; + } + return SessionIdState.AUTHENTICATED.equals(sessionId.getState()); + } + + /** + * By definition we expects space separated acr values as it is defined in spec. But we also try maybe some client + * sent it to us as json array. So we try both. + * + * @return acr value list + */ + public List acrValuesList(String acrValues) { + List acrs; + try { + acrs = Util.jsonArrayStringAsList(acrValues); + } catch (JSONException ex) { + acrs = Util.splittedStringAsList(acrValues, " "); + } + + + LinkedHashSet resultAcrs = new LinkedHashSet(); + for (String acr : acrs) { + resultAcrs.add(externalAuthenticationService.scriptName(acr)); + } + + return new ArrayList(resultAcrs); + } + + private void auditLogging(SessionId sessionId) { + HttpServletRequest httpServletRequest = ServerUtil.getRequestOrNull(); + if (httpServletRequest != null) { + Action action; + switch (sessionId.getState()) { + case AUTHENTICATED: + action = Action.SESSION_AUTHENTICATED; + break; + case UNAUTHENTICATED: + action = Action.SESSION_UNAUTHENTICATED; + break; + default: + action = Action.SESSION_UNAUTHENTICATED; + } + OAuth2AuditLog oAuth2AuditLog = new OAuth2AuditLog(ServerUtil.getIpAddress(httpServletRequest), action); + oAuth2AuditLog.setSuccess(true); + applicationAuditLogger.sendMessage(oAuth2AuditLog); + } + } + + public User getUser(SessionId sessionId) { + if (sessionId == null) { + return null; + } + + if (sessionId.getUser() != null) { + return sessionId.getUser(); + } + + if (StringUtils.isBlank(sessionId.getUserDn())) { + return null; + } + + final User user = userService.getUserByDn(sessionId.getUserDn()); + if (user != null) { + sessionId.setUser(user); + return user; + } + + return null; + } + + public List findByUser(String userDn) { + if (appConfiguration.getSessionIdPersistInCache()) { + throw new UnsupportedOperationException("Operation is not supported with sessionIdPersistInCache=true. Set it to false to avoid this exception."); + } + Filter filter = Filter.createEqualityFilter("oxAuthUserDN", userDn); + return persistenceEntryManager.findEntries(staticConfiguration.getBaseDn().getSessions(), SessionId.class, filter); + } + + public void externalEvent(SessionEvent event) { + externalApplicationSessionService.externalEvent(event); + } + + public boolean hasAllScopes(SessionId sessionId, Set scopes) { + if (sessionId == null || sessionId.getSessionAttributes().isEmpty() || scopes == null || scopes.isEmpty()) { + return false; + } + + final String scopesAsString = sessionId.getSessionAttributes().get("scope"); + return hasAllScopes(scopesAsString, scopes); + } + + public boolean hasClientAllScopes(SessionId sessionId, String clientId, Set scopes) { + if (sessionId == null || sessionId.getSessionAttributes().isEmpty() || StringUtils.isBlank(clientId) || scopes == null || scopes.isEmpty()) { + return false; + } + final String key = clientId + "_authz_scopes"; + + String clientScopes = sessionId.getSessionAttributes().get(key); + return hasAllScopes(clientScopes, scopes); + } + + public static boolean hasAllScopes(String existingScopes, Set scopes) { + if (StringUtils.isBlank(existingScopes)) { + return false; + } + + for (String scope : scopes) { + if (!existingScopes.contains(scope)) { + return false; + } + } + return true; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/SpontaneousScopeService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/SpontaneousScopeService.java new file mode 100644 index 00000000..acfa3712 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/SpontaneousScopeService.java @@ -0,0 +1,121 @@ +package org.gluu.oxauth.service; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; + +import org.gluu.oxauth.model.common.ScopeType; +import org.gluu.oxauth.model.config.StaticConfiguration; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.util.Pair; +import org.oxauth.persistence.model.Scope; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import java.util.*; +import java.util.regex.Pattern; + +import static org.apache.commons.lang3.BooleanUtils.isFalse; + +@ApplicationScoped +public class SpontaneousScopeService { + + private static final int DEFAULT_SPONTANEOUS_SCOPE_LIFETIME_IN_SECONDS = 60 * 60 * 24; // 24h + @Inject + private Logger log; + @Inject + private StaticConfiguration staticConfiguration; + @Inject + private AppConfiguration appConfiguration; + @Inject + private ScopeService scopeService; + + public Scope createSpontaneousScopeIfNeeded(Set regExps, String scopeId, String clientId) { + Scope fromPersistence = scopeService.getScopeById(scopeId); + if (fromPersistence != null) { // scope already exists + return fromPersistence; + } + + final Pair isAllowed = isAllowedBySpontaneousScopes(regExps, scopeId); + if (!isAllowed.getFirst()) { + log.error("Forbidden by client. Check client configuration."); + return null; + } + + Scope regexpScope = scopeService.getScopeById(isAllowed.getSecond()); + + Scope scope = new Scope(); + scope.setDefaultScope(false); + scope.setDescription("Spontaneous scope: " + scope); + scope.setDisplayName(scopeId); + scope.setId(scopeId); + scope.setInum(UUID.randomUUID().toString()); + scope.setScopeType(ScopeType.SPONTANEOUS); + scope.setDeletable(true); + scope.setExpirationDate(new Date(getLifetime())); + scope.setDn("inum=" + scope.getInum() + "," + staticConfiguration.getBaseDn().getScopes()); + scope.getAttributes().setSpontaneousClientId(clientId); + scope.getAttributes().setSpontaneousClientScopes(Lists.newArrayList(isAllowed.getSecond())); + scope.setUmaAuthorizationPolicies(regexpScope != null ? regexpScope.getUmaAuthorizationPolicies() : new ArrayList<>()); + + scopeService.persist(scope); + log.trace("Created spontaneous scope: " + scope.getId() + ", dn: " + scope.getDn()); + return scope; + } + + public long getLifetime() { + Calendar expiration = Calendar.getInstance(); + int lifetime = DEFAULT_SPONTANEOUS_SCOPE_LIFETIME_IN_SECONDS; + if (appConfiguration.getSpontaneousScopeLifetime() > 0) { + lifetime = appConfiguration.getSpontaneousScopeLifetime(); + } + expiration.add(Calendar.SECOND, lifetime); + return expiration.getTimeInMillis(); + } + + public boolean isAllowedBySpontaneousScopes(Client client, String scopeRequested) { + if (isFalse(appConfiguration.getAllowSpontaneousScopes())) { + return false; + } + + if (isFalse(client.getAttributes().getAllowSpontaneousScopes())) { + return false; + } + + return isAllowedBySpontaneousScopes(Sets.newHashSet(client.getAttributes().getSpontaneousScopes()), scopeRequested).getFirst(); + } + + public boolean isAllowedBySpontaneousScopes_(Set regExps, String scopeRequested) { + return isAllowedBySpontaneousScopes(regExps, scopeRequested).getFirst(); + } + + public Pair isAllowedBySpontaneousScopes(Set regExps, String scopeRequested) { + + for (String spontaneousScope : regExps) { + if (isAllowedBySpontaneousScope(spontaneousScope, scopeRequested)) { + return new Pair<>(true, spontaneousScope); + } + } + + return new Pair<>(false, null); + } + + public boolean isAllowedBySpontaneousScope(String spontaneousScope, String scopeRequested) { + try { + boolean result = spontaneousScope.equals(scopeRequested); + + if (!result) { + result = Pattern.matches(spontaneousScope, scopeRequested); + } + + if (result) { + log.trace("Scope {} allowed by spontaneous scope: {}", scopeRequested, spontaneousScope); + } + return result; + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return false; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/UserGroupService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/UserGroupService.java new file mode 100644 index 00000000..2e22139b --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/UserGroupService.java @@ -0,0 +1,91 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service; + +import java.util.Arrays; +import java.util.List; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.ldap.UserGroup; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.persist.exception.EntryPersistenceException; +import org.gluu.search.filter.Filter; +import org.slf4j.Logger; + +/** + * It's utility service which applications uses in custom authentication scripts + * + * @author Yuriy Zabrovarnyy + * @version 0.9, 27/07/2012 + * @author Yuriy Movchan Date: 04/11/2014 + */ +@ApplicationScoped +public class UserGroupService { + + @Inject + private Logger log; + + @Inject + private PersistenceEntryManager ldapEntryManager; + + public UserGroup loadGroup(String p_groupDN) { + try { + if (StringUtils.isNotBlank(p_groupDN)) { + return ldapEntryManager.find(UserGroup.class, p_groupDN); + } + } catch (Exception e) { + log.debug(e.getMessage(), e); + } + return null; + } + + public boolean isUserInGroup(String p_groupDN, String p_userDN) { + final UserGroup group = loadGroup(p_groupDN); + if (group != null) { + final String[] member = group.getMember(); + if (member != null) { + return Arrays.asList(member).contains(p_userDN); + } + } + return false; + } + + public boolean isUserInGroupOrMember(String groupDn, String personDn) { + Filter ownerFilter = Filter.createEqualityFilter("owner", personDn); + Filter memberFilter = Filter.createEqualityFilter("member", personDn); + Filter searchFilter = Filter.createORFilter(ownerFilter, memberFilter); + + boolean isMemberOrOwner = false; + try { + isMemberOrOwner = ldapEntryManager.findEntries(groupDn, UserGroup.class, searchFilter, 1).size() > 0; + + } catch (EntryPersistenceException ex) { + log.error("Failed to determine if person '{}' memeber or owner of group '{}'", personDn, groupDn, ex); + } + + return isMemberOrOwner; + } + + public boolean isInAnyGroup(String[] p_groupDNs, String p_userDN) { + return p_groupDNs != null && isInAnyGroup(Arrays.asList(p_groupDNs), p_userDN); + } + + public boolean isInAnyGroup(List p_groupDNs, String p_userDN) { + if (p_groupDNs != null && !p_groupDNs.isEmpty() && p_userDN != null && !p_userDN.isEmpty()) { + for (String groupDN : p_groupDNs) { + if (isUserInGroup(groupDN, p_userDN)) { + return true; + } + } + } + return false; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/UserService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/UserService.java new file mode 100644 index 00000000..027bf67c --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/UserService.java @@ -0,0 +1,90 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service; + +import java.util.List; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.config.StaticConfiguration; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.fido.u2f.DeviceRegistration; +import org.gluu.oxauth.model.fido.u2f.DeviceRegistrationStatus; +import org.gluu.persist.model.base.CustomEntry; +import org.gluu.persist.model.base.SimpleBranch; +import org.gluu.persist.model.fido2.Fido2RegistrationEntry; +import org.gluu.search.filter.Filter; +import org.gluu.service.net.NetworkService; +import org.gluu.util.StringHelper; + +/** + * Provides operations with users. + * + * @author Javier Rojas Blum + * @version @version August 20, 2019 + */ +@ApplicationScoped +public class UserService extends org.gluu.oxauth.service.common.UserService { + + public static final String[] USER_OBJECT_CLASSES = new String[] { "gluuPerson" }; + + @Inject + private StaticConfiguration staticConfiguration; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private NetworkService networkService; + + @Override + protected List getPersonCustomObjectClassList() { + return appConfiguration.getPersonCustomObjectClassList(); + } + + @Override + protected String getPeopleBaseDn() { + return staticConfiguration.getBaseDn().getPeople(); + } + + + public long countFido2RegisteredDevices(String username, String domain) { + String userInum = getUserInum(username); + if (userInum == null) { + return 0; + } + + String baseDn = getBaseDnForFido2RegistrationEntries(userInum); + if (persistenceEntryManager.hasBranchesSupport(baseDn)) { + if (!persistenceEntryManager.contains(baseDn, SimpleBranch.class)) { + return 0; + } + } + + Filter userInumFilter = Filter.createEqualityFilter("personInum", userInum); + Filter registeredFilter = Filter.createEqualityFilter("oxStatus", "registered"); + Filter domainFilter = Filter.createEqualityFilter("oxApplication", domain); + Filter filter = Filter.createANDFilter(userInumFilter, registeredFilter, domainFilter); + + return persistenceEntryManager.countEntries(baseDn, Fido2RegistrationEntry.class, filter); + } + + public String getBaseDnForFido2RegistrationEntries(String userInum) { + final String userBaseDn = getDnForUser(userInum); // "ou=fido2_register,inum=1234,ou=people,o=gluu" + + return String.format("ou=fido2_register,%s", userBaseDn); + } + + public String getBaseDnForFidoDevices(String userInum) { + final String userBaseDn = getDnForUser(userInum); // "ou=fido,inum=1234,ou=people,o=gluu" + + return String.format("ou=fido,%s", userBaseDn); + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/cdi/event/AuthConfigurationEvent.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/cdi/event/AuthConfigurationEvent.java new file mode 100644 index 00000000..b2dd6ebd --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/cdi/event/AuthConfigurationEvent.java @@ -0,0 +1,7 @@ +package org.gluu.oxauth.service.cdi.event; + +/** + * @author Yuriy Movchan Date: 04/13/2017 + */ +public class AuthConfigurationEvent { +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/cdi/event/ExpirationEvent.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/cdi/event/ExpirationEvent.java new file mode 100644 index 00000000..fb5be432 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/cdi/event/ExpirationEvent.java @@ -0,0 +1,7 @@ +package org.gluu.oxauth.service.cdi.event; + +/** + * @author Yuriy Zabrovarnyy + */ +public class ExpirationEvent { +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/cdi/event/KeyGenerationEvent.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/cdi/event/KeyGenerationEvent.java new file mode 100644 index 00000000..1bf79dd9 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/cdi/event/KeyGenerationEvent.java @@ -0,0 +1,7 @@ +package org.gluu.oxauth.service.cdi.event; + +/** + * @author Yuriy Movchan Date: 04/13/2017 + */ +public class KeyGenerationEvent { +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/cdi/event/ReloadAuthScript.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/cdi/event/ReloadAuthScript.java new file mode 100644 index 00000000..34a7a431 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/cdi/event/ReloadAuthScript.java @@ -0,0 +1,33 @@ +package org.gluu.oxauth.service.cdi.event; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.enterprise.util.AnnotationLiteral; +import javax.inject.Qualifier; + +/** + * @author Yuriy Movchan Date: 04/13/2017 + */ +@Qualifier +@Retention(RUNTIME) +@Target({ METHOD, FIELD, PARAMETER, TYPE }) +@Documented +public @interface ReloadAuthScript { + + public static final class Literal extends AnnotationLiteral implements ReloadAuthScript { + + public static final Literal INSTANCE = new Literal(); + + private static final long serialVersionUID = 1L; + + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/cdi/event/StatEvent.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/cdi/event/StatEvent.java new file mode 100644 index 00000000..b5e9a5b7 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/cdi/event/StatEvent.java @@ -0,0 +1,7 @@ +package org.gluu.oxauth.service.cdi.event; + +/** + * @author Yuriy Zabrovarnyy + */ +public class StatEvent { +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/ciba/CibaEncryptionService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/ciba/CibaEncryptionService.java new file mode 100644 index 00000000..66ab6ce3 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/ciba/CibaEncryptionService.java @@ -0,0 +1,82 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service.ciba; + +import org.gluu.util.StringHelper; +import org.gluu.util.security.PropertiesDecrypter; +import org.gluu.util.security.StringEncrypter; +import org.gluu.util.security.StringEncrypter.EncryptionException; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.inject.Named; +import java.util.Properties; + +/** + * Allows to encrypt/decrypt strings using a pre-configured key from oxCore. + * + * @author Milton BO Date: 27/04/2020 + */ +@ApplicationScoped +@Named +public class CibaEncryptionService { + + @Inject + private Logger log; + + @Inject + private StringEncrypter stringEncrypter; + + public String decrypt(String encryptedString) throws EncryptionException { + if (StringHelper.isEmpty(encryptedString)) { + return null; + } + + return stringEncrypter.decrypt(encryptedString); + } + + public String decrypt(String encryptedValue, boolean returnSource) { + if (encryptedValue == null) { + return encryptedValue; + } + + String resultValue; + if (returnSource) { + resultValue = encryptedValue; + } else { + resultValue = null; + } + + try { + resultValue = stringEncrypter.decrypt(encryptedValue); + } catch (Exception ex) { + if (!returnSource) { + log.error(String.format("Failed to decrypt value: '%s'", encryptedValue, ex)); + } + } + + return resultValue; + } + + public String encrypt(String unencryptedString) throws EncryptionException { + if (StringHelper.isEmpty(unencryptedString)) { + return null; + } + + return stringEncrypter.encrypt(unencryptedString); + } + + public Properties decryptProperties(Properties connectionProperties) { + return PropertiesDecrypter.decryptProperties(stringEncrypter, connectionProperties); + } + + public Properties decryptAllProperties(Properties connectionProperties) { + return PropertiesDecrypter.decryptAllProperties(stringEncrypter, connectionProperties); + } + +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/ciba/CibaRequestService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/ciba/CibaRequestService.java new file mode 100644 index 00000000..1203a633 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/ciba/CibaRequestService.java @@ -0,0 +1,217 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service.ciba; + +import org.apache.commons.lang.time.DateUtils; +import org.gluu.oxauth.model.common.CibaRequestCacheControl; +import org.gluu.oxauth.model.common.CibaRequestStatus; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.config.StaticConfiguration; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.ldap.CIBARequest; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.search.filter.Filter; +import org.gluu.service.CacheService; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import java.util.Date; +import java.util.List; + +/** + * Service used to access to the database for CibaRequest ObjectClass. + * + * @author Milton BO + * @version May 28, 2020 + */ +@ApplicationScoped +public class CibaRequestService { + + @Inject + private Logger log; + + @Inject + private PersistenceEntryManager entryManager; + + @Inject + private StaticConfiguration staticConfiguration; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private CacheService cacheService; + + private String cibaBaseDn() { + return staticConfiguration.getBaseDn().getCiba(); // ou=ciba,o=gluu + } + + /** + * Uses request data and expiration sent by the client and save request data in database. + * @param request Object containing information related to the request. + * @param expiresIn Expiration time that end user has to answer. + */ + public void persistRequest(CibaRequestCacheControl request, int expiresIn) { + Date expirationDate = DateUtils.addSeconds(new Date(), expiresIn); + + String authReqId = request.getAuthReqId(); + CIBARequest cibaRequest = new CIBARequest(); + cibaRequest.setDn("authReqId=" + authReqId + "," + this.cibaBaseDn()); + cibaRequest.setAuthReqId(authReqId); + cibaRequest.setClientId(request.getClient().getClientId()); + cibaRequest.setExpirationDate(expirationDate); + cibaRequest.setCreationDate(new Date()); + cibaRequest.setStatus(CibaRequestStatus.PENDING.getValue()); + cibaRequest.setUserId(request.getUser().getUserId()); + entryManager.persist(cibaRequest); + } + + /** + * Load a CIBARequest entry from database. + * @param authReqId Identifier of the entry. + */ + public CIBARequest load(String authReqId) { + try { + return entryManager.find(CIBARequest.class, authReqId); + } catch (Exception e) { + log.error(e.getMessage(), e); + return null; + } + } + + /** + * Generates a list of requests that are expired and also filter them using a Status. + * @param authorizationStatus Status used to filter entries. + * @param maxRequestsToGet Limit of requests that would be returned. + */ + public List loadExpiredByStatus(CibaRequestStatus authorizationStatus, + int maxRequestsToGet) { + try { + Date now = new Date(); + Filter filter = Filter.createANDFilter( + Filter.createEqualityFilter("oxStatus", authorizationStatus.getValue()), + Filter.createLessOrEqualFilter("exp", entryManager.encodeTime(this.cibaBaseDn(), now))); + return entryManager.findEntries(this.cibaBaseDn(), CIBARequest.class, filter, maxRequestsToGet); + } catch (Exception e) { + log.error(e.getMessage(), e); + return null; + } + } + + /** + * Change the status field in database for a specific request. + * @param cibaRequest Entry containing information of the CIBA request. + * @param authorizationStatus New status. + */ + public void updateStatus(CIBARequest cibaRequest, CibaRequestStatus authorizationStatus) { + try { + cibaRequest.setStatus(authorizationStatus.getValue()); + entryManager.merge(cibaRequest); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + + /** + * Removes a CibaRequest object from the database. + * @param cibaRequest Object to be removed. + */ + public void removeCibaRequest(CIBARequest cibaRequest) { + try { + entryManager.remove(cibaRequest); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + + /** + * Removes a CibaRequest from the database. + * @param authReqId Identifier of the CibaRequest. + */ + public void removeCibaRequest(String authReqId) { + try { + String requestDn = String.format("authReqId=%s,%s", authReqId, this.cibaBaseDn()); + entryManager.remove(requestDn, CIBARequest.class); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + + /** + * Register a new CibaRequestCacheControl instance in Cache and in the database. + * @param request New instance to be saved. + * @param expiresIn Expiration time of the request in Cache and memory. + */ + public void save(CibaRequestCacheControl request, int expiresIn) { + int expiresInCache = expiresIn; + if (appConfiguration.getCibaGrantLifeExtraTimeSec() > 0) { + expiresInCache += appConfiguration.getCibaGrantLifeExtraTimeSec(); + } + + cacheService.put(expiresInCache, request.cacheKey(), request); + this.persistRequest(request, expiresIn); + log.trace("Ciba request saved in cache, authReqId: {} clientId: {}", request.getAuthReqId(), request.getClient().getClientId()); + } + + /** + * Put in cache a CibaRequestCacheControl object, it uses same expiration time that it has. + * @param request Object to be updated, replaced or created. + */ + public void update(CibaRequestCacheControl request) { + int expiresInCache = request.getExpiresIn(); + if (appConfiguration.getCibaGrantLifeExtraTimeSec() > 0) { + expiresInCache += appConfiguration.getCibaGrantLifeExtraTimeSec(); + } + + cacheService.put(expiresInCache, request.cacheKey(), request); + } + + /** + * Get a CibaRequestCacheControl object from Cache service. + * @param authReqId Identifier of the object to be gotten. + */ + public CibaRequestCacheControl getCibaRequest(String authReqId) { + Object cachedObject = cacheService.get(authReqId); + if (cachedObject == null) { + // retry one time : sometimes during high load cache client may be not fast enough + cachedObject = cacheService.get(authReqId); + log.trace("Failed to fetch CIBA request from cache, authReqId: {}", authReqId); + } + return cachedObject instanceof CibaRequestCacheControl ? (CibaRequestCacheControl) cachedObject : null; + } + + /** + * Removes from cache a request. + * @param cacheKey Object to be removed from Cache. + */ + public void removeCibaCacheRequest(String cacheKey) { + try { + cacheService.remove(cacheKey); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + + /** + * Verifies whether a specific client has CIBA compatibility. + * + * @param client Client to check. + */ + public boolean hasCibaCompatibility(Client client) { + if (client.getBackchannelTokenDeliveryMode() == null) { + return false; + } + for (GrantType gt : client.getGrantTypes()) { + if (gt.getValue().equals(GrantType.CIBA.getValue())) { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/ciba/CibaRequestsProcessorJob.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/ciba/CibaRequestsProcessorJob.java new file mode 100644 index 00000000..e0904541 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/ciba/CibaRequestsProcessorJob.java @@ -0,0 +1,179 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service.ciba; + +import org.gluu.oxauth.ciba.CIBAPingCallbackService; +import org.gluu.oxauth.ciba.CIBAPushErrorService; +import org.gluu.oxauth.model.ciba.PushErrorResponseType; +import org.gluu.oxauth.model.common.BackchannelTokenDeliveryMode; +import org.gluu.oxauth.model.common.CibaRequestCacheControl; +import org.gluu.oxauth.model.common.CibaRequestStatus; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.ldap.CIBARequest; +import org.gluu.oxauth.util.ServerUtil; +import org.gluu.service.cdi.async.Asynchronous; +import org.gluu.service.cdi.event.CibaRequestsProcessorEvent; +import org.gluu.service.cdi.event.Scheduled; +import org.gluu.service.timer.event.TimerEvent; +import org.gluu.service.timer.schedule.TimerSchedule; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Event; +import javax.enterprise.event.Observes; +import javax.inject.Inject; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Job responsible to process all expired CIBA requests and update their status. + * + * @author Milton BO + * @version May 20, 2020 + */ +@ApplicationScoped +public class CibaRequestsProcessorJob { + + public static final int CHUNK_SIZE = 500; // Default value whether there isn't backchannelRequestsProcessorJobChunkSize json property value + + @Inject + private Logger log; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private Event processorEvent; + + @Inject + private CIBAPushErrorService cibaPushErrorService; + + @Inject + private CIBAPingCallbackService cibaPingCallbackService; + + @Inject + private CibaRequestService cibaRequestService; + + private long lastFinishedTime; + + private AtomicBoolean isActive; + + private ExecutorService executorService; + + /** + * Method invoked from the appInitializer to start processing every some time. + */ + public void initTimer() { + log.debug("Initializing CIBA requests processor"); + this.isActive = new AtomicBoolean(false); + int intervalSec = appConfiguration.getBackchannelRequestsProcessorJobIntervalSec(); + + // Schedule to start processor every N seconds + processorEvent.fire(new TimerEvent(new TimerSchedule(intervalSec, intervalSec), + new CibaRequestsProcessorEvent(), Scheduled.Literal.INSTANCE)); + + this.lastFinishedTime = System.currentTimeMillis(); + this.executorService = Executors.newCachedThreadPool(ServerUtil.daemonThreadFactory()); + } + + @Asynchronous + public void process(@Observes @Scheduled CibaRequestsProcessorEvent cibaRequestsProcessorEvent) { + if (this.isActive.get()) { + return; + } + if (!this.isActive.compareAndSet(false, true)) { + return; + } + + try { + if (jobIsFree()) { + processImpl(); + this.lastFinishedTime = System.currentTimeMillis(); + } else { + log.trace("Starting conditions aren't reached for CIBA requestes processor"); + } + } finally { + this.isActive.set(false); + } + } + + /** + * Defines whether the job is still in process or it is free according to the time interval defined. + * @return True in case it is free to start a new process. + */ + private boolean jobIsFree() { + int interval = appConfiguration.getBackchannelRequestsProcessorJobIntervalSec(); + if (interval < 0) { + log.info("CIBA Requests processor timer is disabled."); + log.warn("CIBA Requests processor timer Interval (cleanServiceInterval in oxauth configuration) is negative which turns OFF internal clean up by the server. Please set it to positive value if you wish internal CIBA Requests processor up timer run."); + return false; + } + + long timeDiffrence = System.currentTimeMillis() - this.lastFinishedTime; + return timeDiffrence >= interval * 1000; + } + + /** + * Main process that process CIBA requests in cache. + */ + public void processImpl() { + try { + int chunkSize = appConfiguration.getBackchannelRequestsProcessorJobChunkSize() <= 0 ? + CHUNK_SIZE : appConfiguration.getBackchannelRequestsProcessorJobChunkSize(); + + List expiredRequests = cibaRequestService.loadExpiredByStatus( + CibaRequestStatus.PENDING, chunkSize); + expiredRequests.forEach(cibaRequest -> cibaRequestService.updateStatus(cibaRequest, + CibaRequestStatus.IN_PROCESS)); + + for (CIBARequest expiredRequest : expiredRequests) { + CibaRequestCacheControl cibaRequest = cibaRequestService.getCibaRequest(expiredRequest.getAuthReqId()); + if (cibaRequest != null) { + executorService.execute(() -> + processExpiredRequest(cibaRequest, expiredRequest.getAuthReqId()) + ); + } + cibaRequestService.removeCibaRequest(expiredRequest); + } + } catch (Exception e) { + log.error("Failed to process CIBA request from cache.", e); + } + } + + /** + * Method responsible to process expired CIBA requests, set them as expired in cache + * and send callbacks to the client + * @param cibaRequest Object containing data related to the CIBA request. + * @param authReqId Authentication request id. + */ + private void processExpiredRequest(CibaRequestCacheControl cibaRequest, String authReqId) { + if (cibaRequest.getStatus() != CibaRequestStatus.PENDING + && cibaRequest.getStatus() != CibaRequestStatus.EXPIRED) { + return; + } + log.info("Authentication request id {} has expired", authReqId); + + cibaRequestService.removeCibaCacheRequest(cibaRequest.cacheKey()); + + if (cibaRequest.getClient().getBackchannelTokenDeliveryMode() == BackchannelTokenDeliveryMode.PUSH) { + cibaPushErrorService.pushError(cibaRequest.getAuthReqId(), + cibaRequest.getClient().getBackchannelClientNotificationEndpoint(), + cibaRequest.getClientNotificationToken(), + PushErrorResponseType.EXPIRED_TOKEN, + "Request has expired and there was no answer from the end user."); + } else if (cibaRequest.getClient().getBackchannelTokenDeliveryMode() == BackchannelTokenDeliveryMode.PING) { + cibaPingCallbackService.pingCallback( + cibaRequest.getAuthReqId(), + cibaRequest.getClient().getBackchannelClientNotificationEndpoint(), + cibaRequest.getClientNotificationToken() + ); + } + } + +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/custom/CustomScriptService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/custom/CustomScriptService.java new file mode 100644 index 00000000..63f15a9c --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/custom/CustomScriptService.java @@ -0,0 +1,45 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service.custom; + +import java.io.UnsupportedEncodingException; + +import javax.annotation.Priority; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Alternative; +import javax.interceptor.Interceptor; +import javax.inject.Inject; + +import org.gluu.oxauth.model.config.StaticConfiguration; +import org.gluu.oxauth.model.util.Base64Util; +import org.gluu.service.custom.script.AbstractCustomScriptService; + +/** + * Operations with custom scripts + * + * @author Yuriy Movchan Date: 12/03/2014 + */ +@ApplicationScoped +@Alternative +@Priority(Interceptor.Priority.APPLICATION + 1) +public class CustomScriptService extends AbstractCustomScriptService { + + @Inject + private StaticConfiguration staticConfiguration; + + private static final long serialVersionUID = -5283102477313448031L; + + public String baseDn() { + return staticConfiguration.getBaseDn().getScripts(); + } + + public String base64Decode(String encoded) throws IllegalArgumentException, UnsupportedEncodingException { + byte[] decoded = Base64Util.base64urldecode(encoded); + return new String(decoded, "UTF-8"); + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/date/DateFormatterService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/date/DateFormatterService.java new file mode 100644 index 00000000..ced0a3d2 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/date/DateFormatterService.java @@ -0,0 +1,59 @@ +package org.gluu.oxauth.service.date; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.common.CallerType; +import org.gluu.oxauth.model.configuration.AppConfiguration; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.inject.Named; +import java.io.Serializable; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Map; + +/** + * @author Yuriy Z + */ +@ApplicationScoped +@Named +public class DateFormatterService { + + @Inject + private AppConfiguration appConfiguration; + + public Serializable formatClaim(Date date, CallerType callerType) { + return formatClaim(date, callerType.name().toLowerCase()); + } + + /** + * + * @param date date to format + * @param patternKey pattern key. It's by intention is not enum to allow arbitrary key (not "locked" by CallerType) + * @return formatter value + */ + public Serializable formatClaim(Date date, String patternKey) { + // key in map is string by intention to not "lock" it by CallerType + final Map formatterMap = appConfiguration.getDateFormatterPatterns(); + + if (formatterMap.isEmpty()) { + return formatClaimFallback(date); + } + + final String explicitFormatter = formatterMap.get(patternKey); + if (StringUtils.isNotBlank(explicitFormatter)) { + return new SimpleDateFormat(explicitFormatter).format(date); + } + + final String commonFormatter = formatterMap.get(CallerType.COMMON.name().toLowerCase()); + if (StringUtils.isNotBlank(commonFormatter)) { + return new SimpleDateFormat(commonFormatter).format(date); + } + + return formatClaimFallback(date); + } + + public Serializable formatClaimFallback(Date date) { + return date.getTime() / 1000; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/expiration/ExpId.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/expiration/ExpId.java new file mode 100644 index 00000000..a3da755b --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/expiration/ExpId.java @@ -0,0 +1,39 @@ +package org.gluu.oxauth.service.expiration; + +import java.util.Objects; + +/** + * @author Yuriy Zabrovarnyy + */ +class ExpId { + + private String key; + private ExpType type; + + public ExpId(String key, ExpType type) { + this.key = key; + this.type = type; + } + + public String getKey() { + return key; + } + + public ExpType getType() { + return type; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ExpId id = (ExpId) o; + return Objects.equals(key, id.key) && + type == id.type; + } + + @Override + public int hashCode() { + return Objects.hash(key, type); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/expiration/ExpType.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/expiration/ExpType.java new file mode 100644 index 00000000..7798db7b --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/expiration/ExpType.java @@ -0,0 +1,8 @@ +package org.gluu.oxauth.service.expiration; + +/** + * @author Yuriy Zabrovarnyy + */ +public enum ExpType { + SESSION +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/expiration/ExpirationNotificatorTimer.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/expiration/ExpirationNotificatorTimer.java new file mode 100644 index 00000000..83206571 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/expiration/ExpirationNotificatorTimer.java @@ -0,0 +1,174 @@ +package org.gluu.oxauth.service.expiration; + +import net.jodah.expiringmap.ExpirationListener; +import net.jodah.expiringmap.ExpirationPolicy; +import net.jodah.expiringmap.ExpiringMap; + +import org.gluu.oxauth.model.config.StaticConfiguration; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.service.cdi.event.ExpirationEvent; +import org.gluu.oxauth.service.external.ExternalApplicationSessionService; +import org.gluu.oxauth.service.external.session.SessionEvent; +import org.gluu.oxauth.service.external.session.SessionEventType; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.search.filter.Filter; +import org.gluu.service.cdi.async.Asynchronous; +import org.gluu.service.cdi.event.Scheduled; +import org.gluu.service.timer.event.TimerEvent; +import org.gluu.service.timer.schedule.TimerSchedule; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Event; +import javax.enterprise.event.Observes; +import javax.inject.Inject; +import javax.inject.Named; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * @author Yuriy Zabrovarnyy + */ +@ApplicationScoped +@Named +public class ExpirationNotificatorTimer implements ExpirationListener { + + private static final int DEFAULT_INTERVAL = 600; // 10 min + + @Inject + private Logger log; + + @Inject + private Event timerEvent; + + @Inject + private PersistenceEntryManager persistenceEntryManager; + + @Inject + private StaticConfiguration staticConfiguration; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private ExternalApplicationSessionService externalApplicationSessionService; + + private ExpiringMap expiringMap = ExpiringMap.builder() + .expirationPolicy(ExpirationPolicy.CREATED) + .variableExpiration() + .build(); + + private AtomicBoolean isActive; + + private long lastFinishedTime; + + public void initTimer() { + log.debug("Initializing ExpirationNotificatorTimer"); + this.isActive = new AtomicBoolean(false); + + expiringMap = ExpiringMap.builder() + .expirationPolicy(ExpirationPolicy.CREATED) + .maxSize(appConfiguration.getExpirationNotificatorMapSizeLimit()) + .variableExpiration() + .build(); + expiringMap.addExpirationListener(this); + + timerEvent.fire(new TimerEvent(new TimerSchedule(DEFAULT_INTERVAL, DEFAULT_INTERVAL), new ExpirationEvent(), Scheduled.Literal.INSTANCE)); + + this.lastFinishedTime = System.currentTimeMillis(); + } + + @Asynchronous + public void process(@Observes @Scheduled ExpirationEvent expirationEvent) { + if (!appConfiguration.getExpirationNotificatorEnabled()) { + return; + } + + if (this.isActive.get()) { + return; + } + + if (!this.isActive.compareAndSet(false, true)) { + return; + } + + try { + if (!allowToRun()) { + log.trace("Not allowed to run."); + return; + } + fillMap(); + this.lastFinishedTime = System.currentTimeMillis(); + } catch (Exception ex) { + log.error("Exception happened while trying to fill expiringMap update", ex); + } finally { + this.isActive.set(false); + } + } + + private void fillMap() { + Calendar future = Calendar.getInstance(); + future.add(Calendar.SECOND, appConfiguration.getExpirationNotificatorIntervalInSeconds()); + + fillSessions(future.getTime()); + } + + private void fillSessions(Date future) { + final String baseDn = staticConfiguration.getBaseDn().getSessions(); + final Filter filter = Filter.createANDFilter( + Filter.createEqualityFilter("del", true), + Filter.createLessOrEqualFilter("exp", persistenceEntryManager.encodeTime(baseDn, future))); + final List sessions = persistenceEntryManager.findEntries(baseDn, SessionId.class, filter); + if (sessions == null || sessions.isEmpty()) { + return; + } + + long now = new Date().getTime(); + for (SessionId session : sessions) { + final long duration = session.getExpirationDate().getTime() - now; + + if (duration <= 0) { + remove(session); + continue; + } + expiringMap.put(new ExpId(session.getId(), ExpType.SESSION), session, duration, TimeUnit.MILLISECONDS); + } + } + + @Override + public void expired(ExpId key, Object value) { + if (key.getType() == ExpType.SESSION && value instanceof SessionId) { + externalApplicationSessionService.externalEvent(new SessionEvent(SessionEventType.GONE, (SessionId) value)); + } + } + + private boolean allowToRun() { + int interval = appConfiguration.getExpirationNotificatorIntervalInSeconds(); + if (interval < 0) { + log.info("ExpirationNotificator Timer is disabled."); + log.warn("ExpirationNotificator Timer Interval (expirationNotificatorIntervalInSeconds in oxauth configuration) is negative which turns OFF internal clean up by the server. Please set it to positive value if you wish internal clean up timer run."); + return false; + } + + long timerInterval = interval * 1000; + + long timeDiffrence = System.currentTimeMillis() - this.lastFinishedTime; + + return timeDiffrence >= timerInterval; + } + + public boolean remove(SessionId sessionId) { + try { + persistenceEntryManager.remove(sessionId.getDn(), SessionId.class); + externalApplicationSessionService.externalEvent(new SessionEvent(SessionEventType.GONE, sessionId)); + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalApplicationSessionService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalApplicationSessionService.java new file mode 100644 index 00000000..e892ef47 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalApplicationSessionService.java @@ -0,0 +1,112 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service.external; + +import org.gluu.model.SimpleCustomProperty; +import org.gluu.model.custom.script.CustomScriptType; +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.model.custom.script.type.session.ApplicationSessionType; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.service.external.session.SessionEvent; +import org.gluu.service.custom.script.ExternalScriptService; + +import javax.enterprise.context.ApplicationScoped; +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +/** + * Provides factory methods needed to create external application session extension + * + * @author Yuriy Movchan + * @version August 9, 2017 + */ +@ApplicationScoped +public class ExternalApplicationSessionService extends ExternalScriptService { + + private static final long serialVersionUID = 2316361273036208685L; + + public ExternalApplicationSessionService() { + super(CustomScriptType.APPLICATION_SESSION); + } + + public boolean executeExternalStartSessionMethod(CustomScriptConfiguration customScriptConfiguration, HttpServletRequest httpRequest, SessionId sessionId) { + try { + log.trace("Executing python 'startSession' method"); + ApplicationSessionType applicationSessionType = (ApplicationSessionType) customScriptConfiguration.getExternalType(); + Map configurationAttributes = customScriptConfiguration.getConfigurationAttributes(); + return applicationSessionType.startSession(httpRequest, sessionId, configurationAttributes); + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(customScriptConfiguration.getCustomScript(), ex); + } + + return false; + } + + public boolean executeExternalStartSessionMethods(HttpServletRequest httpRequest, SessionId sessionId) { + boolean result = true; + for (CustomScriptConfiguration customScriptConfiguration : this.customScriptConfigurations) { + if (customScriptConfiguration.getExternalType().getApiVersion() > 1) { + result &= executeExternalStartSessionMethod(customScriptConfiguration, httpRequest, sessionId); + if (!result) { + return result; + } + } + } + + return result; + } + + public boolean executeExternalEndSessionMethod(CustomScriptConfiguration customScriptConfiguration, HttpServletRequest httpRequest, SessionId sessionId) { + try { + log.trace("Executing python 'endSession' method"); + ApplicationSessionType applicationSessionType = (ApplicationSessionType) customScriptConfiguration.getExternalType(); + Map configurationAttributes = customScriptConfiguration.getConfigurationAttributes(); + return applicationSessionType.endSession(httpRequest, sessionId, configurationAttributes); + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(customScriptConfiguration.getCustomScript(), ex); + } + + return false; + } + + public boolean executeExternalEndSessionMethods(HttpServletRequest httpRequest, SessionId sessionId) { + boolean result = true; + for (CustomScriptConfiguration customScriptConfiguration : this.customScriptConfigurations) { + result &= executeExternalEndSessionMethod(customScriptConfiguration, httpRequest, sessionId); + if (!result) { + return result; + } + } + + return result; + } + + public void externalEvent(SessionEvent event) { + if (!isEnabled()) { + return; + } + + for (CustomScriptConfiguration scriptConfiguration : this.customScriptConfigurations) { + externalEvent(scriptConfiguration, event); + } + } + + private void externalEvent(CustomScriptConfiguration scriptConfiguration, SessionEvent event) { + try { + log.trace("Executing python 'onEvent' method of script: " + scriptConfiguration.getName() + ", event: " + event); + event.setScriptConfiguration(scriptConfiguration); + ApplicationSessionType applicationSessionType = (ApplicationSessionType) scriptConfiguration.getExternalType(); + applicationSessionType.onEvent(event); + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(scriptConfiguration.getCustomScript(), ex); + } + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalAuthenticationService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalAuthenticationService.java new file mode 100644 index 00000000..b1bbcc71 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalAuthenticationService.java @@ -0,0 +1,561 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service.external; + +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import org.apache.commons.lang.StringUtils; +import org.gluu.model.AuthenticationScriptUsageType; +import org.gluu.model.SimpleCustomProperty; +import org.gluu.model.custom.script.CustomScriptType; +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.model.custom.script.model.CustomScript; +import org.gluu.model.custom.script.model.auth.AuthenticationCustomScript; +import org.gluu.model.custom.script.type.BaseExternalType; +import org.gluu.model.custom.script.type.auth.PersonAuthenticationType; +import org.gluu.model.ldap.GluuLdapConfiguration; +import org.gluu.oxauth.service.cdi.event.ReloadAuthScript; +import org.gluu.oxauth.service.common.ApplicationFactory; +import org.gluu.oxauth.service.external.internal.InternalDefaultPersonAuthenticationType; +import org.gluu.service.custom.script.ExternalScriptService; +import org.gluu.util.OxConstants; +import org.gluu.util.StringHelper; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Observes; +import javax.inject.Inject; +import javax.inject.Named; +import java.util.*; +import java.util.Map.Entry; + +/** + * Provides factory methods needed to create external authenticator + * + * @author Yuriy Movchan Date: 21/08/2012 + */ +@ApplicationScoped +public class ExternalAuthenticationService extends ExternalScriptService { + + public final static String MODIFIED_INTERNAL_TYPES_EVENT_TYPE = "CustomScriptModifiedInternlTypesEvent"; + + @Inject @Named(ApplicationFactory.PERSISTENCE_AUTH_CONFIG_NAME) + private List ldapAuthConfigs; + + @Inject + private InternalDefaultPersonAuthenticationType internalDefaultPersonAuthenticationType; + + private static final long serialVersionUID = 7339887464253044927L; + + private Map> customScriptConfigurationsMapByUsageType; + private Map defaultExternalAuthenticators; + private Map scriptAliasMap; + + public ExternalAuthenticationService() { + super(CustomScriptType.PERSON_AUTHENTICATION); + } + + public void reloadAuthScript(@Observes @ReloadAuthScript String event) { + reload(event); + } + + public String scriptName(String acr) { + if (StringHelper.isEmpty(acr)) { + return null; + } + + if (scriptAliasMap.containsKey(acr)) { + return scriptAliasMap.get(acr); + } + + return acr; + } + + @Override + protected void reloadExternal() { + // Group external authenticator configurations by usage type + this.customScriptConfigurationsMapByUsageType = groupCustomScriptConfigurationsMapByUsageType(this.customScriptConfigurationsNameMap); + + // Build aliases map + this.scriptAliasMap = buildScriptAliases(); + + // Determine default authenticator for every usage type + this.defaultExternalAuthenticators = determineDefaultCustomScriptConfigurationsMap(this.customScriptConfigurationsNameMap); + } + + private HashMap buildScriptAliases() { + HashMap newScriptAliases = new HashMap(); + for (Entry script : customScriptConfigurationsNameMap.entrySet()) { + String name = script.getKey(); + CustomScript customScript = script.getValue().getCustomScript(); + + newScriptAliases.put(name, name); + + List aliases = customScript.getAliases(); + if (aliases != null) { + for (String alias : aliases) { + if (StringUtils.isNotBlank(alias)) { + newScriptAliases.put(alias, name); + } + } + } + } + + return newScriptAliases; + } + + @Override + protected void addExternalConfigurations(List newCustomScriptConfigurations) { + if ((ldapAuthConfigs == null) || (ldapAuthConfigs.size() == 0)) { + newCustomScriptConfigurations.add(getInternalCustomScriptConfiguration()); + } else { + for (GluuLdapConfiguration ldapAuthConfig : ldapAuthConfigs) { + newCustomScriptConfigurations.add(getInternalCustomScriptConfiguration(ldapAuthConfig)); + } + } + } + + private Map> groupCustomScriptConfigurationsMapByUsageType(Map customScriptConfigurationsMap) { + Map> newCustomScriptConfigurationsMapByUsageType = new HashMap>(); + + for (AuthenticationScriptUsageType usageType : AuthenticationScriptUsageType.values()) { + List currCustomScriptConfigurationsMapByUsageType = new ArrayList(); + + for (CustomScriptConfiguration customScriptConfiguration : customScriptConfigurationsMap.values()) { + if (!isValidateUsageType(usageType, customScriptConfiguration)) { + continue; + } + + currCustomScriptConfigurationsMapByUsageType.add(customScriptConfiguration); + } + newCustomScriptConfigurationsMapByUsageType.put(usageType, currCustomScriptConfigurationsMapByUsageType); + } + + return newCustomScriptConfigurationsMapByUsageType; + } + + private Map determineDefaultCustomScriptConfigurationsMap(Map customScriptConfigurationsMap) { + Map newDefaultCustomScriptConfigurationsMap = new HashMap(); + + for (AuthenticationScriptUsageType usageType : AuthenticationScriptUsageType.values()) { + CustomScriptConfiguration defaultExternalAuthenticator = null; + for (CustomScriptConfiguration customScriptConfiguration : customScriptConfigurationsMapByUsageType.get(usageType)) { + // Determine default authenticator. It has bigger level than others + if ((defaultExternalAuthenticator == null) + || (defaultExternalAuthenticator.getLevel() < customScriptConfiguration.getLevel())) { + defaultExternalAuthenticator = customScriptConfiguration; + } + } + + newDefaultCustomScriptConfigurationsMap.put(usageType, defaultExternalAuthenticator); + } + + return newDefaultCustomScriptConfigurationsMap; + } + + private boolean executeExternalIsValidAuthenticationMethod(AuthenticationScriptUsageType usageType, CustomScriptConfiguration customScriptConfiguration) { + try { + log.debug("Executing python 'isValidAuthenticationMethod' authenticator method"); + PersonAuthenticationType externalAuthenticator = (PersonAuthenticationType) customScriptConfiguration.getExternalType(); + Map configurationAttributes = customScriptConfiguration.getConfigurationAttributes(); + final boolean result = externalAuthenticator.isValidAuthenticationMethod(usageType, configurationAttributes); + log.debug("Executed python 'isValidAuthenticationMethod' authenticator method, result: {}", result); + return result; + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(customScriptConfiguration.getCustomScript(), ex); + } + + return false; + } + + private String executeExternalGetAlternativeAuthenticationMethod(AuthenticationScriptUsageType usageType, CustomScriptConfiguration customScriptConfiguration) { + try { + log.trace("Executing python 'getAlternativeAuthenticationMethod' authenticator method"); + PersonAuthenticationType externalAuthenticator = (PersonAuthenticationType) customScriptConfiguration.getExternalType(); + Map configurationAttributes = customScriptConfiguration.getConfigurationAttributes(); + final String result = externalAuthenticator.getAlternativeAuthenticationMethod(usageType, configurationAttributes); + log.trace("Executed python 'getAlternativeAuthenticationMethod' authenticator method, result: {}", result); + return result; + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(customScriptConfiguration.getCustomScript(), ex); + } + + return null; + } + + public int executeExternalGetCountAuthenticationSteps(CustomScriptConfiguration customScriptConfiguration) { + try { + log.trace("Executing python 'getCountAuthenticationSteps' authenticator method"); + PersonAuthenticationType externalAuthenticator = (PersonAuthenticationType) customScriptConfiguration.getExternalType(); + Map configurationAttributes = customScriptConfiguration.getConfigurationAttributes(); + final int result = externalAuthenticator.getCountAuthenticationSteps(configurationAttributes); + log.trace("Executed python 'getCountAuthenticationSteps' authenticator method, result: {}", result); + return result; + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(customScriptConfiguration.getCustomScript(), ex); + } + + return -1; + } + + public boolean executeExternalAuthenticate(CustomScriptConfiguration customScriptConfiguration, Map requestParameters, int step) { + try { + log.trace("Executing python 'authenticate' authenticator method"); + PersonAuthenticationType externalAuthenticator = (PersonAuthenticationType) customScriptConfiguration.getExternalType(); + Map configurationAttributes = customScriptConfiguration.getConfigurationAttributes(); + final boolean result = externalAuthenticator.authenticate(configurationAttributes, requestParameters, step); + log.trace("Executed python 'authenticate' authenticator method, result: {}", result); + return result; + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(customScriptConfiguration.getCustomScript(), ex); + } + + return false; + } + + public int getNextStep(CustomScriptConfiguration customScriptConfiguration, Map requestParameters, int step) { + try { + log.trace("Executing python 'getNextStep' authenticator method"); + PersonAuthenticationType externalAuthenticator = (PersonAuthenticationType) customScriptConfiguration.getExternalType(); + Map configurationAttributes = customScriptConfiguration.getConfigurationAttributes(); + final int result = externalAuthenticator.getNextStep(configurationAttributes, requestParameters, step); + log.trace("Executed python 'getNextStep' authenticator method, result: {}", result); + return result; + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(customScriptConfiguration.getCustomScript(), ex); + } + + return -1; + } + + public boolean executeExternalLogout(CustomScriptConfiguration customScriptConfiguration, Map requestParameters) { + try { + log.trace("Executing python 'logout' authenticator method"); + PersonAuthenticationType externalAuthenticator = (PersonAuthenticationType) customScriptConfiguration.getExternalType(); + Map configurationAttributes = customScriptConfiguration.getConfigurationAttributes(); + final boolean result = externalAuthenticator.logout(configurationAttributes, requestParameters); + log.trace("Executed python 'logout' authenticator method, result: {}", result); + return result; + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(customScriptConfiguration.getCustomScript(), ex); + } + + return false; + } + + public String getLogoutExternalUrl(CustomScriptConfiguration customScriptConfiguration, Map requestParameters) { + try { + log.trace("Executing python 'getLogouExternalUrl' authenticator method"); + PersonAuthenticationType externalAuthenticator = (PersonAuthenticationType) customScriptConfiguration.getExternalType(); + Map configurationAttributes = customScriptConfiguration.getConfigurationAttributes(); + final String result = externalAuthenticator.getLogoutExternalUrl(configurationAttributes, requestParameters); + log.trace("Executed python 'getLogouExternalUrl' authenticator method, result: {}", result); + return result; + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(customScriptConfiguration.getCustomScript(), ex); + } + + return null; + } + + public boolean executeExternalPrepareForStep(CustomScriptConfiguration customScriptConfiguration, Map requestParameters, int step) { + try { + log.trace("Executing python 'prepareForStep' authenticator method"); + PersonAuthenticationType externalAuthenticator = (PersonAuthenticationType) customScriptConfiguration.getExternalType(); + Map configurationAttributes = customScriptConfiguration.getConfigurationAttributes(); + final boolean result = externalAuthenticator.prepareForStep(configurationAttributes, requestParameters, step); + log.trace("Executed python 'prepareForStep' authenticator method, result: {}", result); + return result; + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(customScriptConfiguration.getCustomScript(), ex); + } + + return false; + } + + public List executeExternalGetExtraParametersForStep(CustomScriptConfiguration customScriptConfiguration, int step) { + try { + log.trace("Executing python 'getExtraParametersForStep' authenticator method"); + PersonAuthenticationType externalAuthenticator = (PersonAuthenticationType) customScriptConfiguration.getExternalType(); + Map configurationAttributes = customScriptConfiguration.getConfigurationAttributes(); + final List result = externalAuthenticator.getExtraParametersForStep(configurationAttributes, step); + log.trace("Executed python 'getExtraParametersForStep' authenticator method, result: {}", result); + return result; + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(customScriptConfiguration.getCustomScript(), ex); + } + + return null; + } + + public String executeExternalGetPageForStep(CustomScriptConfiguration customScriptConfiguration, int step) { + try { + log.trace("Executing python 'getPageForStep' authenticator method"); + PersonAuthenticationType externalAuthenticator = (PersonAuthenticationType) customScriptConfiguration.getExternalType(); + Map configurationAttributes = customScriptConfiguration.getConfigurationAttributes(); + final String result = externalAuthenticator.getPageForStep(configurationAttributes, step); + log.trace("Executed python 'getPageForStep' authenticator method, result: {}", result); + return result; + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(customScriptConfiguration.getCustomScript(), ex); + + return null; + } + } + + public int executeExternalGetApiVersion(CustomScriptConfiguration customScriptConfiguration) { + try { + log.trace("Executing python 'getApiVersion' authenticator method"); + PersonAuthenticationType externalAuthenticator = (PersonAuthenticationType) customScriptConfiguration.getExternalType(); + final int result = externalAuthenticator.getApiVersion(); + log.trace("Executed python 'getApiVersion' authenticator method, result: {}", result); + return result; + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(customScriptConfiguration.getCustomScript(), ex); + } + + return -1; + } + + public boolean isEnabled(AuthenticationScriptUsageType usageType) { + return this.customScriptConfigurationsMapByUsageType != null && + this.customScriptConfigurationsMapByUsageType.get(usageType).size() > 0; + } + + public CustomScriptConfiguration getExternalAuthenticatorByAuthLevel(AuthenticationScriptUsageType usageType, int authLevel) { + CustomScriptConfiguration resultDefaultExternalAuthenticator = null; + for (CustomScriptConfiguration customScriptConfiguration : this.customScriptConfigurationsMapByUsageType.get(usageType)) { + // Determine authenticator + if (customScriptConfiguration.getLevel() != authLevel) { + continue; + } + + if (resultDefaultExternalAuthenticator == null) { + resultDefaultExternalAuthenticator = customScriptConfiguration; + } + } + + return resultDefaultExternalAuthenticator; + } + + public CustomScriptConfiguration determineCustomScriptConfiguration(AuthenticationScriptUsageType usageType, int authStep, String acr) { + CustomScriptConfiguration customScriptConfiguration; + if (authStep == 1) { + if (StringHelper.isNotEmpty(acr)) { + customScriptConfiguration = getCustomScriptConfiguration(usageType, acr); + } else { + customScriptConfiguration = getDefaultExternalAuthenticator(usageType); + } + } else { + customScriptConfiguration = getCustomScriptConfiguration(usageType, acr); + } + + return customScriptConfiguration; + } + + public CustomScriptConfiguration determineCustomScriptConfiguration(AuthenticationScriptUsageType usageType, List acrValues) { + List authModes = getAuthModesByAcrValues(acrValues); + + if (authModes.size() > 0) { + for (String authMode : authModes) { + for (CustomScriptConfiguration customScriptConfiguration : this.customScriptConfigurationsMapByUsageType.get(usageType)) { + if (StringHelper.equalsIgnoreCase(authMode, customScriptConfiguration.getName())) { + return customScriptConfiguration; + } + } + } + } + + return null; + } + + public List getAuthModesByAcrValues(List acrValues) { + List authModes = new ArrayList(); + + for (String acrValue : acrValues) { + if (StringHelper.isNotEmpty(acrValue)) { + String customScriptName = StringHelper.toLowerCase(scriptName(acrValue)); + if (customScriptConfigurationsNameMap.containsKey(customScriptName)) { + CustomScriptConfiguration customScriptConfiguration = customScriptConfigurationsNameMap.get(customScriptName); + CustomScript customScript = customScriptConfiguration.getCustomScript(); + + // Handle internal authentication method + if (customScript.isInternal()) { + authModes.add(scriptName(acrValue)); + continue; + } + + CustomScriptType customScriptType = customScriptConfiguration.getCustomScript().getScriptType(); + BaseExternalType defaultImplementation = customScriptType.getDefaultImplementation(); + BaseExternalType pythonImplementation = customScriptConfiguration.getExternalType(); + if ((pythonImplementation != null) && (defaultImplementation != pythonImplementation)) { + authModes.add(scriptName(acrValue)); + } + } + } + } + return authModes; + } + + public CustomScriptConfiguration determineExternalAuthenticatorForWorkflow(AuthenticationScriptUsageType usageType, CustomScriptConfiguration customScriptConfiguration) { + String authMode = customScriptConfiguration.getName(); + log.trace("Validating acr_values: '{}'", authMode); + + boolean isValidAuthenticationMethod = executeExternalIsValidAuthenticationMethod(usageType, customScriptConfiguration); + if (!isValidAuthenticationMethod) { + log.warn("Current acr_values: '{}' isn't valid", authMode); + + String alternativeAuthenticationMethod = executeExternalGetAlternativeAuthenticationMethod(usageType, customScriptConfiguration); + if (StringHelper.isEmpty(alternativeAuthenticationMethod)) { + log.error("Failed to determine alternative authentication mode for acr_values: '{}'", authMode); + return null; + } else { + CustomScriptConfiguration alternativeCustomScriptConfiguration = getCustomScriptConfiguration(AuthenticationScriptUsageType.INTERACTIVE, alternativeAuthenticationMethod); + if (alternativeCustomScriptConfiguration == null) { + log.error("Failed to get alternative CustomScriptConfiguration '{}' for acr_values: '{}'", alternativeAuthenticationMethod, authMode); + return null; + } else { + return alternativeCustomScriptConfiguration; + } + } + } + + return customScriptConfiguration; + } + + public CustomScriptConfiguration getDefaultExternalAuthenticator(AuthenticationScriptUsageType usageType) { + if (this.defaultExternalAuthenticators != null) { + return this.defaultExternalAuthenticators.get(usageType); + } + + return null; + } + + public CustomScriptConfiguration getCustomScriptConfiguration(AuthenticationScriptUsageType usageType, String name) { + for (CustomScriptConfiguration customScriptConfiguration : this.customScriptConfigurationsMapByUsageType.get(usageType)) { + if (StringHelper.equalsIgnoreCase(scriptName(name), customScriptConfiguration.getName())) { + return customScriptConfiguration; + } + } + + return null; + } + + public CustomScriptConfiguration getCustomScriptConfigurationByName(String name) { + for (Entry customScriptConfigurationEntry : this.customScriptConfigurationsNameMap.entrySet()) { + if (StringHelper.equalsIgnoreCase(scriptName(name), customScriptConfigurationEntry.getKey())) { + return customScriptConfigurationEntry.getValue(); + } + } + + return null; + } + + public List getCustomScriptConfigurationsMap() { + if (this.customScriptConfigurationsNameMap == null) { + return new ArrayList(0); + } + + List configurations = new ArrayList(this.customScriptConfigurationsNameMap.values()); + return configurations; + } + + public List getAcrValuesList() { + return new ArrayList(scriptAliasMap.keySet()); + } + + private boolean isValidateUsageType(AuthenticationScriptUsageType usageType, CustomScriptConfiguration customScriptConfiguration) { + if (customScriptConfiguration == null) { + return false; + } + + AuthenticationScriptUsageType externalAuthenticatorUsageType = ((AuthenticationCustomScript) customScriptConfiguration.getCustomScript()).getUsageType(); + + // Set default usage type + if (externalAuthenticatorUsageType == null) { + externalAuthenticatorUsageType = AuthenticationScriptUsageType.INTERACTIVE; + } + + if (AuthenticationScriptUsageType.BOTH.equals(externalAuthenticatorUsageType)) { + return true; + } + + if (AuthenticationScriptUsageType.INTERACTIVE.equals(usageType) && AuthenticationScriptUsageType.INTERACTIVE.equals(externalAuthenticatorUsageType)) { + return true; + } + + if (AuthenticationScriptUsageType.SERVICE.equals(usageType) && AuthenticationScriptUsageType.SERVICE.equals(externalAuthenticatorUsageType)) { + return true; + } + + return false; + } + + public Map> levelToAcrMapping() { + Map> map = Maps.newHashMap(); + for (CustomScriptConfiguration script : getCustomScriptConfigurationsMap()) { + int level = script.getLevel(); + String acr = script.getName(); + + Set acrs = map.get(level); + if (acrs == null) { + acrs = Sets.newHashSet(); + map.put(level, acrs); + } + acrs.add(acr); + } + return map; + } + + public Map acrToLevelMapping() { + Map map = Maps.newHashMap(); + for (CustomScriptConfiguration script : getCustomScriptConfigurationsMap()) { + if (script.getCustomScript().isInternal()) { + map.put(script.getName(), -1); + continue; + } + map.put(script.getName(), script.getLevel()); + } + return map; + } + + private CustomScriptConfiguration getInternalCustomScriptConfiguration(GluuLdapConfiguration ldapAuthConfig) { + CustomScriptConfiguration customScriptConfiguration = getInternalCustomScriptConfiguration(); + customScriptConfiguration.getCustomScript().setName(ldapAuthConfig.getConfigId()); + + return customScriptConfiguration; + } + + private CustomScriptConfiguration getInternalCustomScriptConfiguration() { + CustomScript customScript = new AuthenticationCustomScript() { + @Override + public AuthenticationScriptUsageType getUsageType() { + return AuthenticationScriptUsageType.INTERACTIVE; + } + + }; + customScript.setName(OxConstants.SCRIPT_TYPE_INTERNAL_RESERVED_NAME); + customScript.setLevel(-1); + customScript.setInternal(true); + + CustomScriptConfiguration customScriptConfiguration = new CustomScriptConfiguration(customScript, internalDefaultPersonAuthenticationType, + new HashMap(0)); + + return customScriptConfiguration; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalCibaEndUserNotificationService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalCibaEndUserNotificationService.java new file mode 100644 index 00000000..786ef0b7 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalCibaEndUserNotificationService.java @@ -0,0 +1,60 @@ +package org.gluu.oxauth.service.external; + +import org.gluu.model.custom.script.CustomScriptType; +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.model.custom.script.type.ciba.EndUserNotificationType; +import org.gluu.oxauth.service.external.context.ExternalCibaEndUserNotificationContext; +import org.gluu.service.custom.script.ExternalScriptService; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +/** + * @author Milton BO + */ +@ApplicationScoped +public class ExternalCibaEndUserNotificationService extends ExternalScriptService { + + private static final long serialVersionUID = -8609727759114795446L; + + @Inject + private Logger log; + + public ExternalCibaEndUserNotificationService() { + super(CustomScriptType.CIBA_END_USER_NOTIFICATION); + } + + public boolean executeExternalNotifyEndUser(ExternalCibaEndUserNotificationContext context) { + if (customScriptConfigurations == null || customScriptConfigurations.isEmpty()) { + log.trace("There is no any external interception scripts defined."); + return false; + } + + for (CustomScriptConfiguration script : customScriptConfigurations) { + if (!executeExternalNotifyEndUser(script, context)) { + log.trace("Stopped running external interception scripts because script {} returns false.", script.getName()); + return false; + } + } + return true; + } + + private boolean executeExternalNotifyEndUser(CustomScriptConfiguration customScriptConfiguration, + ExternalCibaEndUserNotificationContext context) { + try { + log.trace("Executing external 'executeExternalNotifyEndUser' method, script name: {}, context: {}", + customScriptConfiguration.getName(), context); + + EndUserNotificationType script = (EndUserNotificationType) customScriptConfiguration.getExternalType(); + final boolean result = script.notifyEndUser(context); + log.trace("Finished external 'executeExternalNotifyEndUser' method, script name: {}, context: {}, result: {}", + customScriptConfiguration.getName(), context, result); + return result; + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(customScriptConfiguration.getCustomScript(), ex); + return false; + } + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalConsentGatheringService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalConsentGatheringService.java new file mode 100644 index 00000000..d94f336f --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalConsentGatheringService.java @@ -0,0 +1,136 @@ +package org.gluu.oxauth.service.external; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.gluu.model.custom.script.CustomScriptType; +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.model.custom.script.type.authz.ConsentGatheringType; +import org.gluu.oxauth.service.external.context.ConsentGatheringContext; +import org.gluu.service.LookupService; +import org.gluu.service.custom.script.ExternalScriptService; +import org.gluu.util.StringHelper; +import org.slf4j.Logger; + +/** + * @author Yuriy Movchan Date: 10/30/2017 + */ +@ApplicationScoped +public class ExternalConsentGatheringService extends ExternalScriptService { + + private static final long serialVersionUID = 1741073794567832914L; + + @Inject + private Logger log; + + @Inject + private LookupService lookupService; + + protected Map scriptInumMap; + + public ExternalConsentGatheringService() { + super(CustomScriptType.CONSENT_GATHERING); + } + + @Override + protected void reloadExternal() { + this.scriptInumMap = buildExternalConfigurationsInumMap(this.customScriptConfigurations); + } + + private Map buildExternalConfigurationsInumMap(List customScriptConfigurations) { + Map reloadedExternalConfigurations = new HashMap(customScriptConfigurations.size()); + + for (CustomScriptConfiguration customScriptConfiguration : customScriptConfigurations) { + reloadedExternalConfigurations.put(customScriptConfiguration.getInum(), customScriptConfiguration); + } + + return reloadedExternalConfigurations; + } + + public CustomScriptConfiguration getScriptByDn(String scriptDn) { + String consentScriptInum = lookupService.getInumFromDn(scriptDn); + + return getScriptByInum(consentScriptInum); + } + + public CustomScriptConfiguration getScriptByInum(String inum) { + if (StringHelper.isEmpty(inum)) { + return null; + } + + return this.scriptInumMap.get(inum); + } + + private ConsentGatheringType consentScript(CustomScriptConfiguration script) { + return (ConsentGatheringType) script.getExternalType(); + } + + public boolean authorize(CustomScriptConfiguration script, int step, ConsentGatheringContext context) { + try { + log.trace("Executing python 'authorize' method, script: " + script.getName()); + boolean result = consentScript(script).authorize(step, context); + log.trace("python 'authorize' result: " + result); + return result; + } catch (Exception ex) { + log.error("Failed to execute python 'authorize' method, script: " + script.getName() + ", message: " + ex.getMessage(), ex); + saveScriptError(script.getCustomScript(), ex); + return false; + } + } + + public int getNextStep(CustomScriptConfiguration script, int step, ConsentGatheringContext context) { + try { + log.trace("Executing python 'getNextStep' method, script: " + script.getName()); + int result = consentScript(script).getNextStep(step, context); + log.trace("python 'getNextStep' result: " + result); + return result; + } catch (Exception ex) { + log.error("Failed to execute python 'getNextStep' method, script: " + script.getName() + ", message: " + ex.getMessage(), ex); + saveScriptError(script.getCustomScript(), ex); + return -1; + } + } + + public boolean prepareForStep(CustomScriptConfiguration script, int step, ConsentGatheringContext context) { + try { + log.trace("Executing python 'prepareForStep' method, script: " + script.getName()); + boolean result = consentScript(script).prepareForStep(step, context); + log.trace("python 'prepareForStep' result: " + result); + return result; + } catch (Exception ex) { + log.error("Failed to execute python 'prepareForStep' method, script: " + script.getName() + ", message: " + ex.getMessage(), ex); + saveScriptError(script.getCustomScript(), ex); + return false; + } + } + + public int getStepsCount(CustomScriptConfiguration script, ConsentGatheringContext context) { + try { + log.trace("Executing python 'getStepsCount' method, script: " + script.getName()); + int result = consentScript(script).getStepsCount(context); + log.trace("python 'getStepsCount' result: " + result); + return result; + } catch (Exception ex) { + log.error("Failed to execute python 'getStepsCount' method, script: " + script.getName() + ", message: " + ex.getMessage(), ex); + saveScriptError(script.getCustomScript(), ex); + return -1; + } + } + + public String getPageForStep(CustomScriptConfiguration script, int step, ConsentGatheringContext context) { + try { + log.trace("Executing python 'getPageForStep' method, script: " + script.getName()); + String result = consentScript(script).getPageForStep(step, context); + log.trace("python 'getPageForStep' result: " + result); + return result; + } catch (Exception ex) { + log.error("Failed to execute python 'getPageForStep' method, script: " + script.getName() + ", message: " + ex.getMessage(), ex); + saveScriptError(script.getCustomScript(), ex); + return ""; + } + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalDynamicClientRegistrationService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalDynamicClientRegistrationService.java new file mode 100644 index 00000000..2b7d07cb --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalDynamicClientRegistrationService.java @@ -0,0 +1,127 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service.external; + +import org.gluu.model.SimpleCustomProperty; +import org.gluu.model.custom.script.CustomScriptType; +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.model.custom.script.type.client.ClientRegistrationType; +import org.gluu.oxauth.client.RegisterRequest; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.service.external.context.DynamicClientRegistrationContext; +import org.gluu.service.custom.script.ExternalScriptService; +import org.json.JSONObject; + +import javax.enterprise.context.ApplicationScoped; +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +/** + * Provides factory methods needed to create external dynamic client registration extension + * + * @author Yuriy Movchan Date: 01/08/2015 + */ +@ApplicationScoped +public class ExternalDynamicClientRegistrationService extends ExternalScriptService { + + private static final long serialVersionUID = 1416361273036208686L; + + public ExternalDynamicClientRegistrationService() { + super(CustomScriptType.CLIENT_REGISTRATION); + } + + public boolean executeExternalCreateClientMethod(CustomScriptConfiguration customScriptConfiguration, RegisterRequest registerRequest, Client client) { + try { + log.trace("Executing python 'createClient' method"); + ClientRegistrationType externalClientRegistrationType = (ClientRegistrationType) customScriptConfiguration.getExternalType(); + Map configurationAttributes = customScriptConfiguration.getConfigurationAttributes(); + return externalClientRegistrationType.createClient(registerRequest, client, configurationAttributes); + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(customScriptConfiguration.getCustomScript(), ex); + } + + return false; + } + + public boolean executeExternalCreateClientMethods(RegisterRequest registerRequest, Client client) { + boolean result = true; + for (CustomScriptConfiguration customScriptConfiguration : this.customScriptConfigurations) { + if (customScriptConfiguration.getExternalType().getApiVersion() > 1) { + result &= executeExternalCreateClientMethod(customScriptConfiguration, registerRequest, client); + if (!result) { + return result; + } + } + } + + return result; + } + + public boolean executeExternalUpdateClientMethod(CustomScriptConfiguration customScriptConfiguration, RegisterRequest registerRequest, Client client) { + try { + log.trace("Executing python 'updateClient' method"); + ClientRegistrationType externalClientRegistrationType = (ClientRegistrationType) customScriptConfiguration.getExternalType(); + Map configurationAttributes = customScriptConfiguration.getConfigurationAttributes(); + return externalClientRegistrationType.updateClient(registerRequest, client, configurationAttributes); + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(customScriptConfiguration.getCustomScript(), ex); + } + + return false; + } + + public boolean executeExternalUpdateClientMethods(RegisterRequest registerRequest, Client client) { + boolean result = true; + for (CustomScriptConfiguration customScriptConfiguration : this.customScriptConfigurations) { + result &= executeExternalUpdateClientMethod(customScriptConfiguration, registerRequest, client); + if (!result) { + return result; + } + } + + return result; + } + + public JSONObject getSoftwareStatementJwks(HttpServletRequest httpRequest, JSONObject registerRequest, Jwt softwareStatement) { + try { + log.trace("Executing python 'getSoftwareStatementJwks' method"); + + DynamicClientRegistrationContext context = new DynamicClientRegistrationContext(httpRequest, registerRequest, defaultExternalCustomScript); + context.setSoftwareStatement(softwareStatement); + + ClientRegistrationType externalType = (ClientRegistrationType) defaultExternalCustomScript.getExternalType(); + final String result = externalType.getSoftwareStatementJwks(context); + log.trace("Result of python 'getSoftwareStatementJwks' method: " + result); + return new JSONObject(result); + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(defaultExternalCustomScript.getCustomScript(), ex); + return null; + } + } + + public String getSoftwareStatementHmacSecret(HttpServletRequest httpRequest, JSONObject registerRequest, Jwt softwareStatement) { + try { + log.trace("Executing python 'getSoftwareStatementHmacSecret' method"); + + DynamicClientRegistrationContext context = new DynamicClientRegistrationContext(httpRequest, registerRequest, defaultExternalCustomScript); + context.setSoftwareStatement(softwareStatement); + + ClientRegistrationType externalType = (ClientRegistrationType) defaultExternalCustomScript.getExternalType(); + final String result = externalType.getSoftwareStatementHmacSecret(context); + log.trace("Result of python 'getSoftwareStatementHmacSecret' method: " + result); + return result; + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(defaultExternalCustomScript.getCustomScript(), ex); + return ""; + } + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalDynamicScopeService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalDynamicScopeService.java new file mode 100644 index 00000000..8a9809a7 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalDynamicScopeService.java @@ -0,0 +1,119 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service.external; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.enterprise.context.ApplicationScoped; + +import org.gluu.model.SimpleCustomProperty; +import org.gluu.model.custom.script.CustomScriptType; +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.model.custom.script.type.scope.DynamicScopeType; +import org.gluu.oxauth.service.external.context.DynamicScopeExternalContext; +import org.gluu.service.custom.script.ExternalScriptService; +import org.oxauth.persistence.model.Scope; + +import com.google.common.collect.Sets; + +/** + * Provides factory methods needed to create dynamic scope extension + * + * @author Yuriy Movchan Date: 01/08/2015 + */ +@ApplicationScoped +public class ExternalDynamicScopeService extends ExternalScriptService { + + private static final long serialVersionUID = 1416361273036208685L; + + public ExternalDynamicScopeService() { + super(CustomScriptType.DYNAMIC_SCOPE); + } + + public boolean executeExternalUpdateMethod(CustomScriptConfiguration customScriptConfiguration, DynamicScopeExternalContext dynamicScopeContext) { + try { + log.trace("Executing python 'update' method"); + DynamicScopeType dynamicScopeType = (DynamicScopeType) customScriptConfiguration.getExternalType(); + Map configurationAttributes = customScriptConfiguration.getConfigurationAttributes(); + return dynamicScopeType.update(dynamicScopeContext, configurationAttributes); + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(customScriptConfiguration.getCustomScript(), ex); + } + + return false; + } + + public List executeExternalGetSupportedClaimsMethod(CustomScriptConfiguration customScriptConfiguration) { + int apiVersion = executeExternalGetApiVersion(customScriptConfiguration); + + if (apiVersion > 1) { + try { + log.trace("Executing python 'get supported claims' method"); + DynamicScopeType dynamicScopeType = (DynamicScopeType) customScriptConfiguration.getExternalType(); + Map configurationAttributes = customScriptConfiguration.getConfigurationAttributes(); + return dynamicScopeType.getSupportedClaims(configurationAttributes); + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(customScriptConfiguration.getCustomScript(), ex); + } + } + + return null; + } + + private Set getScriptsToExecute(DynamicScopeExternalContext context) { + Set allowedScripts = Sets.newHashSet(); + for (org.oxauth.persistence.model.Scope scope : context.getScopes()) { + List scopeScripts = scope.getDynamicScopeScripts(); + if (scopeScripts != null) { + allowedScripts.addAll(scopeScripts); + } + } + + Set result = Sets.newHashSet(); + if (this.customScriptConfigurations != null) { + for (CustomScriptConfiguration script : this.customScriptConfigurations) { + if (allowedScripts.contains(script.getCustomScript().getDn())) { + result.add(script); + } + } + } + return result; + } + + public boolean executeExternalUpdateMethods(DynamicScopeExternalContext dynamicScopeContext) { + boolean result = true; + for (CustomScriptConfiguration customScriptConfiguration : getScriptsToExecute(dynamicScopeContext)) { + result &= executeExternalUpdateMethod(customScriptConfiguration, dynamicScopeContext); + if (!result) { + return result; + } + } + + return result; + } + + public List executeExternalGetSupportedClaimsMethods(List dynamicScope) { + DynamicScopeExternalContext context = new DynamicScopeExternalContext(dynamicScope, null, null); + + Set result = new HashSet(); + for (CustomScriptConfiguration customScriptConfiguration : getScriptsToExecute(context)) { + List scriptResult = executeExternalGetSupportedClaimsMethod(customScriptConfiguration); + if (scriptResult != null) { + result.addAll(scriptResult); + } + } + + return new ArrayList(result); + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalEndSessionService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalEndSessionService.java new file mode 100644 index 00000000..e421d267 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalEndSessionService.java @@ -0,0 +1,54 @@ +package org.gluu.oxauth.service.external; + +import org.apache.commons.lang.StringUtils; +import org.gluu.model.custom.script.CustomScriptType; +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.model.custom.script.type.logout.EndSessionType; +import org.gluu.oxauth.service.external.context.EndSessionContext; +import org.gluu.service.custom.script.ExternalScriptService; + +import javax.enterprise.context.ApplicationScoped; + +/** + * @author Yuriy Zabrovarnyy + */ +@ApplicationScoped +public class ExternalEndSessionService extends ExternalScriptService { + + public ExternalEndSessionService() { + super(CustomScriptType.END_SESSION); + } + + public String getFrontchannelHtml(EndSessionContext context) { + if (customScriptConfigurations == null || customScriptConfigurations.isEmpty()) { + log.trace("There is no any external interception script defined (getFrontchannelHtml)."); + return ""; + } + + for (CustomScriptConfiguration script : customScriptConfigurations) { + final String html = getFrontchannelHtml(script, context); + if (StringUtils.isNotBlank(html)) { + return html; + } + } + + return null; + } + + private String getFrontchannelHtml(CustomScriptConfiguration scriptConf, EndSessionContext context) { + try { + log.trace("Executing external 'getFrontchannelHtml' method, script name: {}, context: {}", scriptConf.getName(), context); + EndSessionType script = (EndSessionType) scriptConf.getExternalType(); + context.setScript(scriptConf); + + final String html = script.getFrontchannelHtml(context); + log.trace("Finished external 'getFrontchannelHtml' method, script name: {}, context {}, html: {}", scriptConf.getName(), context, html); + + return html; + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(scriptConf.getCustomScript(), ex); + return null; + } + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalIdGeneratorService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalIdGeneratorService.java new file mode 100644 index 00000000..70960910 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalIdGeneratorService.java @@ -0,0 +1,51 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service.external; + +import java.util.Map; + +import javax.enterprise.context.ApplicationScoped; + +import org.gluu.model.SimpleCustomProperty; +import org.gluu.model.custom.script.CustomScriptType; +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.model.custom.script.type.id.IdGeneratorType; +import org.gluu.service.custom.script.ExternalScriptService; + +/** + * Provides factory methods needed to create external id generator extension + * + * @author Yuriy Movchan Date: 01/16/2015 + */ +@ApplicationScoped +public class ExternalIdGeneratorService extends ExternalScriptService { + + private static final long serialVersionUID = 1727751544454591273L; + + public ExternalIdGeneratorService() { + super(CustomScriptType.ID_GENERATOR); + } + + public String executeExternalGenerateIdMethod(CustomScriptConfiguration customScriptConfiguration, String appId, String idType, String idPrefix) { + try { + log.trace("Executing python 'generateId' method"); + IdGeneratorType externalType = (IdGeneratorType) customScriptConfiguration.getExternalType(); + Map configurationAttributes = customScriptConfiguration.getConfigurationAttributes(); + return externalType.generateId(appId, idType, idPrefix, configurationAttributes); + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(customScriptConfiguration.getCustomScript(), ex); + } + + return null; + } + + public String executeExternalDefaultGenerateIdMethod(String appId, String idType, String idPrefix) { + return executeExternalGenerateIdMethod(this.defaultExternalCustomScript, appId, idType, idPrefix); + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalIntrospectionService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalIntrospectionService.java new file mode 100644 index 00000000..fb185296 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalIntrospectionService.java @@ -0,0 +1,101 @@ +package org.gluu.oxauth.service.external; + +import com.google.common.collect.Lists; +import org.gluu.model.custom.script.CustomScriptType; +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.model.custom.script.type.introspection.IntrospectionType; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.service.external.context.ExternalIntrospectionContext; +import org.gluu.service.custom.script.ExternalScriptService; +import org.jetbrains.annotations.NotNull; +import org.json.JSONObject; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.WebApplicationException; +import java.util.List; + +/** + * @author Yuriy Zabrovarnyy + */ +@ApplicationScoped +public class ExternalIntrospectionService extends ExternalScriptService { + + private static final long serialVersionUID = -8609727759114795446L; + + @Inject + private Logger log; + @Inject + private AppConfiguration appConfiguration; + + public ExternalIntrospectionService() { + super(CustomScriptType.INTROSPECTION); + } + + @NotNull + private List getScripts(@NotNull ExternalIntrospectionContext context) { + if (customScriptConfigurations == null) { + return Lists.newArrayList(); + } + if (appConfiguration.getIntrospectionScriptBackwardCompatibility()) { + return customScriptConfigurations; + } + + if (context.getGrantOfIntrospectionToken() != null && context.getGrantOfIntrospectionToken().getClient() != null) { + final List scripts = getCustomScriptConfigurationsByDns(context.getGrantOfIntrospectionToken().getClient().getAttributes().getIntrospectionScripts()); + if (!scripts.isEmpty()) { + return scripts; + } + } + + if (context.getTokenGrant() != null && context.getTokenGrant().getClient() != null) { // fallback to authorization grant + final List scripts = getCustomScriptConfigurationsByDns(context.getTokenGrant().getClient().getAttributes().getIntrospectionScripts()); + if (!scripts.isEmpty()) { + return scripts; + } + } + + log.trace("No introspection scripts associated with client which was used to obtain access_token."); + return Lists.newArrayList(); + } + + public boolean executeExternalModifyResponse(JSONObject responseAsJsonObject, ExternalIntrospectionContext context) { + final List scripts = getScripts(context); + if (scripts.isEmpty()) { + log.trace("There is no any external interception scripts defined."); + return false; + } + + for (CustomScriptConfiguration script : scripts) { + if (!executeExternalModifyResponse(script, responseAsJsonObject, context)) { + log.debug("Stopped running external interception scripts because script {} returns false.", script.getName()); + return false; + } + } + + return true; + } + + private boolean executeExternalModifyResponse(CustomScriptConfiguration scriptConf, JSONObject responseAsJsonObject, ExternalIntrospectionContext context) { + try { + log.trace("Executing external 'executeExternalModifyResponse' method, script name: {}, responseAsJsonObject: {} , context: {}", + scriptConf.getName(), responseAsJsonObject, context); + + IntrospectionType script = (IntrospectionType) scriptConf.getExternalType(); + context.setScript(scriptConf); + final boolean result = script.modifyResponse(responseAsJsonObject, context); + log.trace("Finished external 'executeExternalModifyResponse' method, script name: {}, responseAsJsonObject: {} , context: {}, result: {}", + scriptConf.getName(), responseAsJsonObject, context, result); + + context.throwWebApplicationExceptionIfSet(); + return result; + } catch (WebApplicationException e) { + throw e; + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(scriptConf.getCustomScript(), ex); + return false; + } + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalPostAuthnService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalPostAuthnService.java new file mode 100644 index 00000000..caa79116 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalPostAuthnService.java @@ -0,0 +1,96 @@ +package org.gluu.oxauth.service.external; + +import org.gluu.model.custom.script.CustomScriptType; +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.model.custom.script.type.postauthn.PostAuthnType; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.service.external.context.ExternalPostAuthnContext; +import org.gluu.service.custom.script.ExternalScriptService; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import java.util.List; + +/** + * @author Yuriy Zabrovarnyy + */ +@ApplicationScoped +public class ExternalPostAuthnService extends ExternalScriptService { + + @Inject + private Logger log; + + public ExternalPostAuthnService() { + super(CustomScriptType.POST_AUTHN); + } + + public boolean externalForceReAuthentication(Client client, ExternalPostAuthnContext context) { + final List scripts = getCustomScriptConfigurationsByDns(client.getAttributes().getPostAuthnScripts()); + if (scripts.isEmpty()) { + return false; + } + log.trace("Found {} post-authn scripts.", scripts.size()); + + for (CustomScriptConfiguration script : scripts) { + if (!externalForceReAuthentication(script, context)) { + return false; + } + } + + log.debug("Forcing re-authentication via post-authn script."); + return true; + } + + public boolean externalForceAuthorization(Client client, ExternalPostAuthnContext context) { + final List scripts = getCustomScriptConfigurationsByDns(client.getAttributes().getPostAuthnScripts()); + if (scripts.isEmpty()) { + return false; + } + log.trace("Found {} post-authn scripts.", scripts.size()); + + for (CustomScriptConfiguration script : scripts) { + if (!externalForceAuthorization(script, context)) { + return false; + } + } + + log.debug("Forcing authorization via post-authn script."); + return true; + } + + + public boolean externalForceReAuthentication(CustomScriptConfiguration scriptConfiguration, ExternalPostAuthnContext context) { + try { + log.trace("Executing external 'externalForceReAuthentication' method, script name: {}, context: {}", scriptConfiguration.getName(), context); + + PostAuthnType script = (PostAuthnType) scriptConfiguration.getExternalType(); + context.setScript(scriptConfiguration); + final boolean result = script.forceReAuthentication(context); + + log.trace("Finished external 'externalForceReAuthentication' method, script name: {}, context: {}, result: {}", scriptConfiguration.getName(), context, result); + return result; + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(scriptConfiguration.getCustomScript(), ex); + return false; + } + } + + public boolean externalForceAuthorization(CustomScriptConfiguration scriptConfiguration, ExternalPostAuthnContext context) { + try { + log.trace("Executing external 'externalForceAuthorization' method, script name: {}, context: {}", scriptConfiguration.getName(), context); + + PostAuthnType script = (PostAuthnType) scriptConfiguration.getExternalType(); + context.setScript(scriptConfiguration); + final boolean result = script.forceAuthorization(context); + + log.trace("Finished external 'externalForceAuthorization' method, script name: {}, context: {}, result: {}", scriptConfiguration.getName(), context, result); + return result; + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(scriptConfiguration.getCustomScript(), ex); + return false; + } + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalResourceOwnerPasswordCredentialsService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalResourceOwnerPasswordCredentialsService.java new file mode 100644 index 00000000..14c28163 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalResourceOwnerPasswordCredentialsService.java @@ -0,0 +1,69 @@ +package org.gluu.oxauth.service.external; + +import org.gluu.model.custom.script.CustomScriptType; +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.model.custom.script.type.owner.ResourceOwnerPasswordCredentialsType; +import org.gluu.oxauth.service.external.context.ExternalResourceOwnerPasswordCredentialsContext; +import org.gluu.service.custom.script.ExternalScriptService; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +/** + * @author Yuriy Zabrovarnyy + */ +@ApplicationScoped +public class ExternalResourceOwnerPasswordCredentialsService extends ExternalScriptService { + + private static final long serialVersionUID = -1070021905117551202L; + + @Inject + private Logger log; + + public ExternalResourceOwnerPasswordCredentialsService() { + super(CustomScriptType.RESOURCE_OWNER_PASSWORD_CREDENTIALS); + } + + public boolean executeExternalAuthenticate(ExternalResourceOwnerPasswordCredentialsContext context) { + if (customScriptConfigurations == null || customScriptConfigurations.isEmpty()) { + log.debug("There is no any external interception scripts defined."); + return false; + } + + for (CustomScriptConfiguration script : customScriptConfigurations) { + if (!executeExternalAuthenticate(script, context)) { + log.debug("Stopped running external RO PC scripts because script {} returns false.", script.getName()); + return false; + } + } + + return true; + } + + private boolean executeExternalAuthenticate(CustomScriptConfiguration customScriptConfiguration, ExternalResourceOwnerPasswordCredentialsContext context) { + try { + log.debug("Executing external 'executeExternalAuthenticate' method, script name: {}, context: {}", + customScriptConfiguration.getName(), context); + + ResourceOwnerPasswordCredentialsType script = (ResourceOwnerPasswordCredentialsType) customScriptConfiguration.getExternalType(); + context.setScript(customScriptConfiguration); + + if (script == null) { + log.error("Failed to load script, name: " + customScriptConfiguration.getName()); + return false; + } + + final boolean result = script.authenticate(context); + + log.debug("Finished external 'executeExternalAuthenticate' method, script name: {}, context: {}, result: {}", + customScriptConfiguration.getName(), context, result); + return result; + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(customScriptConfiguration.getCustomScript(), ex); + return false; + } + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalRevokeTokenService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalRevokeTokenService.java new file mode 100644 index 00000000..493902d3 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalRevokeTokenService.java @@ -0,0 +1,52 @@ +package org.gluu.oxauth.service.external; + +import org.gluu.model.custom.script.CustomScriptType; +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.model.custom.script.type.revoke.RevokeTokenType; +import org.gluu.oxauth.service.external.context.RevokeTokenContext; +import org.gluu.service.custom.script.ExternalScriptService; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +/** + * @author Yuriy Zabrovarnyy + */ +@ApplicationScoped +public class ExternalRevokeTokenService extends ExternalScriptService { + + @Inject + private Logger log; + + public ExternalRevokeTokenService() { + super(CustomScriptType.REVOKE_TOKEN); + } + + public boolean revokeToken(CustomScriptConfiguration script, RevokeTokenContext context) { + try { + log.trace("Executing python 'revokeToken' method, context: {}", context); + context.setScript(script); + RevokeTokenType revokeTokenType = (RevokeTokenType) script.getExternalType(); + final boolean result = revokeTokenType.revoke(context); + log.trace("Finished 'revokeToken' method, result: {}, context: {}", result, context); + return result; + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(script.getCustomScript(), ex); + } + + return false; + } + + public boolean revokeTokenMethods(RevokeTokenContext context) { + for (CustomScriptConfiguration script : this.customScriptConfigurations) { + if (script.getExternalType().getApiVersion() > 1) { + if (!revokeToken(script, context)) { + return false; + } + } + } + return true; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalSpontaneousScopeService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalSpontaneousScopeService.java new file mode 100644 index 00000000..cb723f87 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalSpontaneousScopeService.java @@ -0,0 +1,60 @@ +package org.gluu.oxauth.service.external; + +import com.google.common.collect.Sets; +import org.gluu.model.custom.script.CustomScriptType; +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.model.custom.script.type.spontaneous.SpontaneousScopeType; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.service.external.context.SpontaneousScopeExternalContext; +import org.gluu.service.custom.script.ExternalScriptService; + +import javax.enterprise.context.ApplicationScoped; +import java.util.List; +import java.util.Set; + +@ApplicationScoped +public class ExternalSpontaneousScopeService extends ExternalScriptService { + + public ExternalSpontaneousScopeService() { + super(CustomScriptType.SPONTANEOUS_SCOPE); + } + + public void executeExternalManipulateScope(SpontaneousScopeExternalContext context) { + for (CustomScriptConfiguration script : getScriptsToExecute(context.getClient())) { + executeExternalManipulateScope(script, context); + + log.debug("GrantedScopes {} after execution of interception script {}.", context.getGrantedScopes(), script.getName()); + } + } + + private void executeExternalManipulateScope(CustomScriptConfiguration scriptConfiguration, SpontaneousScopeExternalContext context) { + try { + log.debug("Executing external 'executeExternalManipulateScope' method, script name: {}, grantedScopes: {} , context: {}", + scriptConfiguration.getName(), context.getGrantedScopes(), context); + + SpontaneousScopeType script = (SpontaneousScopeType) scriptConfiguration.getExternalType(); + + script.manipulateScopes(context); + log.debug("Finished external 'executeExternalManipulateScope' method, script name: {}, grantedScopes: {} , context: {}", + scriptConfiguration.getName(), context.getGrantedScopes(), context); + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(scriptConfiguration.getCustomScript(), ex); + } + } + + private Set getScriptsToExecute(Client client) { + Set result = Sets.newHashSet(); + if (this.customScriptConfigurations == null) { + return result; + } + + List scriptDns = client.getAttributes().getSpontaneousScopeScriptDns(); + for (CustomScriptConfiguration script : this.customScriptConfigurations) { + if (scriptDns.contains(script.getCustomScript().getDn())) { + result.add(script); + } + } + return result; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalUmaClaimsGatheringService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalUmaClaimsGatheringService.java new file mode 100644 index 00000000..73b5aeef --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalUmaClaimsGatheringService.java @@ -0,0 +1,168 @@ +package org.gluu.oxauth.service.external; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.gluu.model.custom.script.CustomScriptType; +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.model.custom.script.type.uma.UmaClaimsGatheringType; +import org.gluu.oxauth.uma.authorization.UmaGatherContext; +import org.gluu.service.LookupService; +import org.gluu.service.custom.script.CustomScriptManager; +import org.gluu.service.custom.script.ExternalScriptService; +import org.gluu.util.StringHelper; +import org.slf4j.Logger; + +/** + * @author yuriyz on 06/18/2017. + */ +@ApplicationScoped +public class ExternalUmaClaimsGatheringService extends ExternalScriptService { + + @Inject + private Logger log; + @Inject + private LookupService lookupService; + @Inject + private CustomScriptManager scriptManager; + + protected Map scriptInumMap; + + public ExternalUmaClaimsGatheringService() { + super(CustomScriptType.UMA_CLAIMS_GATHERING); + } + + @Override + protected void reloadExternal() { + this.scriptInumMap = buildExternalConfigurationsInumMap(this.customScriptConfigurations); + } + + public CustomScriptConfiguration determineScript(String[] scriptNames) { + log.trace("Trying to determine claims-gathering script, scriptNames: {} ...", Arrays.toString(scriptNames)); + + List scripts = new ArrayList(); + + for (String scriptName : scriptNames) { + CustomScriptConfiguration script = getCustomScriptConfigurationByName(scriptName); + if (script != null) { + scripts.add(script); + } else { + log.error("Failed to load claims-gathering script with name: {}", scriptName); + } + } + + if (scripts.isEmpty()) { + return null; + } + + CustomScriptConfiguration highestPriority = Collections.max(scripts, new Comparator() { + @Override + public int compare(CustomScriptConfiguration o1, CustomScriptConfiguration o2) { + return Integer.compare(o1.getLevel(), o2.getLevel()); + } + }); + log.trace("Determined claims-gathering script successfully. Name: {}, inum: {}", highestPriority.getName(), highestPriority.getInum()); + return highestPriority; + } + + private Map buildExternalConfigurationsInumMap(List customScriptConfigurations) { + Map reloadedExternalConfigurations = new HashMap(customScriptConfigurations.size()); + + for (CustomScriptConfiguration customScriptConfiguration : customScriptConfigurations) { + reloadedExternalConfigurations.put(customScriptConfiguration.getInum(), customScriptConfiguration); + } + + return reloadedExternalConfigurations; + } + + public CustomScriptConfiguration getScriptByDn(String scriptDn) { + String authorizationPolicyInum = lookupService.getInumFromDn(scriptDn); + + return getScriptByInum(authorizationPolicyInum); + } + + public CustomScriptConfiguration getScriptByInum(String inum) { + if (StringHelper.isEmpty(inum)) { + return null; + } + + return this.scriptInumMap.get(inum); + } + + private UmaClaimsGatheringType gatherScript(CustomScriptConfiguration script) { + return ExternalUmaRptPolicyService.HOTSWAP_UMA_SCRIPT ? (UmaClaimsGatheringType) ExternalUmaRptPolicyService.hotswap(scriptManager, script, false) : (UmaClaimsGatheringType) script.getExternalType(); + } + + public boolean gather(CustomScriptConfiguration script, int step, UmaGatherContext context) { + try { + log.debug("Executing python 'gather' method, script: " + script.getName()); + boolean result = gatherScript(script).gather(step, context); + log.debug("python 'gather' result: " + result); + return result; + } catch (Exception ex) { + log.error("Failed to execute python 'gather' method, script: " + script.getName() + ", message: " + ex.getMessage(), ex); + saveScriptError(script.getCustomScript(), ex); + return false; + } + } + + public int getNextStep(CustomScriptConfiguration script, int step, UmaGatherContext context) { + try { + log.debug("Executing python 'getNextStep' method, script: " + script.getName()); + int result = gatherScript(script).getNextStep(step, context); + log.debug("python 'getNextStep' result: " + result); + return result; + } catch (Exception ex) { + log.error("Failed to execute python 'getNextStep' method, script: " + script.getName() + ", message: " + ex.getMessage(), ex); + saveScriptError(script.getCustomScript(), ex); + return -1; + } + } + + public boolean prepareForStep(CustomScriptConfiguration script, int step, UmaGatherContext context) { + try { + log.debug("Executing python 'prepareForStep' method, script: " + script.getName()); + boolean result = gatherScript(script).prepareForStep(step, context); + log.debug("python 'prepareForStep' result: " + result); + return result; + } catch (Exception ex) { + log.error("Failed to execute python 'prepareForStep' method, script: " + script.getName() + ", message: " + ex.getMessage(), ex); + saveScriptError(script.getCustomScript(), ex); + return false; + } + } + + public int getStepsCount(CustomScriptConfiguration script, UmaGatherContext context) { + try { + log.debug("Executing python 'getStepsCount' method, script: " + script.getName()); + int result = gatherScript(script).getStepsCount(context); + log.debug("python 'getStepsCount' result: " + result); + return result; + } catch (Exception ex) { + log.error("Failed to execute python 'getStepsCount' method, script: " + script.getName() + ", message: " + ex.getMessage(), ex); + saveScriptError(script.getCustomScript(), ex); + return -1; + } + } + + public String getPageForStep(CustomScriptConfiguration script, int step, UmaGatherContext context) { + try { + log.debug("Executing python 'getPageForStep' method, script: " + script.getName()); + String result = gatherScript(script).getPageForStep(step, context); + log.debug("python 'getPageForStep' result: " + result); + return result; + } catch (Exception ex) { + log.error("Failed to execute python 'getPageForStep' method, script: " + script.getName() + ", message: " + ex.getMessage(), ex); + saveScriptError(script.getCustomScript(), ex); + return ""; + } + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalUmaRptClaimsService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalUmaRptClaimsService.java new file mode 100644 index 00000000..9db9be52 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalUmaRptClaimsService.java @@ -0,0 +1,61 @@ +package org.gluu.oxauth.service.external; + +import org.gluu.model.custom.script.CustomScriptType; +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.model.custom.script.type.uma.UmaRptClaimsType; +import org.gluu.oxauth.service.external.context.ExternalUmaRptClaimsContext; +import org.gluu.service.custom.script.ExternalScriptService; +import org.json.JSONObject; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import java.util.List; + +/** + * @author Yuriy Zabrovarnyy + */ +@ApplicationScoped +public class ExternalUmaRptClaimsService extends ExternalScriptService { + + @Inject + private Logger log; + + public ExternalUmaRptClaimsService() { + super(CustomScriptType.UMA_RPT_CLAIMS); + } + + public boolean externalModify(JSONObject rptAsJson, ExternalUmaRptClaimsContext context) { + final List scripts = getCustomScriptConfigurationsByDns(context.getClient().getAttributes().getRptClaimsScripts()); + if (scripts.isEmpty()) { + return false; + } + log.trace("Found {} RPT Claims scripts.", scripts.size()); + + for (CustomScriptConfiguration script : scripts) { + if (!externalModify(rptAsJson, script, context)) { + return false; + } + } + + log.debug("ExternalModify returned 'true'."); + return true; + } + + public boolean externalModify(JSONObject rptAsJson, CustomScriptConfiguration scriptConfiguration, ExternalUmaRptClaimsContext context) { + try { + log.trace("Executing external 'externalModify' method, script name: {}, context: {}", scriptConfiguration.getName(), context); + + UmaRptClaimsType script = (UmaRptClaimsType) scriptConfiguration.getExternalType(); + context.setScript(scriptConfiguration); + final boolean result = script.modify(rptAsJson, context); + + log.trace("Finished external 'externalModify' method, script name: {}, context: {}, result: {}", scriptConfiguration.getName(), context, result); + return result; + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(scriptConfiguration.getCustomScript(), ex); + return false; + } + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalUmaRptPolicyService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalUmaRptPolicyService.java new file mode 100644 index 00000000..6c56cb4b --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalUmaRptPolicyService.java @@ -0,0 +1,148 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service.external; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.apache.commons.io.FileUtils; +import org.gluu.model.custom.script.CustomScriptType; +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.model.custom.script.type.uma.UmaRptPolicyType; +import org.gluu.model.uma.ClaimDefinition; +import org.gluu.oxauth.uma.authorization.UmaAuthorizationContext; +import org.gluu.service.LookupService; +import org.gluu.service.custom.script.CustomScriptManager; +import org.gluu.service.custom.script.ExternalScriptService; +import org.gluu.util.StringHelper; +import org.slf4j.Logger; + +/** + * Provides factory methods needed to create external UMA authorization policies extension + * + * @author Yuriy Zabrovarnyy + * @author Yuriy Movchan + */ +@ApplicationScoped +public class ExternalUmaRptPolicyService extends ExternalScriptService { + + private static final long serialVersionUID = -8609727759114795435L; + + public static final boolean HOTSWAP_UMA_SCRIPT = Boolean.parseBoolean(System.getProperty("uma.hotswap.script")); + + @Inject + private Logger log; + @Inject + private LookupService lookupService; + @Inject + private CustomScriptManager scriptManager; + + protected Map scriptInumMap; + + public ExternalUmaRptPolicyService() { + super(CustomScriptType.UMA_RPT_POLICY); + } + + @Override + protected void reloadExternal() { + this.scriptInumMap = buildExternalConfigurationsInumMap(this.customScriptConfigurations); + } + + private Map buildExternalConfigurationsInumMap(List customScriptConfigurations) { + Map reloadedExternalConfigurations = new HashMap(customScriptConfigurations.size()); + + for (CustomScriptConfiguration customScriptConfiguration : customScriptConfigurations) { + reloadedExternalConfigurations.put(customScriptConfiguration.getInum(), customScriptConfiguration); + } + + return reloadedExternalConfigurations; + } + + public CustomScriptConfiguration getScriptByDn(String scriptDn) { + String authorizationPolicyInum = lookupService.getInumFromDn(scriptDn); + + return getScriptByInum(authorizationPolicyInum); + } + + public CustomScriptConfiguration getScriptByInum(String inum) { + if (StringHelper.isEmpty(inum)) { + return null; + } + + return this.scriptInumMap.get(inum); + } + + private UmaRptPolicyType policyScript(CustomScriptConfiguration script) { + return HOTSWAP_UMA_SCRIPT ? (UmaRptPolicyType) hotswap(scriptManager, script, true) : + (UmaRptPolicyType) script.getExternalType(); + } + + public boolean authorize(CustomScriptConfiguration script, UmaAuthorizationContext context) { + try { + log.debug("Executing python 'authorize' method, script: " + script.getName()); + boolean result = policyScript(script).authorize(context); + log.debug("python 'authorize' result: " + result); + return result; + } catch (Exception ex) { + log.error("Failed to execute python 'authorize' method, script: " + script.getName() + ", message: " + ex.getMessage(), ex); + saveScriptError(script.getCustomScript(), ex); + return false; + } + } + + public List getRequiredClaims(CustomScriptConfiguration script, UmaAuthorizationContext context) { + try { + log.debug("Executing python 'getRequiredClaims' method, script: " + script.getName()); + List result = policyScript(script).getRequiredClaims(context); + log.debug("python 'getRequiredClaims' result: " + result); + return result; + } catch (Exception ex) { + log.error("Failed to execute python 'getRequiredClaims' method, script: " + script.getName() + ", message: " + ex.getMessage(), ex); + saveScriptError(script.getCustomScript(), ex); + return new ArrayList(); + } + } + + public String getClaimsGatheringScriptName(CustomScriptConfiguration script, UmaAuthorizationContext context) { + try { + log.debug("Executing python 'getClaimsGatheringScriptName' method, script: " + script.getName()); + String result = policyScript(script).getClaimsGatheringScriptName(context); + log.debug("python 'getClaimsGatheringScriptName' result: " + result); + return result; + } catch (Exception ex) { + log.error("Failed to execute python 'getClaimsGatheringScriptName' method, script: " + script.getName() + ", message: " + ex.getMessage(), ex); + saveScriptError(script.getCustomScript(), ex); + return ""; + } + } + + public static T hotswap(CustomScriptManager scriptManager, CustomScriptConfiguration script, boolean rptPolicyScript) { + if (!HOTSWAP_UMA_SCRIPT) { + throw new RuntimeException("UMA script hotswap is not allowed"); + } + + final String scriptPath; + if (rptPolicyScript) { + scriptPath = System.getProperty("uma.hotswap.rpt_policy_script.path"); + } else { + scriptPath = System.getProperty("uma.hotswap.claims_gathering_script.path"); + } + try { + String scriptCode = FileUtils.readFileToString(new File(scriptPath)); + script.getCustomScript().setScript(scriptCode); + return (T) scriptManager.createExternalTypeFromStringWithPythonException(script.getCustomScript(), script.getConfigurationAttributes()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalUpdateTokenService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalUpdateTokenService.java new file mode 100644 index 00000000..8dc8fb5d --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/ExternalUpdateTokenService.java @@ -0,0 +1,278 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2021, Gluu + */ + +package org.gluu.oxauth.service.external; + +import com.google.common.base.Function; +import com.google.common.collect.Lists; +import org.gluu.model.custom.script.CustomScriptType; +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.model.custom.script.type.token.UpdateTokenType; +import org.gluu.oxauth.model.common.AccessToken; +import org.gluu.oxauth.model.common.RefreshToken; +import org.gluu.oxauth.model.token.JsonWebResponse; +import org.gluu.oxauth.service.external.context.ExternalUpdateTokenContext; +import org.gluu.service.custom.script.ExternalScriptService; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.WebApplicationException; +import java.util.List; + +/** + * @author Yuriy Movchan + */ +@ApplicationScoped +public class ExternalUpdateTokenService extends ExternalScriptService { + + private static final long serialVersionUID = -1033475075863270249L; + + @Inject + private Logger log; + + public ExternalUpdateTokenService() { + super(CustomScriptType.UPDATE_TOKEN); + } + + public boolean modifyIdTokenMethod(CustomScriptConfiguration script, JsonWebResponse jsonWebResponse, ExternalUpdateTokenContext context) { + try { + log.trace("Executing python 'updateToken' method, script name: {}, jsonWebResponse: {}, context: {}", script.getName(), jsonWebResponse, context); + context.setScript(script); + + UpdateTokenType updateTokenType = (UpdateTokenType) script.getExternalType(); + final boolean result = updateTokenType.modifyIdToken(jsonWebResponse, context); + log.trace("Finished 'updateToken' method, script name: {}, jsonWebResponse: {}, context: {}, result: {}", script.getName(), jsonWebResponse, context, result); + + context.throwWebApplicationExceptionIfSet(); + return result; + } catch (WebApplicationException e) { + throw e; + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(script.getCustomScript(), ex); + } + + return false; + } + + public boolean modifyIdTokenMethods(JsonWebResponse jsonWebResponse, ExternalUpdateTokenContext context) { + for (CustomScriptConfiguration script : getScripts()) { + if (!modifyIdTokenMethod(script, jsonWebResponse, context)) { + return false; + } + } + + return true; + } + + @NotNull + private List getScripts() { + if (customScriptConfigurations == null) { + return Lists.newArrayList(); + } + + return customScriptConfigurations; + } + + + public Function buildModifyIdTokenProcessor(final ExternalUpdateTokenContext context) { + return new Function() { + @Override + public Void apply(JsonWebResponse jsonWebResponse) { + modifyIdTokenMethods(jsonWebResponse, context); + + return null; + } + }; + } + + public boolean modifyRefreshToken(CustomScriptConfiguration script, RefreshToken refreshToken, ExternalUpdateTokenContext context) { + try { + log.trace("Executing python 'modifyRefreshToken' method, script name: {}, context: {}", script.getName(), context); + context.setScript(script); + + UpdateTokenType updateTokenType = (UpdateTokenType) script.getExternalType(); + final boolean result = updateTokenType.modifyRefreshToken(refreshToken, context); + log.trace("Finished 'modifyRefreshToken' method, script name: {}, context: {}, result: {}", script.getName(), context, result); + + context.throwWebApplicationExceptionIfSet(); + return result; + } catch (WebApplicationException e) { + throw e; + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(script.getCustomScript(), ex); + } + + return false; + } + + public boolean modifyRefreshToken(RefreshToken refreshToken, ExternalUpdateTokenContext context) { + List scripts = getScripts(); + if (scripts.isEmpty()) { + return true; + } + log.trace("Executing {} update-token modifyRefreshToken scripts.", scripts.size()); + + for (CustomScriptConfiguration script : scripts) { + if (!modifyRefreshToken(script, refreshToken, context)) { + return false; + } + } + + return true; + } + + public boolean modifyAccessToken(CustomScriptConfiguration script, AccessToken accessToken, ExternalUpdateTokenContext context) { + try { + log.trace("Executing python 'modifyAccessToken' method, script name: {}, context: {}", script.getName(), context); + context.setScript(script); + + UpdateTokenType updateTokenType = (UpdateTokenType) script.getExternalType(); + final boolean result = updateTokenType.modifyAccessToken(accessToken, context); + log.trace("Finished 'modifyAccessToken' method, script name: {}, context: {}, result: {}", script.getName(), context, result); + + context.throwWebApplicationExceptionIfSet(); + return result; + } catch (WebApplicationException e) { + throw e; + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(script.getCustomScript(), ex); + } + + return false; + } + + public boolean modifyAccessToken(AccessToken accessToken, ExternalUpdateTokenContext context) { + List scripts = getScripts(); + if (scripts.isEmpty()) { + return true; + } + log.trace("Executing {} update-token modifyAccessToken scripts.", scripts.size()); + + for (CustomScriptConfiguration script : scripts) { + if (!modifyAccessToken(script, accessToken, context)) { + return false; + } + } + + return true; + } + + public int getAccessTokenLifetimeInSeconds(CustomScriptConfiguration script, ExternalUpdateTokenContext context) { + try { + log.trace("Executing python 'getAccessTokenLifetimeInSeconds' method, script name: {}, context: {}", script.getName(), context); + context.setScript(script); + + UpdateTokenType updateTokenType = (UpdateTokenType) script.getExternalType(); + final int result = updateTokenType.getAccessTokenLifetimeInSeconds(context); + log.trace("Finished 'getAccessTokenLifetimeInSeconds' method, script name: {}, context: {}, result: {}", script.getName(), context, result); + + context.throwWebApplicationExceptionIfSet(); + return result; + } catch (WebApplicationException e) { + throw e; + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(script.getCustomScript(), ex); + } + return 0; + } + + public int getAccessTokenLifetimeInSeconds(ExternalUpdateTokenContext context) { + List scripts = getScripts(); + if (scripts.isEmpty()) { + return 0; + } + log.trace("Executing {} 'getAccessTokenLifetimeInSeconds' scripts.", scripts.size()); + + for (CustomScriptConfiguration script : scripts) { + final int lifetime = getAccessTokenLifetimeInSeconds(script, context); + if (lifetime > 0) { + log.trace("Finished 'getAccessTokenLifetimeInSeconds' methods, lifetime: {}", lifetime); + return lifetime; + } + } + return 0; + } + + public int getIdTokenLifetimeInSeconds(CustomScriptConfiguration script, ExternalUpdateTokenContext context) { + try { + log.trace("Executing python 'getIdTokenLifetimeInSeconds' method, script name: {}, context: {}", script.getName(), context); + context.setScript(script); + + UpdateTokenType updateTokenType = (UpdateTokenType) script.getExternalType(); + final int result = updateTokenType.getIdTokenLifetimeInSeconds(context); + log.trace("Finished 'getIdTokenLifetimeInSeconds' method, script name: {}, context: {}, result: {}", script.getName(), context, result); + + context.throwWebApplicationExceptionIfSet(); + return result; + } catch (WebApplicationException e) { + throw e; + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(script.getCustomScript(), ex); + } + return 0; + } + + public int getIdTokenLifetimeInSeconds(ExternalUpdateTokenContext context) { + List scripts = getScripts(); + if (scripts.isEmpty()) { + return 0; + } + log.trace("Executing {} 'getIdTokenLifetimeInSeconds' scripts.", scripts.size()); + + for (CustomScriptConfiguration script : scripts) { + final int lifetime = getIdTokenLifetimeInSeconds(script, context); + if (lifetime > 0) { + log.trace("Finished 'getIdTokenLifetimeInSeconds' methods, lifetime: {}", lifetime); + return lifetime; + } + } + return 0; + } + + public int getRefreshTokenLifetimeInSeconds(CustomScriptConfiguration script, ExternalUpdateTokenContext context) { + try { + log.trace("Executing python 'getRefreshTokenLifetimeInSeconds' method, script name: {}, context: {}", script.getName(), context); + context.setScript(script); + + UpdateTokenType updateTokenType = (UpdateTokenType) script.getExternalType(); + final int result = updateTokenType.getRefreshTokenLifetimeInSeconds(context); + log.trace("Finished 'getRefreshTokenLifetimeInSeconds' method, script name: {}, context: {}, result: {}", script.getName(), context, result); + + context.throwWebApplicationExceptionIfSet(); + return result; + } catch (WebApplicationException e) { + throw e; + } catch (Exception ex) { + log.error(ex.getMessage(), ex); + saveScriptError(script.getCustomScript(), ex); + } + return 0; + } + + public int getRefreshTokenLifetimeInSeconds(ExternalUpdateTokenContext context) { + List scripts = getScripts(); + if (scripts.isEmpty()) { + return 0; + } + log.trace("Executing {} 'getRefreshTokenLifetimeInSeconds' scripts.", scripts.size()); + + for (CustomScriptConfiguration script : scripts) { + final int lifetime = getRefreshTokenLifetimeInSeconds(script, context); + if (lifetime > 0) { + log.trace("Finished 'getRefreshTokenLifetimeInSeconds' methods, lifetime: {}", lifetime); + return lifetime; + } + } + return 0; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ConsentGatheringContext.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ConsentGatheringContext.java new file mode 100644 index 00000000..0fccc808 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ConsentGatheringContext.java @@ -0,0 +1,128 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2015, Gluu + */ + +package org.gluu.oxauth.service.external.context; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.gluu.jsf2.service.FacesService; +import org.gluu.model.SimpleCustomProperty; +import org.gluu.oxauth.authorize.ws.rs.ConsentGatheringSessionService; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.service.common.UserService; +import org.gluu.oxauth.model.common.User; + +/** + * @author Yuriy Movchan Date: 10/30/2017 + */ +public class ConsentGatheringContext extends ExternalScriptContext { + + private final ConsentGatheringSessionService sessionService; + private final UserService userService; + private final FacesService facesService; + private final AppConfiguration appConfiguration; + + private final Map configurationAttributes; + private final SessionId session; + private final Map pageAttributes; + + public ConsentGatheringContext(Map configurationAttributes, HttpServletRequest httpRequest, HttpServletResponse httpResponse, SessionId session, + Map pageAttributes, + ConsentGatheringSessionService sessionService, UserService userService, FacesService facesService, AppConfiguration appConfiguration) { + super(httpRequest, httpResponse); + this.configurationAttributes = configurationAttributes; + this.session = session; + this.pageAttributes = pageAttributes; + this.sessionService = sessionService; + this.userService = userService; + this.facesService = facesService; + this.appConfiguration = appConfiguration; + } + + public Map getConfigurationAttributes() { + return configurationAttributes; + } + + public User getUser(String... returnAttributes) { + return sessionService.getUser(httpRequest, returnAttributes); + } + + public String getUserDn() { + return sessionService.getUserDn(httpRequest); + } + + public Client getClient() { + return sessionService.getClient(session); + } + + public Map getConnectSessionAttributes() { + SessionId connectSession = sessionService.getConnectSession(httpRequest); + if (connectSession != null) { + return new HashMap(connectSession.getSessionAttributes()); + } + return new HashMap(); + } + + public boolean isAuthenticated() { + return getUser() != null; + } + + public Map getPageAttributes() { + return pageAttributes; + } + + public Map getRequestParameters() { + return httpRequest.getParameterMap(); + } + + public int getStep() { + return sessionService.getStep(session); + } + + public void setStep(int step) { + sessionService.setStep(step, session); + } + + public void addSessionAttribute(String key, String value) { + session.getSessionAttributes().put(key, value); + } + + public void removeSessionAttribute(String key) { + session.getSessionAttributes().remove(key); + } + + public Map getSessionAttributes() { + return session.getSessionAttributes(); + } + + /** + * Must not take any parameters + */ + public void persist() { + session.getSessionAttributes().putAll(this.pageAttributes); + + sessionService.persist(session); + } + + public UserService getUserService() { + return userService; + } + + public FacesService getFacesService() { + return facesService; + } + + public AppConfiguration getAppConfiguration() { + return appConfiguration; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/DynamicClientRegistrationContext.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/DynamicClientRegistrationContext.java new file mode 100644 index 00000000..2f524afa --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/DynamicClientRegistrationContext.java @@ -0,0 +1,56 @@ +package org.gluu.oxauth.service.external.context; + +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.oxauth.model.jwt.Jwt; +import org.json.JSONObject; + +import javax.servlet.http.HttpServletRequest; + +/** + * @author Yuriy Zabrovarnyy + */ +public class DynamicClientRegistrationContext extends ExternalScriptContext { + + private CustomScriptConfiguration script; + private JSONObject registerRequest; + private Jwt softwareStatement; + + public DynamicClientRegistrationContext(HttpServletRequest httpRequest, JSONObject registerRequest, CustomScriptConfiguration script) { + super(httpRequest); + this.script = script; + this.registerRequest = registerRequest; + } + + public Jwt getSoftwareStatement() { + return softwareStatement; + } + + public void setSoftwareStatement(Jwt softwareStatement) { + this.softwareStatement = softwareStatement; + } + + public CustomScriptConfiguration getScript() { + return script; + } + + public void setScript(CustomScriptConfiguration script) { + this.script = script; + } + + public JSONObject getRegisterRequest() { + return registerRequest; + } + + public void setRegisterRequest(JSONObject registerRequest) { + this.registerRequest = registerRequest; + } + + @Override + public String toString() { + return "DynamicClientRegistrationContext{" + + "softwareStatement=" + softwareStatement + + "registerRequest=" + registerRequest + + "script=" + script + + "} " + super.toString(); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/DynamicScopeExternalContext.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/DynamicScopeExternalContext.java new file mode 100644 index 00000000..7b0ed659 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/DynamicScopeExternalContext.java @@ -0,0 +1,68 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service.external.context; + +import org.gluu.oxauth.model.common.IAuthorizationGrant; +import org.gluu.oxauth.model.common.User; +import org.gluu.oxauth.model.token.JsonWebResponse; +import org.oxauth.persistence.model.Scope; + +import java.util.ArrayList; +import java.util.List; + +/** + * Holds object required in dynamic scope custom scripts + * + * @author Yuriy Movchan Date: 07/01/2015 + */ + +public class DynamicScopeExternalContext extends ExternalScriptContext { + + private List dynamicScopes; + private JsonWebResponse jsonWebResponse; + private IAuthorizationGrant authorizationGrant; + + public DynamicScopeExternalContext(List dynamicScopes, JsonWebResponse jsonWebResponse, IAuthorizationGrant authorizationGrant) { + super(null); + + this.dynamicScopes = dynamicScopes; + this.jsonWebResponse = jsonWebResponse; + this.authorizationGrant = authorizationGrant; + } + + /** + * This method is used by scripts. + * @return dynamic scopes as string + * + */ + public List getDynamicScopes() { + List scopes = new ArrayList(); + if (dynamicScopes != null) { + for (Scope scope : dynamicScopes) { + scopes.add(scope.getId()); + } + } + return scopes; + } + + public List getScopes() { + return dynamicScopes; + } + + public JsonWebResponse getJsonWebResponse() { + return jsonWebResponse; + } + + public IAuthorizationGrant getAuthorizationGrant() { + return authorizationGrant; + } + + public User getUser() { + return authorizationGrant.getUser(); + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/EndSessionContext.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/EndSessionContext.java new file mode 100644 index 00000000..71eef38c --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/EndSessionContext.java @@ -0,0 +1,56 @@ +package org.gluu.oxauth.service.external.context; + +import com.google.common.collect.Sets; +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.oxauth.model.session.SessionId; + +import javax.servlet.http.HttpServletRequest; +import java.util.Set; + +/** + * @author Yuriy Zabrovarnyy + */ +public class EndSessionContext extends ExternalScriptContext { + + private CustomScriptConfiguration script; + private final Set frontchannelLogoutUris; + private final String postLogoutRedirectUri; + private SessionId sessionId; + + public EndSessionContext(HttpServletRequest httpRequest, Set frontchannelLogoutUris, String postLogoutRedirectUri, SessionId sessionId) { + super(httpRequest); + this.frontchannelLogoutUris = frontchannelLogoutUris; + this.postLogoutRedirectUri = postLogoutRedirectUri; + this.sessionId = sessionId; + } + + public SessionId getSessionId() { + return sessionId; + } + + public CustomScriptConfiguration getScript() { + return script; + } + + public void setScript(CustomScriptConfiguration script) { + this.script = script; + } + + public Set getFrontchannelLogoutUris() { + return Sets.newHashSet(frontchannelLogoutUris); + } + + public String getPostLogoutRedirectUri() { + return postLogoutRedirectUri; + } + + @Override + public String toString() { + return "EndSessionContext{" + + "script=" + (script != null ? script.getName() : "") + + ", frontchannelLogoutUris=" + frontchannelLogoutUris + + ", postLogoutRedirectUri='" + postLogoutRedirectUri + '\'' + + ", sessionId='" + sessionId + '\'' + + "} " + super.toString(); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ExternalCibaEndUserNotificationContext.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ExternalCibaEndUserNotificationContext.java new file mode 100644 index 00000000..8a5cbfd9 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ExternalCibaEndUserNotificationContext.java @@ -0,0 +1,63 @@ +package org.gluu.oxauth.service.external.context; + +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.service.ciba.CibaEncryptionService; + +/** + * @author Milton BO + */ +public class ExternalCibaEndUserNotificationContext { + + private final AppConfiguration appConfiguration; + private final CibaEncryptionService encryptionService; + private final String scope; + private final String acrValues; + private final String authReqId; + private final String deviceRegistrationToken; + + public ExternalCibaEndUserNotificationContext(String scope, String acrValues, String authReqId, + String deviceRegistrationToken, AppConfiguration appConfiguration, + CibaEncryptionService encryptionService) { + this.appConfiguration = appConfiguration; + this.scope = scope; + this.acrValues = acrValues; + this.authReqId = authReqId; + this.deviceRegistrationToken = deviceRegistrationToken; + this.encryptionService = encryptionService; + } + + public AppConfiguration getAppConfiguration() { + return appConfiguration; + } + + public String getScope() { + return scope; + } + + public String getAcrValues() { + return acrValues; + } + + public String getAuthReqId() { + return authReqId; + } + + public String getDeviceRegistrationToken() { + return deviceRegistrationToken; + } + + public CibaEncryptionService getEncryptionService() { + return encryptionService; + } + + @Override + public String toString() { + return "ExternalCibaEndUserNotificationContext{" + + ", scope='" + scope + '\'' + + ", acrValues='" + acrValues + '\'' + + ", authReqId='" + authReqId + '\'' + + ", deviceRegistrationToken='" + deviceRegistrationToken + '\'' + + '}'; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ExternalIntrospectionContext.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ExternalIntrospectionContext.java new file mode 100644 index 00000000..b3fa0faf --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ExternalIntrospectionContext.java @@ -0,0 +1,82 @@ +package org.gluu.oxauth.service.external.context; + +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.oxauth.model.common.AuthorizationGrant; +import org.gluu.oxauth.model.common.User; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.service.AttributeService; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author Yuriy Zabrovarnyy + */ +public class ExternalIntrospectionContext extends ExternalScriptContext { + + private final AuthorizationGrant tokenGrant; + private final AppConfiguration appConfiguration; + private final AttributeService attributeService; + + private CustomScriptConfiguration script; + private Jwt accessTokenAsJwt; + private boolean tranferIntrospectionPropertiesIntoJwtClaims = true; + private AuthorizationGrant grantOfIntrospectionToken; + + public ExternalIntrospectionContext(AuthorizationGrant tokenGrant, HttpServletRequest httpRequest, HttpServletResponse httpResponse, + AppConfiguration appConfiguration, AttributeService attributeService) { + super(httpRequest, httpResponse); + this.tokenGrant = tokenGrant; + this.appConfiguration = appConfiguration; + this.attributeService = attributeService; + } + + public AuthorizationGrant getTokenGrant() { + return tokenGrant; + } + + public AppConfiguration getAppConfiguration() { + return appConfiguration; + } + + public AttributeService getAttributeService() { + return attributeService; + } + + public CustomScriptConfiguration getScript() { + return script; + } + + public void setScript(CustomScriptConfiguration script) { + this.script = script; + } + + public Jwt getAccessTokenAsJwt() { + return accessTokenAsJwt; + } + + public void setAccessTokenAsJwt(Jwt accessTokenAsJwt) { + this.accessTokenAsJwt = accessTokenAsJwt; + } + + public boolean isTranferIntrospectionPropertiesIntoJwtClaims() { + return tranferIntrospectionPropertiesIntoJwtClaims; + } + + public void setTranferIntrospectionPropertiesIntoJwtClaims(boolean tranferIntrospectionPropertiesIntoJwtClaims) { + this.tranferIntrospectionPropertiesIntoJwtClaims = tranferIntrospectionPropertiesIntoJwtClaims; + } + + public AuthorizationGrant getGrantOfIntrospectionToken() { + return grantOfIntrospectionToken; + } + + public void setGrantOfIntrospectionToken(AuthorizationGrant grantOfIntrospectionToken) { + this.grantOfIntrospectionToken = grantOfIntrospectionToken; + } + + public User getUser() { + return grantOfIntrospectionToken != null ? grantOfIntrospectionToken.getUser() : null; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ExternalPostAuthnContext.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ExternalPostAuthnContext.java new file mode 100644 index 00000000..c9852e49 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ExternalPostAuthnContext.java @@ -0,0 +1,49 @@ +package org.gluu.oxauth.service.external.context; + +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.session.SessionId; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author Yuriy Zabrovarnyy + */ +public class ExternalPostAuthnContext extends ExternalScriptContext { + + private final Client client; + private final SessionId session; + private CustomScriptConfiguration script; + + public ExternalPostAuthnContext(Client client, SessionId session, HttpServletRequest httpRequest, HttpServletResponse httpResponse) { + super(httpRequest, httpResponse); + this.client = client; + this.session = session; + } + + public CustomScriptConfiguration getScript() { + return script; + } + + public void setScript(CustomScriptConfiguration script) { + this.script = script; + } + + public Client getClient() { + return client; + } + + public SessionId getSession() { + return session; + } + + @Override + public String toString() { + return "ExternalPostAuthnContext{" + + "client=" + client + + ", session=" + (session != null ? session.getId() : "") + + ", script=" + script + + "} " + super.toString(); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ExternalResourceOwnerPasswordCredentialsContext.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ExternalResourceOwnerPasswordCredentialsContext.java new file mode 100644 index 00000000..8cfa436f --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ExternalResourceOwnerPasswordCredentialsContext.java @@ -0,0 +1,67 @@ +package org.gluu.oxauth.service.external.context; + +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.service.AttributeService; +import org.gluu.oxauth.service.common.UserService; +import org.gluu.oxauth.model.common.User; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author Yuriy Zabrovarnyy + */ +public class ExternalResourceOwnerPasswordCredentialsContext extends ExternalScriptContext { + + private final AppConfiguration appConfiguration; + private final AttributeService attributeService; + private final UserService userService; + + private User user; + private CustomScriptConfiguration script; + + public ExternalResourceOwnerPasswordCredentialsContext(HttpServletRequest httpRequest, HttpServletResponse httpResponse, + AppConfiguration appConfiguration, AttributeService attributeService, UserService userService) { + super(httpRequest, httpResponse); + this.appConfiguration = appConfiguration; + this.attributeService = attributeService; + this.userService = userService; + } + + public void setUser(User user) { + this.user = user; + } + + public AppConfiguration getAppConfiguration() { + return appConfiguration; + } + + public AttributeService getAttributeService() { + return attributeService; + } + + public UserService getUserService() { + return userService; + } + + public User getUser() { + return user; + } + + public CustomScriptConfiguration getScript() { + return script; + } + + public void setScript(CustomScriptConfiguration script) { + this.script = script; + } + + @Override + public String toString() { + return "ExternalResourceOwnerPasswordCredentialsContext{" + + "user=" + user + + "script=" + script + + "} " + super.toString(); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ExternalScriptContext.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ExternalScriptContext.java new file mode 100644 index 00000000..cae07b12 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ExternalScriptContext.java @@ -0,0 +1,105 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service.external.context; + +import org.apache.commons.net.util.SubnetUtils; +import org.gluu.oxauth.model.util.Util; +import org.gluu.oxauth.util.ServerUtil; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.persist.exception.EntryPersistenceException; +import org.gluu.persist.model.base.CustomEntry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +/** + * Holds object required in custom scripts + * + * @author Yuriy Movchan Date: 07/01/2015 + */ + +public class ExternalScriptContext extends org.gluu.service.external.context.ExternalScriptContext { + + private static final Logger log = LoggerFactory.getLogger(ExternalScriptContext.class); + + private final PersistenceEntryManager ldapEntryManager; + + private WebApplicationException webApplicationException; + + public ExternalScriptContext(HttpServletRequest httpRequest) { + this(httpRequest, null); + } + + public ExternalScriptContext(HttpServletRequest httpRequest, HttpServletResponse httpResponse) { + super(httpRequest, httpResponse); + this.ldapEntryManager = ServerUtil.getLdapManager(); + } + + public PersistenceEntryManager getPersistenceEntryManager() { + return ldapEntryManager; + } + + public boolean isInNetwork(String cidrNotation) { + final String ip = getIpAddress(); + if (Util.allNotBlank(ip, cidrNotation)) { + final SubnetUtils utils = new SubnetUtils(cidrNotation); + return utils.getInfo().isInRange(ip); + } + return false; + } + + protected CustomEntry getEntryByDn(String dn, String... ldapReturnAttributes) { + try { + return ldapEntryManager.find(dn, CustomEntry.class, ldapReturnAttributes); + } catch (EntryPersistenceException epe) { + log.error("Failed to find entry '{}'", dn); + } + + return null; + } + + protected String getEntryAttributeValue(String dn, String attributeName) { + final CustomEntry entry = getEntryByDn(dn, attributeName); + if (entry != null) { + final String attributeValue = entry.getCustomAttributeValue(attributeName); + return attributeValue; + } + + return ""; + } + + public WebApplicationException getWebApplicationException() { + return webApplicationException; + } + + public void setWebApplicationException(WebApplicationException webApplicationException) { + this.webApplicationException = webApplicationException; + } + + public WebApplicationException createWebApplicationException(Response response) { + return new WebApplicationException(response); + } + + public WebApplicationException createWebApplicationException(int status, String entity) { + this.webApplicationException = new WebApplicationException(Response + .status(status) + .entity(entity) + .type(MediaType.APPLICATION_JSON_TYPE) + .build()); + return this.webApplicationException; + } + + public void throwWebApplicationExceptionIfSet() { + if (webApplicationException != null) + throw webApplicationException; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ExternalUmaRptClaimsContext.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ExternalUmaRptClaimsContext.java new file mode 100644 index 00000000..cda31491 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ExternalUmaRptClaimsContext.java @@ -0,0 +1,56 @@ +package org.gluu.oxauth.service.external.context; + +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.oxauth.model.common.ExecutionContext; +import org.gluu.oxauth.model.registration.Client; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author Yuriy Zabrovarnyy + */ +public class ExternalUmaRptClaimsContext extends ExternalScriptContext { + + private final Client client; + private CustomScriptConfiguration script; + private boolean isTranferPropertiesIntoJwtClaims = true; + + public ExternalUmaRptClaimsContext(Client client, HttpServletRequest httpRequest, HttpServletResponse httpResponse) { + super(httpRequest, httpResponse); + this.client = client; + } + + public ExternalUmaRptClaimsContext(ExecutionContext executionContext) { + this(executionContext.getClient(), executionContext.getHttpRequest(), executionContext.getHttpResponse()); + } + + public Client getClient() { + return client; + } + + public CustomScriptConfiguration getScript() { + return script; + } + + public void setScript(CustomScriptConfiguration script) { + this.script = script; + } + + public boolean isTranferPropertiesIntoJwtClaims() { + return isTranferPropertiesIntoJwtClaims; + } + + public void setTranferPropertiesIntoJwtClaims(boolean tranferPropertiesIntoJwtClaims) { + isTranferPropertiesIntoJwtClaims = tranferPropertiesIntoJwtClaims; + } + + @Override + public String toString() { + return "ExternalUmaRptClaimsContext{" + + "client=" + client + + ", script=" + script + + ", isTranferPropertiesIntoJwtClaims=" + isTranferPropertiesIntoJwtClaims + + "} " + super.toString(); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ExternalUpdateTokenContext.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ExternalUpdateTokenContext.java new file mode 100644 index 00000000..608a64b5 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/ExternalUpdateTokenContext.java @@ -0,0 +1,131 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2021, Gluu + */ + +package org.gluu.oxauth.service.external.context; + +import com.google.common.collect.Lists; +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.oxauth.model.common.AccessToken; +import org.gluu.oxauth.model.common.AuthorizationGrant; +import org.gluu.oxauth.model.common.ExecutionContext; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaims; +import org.gluu.oxauth.model.jwt.JwtHeader; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.token.JwtSigner; +import org.gluu.oxauth.service.AttributeService; + +import javax.servlet.http.HttpServletRequest; +import java.util.Set; + +/** + * @author Yuriy Movchan + */ +public class ExternalUpdateTokenContext extends ExternalScriptContext { + + private final Client client; + private final AuthorizationGrant grant; + + private ExecutionContext executionContext; + private CustomScriptConfiguration script; + private JwtSigner jwtSigner; + + private final AppConfiguration appConfiguration; + private final AttributeService attributeService; + + public ExternalUpdateTokenContext(HttpServletRequest httpRequest, AuthorizationGrant grant, + Client client, AppConfiguration appConfiguration, AttributeService attributeService) { + super(httpRequest); + this.client = client; + this.grant = grant; + this.appConfiguration = appConfiguration; + this.attributeService = attributeService; + } + + public static ExternalUpdateTokenContext of(ExecutionContext executionContext) { + return of(executionContext, null); + } + + public static ExternalUpdateTokenContext of(ExecutionContext executionContext, JwtSigner jwtSigner) { + ExternalUpdateTokenContext context = new ExternalUpdateTokenContext(executionContext.getHttpRequest(), executionContext.getGrant(), executionContext.getClient(), executionContext.getAppConfiguration(), executionContext.getAttributeService()); + context.setExecutionContext(executionContext); + context.setJwtSigner(jwtSigner); + return context; + } + + // Usually expected to be called in : "def modifyAccessToken(self, accessToken, context):" + public void overwriteAccessTokenScopes(AccessToken accessToken, Set newScopes) { + if (grant == null) { + return; + } + + grant.setScopes(newScopes); + + final Jwt jwt = getJwt(); + if (jwt != null) { + jwt.getClaims().setClaim("scope", Lists.newArrayList(newScopes)); + } + } + + public JwtClaims getClaims() { + Jwt jwt = getJwt(); + return jwt != null ? jwt.getClaims() : null; + } + + public JwtHeader getHeader() { + Jwt jwt = getJwt(); + return jwt != null ? jwt.getHeader() : null; + } + + public Jwt getJwt() { + return jwtSigner != null ? jwtSigner.getJwt() : null; + } + + private boolean isValidJwt(String jwt) { + return Jwt.parseSilently(jwt) != null; + } + + public CustomScriptConfiguration getScript() { + return script; + } + + public void setScript(CustomScriptConfiguration script) { + this.script = script; + } + + public Client getClient() { + return client; + } + + public AuthorizationGrant getGrant() { + return grant; + } + + public AppConfiguration getAppConfiguration() { + return appConfiguration; + } + + public AttributeService getAttributeService() { + return attributeService; + } + + public ExecutionContext getExecutionContext() { + return executionContext; + } + + public void setExecutionContext(ExecutionContext executionContext) { + this.executionContext = executionContext; + } + + public JwtSigner getJwtSigner() { + return jwtSigner; + } + + public void setJwtSigner(JwtSigner jwtSigner) { + this.jwtSigner = jwtSigner; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/RevokeTokenContext.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/RevokeTokenContext.java new file mode 100644 index 00000000..0fd74bd6 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/RevokeTokenContext.java @@ -0,0 +1,54 @@ +package org.gluu.oxauth.service.external.context; + +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.oxauth.model.common.AuthorizationGrant; +import org.gluu.oxauth.model.registration.Client; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.Response; + +/** + * @author Yuriy Zabrovarnyy + */ +public class RevokeTokenContext extends ExternalScriptContext { + + private final Client client; + private final AuthorizationGrant grant; + private final Response.ResponseBuilder responseBuilder; + private CustomScriptConfiguration script; + + public RevokeTokenContext(HttpServletRequest httpRequest, Client client, AuthorizationGrant grant, Response.ResponseBuilder responseBuilder) { + super(httpRequest); + this.client = client; + this.grant = grant; + this.responseBuilder = responseBuilder; + } + + public Client getClient() { + return client; + } + + public AuthorizationGrant getGrant() { + return grant; + } + + public Response.ResponseBuilder getResponseBuilder() { + return responseBuilder; + } + + public CustomScriptConfiguration getScript() { + return script; + } + + public void setScript(CustomScriptConfiguration script) { + this.script = script; + } + + @Override + public String toString() { + return "RevokeTokenContext{" + + "clientId=" + (client != null ? client.getClientId() : "") + + ", script=" + (script != null ? script.getName() : "") + + "} " + super.toString(); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/SpontaneousScopeExternalContext.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/SpontaneousScopeExternalContext.java new file mode 100644 index 00000000..9d7bc6b8 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/context/SpontaneousScopeExternalContext.java @@ -0,0 +1,57 @@ +package org.gluu.oxauth.service.external.context; + +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.service.SpontaneousScopeService; + +import java.util.Set; + +public class SpontaneousScopeExternalContext extends ExternalScriptContext { + + private Client client; + private String scopeRequested; + private Set grantedScopes; + private SpontaneousScopeService spontaneousScopeService; + private boolean allowSpontaneousScopePersistence = true; + + public SpontaneousScopeExternalContext(Client client, String scopeRequested, Set grantedScopes, SpontaneousScopeService spontaneousScopeService) { + super(null, null); + this.client = client; + this.scopeRequested = scopeRequested; + this.grantedScopes = grantedScopes; + this.spontaneousScopeService = spontaneousScopeService; + } + + public Client getClient() { + return client; + } + + public String getScopeRequested() { + return scopeRequested; + } + + public Set getGrantedScopes() { + return grantedScopes; + } + + public SpontaneousScopeService getSpontaneousScopeService() { + return spontaneousScopeService; + } + + public boolean isAllowSpontaneousScopePersistence() { + return allowSpontaneousScopePersistence; + } + + public void setAllowSpontaneousScopePersistence(boolean allowSpontaneousScopePersistence) { + this.allowSpontaneousScopePersistence = allowSpontaneousScopePersistence; + } + + @Override + public String toString() { + return "SpontaneousScopeExternalContext{" + + "scopeRequested='" + scopeRequested + '\'' + + ", grantedScopes=" + grantedScopes + + ", contextVariables=" + super.getContextVariables() + + ", allowSpontaneousScopePersistence=" + allowSpontaneousScopePersistence + + '}'; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/internal/InternalDefaultPersonAuthenticationType.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/internal/InternalDefaultPersonAuthenticationType.java new file mode 100644 index 00000000..26347759 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/internal/InternalDefaultPersonAuthenticationType.java @@ -0,0 +1,59 @@ +package org.gluu.oxauth.service.external.internal; + +import java.util.Map; + +import javax.inject.Inject; + +import org.gluu.model.SimpleCustomProperty; +import org.gluu.model.custom.script.type.auth.DummyPersonAuthenticationType; +import org.gluu.model.security.Credentials; +import org.gluu.oxauth.service.AuthenticationService; + +import javax.enterprise.context.ApplicationScoped; + +/** + * Wrapper to call internal authentication method + * + * @author Yuriy Movchan Date: 06/04/2015 + */ +@ApplicationScoped +public class InternalDefaultPersonAuthenticationType extends DummyPersonAuthenticationType { + + @Inject + private AuthenticationService authenticationService; + + @Inject + private Credentials credentials; + + public InternalDefaultPersonAuthenticationType() { + } + + @Override + public boolean authenticate(Map configurationAttributes, Map requestParameters, int step) { + if (!credentials.isSet()) { + return false; + } + + return authenticationService.authenticate(credentials.getUsername(), credentials.getPassword()); + } + + @Override + public boolean prepareForStep(Map configurationAttributes, Map requestParameters, int step) { + if (step == 1) { + return true; + } + + return super.prepareForStep(configurationAttributes, requestParameters, step); + } + + @Override + public int getCountAuthenticationSteps(Map configurationAttributes) { + return 1; + } + + @Override + public boolean logout(Map configurationAttributes, Map requestParameters) { + return true; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/session/SessionEvent.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/session/SessionEvent.java new file mode 100644 index 00000000..0bb32e96 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/session/SessionEvent.java @@ -0,0 +1,66 @@ +package org.gluu.oxauth.service.external.session; + +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.oxauth.model.session.SessionId; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author Yuriy Zabrovarnyy + */ +public class SessionEvent { + + private final SessionEventType type; + private final SessionId sessionId; + private CustomScriptConfiguration scriptConfiguration; + private HttpServletRequest httpRequest; + private HttpServletResponse httpResponse; + + public SessionEvent(SessionEventType type, SessionId sessionId) { + this.type = type; + this.sessionId = sessionId; + } + + public SessionEventType getType() { + return type; + } + + public SessionId getSessionId() { + return sessionId; + } + + public CustomScriptConfiguration getScriptConfiguration() { + return scriptConfiguration; + } + + public void setScriptConfiguration(CustomScriptConfiguration scriptConfiguration) { + this.scriptConfiguration = scriptConfiguration; + } + + public HttpServletRequest getHttpRequest() { + return httpRequest; + } + + public SessionEvent setHttpRequest(HttpServletRequest httpRequest) { + this.httpRequest = httpRequest; + return this; + } + + public HttpServletResponse getHttpResponse() { + return httpResponse; + } + + public SessionEvent setHttpResponse(HttpServletResponse httpResponse) { + this.httpResponse = httpResponse; + return this; + } + + @Override + public String toString() { + return "SessionEvent{" + + "type=" + type + + ", sessionId=" + sessionId.getId() + + '}'; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/session/SessionEventType.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/session/SessionEventType.java new file mode 100644 index 00000000..dcbef411 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/external/session/SessionEventType.java @@ -0,0 +1,11 @@ +package org.gluu.oxauth.service.external.session; + +/** + * @author Yuriy Zabrovarnyy + */ +public enum SessionEventType { + AUTHENTICATED, + UNAUTHENTICATED, + UPDATED, + GONE // it can be time out, or expired or ended by /end_session endpoint +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/ApplicationService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/ApplicationService.java new file mode 100644 index 00000000..2158f053 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/ApplicationService.java @@ -0,0 +1,77 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service.fido.u2f; + +import java.net.URI; +import java.net.URISyntaxException; + +import javax.enterprise.context.ApplicationScoped; + +import org.gluu.net.InetAddressUtility; +import org.gluu.oxauth.exception.fido.u2f.BadConfigurationException; + +/** + * Provides operations with U2F applications + * + * @author Yuriy Movchan Date: 05/19/2015 + */ +@ApplicationScoped +public class ApplicationService { + + private boolean validateApplication = true; + + public boolean isValidateApplication() { + return validateApplication; + } + + /** + * Throws {@link BadConfigurationException} if the given App ID is found to + * be incompatible with the U2F specification or any major U2F Client + * implementation. + * + * @param appId + * the App ID to be validated + */ + public void checkIsValid(String appId) { + if (!appId.contains(":")) { + throw new BadConfigurationException("App ID does not look like a valid facet or URL. Web facets must start with 'https://'."); + } + + if (appId.startsWith("http:")) { + throw new BadConfigurationException("HTTP is not supported for App IDs. Use HTTPS instead."); + } + + if (appId.startsWith("https://")) { + URI url = checkValidUrl(appId); + checkPathIsNotSlash(url); +// checkNotIpAddress(url); + } + } + + private void checkPathIsNotSlash(URI url) { + if ("/".equals(url.getPath())) { + throw new BadConfigurationException( + "The path of the URL set as App ID is '/'. This is probably not what you want -- remove the trailing slash of the App ID URL."); + } + } + + private URI checkValidUrl(String appId) { + URI url = null; + try { + url = new URI(appId); + } catch (URISyntaxException e) { + throw new BadConfigurationException("App ID looks like a HTTPS URL, but has syntax errors.", e); + } + return url; + } + + private void checkNotIpAddress(URI url) { + if (InetAddressUtility.isIpAddress(url.getAuthority()) || (url.getHost() != null && InetAddressUtility.isIpAddress(url.getHost()))) { + throw new BadConfigurationException("App ID must not be an IP-address, since it is not supported. Use a host name instead."); + } + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/AuthenticationService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/AuthenticationService.java new file mode 100644 index 00000000..280fb37b --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/AuthenticationService.java @@ -0,0 +1,338 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service.fido.u2f; + +import java.util.ArrayList; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.Set; +import java.util.TimeZone; +import java.util.UUID; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Produces; +import javax.inject.Inject; +import javax.inject.Named; + +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Hex; +import org.gluu.oxauth.crypto.random.ChallengeGenerator; +import org.gluu.oxauth.crypto.signature.SHA256withECDSASignatureVerification; +import org.gluu.oxauth.exception.fido.u2f.DeviceCompromisedException; +import org.gluu.oxauth.exception.fido.u2f.InvalidKeyHandleDeviceException; +import org.gluu.oxauth.exception.fido.u2f.NoEligableDevicesException; +import org.gluu.oxauth.model.fido.u2f.AuthenticateRequestMessageLdap; +import org.gluu.oxauth.model.fido.u2f.DeviceRegistration; +import org.gluu.oxauth.model.fido.u2f.DeviceRegistrationResult; +import org.gluu.oxauth.model.fido.u2f.exception.BadInputException; +import org.gluu.oxauth.model.fido.u2f.message.RawAuthenticateResponse; +import org.gluu.oxauth.model.fido.u2f.protocol.AuthenticateRequest; +import org.gluu.oxauth.model.fido.u2f.protocol.AuthenticateRequestMessage; +import org.gluu.oxauth.model.fido.u2f.protocol.AuthenticateResponse; +import org.gluu.oxauth.model.fido.u2f.protocol.ClientData; +import org.gluu.oxauth.model.fido.u2f.protocol.DeviceData; +import org.gluu.oxauth.model.fido.u2f.protocol.DeviceNotificationConf; +import org.gluu.oxauth.model.util.Base64Util; +import org.gluu.oxauth.service.common.UserService; +import org.gluu.oxauth.util.ServerUtil; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.persist.reflect.property.Setter; +import org.gluu.persist.reflect.util.ReflectHelper; +import org.gluu.search.filter.Filter; +import org.gluu.util.StringHelper; +import org.gluu.util.io.ByteDataInputStream; +import org.gluu.util.security.SecurityProviderUtility; +import org.slf4j.Logger; + +import com.fasterxml.jackson.core.JsonProcessingException; + +import org.gluu.oxauth.model.config.StaticConfiguration; + +/** + * Provides operations with U2F authentication request + * + * @author Yuriy Movchan + * @version August 9, 2017 + */ +@ApplicationScoped +@Named("u2fAuthenticationService") +public class AuthenticationService extends RequestService { + + @Inject + private Logger log; + + @Inject + private PersistenceEntryManager ldapEntryManager; + + @Inject + private ApplicationService applicationService; + + @Inject + private RawAuthenticationService rawAuthenticationService; + + @Inject + private ClientDataValidationService clientDataValidationService; + + @Inject + private DeviceRegistrationService deviceRegistrationService; + + @Inject + private UserService userService; + + @Inject + @Named("randomChallengeGenerator") + private ChallengeGenerator challengeGenerator; + + @Inject + private StaticConfiguration staticConfiguration; + + @Produces @ApplicationScoped @Named("sha256withECDSASignatureVerification") + public SHA256withECDSASignatureVerification getBouncyCastleSignatureVerification() { + return new SHA256withECDSASignatureVerification(); + } + + public AuthenticateRequestMessage buildAuthenticateRequestMessage(String appId, String userInum) throws BadInputException, NoEligableDevicesException { + if (applicationService.isValidateApplication()) { + applicationService.checkIsValid(appId); + } + + List authenticateRequests = new ArrayList(); + byte[] challenge = challengeGenerator.generateChallenge(); + + List deviceRegistrations = deviceRegistrationService.findUserDeviceRegistrations(userInum, appId); + for (DeviceRegistration deviceRegistration : deviceRegistrations) { + if (!deviceRegistration.isCompromised()) { + AuthenticateRequest request; + try { + request = startAuthentication(appId, deviceRegistration, challenge); + authenticateRequests.add(request); + } catch (DeviceCompromisedException ex) { + log.error("Faield to authenticate device", ex); + } + } + } + + if (authenticateRequests.isEmpty()) { + if (deviceRegistrations.isEmpty()) { + throw new NoEligableDevicesException(deviceRegistrations, "No devices registrered"); + } else { + throw new NoEligableDevicesException(deviceRegistrations, "All devices compromised"); + } + } + + return new AuthenticateRequestMessage(authenticateRequests); + } + + public AuthenticateRequest startAuthentication(String appId, DeviceRegistration device) throws DeviceCompromisedException { + return startAuthentication(appId, device, challengeGenerator.generateChallenge()); + } + + public AuthenticateRequest startAuthentication(String appId, DeviceRegistration device, byte[] challenge) throws DeviceCompromisedException { + if (device.isCompromised()) { + throw new DeviceCompromisedException(device, "Device has been marked as compromised, cannot authenticate"); + } + + return new AuthenticateRequest(Base64Util.base64urlencode(challenge), appId, device.getKeyHandle()); + } + + public DeviceRegistrationResult finishAuthentication(AuthenticateRequestMessage requestMessage, AuthenticateResponse response, String userInum) + throws BadInputException, DeviceCompromisedException { + return finishAuthentication(requestMessage, response, userInum, null); + } + + public DeviceRegistrationResult finishAuthentication(AuthenticateRequestMessage requestMessage, AuthenticateResponse response, String userInum, Set facets) + throws BadInputException, DeviceCompromisedException { + List deviceRegistrations = deviceRegistrationService.findUserDeviceRegistrations(userInum, requestMessage.getAppId()); + + final AuthenticateRequest request = getAuthenticateRequest(requestMessage, response); + + DeviceRegistration usedDeviceRegistration = null; + for (DeviceRegistration deviceRegistration : deviceRegistrations) { + if (StringHelper.equals(request.getKeyHandle(), deviceRegistration.getKeyHandle())) { + usedDeviceRegistration = deviceRegistration; + break; + } + } + + if (usedDeviceRegistration == null) { + throw new BadInputException("Failed to find DeviceRegistration for the given AuthenticateRequest"); + } + + if (usedDeviceRegistration.isCompromised()) { + throw new DeviceCompromisedException(usedDeviceRegistration, "The device is marked as possibly compromised, and cannot be authenticated"); + } + + ClientData clientData = response.getClientData(); + log.debug("Client data HEX '{}'", Hex.encodeHexString(response.getClientDataRaw().getBytes())); + log.debug("Signature data HEX '{}'", Hex.encodeHexString(response.getSignatureData().getBytes())); + + clientDataValidationService.checkContent(clientData, RawAuthenticationService.SUPPORTED_AUTHENTICATE_TYPES, request.getChallenge(), facets); + + RawAuthenticateResponse rawAuthenticateResponse = rawAuthenticationService.parseRawAuthenticateResponse(response.getSignatureData()); + rawAuthenticationService.checkSignature(request.getAppId(), clientData, rawAuthenticateResponse, + Base64Util.base64urldecode(usedDeviceRegistration.getDeviceRegistrationConfiguration().getPublicKey())); + rawAuthenticateResponse.checkUserPresence(); + + log.debug("Counter in finish authentication request'{}', counter in database '{}'", rawAuthenticateResponse.getCounter(), usedDeviceRegistration.getCounter()); + usedDeviceRegistration.checkAndUpdateCounter(rawAuthenticateResponse.getCounter()); + + String responseDeviceData = response.getDeviceData(); + if (StringHelper.isNotEmpty(responseDeviceData)) { + try { + String responseDeviceDataDecoded = new String(Base64Util.base64urldecode(responseDeviceData)); + DeviceData deviceData = ServerUtil.jsonMapperWithWrapRoot().readValue(responseDeviceDataDecoded, DeviceData.class); + + boolean pushTokenUpdated = !StringHelper.equals(usedDeviceRegistration.getDeviceData().getPushToken(), deviceData.getPushToken()); + if (pushTokenUpdated) { + prepareForPushTokenChange(usedDeviceRegistration); + } + usedDeviceRegistration.setDeviceData(deviceData); + } catch (Exception ex) { + throw new BadInputException(String.format("Device data is invalid: %s", responseDeviceData), ex); + } + } + + usedDeviceRegistration.setLastAccessTime(new Date()); + + deviceRegistrationService.updateDeviceRegistration(userInum, usedDeviceRegistration); + + DeviceRegistrationResult.Status status = DeviceRegistrationResult.Status.APPROVED; + + boolean approved = StringHelper.equals(RawAuthenticationService.AUTHENTICATE_GET_TYPE, clientData.getTyp()); + if (!approved) { + status = DeviceRegistrationResult.Status.CANCELED; + log.debug("Authentication request with keyHandle '{}' was canceled", response.getKeyHandle()); + } + + return new DeviceRegistrationResult(usedDeviceRegistration, status); + } + + private void prepareForPushTokenChange(DeviceRegistration deviceRegistration) { + String deviceNotificationConfString = deviceRegistration.getDeviceNotificationConf(); + if (deviceNotificationConfString == null) { + return; + } + + DeviceNotificationConf deviceNotificationConf = null; + try { + deviceNotificationConf = ServerUtil.jsonMapperWithWrapRoot().readValue(deviceNotificationConfString, DeviceNotificationConf.class); + } catch (Exception ex) { + log.error("Failed to parse device notification configuration '{}'", deviceNotificationConfString); + } + + if (deviceNotificationConf == null) { + return; + } + + String snsEndpointArn = deviceNotificationConf.getSnsEndpointArn(); + if (StringHelper.isEmpty(snsEndpointArn)) { + return; + } + + deviceNotificationConf.setSnsEndpointArn(null); + deviceNotificationConf.setSnsEndpointArnRemove(snsEndpointArn); + List snsEndpointArnHistory = deviceNotificationConf.getSnsEndpointArnHistory(); + if (snsEndpointArnHistory == null) { + snsEndpointArnHistory = new ArrayList<>(); + deviceNotificationConf.setSnsEndpointArnHistory(snsEndpointArnHistory); + } + + snsEndpointArnHistory.add(snsEndpointArn); + + try { + deviceRegistration.setDeviceNotificationConf(ServerUtil.jsonMapperWithUnwrapRoot().writeValueAsString(deviceNotificationConf)); + } catch (Exception ex) { + log.error("Failed to update device notification configuration '{}'", deviceNotificationConf); + } + } + + public AuthenticateRequest getAuthenticateRequest(AuthenticateRequestMessage requestMessage, AuthenticateResponse response) throws BadInputException { + if (!StringHelper.equals(requestMessage.getRequestId(), response.getRequestId())) { + throw new BadInputException("Wrong request for response data"); + } + + for (AuthenticateRequest request : requestMessage.getAuthenticateRequests()) { + if (StringHelper.equals(request.getKeyHandle(), response.getKeyHandle())) { + return request; + } + } + + throw new BadInputException("Responses keyHandle does not match any contained request"); + } + + public void storeAuthenticationRequestMessage(AuthenticateRequestMessage requestMessage, String userInum, String sessionId) { + Date now = new GregorianCalendar(TimeZone.getTimeZone("UTC")).getTime(); + final String authenticateRequestMessageId = UUID.randomUUID().toString(); + + AuthenticateRequestMessageLdap authenticateRequestMessageLdap = new AuthenticateRequestMessageLdap(getDnForAuthenticateRequestMessage(authenticateRequestMessageId), + authenticateRequestMessageId, now, sessionId, userInum, requestMessage); + + ldapEntryManager.persist(authenticateRequestMessageLdap); + } + + public AuthenticateRequestMessage getAuthenticationRequestMessage(String oxId) { + String requestDn = getDnForAuthenticateRequestMessage(oxId); + + AuthenticateRequestMessageLdap authenticateRequestMessageLdap = ldapEntryManager.find(AuthenticateRequestMessageLdap.class, requestDn); + if (authenticateRequestMessageLdap == null) { + return null; + } + + return authenticateRequestMessageLdap.getAuthenticateRequestMessage(); + } + + public AuthenticateRequestMessageLdap getAuthenticationRequestMessageByRequestId(String requestId) { + String baseDn = getDnForAuthenticateRequestMessage(null); + Filter requestIdFilter = Filter.createEqualityFilter("oxRequestId", requestId); + + List authenticateRequestMessagesLdap = ldapEntryManager.findEntries(baseDn, AuthenticateRequestMessageLdap.class, + requestIdFilter); + if ((authenticateRequestMessagesLdap == null) || authenticateRequestMessagesLdap.isEmpty()) { + return null; + } + + return authenticateRequestMessagesLdap.get(0); + } + + public void removeAuthenticationRequestMessage(AuthenticateRequestMessageLdap authenticateRequestMessageLdap) { + removeRequestMessage(authenticateRequestMessageLdap); + } + + public String getUserInumByKeyHandle(String appId, String keyHandle) throws InvalidKeyHandleDeviceException { + if (org.gluu.util.StringHelper.isEmpty(appId) || StringHelper.isEmpty(keyHandle)) { + return null; + } + + List deviceRegistrations = deviceRegistrationService.findDeviceRegistrationsByKeyHandle(appId, keyHandle, "oxId"); + if (deviceRegistrations.isEmpty()) { + throw new InvalidKeyHandleDeviceException(String.format("Failed to find device by keyHandle '%s' in LDAP", keyHandle)); + } + + if (deviceRegistrations.size() != 1) { + throw new BadInputException(String.format("There are '%d' devices with keyHandle '%s' in LDAP", deviceRegistrations.size(), keyHandle)); + } + + DeviceRegistration deviceRegistration = deviceRegistrations.get(0); + + return userService.getUserInumByDn(deviceRegistration.getDn()); + } + + /** + * Build DN string for U2F authentication request + */ + public String getDnForAuthenticateRequestMessage(String oxId) { + final String u2fBaseDn = staticConfiguration.getBaseDn().getU2fBase(); // ou=authentication_requests,ou=u2f,o=gluu + if (StringHelper.isEmpty(oxId)) { + return String.format("ou=authentication_requests,%s", u2fBaseDn); + } + + return String.format("oxid=%s,ou=authentication_requests,%s", oxId, u2fBaseDn); + } + + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/ClientDataValidationService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/ClientDataValidationService.java new file mode 100644 index 00000000..5978075f --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/ClientDataValidationService.java @@ -0,0 +1,80 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service.fido.u2f; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashSet; +import java.util.Set; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.apache.commons.lang.ArrayUtils; +import org.gluu.oxauth.model.fido.u2f.exception.BadInputException; +import org.gluu.oxauth.model.fido.u2f.protocol.ClientData; +import org.slf4j.Logger; + +/** + * Client data validation service + * + * @author Yuriy Movchan Date: 05/20/2015 + */ +@ApplicationScoped +public class ClientDataValidationService { + + @Inject + private Logger log; + + public void checkContent(ClientData clientData, String[] types, String challenge, Set facets) throws BadInputException { + if (!ArrayUtils.contains(types, clientData.getTyp())) { + throw new BadInputException("Bad clientData: wrong typ " + clientData.getTyp()); + } + + if (!challenge.equals(clientData.getChallenge())) { + throw new BadInputException("Bad clientData: wrong challenge"); + } + + if (facets != null && !facets.isEmpty()) { + Set allowedFacets = canonicalizeOrigins(facets); + String canonicalOrigin; + try { + canonicalOrigin = canonicalizeOrigin(clientData.getOrigin()); + } catch (RuntimeException e) { + throw new BadInputException("Bad clientData: Malformed origin", e); + } + verifyOrigin(canonicalOrigin, allowedFacets); + } + } + + private static void verifyOrigin(String origin, Set allowedOrigins) throws BadInputException { + if (!allowedOrigins.contains(origin)) { + throw new BadInputException(origin + " is not a recognized facet for this application"); + } + } + + public static Set canonicalizeOrigins(Set origins) { + Set result = new HashSet(); + for (String origin : origins) { + result.add(canonicalizeOrigin(origin)); + } + return result; + } + + public static String canonicalizeOrigin(String url) { + try { + URI uri = new URI(url); + if (uri.getAuthority() == null) { + return url; + } + return uri.getScheme() + "://" + uri.getAuthority(); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Specified bad origin", e); + } + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/DeviceRegistrationService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/DeviceRegistrationService.java new file mode 100644 index 00000000..78c94311 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/DeviceRegistrationService.java @@ -0,0 +1,241 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service.fido.u2f; + +import org.gluu.oxauth.model.fido.u2f.DeviceRegistration; +import org.gluu.oxauth.model.fido.u2f.DeviceRegistrationStatus; +import org.gluu.oxauth.model.util.Base64Util; +import org.gluu.oxauth.service.common.UserService; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.persist.model.BatchOperation; +import org.gluu.persist.model.SearchScope; +import org.gluu.persist.model.base.SimpleBranch; +import org.gluu.search.filter.Filter; +import org.gluu.util.StringHelper; +import org.slf4j.Logger; +import org.gluu.oxauth.model.config.StaticConfiguration; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * Provides operations with user U2F devices + * + * @author Yuriy Movchan Date: 05/14/2015 + */ +@ApplicationScoped +public class DeviceRegistrationService { + + @Inject + private Logger log; + + @Inject + private PersistenceEntryManager ldapEntryManager; + + @Inject + private UserService userService; + + @Inject + private StaticConfiguration staticConfiguration; + + public void addBranch(final String userInum) { + SimpleBranch branch = new SimpleBranch(); + branch.setOrganizationalUnitName("fido"); + branch.setDn(getBaseDnForU2fUserDevices(userInum)); + + ldapEntryManager.persist(branch); + } + + public boolean containsBranch(final String userInum) { + return ldapEntryManager.contains(getBaseDnForU2fUserDevices(userInum), SimpleBranch.class); + } + + public void prepareBranch(final String userInum) { + String baseDn = getBaseDnForU2fUserDevices(userInum); + if (!ldapEntryManager.hasBranchesSupport(baseDn)) { + return; + } + + // Create U2F user device registrations branch if needed + if (!containsBranch(userInum)) { + addBranch(userInum); + } + } + + public DeviceRegistration findUserDeviceRegistration(String userInum, String deviceId, String... returnAttributes) { + prepareBranch(userInum); + + String deviceDn = getDnForU2fDevice(userInum, deviceId); + + return ldapEntryManager.find(deviceDn, DeviceRegistration.class, returnAttributes); + } + + public List findUserDeviceRegistrations(String userInum, String appId, String ... returnAttributes) { + prepareBranch(userInum); + + String baseDnForU2fDevices = getBaseDnForU2fUserDevices(userInum); + Filter userInumFilter = Filter.createEqualityFilter("personInum", userInum); + Filter appIdFilter = Filter.createEqualityFilter("oxApplication", appId); + + Filter filter = Filter.createANDFilter(userInumFilter, appIdFilter); + + return ldapEntryManager.findEntries(baseDnForU2fDevices, DeviceRegistration.class, filter, returnAttributes); + } + + public List findDeviceRegistrationsByKeyHandle(String appId, String keyHandle, String ... returnAttributes) { + if (org.gluu.util.StringHelper.isEmpty(appId) || StringHelper.isEmpty(keyHandle)) { + return new ArrayList(0); + } + + byte[] keyHandleDecoded = Base64Util.base64urldecode(keyHandle); + + String baseDn = userService.getDnForUser(null); + + Filter deviceObjectClassFilter = Filter.createEqualityFilter("objectClass", "oxDeviceRegistration"); + Filter deviceHashCodeFilter = Filter.createEqualityFilter("oxDeviceHashCode", getKeyHandleHashCode(keyHandleDecoded)); + Filter deviceKeyHandleFilter = Filter.createEqualityFilter("oxDeviceKeyHandle", keyHandle); + Filter appIdFilter = Filter.createEqualityFilter("oxApplication", appId); + + Filter filter = Filter.createANDFilter(deviceObjectClassFilter, deviceHashCodeFilter, appIdFilter, deviceKeyHandleFilter); + + return ldapEntryManager.findEntries(baseDn, DeviceRegistration.class, filter, returnAttributes); + } + + public DeviceRegistration findOneStepUserDeviceRegistration(String deviceId, String... returnAttributes) { + String deviceDn = getDnForOneStepU2fDevice(deviceId); + + return ldapEntryManager.find(DeviceRegistration.class, deviceDn); + } + + public void addUserDeviceRegistration(String userInum, DeviceRegistration deviceRegistration) { + prepareBranch(userInum); + + // Final registration entry should be without expiration + deviceRegistration.clearExpiration(); + + ldapEntryManager.persist(deviceRegistration); + } + + public boolean attachUserDeviceRegistration(String userInum, String oneStepDeviceId) { + String oneStepDeviceDn = getDnForOneStepU2fDevice(oneStepDeviceId); + + // Load temporary stored device registration + DeviceRegistration deviceRegistration = ldapEntryManager.find(DeviceRegistration.class, oneStepDeviceDn); + if (deviceRegistration == null) { + return false; + } + + // Remove temporary stored device registration + removeUserDeviceRegistration(deviceRegistration); + + // Attach user device registration to user + String deviceDn = getDnForU2fDevice(userInum, deviceRegistration.getId()); + + deviceRegistration.setDn(deviceDn); + + // Final registration entry should be without expiration + deviceRegistration.clearExpiration(); + + //fix: personInum should be populated + deviceRegistration.setUserInum(userInum); + + addUserDeviceRegistration(userInum, deviceRegistration); + + return true; + } + + public void addOneStepDeviceRegistration(DeviceRegistration deviceRegistration) { + // Set expiration for one step flow + deviceRegistration.setExpiration(); + + ldapEntryManager.persist(deviceRegistration); + } + + public void updateDeviceRegistration(String userInum, DeviceRegistration deviceRegistration) { + prepareBranch(userInum); + + ldapEntryManager.merge(deviceRegistration); + } + + public void disableUserDeviceRegistration(DeviceRegistration deviceRegistration) { + deviceRegistration.setStatus(DeviceRegistrationStatus.COMPROMISED); + + ldapEntryManager.merge(deviceRegistration); + } + + public void removeUserDeviceRegistration(DeviceRegistration deviceRegistration) { + ldapEntryManager.remove(deviceRegistration); + } + + public List getExpiredDeviceRegistrations(BatchOperation batchOperation, Date expirationDate, String[] returnAttributes, int sizeLimit, int chunkSize) { + final String u2fBaseDn = getDnForOneStepU2fDevice(null); + Filter expirationFilter = Filter.createLessOrEqualFilter("creationDate", ldapEntryManager.encodeTime(u2fBaseDn, expirationDate)); + + List deviceRegistrations = ldapEntryManager.findEntries(u2fBaseDn, DeviceRegistration.class, expirationFilter, SearchScope.SUB, returnAttributes, batchOperation, 0, sizeLimit, chunkSize); + + return deviceRegistrations; + } + + public int getCountDeviceRegistrations(String appId) { + String baseDn = userService.getDnForUser(null); + + Filter appIdFilter = Filter.createEqualityFilter("oxApplication", appId); + Filter activeDeviceFilter = Filter.createEqualityFilter("oxStatus", DeviceRegistrationStatus.ACTIVE.getValue()); + Filter resultFilter = Filter.createANDFilter(appIdFilter, activeDeviceFilter); + + return ldapEntryManager.countEntries(baseDn, DeviceRegistration.class, resultFilter); + } + + /** + * Build DN string for U2F user device + */ + public String getDnForU2fDevice(String userInum, String oxId) { + String baseDnForU2fDevices = getBaseDnForU2fUserDevices(userInum); + if (StringHelper.isEmpty(oxId)) { + return baseDnForU2fDevices; + } + return String.format("oxId=%s,%s", oxId, baseDnForU2fDevices); + } + + public String getBaseDnForU2fUserDevices(String userInum) { + if (StringHelper.isEmpty(userInum)) { + return getDnForOneStepU2fDevice(""); + } + final String userBaseDn = userService.getDnForUser(userInum); // "ou=fido,inum=1234,ou=people,o=gluu" + return String.format("ou=fido,%s", userBaseDn); + } + + public String getDnForOneStepU2fDevice(String deviceRegistrationId) { + final String u2fBaseDn = staticConfiguration.getBaseDn().getU2fBase(); // ou=registered_devices,ou=u2f,o=gluu + if (StringHelper.isEmpty(deviceRegistrationId)) { + return String.format("ou=registered_devices,%s", u2fBaseDn); + } + + return String.format("oxid=%s,ou=registered_devices,%s", deviceRegistrationId, u2fBaseDn); + } + + /* + * Generate non unique hash code to split keyHandle among small cluster with 10-20 elements + * + * This hash code will be used to generate small LDAP indexes + */ + public int getKeyHandleHashCode(byte[] keyHandle) { + int hash = 0; + for (int j = 0; j < keyHandle.length; j++) { + hash += keyHandle[j]*j; + } + + return hash; + } + + public void merge(DeviceRegistration device) { + ldapEntryManager.merge(device); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/RawAuthenticationService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/RawAuthenticationService.java new file mode 100644 index 00000000..f8ebe7aa --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/RawAuthenticationService.java @@ -0,0 +1,86 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service.fido.u2f; + +import java.io.IOException; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.inject.Named; + +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.io.IOUtils; +import org.gluu.oxauth.crypto.signature.SHA256withECDSASignatureVerification; +import org.gluu.oxauth.model.exception.SignatureException; +import org.gluu.oxauth.model.fido.u2f.exception.BadInputException; +import org.gluu.oxauth.model.fido.u2f.message.RawAuthenticateResponse; +import org.gluu.oxauth.model.fido.u2f.protocol.ClientData; +import org.gluu.oxauth.model.util.Base64Util; +import org.gluu.util.io.ByteDataInputStream; +import org.slf4j.Logger; + +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; + +/** + * Provides operations with U2F RAW authentication response + * + * @author Yuriy Movchan Date: 05/20/2015 + */ +@ApplicationScoped +public class RawAuthenticationService { + + public static final String AUTHENTICATE_GET_TYPE = "navigator.id.getAssertion"; + public static final String AUTHENTICATE_CANCEL_TYPE = "navigator.id.cancelAssertion"; + public static final String[] SUPPORTED_AUTHENTICATE_TYPES = new String[] { AUTHENTICATE_GET_TYPE, AUTHENTICATE_CANCEL_TYPE }; + + @Inject + private Logger log; + + @Inject @Named(value = "sha256withECDSASignatureVerification") + private SHA256withECDSASignatureVerification signatureVerification; + + public RawAuthenticateResponse parseRawAuthenticateResponse(String rawDataBase64) { + ByteDataInputStream bis = new ByteDataInputStream(Base64Util.base64urldecode(rawDataBase64)); + try { + return new RawAuthenticateResponse(bis.readSigned(), bis.readInt(), bis.readAll()); + } catch (IOException ex) { + throw new BadInputException("Failed to parse RAW authenticate response", ex); + } finally { + IOUtils.closeQuietly(bis); + } + } + + public void checkSignature(String appId, ClientData clientData, RawAuthenticateResponse rawAuthenticateResponse, byte[] publicKey) throws BadInputException { + String rawClientData = clientData.getRawClientData(); + + byte[] signedBytes = packBytesToSign(signatureVerification.hash(appId), rawAuthenticateResponse.getUserPresence(), + rawAuthenticateResponse.getCounter(), signatureVerification.hash(rawClientData)); + + log.debug("Packed bytes to sign in HEX '{}'", Hex.encodeHexString(signedBytes)); + log.debug("Signature from authentication response in HEX '{}'", Hex.encodeHexString(rawAuthenticateResponse.getSignature())); + try { + boolean isValid = signatureVerification.checkSignature(signatureVerification.decodePublicKey(publicKey), signedBytes, rawAuthenticateResponse.getSignature()); + if (!isValid) { + throw new BadInputException("Signature is not valid"); + } + } catch (SignatureException ex) { + throw new BadInputException("Failed to checkSignature", ex); + } + } + + private byte[] packBytesToSign(byte[] appIdHash, byte userPresence, long counter, byte[] challengeHash) { + ByteArrayDataOutput encoded = ByteStreams.newDataOutput(); + encoded.write(appIdHash); + encoded.write(userPresence); + encoded.writeInt((int) counter); + encoded.write(challengeHash); + + return encoded.toByteArray(); + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/RawRegistrationService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/RawRegistrationService.java new file mode 100644 index 00000000..e0700432 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/RawRegistrationService.java @@ -0,0 +1,111 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service.fido.u2f; + +import java.io.IOException; +import java.io.InputStream; +import java.security.NoSuchProviderException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.inject.Named; + +import org.apache.commons.io.IOUtils; +import org.gluu.oxauth.crypto.signature.SHA256withECDSASignatureVerification; +import org.gluu.oxauth.model.exception.SignatureException; +import org.gluu.oxauth.model.fido.u2f.DeviceRegistration; +import org.gluu.oxauth.model.fido.u2f.exception.BadInputException; +import org.gluu.oxauth.model.fido.u2f.message.RawRegisterResponse; +import org.gluu.oxauth.model.fido.u2f.protocol.ClientData; +import org.gluu.oxauth.model.util.Base64Util; +import org.gluu.util.io.ByteDataInputStream; +import org.gluu.util.security.SecurityProviderUtility; +import org.slf4j.Logger; + +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; + +/** + * Provides operations with U2F RAW registration response + * + * @author Yuriy Movchan Date: 05/20/2015 + */ +@ApplicationScoped +public class RawRegistrationService { + + @Inject + private Logger log; + + @Inject + @Named("sha256withECDSASignatureVerification") + private SHA256withECDSASignatureVerification signatureVerification; + + public static final byte REGISTRATION_RESERVED_BYTE_VALUE = (byte) 0x05; + public static final byte REGISTRATION_SIGNED_RESERVED_BYTE_VALUE = (byte) 0x00; + public static final long INITIAL_DEVICE_COUNTER_VALUE = -1; + + public static final String REGISTER_FINISH_TYPE = "navigator.id.finishEnrollment"; + public static final String REGISTER_CANCEL_TYPE = "navigator.id.cancelEnrollment"; + public static final String[] SUPPORTED_REGISTER_TYPES = new String[] { REGISTER_FINISH_TYPE, REGISTER_CANCEL_TYPE }; + + + public RawRegisterResponse parseRawRegisterResponse(String rawDataBase64) throws BadInputException { + ByteDataInputStream bis = new ByteDataInputStream(Base64Util.base64urldecode(rawDataBase64)); + try { + try { + byte reservedByte = bis.readSigned(); + if (reservedByte != REGISTRATION_RESERVED_BYTE_VALUE) { + throw new BadInputException("Incorrect value of reserved byte. Expected: " + REGISTRATION_RESERVED_BYTE_VALUE + ". Was: " + reservedByte); + } + return new RawRegisterResponse(bis.read(65), bis.read(bis.readUnsigned()), parseDer(bis), bis.readAll()); + } catch (IOException ex) { + throw new BadInputException("Failed to parse RAW register response", ex); + } catch (CertificateException e) { + throw new BadInputException("Malformed attestation certificate", e); + } catch (NoSuchProviderException e) { + throw new BadInputException("Failed to parse attestation certificate", e); + } + } finally { + IOUtils.closeQuietly(bis); + } + } + + public X509Certificate parseDer(InputStream is) throws CertificateException, NoSuchProviderException { + return (X509Certificate) CertificateFactory.getInstance("X.509", SecurityProviderUtility.getBCProvider()).generateCertificate(is); + } + + public void checkSignature(String appId, ClientData clientData, RawRegisterResponse rawRegisterResponse) throws BadInputException { + String rawClientData = clientData.getRawClientData(); + byte[] signedBytes = packBytesToSign(signatureVerification.hash(appId), signatureVerification.hash(rawClientData), rawRegisterResponse.getKeyHandle(), + rawRegisterResponse.getUserPublicKey()); + try { + signatureVerification.checkSignature(rawRegisterResponse.getAttestationCertificate(), signedBytes, rawRegisterResponse.getSignature()); + } catch (SignatureException ex) { + throw new BadInputException("Failed to checkSignature", ex); + } + } + + private byte[] packBytesToSign(byte[] appIdHash, byte[] clientDataHash, byte[] keyHandle, byte[] userPublicKey) { + ByteArrayDataOutput encoded = ByteStreams.newDataOutput(); + encoded.write(REGISTRATION_SIGNED_RESERVED_BYTE_VALUE); + encoded.write(appIdHash); + encoded.write(clientDataHash); + encoded.write(keyHandle); + encoded.write(userPublicKey); + + return encoded.toByteArray(); + } + + public DeviceRegistration createDevice(String userInum, RawRegisterResponse rawRegisterResponse) throws BadInputException { + return new DeviceRegistration(userInum, Base64Util.base64urlencode(rawRegisterResponse.getKeyHandle()), Base64Util.base64urlencode(rawRegisterResponse + .getUserPublicKey()), rawRegisterResponse.getAttestationCertificate(), INITIAL_DEVICE_COUNTER_VALUE); + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/RegistrationService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/RegistrationService.java new file mode 100644 index 00000000..534ef5f5 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/RegistrationService.java @@ -0,0 +1,225 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service.fido.u2f; + +import org.gluu.oxauth.crypto.random.ChallengeGenerator; +import org.gluu.oxauth.exception.fido.u2f.DeviceCompromisedException; +import org.gluu.oxauth.model.fido.u2f.*; +import org.gluu.oxauth.model.fido.u2f.exception.BadInputException; +import org.gluu.oxauth.model.fido.u2f.message.RawRegisterResponse; +import org.gluu.oxauth.model.fido.u2f.protocol.*; +import org.gluu.oxauth.model.util.Base64Util; +import org.gluu.oxauth.service.common.UserService; +import org.gluu.oxauth.util.ServerUtil; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.search.filter.Filter; +import org.gluu.util.StringHelper; +import org.slf4j.Logger; +import org.gluu.oxauth.model.config.StaticConfiguration; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.inject.Named; +import java.util.*; + +/** + * Provides operations with U2F registration requests + * + * @author Yuriy Movchan + * @version August 9, 2017 + */ +@ApplicationScoped +@Named("u2fRegistrationService") +public class RegistrationService extends RequestService { + + @Inject + private Logger log; + + @Inject + private PersistenceEntryManager ldapEntryManager; + + @Inject + private ApplicationService applicationService; + + @Inject + private UserService userService; + + @Inject + private AuthenticationService u2fAuthenticationService; + + @Inject + private RawRegistrationService rawRegistrationService; + + @Inject + private ClientDataValidationService clientDataValidationService; + + @Inject + private DeviceRegistrationService deviceRegistrationService; + + @Inject + @Named("randomChallengeGenerator") + private ChallengeGenerator challengeGenerator; + + @Inject + private StaticConfiguration staticConfiguration; + + public RegisterRequestMessage builRegisterRequestMessage(String appId, String userInum) { + if (applicationService.isValidateApplication()) { + applicationService.checkIsValid(appId); + } + + List authenticateRequests = new ArrayList(); + List registerRequests = new ArrayList(); + + boolean twoStep = StringHelper.isNotEmpty(userInum); + if (twoStep) { + // In two steps we expects not empty userInum + List deviceRegistrations = deviceRegistrationService.findUserDeviceRegistrations(userInum, appId); + for (DeviceRegistration deviceRegistration : deviceRegistrations) { + if (!deviceRegistration.isCompromised()) { + try { + AuthenticateRequest authenticateRequest = u2fAuthenticationService.startAuthentication(appId, deviceRegistration); + authenticateRequests.add(authenticateRequest); + } catch (DeviceCompromisedException ex) { + log.error("Faield to authenticate device", ex); + } + } + } + } + + RegisterRequest request = startRegistration(appId); + registerRequests.add(request); + + return new RegisterRequestMessage(authenticateRequests, registerRequests); + } + + public RegisterRequest startRegistration(String appId) { + return startRegistration(appId, challengeGenerator.generateChallenge()); + } + + public RegisterRequest startRegistration(String appId, byte[] challenge) { + return new RegisterRequest(Base64Util.base64urlencode(challenge), appId); + } + + public DeviceRegistrationResult finishRegistration(RegisterRequestMessage requestMessage, RegisterResponse response, String userInum) throws BadInputException { + return finishRegistration(requestMessage, response, userInum, null); + } + + public DeviceRegistrationResult finishRegistration(RegisterRequestMessage requestMessage, RegisterResponse response, String userInum, Set facets) + throws BadInputException { + RegisterRequest request = requestMessage.getRegisterRequest(); + String appId = request.getAppId(); + + ClientData clientData = response.getClientData(); + clientDataValidationService.checkContent(clientData, RawRegistrationService.SUPPORTED_REGISTER_TYPES, request.getChallenge(), facets); + + RawRegisterResponse rawRegisterResponse = rawRegistrationService.parseRawRegisterResponse(response.getRegistrationData()); + rawRegistrationService.checkSignature(appId, clientData, rawRegisterResponse); + + Date now = new GregorianCalendar(TimeZone.getTimeZone("UTC")).getTime(); + DeviceRegistration deviceRegistration = rawRegistrationService.createDevice(userInum, rawRegisterResponse); + deviceRegistration.setStatus(DeviceRegistrationStatus.ACTIVE); + deviceRegistration.setApplication(appId); + deviceRegistration.setCreationDate(now); + + int keyHandleHashCode = deviceRegistrationService.getKeyHandleHashCode(rawRegisterResponse.getKeyHandle()); + deviceRegistration.setKeyHandleHashCode(keyHandleHashCode); + + final String deviceRegistrationId = String.valueOf(System.currentTimeMillis()); + deviceRegistration.setId(deviceRegistrationId); + + String responseDeviceData = response.getDeviceData(); + if (StringHelper.isNotEmpty(responseDeviceData)) { + try { + String responseDeviceDataDecoded = new String(Base64Util.base64urldecode(responseDeviceData)); + DeviceData deviceData = ServerUtil.jsonMapperWithWrapRoot().readValue(responseDeviceDataDecoded, DeviceData.class); + deviceRegistration.setDeviceData(deviceData); + } catch (Exception ex) { + throw new BadInputException(String.format("Device data is invalid: %s", responseDeviceData), ex); + } + } + + boolean approved = StringHelper.equals(RawRegistrationService.REGISTER_FINISH_TYPE, response.getClientData().getTyp()); + if (!approved) { + log.debug("Registratio request with keyHandle '{}' was canceled", rawRegisterResponse.getKeyHandle()); + return new DeviceRegistrationResult(deviceRegistration, DeviceRegistrationResult.Status.CANCELED); + } + + boolean twoStep = StringHelper.isNotEmpty(userInum); + if (twoStep) { + deviceRegistration.setDn(deviceRegistrationService.getDnForU2fDevice(userInum, deviceRegistrationId)); + + // Check if there is device registration with keyHandle in LDAP already + List foundDeviceRegistrations = deviceRegistrationService.findDeviceRegistrationsByKeyHandle(appId, deviceRegistration.getKeyHandle(), "oxId"); + if (foundDeviceRegistrations.size() != 0) { + throw new BadInputException(String.format("KeyHandle %s was compromised", deviceRegistration.getKeyHandle())); + } + + deviceRegistrationService.addUserDeviceRegistration(userInum, deviceRegistration); + } else { + deviceRegistration.setDn(deviceRegistrationService.getDnForOneStepU2fDevice(deviceRegistrationId)); + deviceRegistrationService.addOneStepDeviceRegistration(deviceRegistration); + } + + return new DeviceRegistrationResult(deviceRegistration, DeviceRegistrationResult.Status.APPROVED); + } + + public RequestMessageLdap storeRegisterRequestMessage(RegisterRequestMessage requestMessage, String userInum, String sessionId) { + Date now = new GregorianCalendar(TimeZone.getTimeZone("UTC")).getTime(); + final String registerRequestMessageId = UUID.randomUUID().toString(); + + RequestMessageLdap registerRequestMessageLdap = new RegisterRequestMessageLdap(getDnForRegisterRequestMessage(registerRequestMessageId), + registerRequestMessageId, now, sessionId, userInum, requestMessage); + + ldapEntryManager.persist(registerRequestMessageLdap); + return registerRequestMessageLdap; + } + + public RegisterRequestMessage getRegisterRequestMessage(String oxId) { + String requestDn = getDnForRegisterRequestMessage(oxId); + + RegisterRequestMessageLdap registerRequestMessageLdap = ldapEntryManager.find(RegisterRequestMessageLdap.class, requestDn); + if (registerRequestMessageLdap == null) { + return null; + } + + return registerRequestMessageLdap.getRegisterRequestMessage(); + } + + public RegisterRequestMessageLdap getRegisterRequestMessageByRequestId(String requestId) { + String baseDn = getDnForRegisterRequestMessage(null); + Filter requestIdFilter = Filter.createEqualityFilter("oxRequestId", requestId); + + List registerRequestMessagesLdap = ldapEntryManager.findEntries(baseDn, RegisterRequestMessageLdap.class, + requestIdFilter); + if ((registerRequestMessagesLdap == null) || registerRequestMessagesLdap.isEmpty()) { + return null; + } + + return registerRequestMessagesLdap.get(0); + } + + public void removeRegisterRequestMessage(RequestMessageLdap registerRequestMessageLdap) { + removeRequestMessage(registerRequestMessageLdap); + } + + /** + * Build DN string for U2F register request + */ + public String getDnForRegisterRequestMessage(String oxId) { + final String u2fBaseDn = staticConfiguration.getBaseDn().getU2fBase(); // ou=registration_requests,ou=u2f,o=gluu + if (StringHelper.isEmpty(oxId)) { + return String.format("ou=registration_requests,%s", u2fBaseDn); + } + + return String.format("oxid=%s,ou=registration_requests,%s", oxId, u2fBaseDn); + } + + public void merge(RequestMessageLdap request) { + ldapEntryManager.merge(request); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/RequestService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/RequestService.java new file mode 100644 index 00000000..df8813d7 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/RequestService.java @@ -0,0 +1,55 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service.fido.u2f; + +import java.util.Date; +import java.util.List; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.inject.Named; + +import org.gluu.oxauth.model.fido.u2f.RequestMessageLdap; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.persist.model.BatchOperation; +import org.gluu.persist.model.SearchScope; +import org.gluu.search.filter.Filter; +import org.slf4j.Logger; +import org.gluu.oxauth.model.config.StaticConfiguration; + +/** + * Provides generic operations with U2F requests + * + * @author Yuriy Movchan Date: 05/19/2015 + */ +@ApplicationScoped +@Named("u2fRequestService") +public class RequestService { + + @Inject + private Logger log; + + @Inject + private PersistenceEntryManager ldapEntryManager; + + @Inject + private StaticConfiguration staticConfiguration; + + public List getExpiredRequestMessages(BatchOperation batchOperation, Date expirationDate, String[] returnAttributes, int sizeLimit, int chunkSize) { + final String u2fBaseDn = staticConfiguration.getBaseDn().getU2fBase(); // ou=u2f,o=gluu + Filter expirationFilter = Filter.createLessOrEqualFilter("creationDate", ldapEntryManager.encodeTime(u2fBaseDn, expirationDate)); + + List requestMessageLdap = ldapEntryManager.findEntries(u2fBaseDn, RequestMessageLdap.class, expirationFilter, SearchScope.SUB, returnAttributes, batchOperation, 0, sizeLimit, chunkSize); + + return requestMessageLdap; + } + + public void removeRequestMessage(RequestMessageLdap requestMessageLdap) { + ldapEntryManager.remove(requestMessageLdap); + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/UserSessionIdService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/UserSessionIdService.java new file mode 100644 index 00000000..e8b0c17b --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/UserSessionIdService.java @@ -0,0 +1,88 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service.fido.u2f; + +import java.util.Map; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.gluu.oxauth.model.fido.u2f.DeviceRegistrationResult; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.model.session.SessionIdState; +import org.gluu.oxauth.service.SessionIdService; +import org.gluu.oxauth.ws.rs.fido.u2f.U2fAuthenticationWS; +import org.gluu.util.StringHelper; +import org.slf4j.Logger; + +/** + * Configure user session to confirm user {@link U2fAuthenticationWS} authentication + * + * @author Yuriy Movchan + * @version August 9, 2017 + */ +@ApplicationScoped +public class UserSessionIdService { + + @Inject + private Logger log; + + @Inject + private SessionIdService sessionIdService; + + public void updateUserSessionIdOnFinishRequest(String sessionId, String userInum, DeviceRegistrationResult deviceRegistrationResult, boolean enroll, boolean oneStep) { + SessionId ldapSessionId = getLdapSessionId(sessionId); + if (ldapSessionId == null) { + return; + } + + Map sessionAttributes = ldapSessionId.getSessionAttributes(); + if (DeviceRegistrationResult.Status.APPROVED == deviceRegistrationResult.getStatus()) { + sessionAttributes.put("session_custom_state", "approved"); + } else { + sessionAttributes.put("session_custom_state", "declined"); + } + sessionAttributes.put("oxpush2_u2f_device_id", deviceRegistrationResult.getDeviceRegistration().getId()); + sessionAttributes.put("oxpush2_u2f_device_user_inum", userInum); + sessionAttributes.put("oxpush2_u2f_device_enroll", Boolean.toString(enroll)); + sessionAttributes.put("oxpush2_u2f_device_one_step", Boolean.toString(oneStep)); + + sessionIdService.updateSessionId(ldapSessionId, true); + } + + public void updateUserSessionIdOnError(String sessionId) { + SessionId ldapSessionId = getLdapSessionId(sessionId); + if (ldapSessionId == null) { + return; + } + + Map sessionAttributes = ldapSessionId.getSessionAttributes(); + sessionAttributes.put("session_custom_state", "declined"); + + sessionIdService.updateSessionId(ldapSessionId, true); + } + + private SessionId getLdapSessionId(String sessionId) { + if (StringHelper.isEmpty(sessionId)) { + return null; + } + + SessionId ldapSessionId = sessionIdService.getSessionId(sessionId); + if (ldapSessionId == null) { + log.warn("Failed to load session id '{}'", sessionId); + return null; + } + + if (SessionIdState.UNAUTHENTICATED != ldapSessionId.getState()) { + log.warn("Unexpected session id '{}' state: '{}'", sessionId, ldapSessionId.getState()); + return null; + } + + return ldapSessionId; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/ValidationService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/ValidationService.java new file mode 100644 index 00000000..d384ed0a --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/ValidationService.java @@ -0,0 +1,88 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service.fido.u2f; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.inject.Named; + +import org.gluu.oxauth.model.config.Constants; +import org.gluu.oxauth.model.fido.u2f.U2fConstants; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.service.SessionIdService; +import org.gluu.oxauth.service.common.UserService; +import org.gluu.util.StringHelper; +import org.slf4j.Logger; +import org.gluu.oxauth.model.common.User; + +/** + * Utility to validate U2F input data + * + * @author Yuriy Movchan + * @version August 9, 2017 + */ +@ApplicationScoped +@Named("u2fValidationService") +public class ValidationService { + + @Inject + private Logger log; + + @Inject + private SessionIdService sessionIdService; + + @Inject + private UserService userService; + + public boolean isValidSessionId(String userName, String sessionId) { + if (sessionId == null) { + log.error("In two step authentication workflow session_id is mandatory"); + return false; + } + + SessionId ldapSessionId = sessionIdService.getSessionId(sessionId); + if (ldapSessionId == null) { + log.error("Specified session_id '{}' is invalid", sessionId); + return false; + } + + String sessionIdUser = ldapSessionId.getSessionAttributes().get(Constants.AUTHENTICATED_USER); + if (!StringHelper.equalsIgnoreCase(userName, sessionIdUser)) { + log.error("Username '{}' and session_id '{}' don't match", userName, sessionId); + return false; + } + + return true; + } + + public boolean isValidEnrollmentCode(String userName, String enrollmentCode) { + if (enrollmentCode == null) { + log.error("In two step authentication workflow enrollment_code is mandatory"); + return false; + } + + User user = userService.getUser(userName, U2fConstants.U2F_ENROLLMENT_CODE_ATTRIBUTE); + if (user == null) { + log.error("Specified user_name '{}' is invalid", userName); + return false; + } + + String userEnrollmentCode = user.getAttribute(U2fConstants.U2F_ENROLLMENT_CODE_ATTRIBUTE); + if (userEnrollmentCode == null) { + log.error("Specified enrollment_code '{}' is invalid", enrollmentCode); + return false; + } + + if (!StringHelper.equalsIgnoreCase(userEnrollmentCode, enrollmentCode)) { + log.error("Username '{}' and enrollment_code '{}' don't match", userName, enrollmentCode); + return false; + } + + return true; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/util/KeyGenerator.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/util/KeyGenerator.java new file mode 100644 index 00000000..7a2f2113 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/fido/u2f/util/KeyGenerator.java @@ -0,0 +1,65 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service.fido.u2f.util; + +import java.util.Calendar; +import java.util.Date; +import java.util.UUID; + +import org.json.JSONObject; +import org.gluu.oxauth.model.crypto.Certificate; +import org.gluu.oxauth.model.crypto.Key; +import org.gluu.oxauth.model.crypto.signature.ECDSAKeyFactory; +import org.gluu.oxauth.model.crypto.signature.ECDSAPrivateKey; +import org.gluu.oxauth.model.crypto.signature.ECDSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jwk.Use; +import org.gluu.util.security.SecurityProviderUtility; + +/** + * @author Yuriy Movchan + * @author Javier Rojas Blum + * @version August 28, 2017 + */ +public class KeyGenerator { + public static void main(String[] args) throws Exception { + SecurityProviderUtility.installBCProvider(true); + + Calendar cal = Calendar.getInstance(); + Date startDate = cal.getTime(); + + cal.add(Calendar.YEAR, 3); + Date expirationDate = cal.getTime(); + + String dnName = "C=US,ST=TX,L=Austin,O=Gluu,CN=Gluu oxPush2 U2F v1.0.0"; + + generateU2fAttestationKeys(startDate, expirationDate, dnName); + } + + public static void generateU2fAttestationKeys(Date startDate, Date expirationDate, String dnName) throws Exception { + ECDSAKeyFactory keyFactory = new ECDSAKeyFactory( + SignatureAlgorithm.ES256, + null); + Key key = keyFactory.getKey(); + Certificate certificate = keyFactory.generateV3Certificate(startDate, expirationDate, dnName); + key.setCertificate(certificate); + + key.setKeyType(SignatureAlgorithm.ES256.getFamily().getValue()); + key.setUse(Use.SIGNATURE.toString()); + key.setAlgorithm(SignatureAlgorithm.ES256.getName()); + key.setKeyId(UUID.randomUUID().toString()); + key.setExpirationTime(expirationDate.getTime()); + key.setCurve(SignatureAlgorithm.ES256.getCurve()); + + JSONObject jsonKey = key.toJSONObject(); + System.out.println(jsonKey); + + System.out.println("CERTIFICATE:"); + System.out.println(certificate); + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/logger/LoggerService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/logger/LoggerService.java new file mode 100644 index 00000000..13bf317e --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/logger/LoggerService.java @@ -0,0 +1,42 @@ +package org.gluu.oxauth.service.logger; + +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.util.ServerUtil; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.inject.Named; + +/** + * Logger service + * + * @author Yuriy Movchan Date: 08/19/2018 + */ +@ApplicationScoped +@Named +public class LoggerService extends org.gluu.service.logger.LoggerService { + + @Inject + private AppConfiguration appConfiguration; + + @Override + public boolean isDisableJdkLogger() { + return ServerUtil.isTrue(appConfiguration.getDisableJdkLogger()); + } + + @Override + public String getLoggingLevel() { + return appConfiguration.getLoggingLevel(); + } + + @Override + public String getExternalLoggerConfiguration() { + return appConfiguration.getExternalLoggerConfiguration(); + } + + @Override + public String getLoggingLayout() { + return appConfiguration.getLoggingLayout(); + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/net/HttpService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/net/HttpService.java new file mode 100644 index 00000000..e2fdbf61 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/net/HttpService.java @@ -0,0 +1,285 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service.net; + +import java.io.IOException; +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Map; +import java.util.Map.Entry; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.inject.Named; +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.codec.binary.Base64; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.conn.scheme.PlainSocketFactory; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.conn.ssl.AllowAllHostnameVerifier; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.conn.ssl.TrustStrategy; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.PoolingClientConnectionManager; +import org.apache.http.util.EntityUtils; +import org.gluu.net.SslDefaultHttpClient; +import org.gluu.model.net.HttpServiceResponse; +import org.gluu.util.StringHelper; +import org.gluu.util.Util; +import org.slf4j.Logger; +/** + * Provides operations with http requests + * + * @author Yuriy Movchan Date: 02/05/2013 + */ +@ApplicationScoped +@Named +@Deprecated +public class HttpService implements Serializable { + + private static final long serialVersionUID = -2398422090669045605L; + + @Inject + private Logger log; + + private Base64 base64; + + @PostConstruct + public void init() { + this.base64 = new Base64(); + } + + public HttpClient getHttpsClientTrustAll() { + try { + SSLSocketFactory sf = new SSLSocketFactory(new TrustStrategy(){ + @Override + public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException { + return true; + } + }, new AllowAllHostnameVerifier()); + + PlainSocketFactory psf = PlainSocketFactory.getSocketFactory(); + + SchemeRegistry registry = new SchemeRegistry(); + registry.register(new Scheme("http", 80, psf)); + registry.register(new Scheme("https", 443, sf)); + ClientConnectionManager ccm = new PoolingClientConnectionManager(registry); + return new DefaultHttpClient(ccm); + } catch (Exception ex) { + log.error("Failed to create TrustAll https client", ex); + return new DefaultHttpClient(); + } + } + + public HttpClient getHttpsClient() { + HttpClient httpClient = new SslDefaultHttpClient(); + + return httpClient; + } + + public HttpClient getHttpsClient(String trustStoreType, String trustStorePath, String trustStorePassword) { + HttpClient httpClient = new SslDefaultHttpClient(trustStoreType, trustStorePath, trustStorePassword); + + return httpClient; + } + + public HttpClient getHttpsClient(String trustStoreType, String trustStorePath, String trustStorePassword, + String keyStoreType, String keyStorePath, String keyStorePassword) { + HttpClient httpClient = new SslDefaultHttpClient(trustStoreType, trustStorePath, trustStorePassword, + keyStoreType, keyStorePath, keyStorePassword); + + return httpClient; + } + + public HttpServiceResponse executePost(HttpClient httpClient, String uri, String authData, Map headers, String postData, ContentType contentType) { + HttpPost httpPost = new HttpPost(uri); + if (StringHelper.isNotEmpty(authData)) { + httpPost.setHeader("Authorization", "Basic " + authData); + } + + if (headers != null) { + for (Entry headerEntry : headers.entrySet()) { + httpPost.setHeader(headerEntry.getKey(), headerEntry.getValue()); + } + } + + StringEntity stringEntity = new StringEntity(postData, contentType); + httpPost.setEntity(stringEntity); + + try { + HttpResponse httpResponse = httpClient.execute(httpPost); + + return new HttpServiceResponse(httpPost, httpResponse); + } catch (IOException ex) { + log.error("Failed to execute post request", ex); + } + + return null; + } + + public HttpServiceResponse executePost(HttpClient httpClient, String uri, String authData, Map headers, String postData) { + return executePost(httpClient, uri, authData, headers, postData, null); + } + + public HttpServiceResponse executePost(HttpClient httpClient, String uri, String authData, String postData, ContentType contentType) { + return executePost(httpClient, uri, authData, null, postData, contentType); + } + + public String encodeBase64(String value) { + try { + return new String(base64.encode((value).getBytes(Util.UTF8)), Util.UTF8); + } catch (UnsupportedEncodingException ex) { + log.error("Failed to convert '{}' to base64", value, ex); + } + + return null; + } + + public String encodeUrl(String value) { + try { + return URLEncoder.encode(value, Util.UTF8); + } catch (UnsupportedEncodingException ex) { + log.error("Failed to encode url '{}'", value, ex); + } + + return null; + } + + public HttpServiceResponse executeGet(HttpClient httpClient, String requestUri, Map headers) { + HttpGet httpGet = new HttpGet(requestUri); + + if (headers != null) { + for (Entry headerEntry : headers.entrySet()) { + httpGet.setHeader(headerEntry.getKey(), headerEntry.getValue()); + } + } + + try { + HttpResponse httpResponse = httpClient.execute(httpGet); + + return new HttpServiceResponse(httpGet, httpResponse); + } catch (IOException ex) { + log.error("Failed to execute get request", ex); + } + + return null; + } + + public HttpServiceResponse executeGet(HttpClient httpClient, String requestUri) throws ClientProtocolException, IOException { + return executeGet(httpClient, requestUri, null); + } + + public byte[] getResponseContent(HttpResponse httpResponse) throws IOException { + if ((httpResponse == null) || (httpResponse.getStatusLine().getStatusCode() != HttpStatus.SC_OK)) { + return null; + } + + HttpEntity entity = httpResponse.getEntity(); + byte[] responseBytes = new byte[0]; + if (entity != null) { + responseBytes = EntityUtils.toByteArray(entity); + } + + // Consume response content + if (entity != null) { + EntityUtils.consume(entity); + } + + return responseBytes; + } + + public void consume(HttpResponse httpResponse) throws IOException { + if ((httpResponse == null) || (httpResponse.getStatusLine().getStatusCode() != HttpStatus.SC_OK)) { + return; + } + + // Consume response content + HttpEntity entity = httpResponse.getEntity(); + if (entity != null) { + EntityUtils.consume(entity); + } + } + + public String convertEntityToString(byte[] responseBytes) { + if (responseBytes == null) { + return null; + } + + return new String(responseBytes); + } + + public String convertEntityToString(byte[] responseBytes, Charset charset) { + if (responseBytes == null) { + return null; + } + + return new String(responseBytes, charset); + } + + public String convertEntityToString(byte[] responseBytes, String charsetName) throws UnsupportedEncodingException { + if (responseBytes == null) { + return null; + } + + return new String(responseBytes, charsetName); + } + + public boolean isResponseStastusCodeOk(HttpResponse httpResponse) { + int responseStastusCode = httpResponse.getStatusLine().getStatusCode(); + if (responseStastusCode == HttpStatus.SC_OK) { + return true; + } + + return false; + } + + + public boolean isContentTypeXml(HttpResponse httpResponse) { + Header contentType = httpResponse.getEntity().getContentType(); + if (contentType == null) { + return false; + } + + String contentTypeValue = contentType.getValue(); + if (StringHelper.equals(contentTypeValue, ContentType.APPLICATION_XML.getMimeType()) || StringHelper.equals(contentTypeValue, ContentType.TEXT_XML.getMimeType())) { + return true; + } + + return false; + } + + public String constructServerUrl(final HttpServletRequest request) { + int serverPort = request.getServerPort(); + + String redirectUrl; + if ((serverPort == 80) || (serverPort == 443)) { + redirectUrl = String.format("%s://%s%s", request.getScheme(), request.getServerName(), request.getContextPath()); + } else { + redirectUrl = String.format("%s://%s:%s%s", request.getScheme(), request.getServerName(), request.getServerPort(), request.getContextPath()); + } + + return redirectUrl.toLowerCase(); + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/net/HttpService2.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/net/HttpService2.java new file mode 100644 index 00000000..8ac278d3 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/net/HttpService2.java @@ -0,0 +1,44 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service.net; + +import java.io.Serializable; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.slf4j.Logger; + +/** + * Provides operations with http/https requests + * + * @author Yuriy Movchan Date: 04/10/2023 + */ +@ApplicationScoped +public class HttpService2 extends org.gluu.net.HttpServiceUtility implements Serializable { + + @Inject + private Logger log; + + @PostConstruct + public void init() { + super.init(); + } + + @PreDestroy + public void destroy() { + super.destroy(); + } + + @Override + public Logger getLogger() { + return log; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/push/sns/PushPlatform.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/push/sns/PushPlatform.java new file mode 100644 index 00000000..d4c5f5e9 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/push/sns/PushPlatform.java @@ -0,0 +1,25 @@ +package org.gluu.oxauth.service.push.sns; + +/** + * Platforms supported AWS SNS + * + * @author Yuriy Movchan Date: 08/31/2017 + */ +public enum PushPlatform { + + // Apple Push Notification Service + APNS, + // Sandbox version of Apple Push Notification Service + APNS_SANDBOX, + // Amazon Device Messaging + ADM, + // Google Cloud Messaging + GCM, + // Baidu CloudMessaging Service + BAIDU, + // Windows Notification Service + WNS, + // Microsoft Push Notificaion Service + MPNS; + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/push/sns/PushSnsService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/push/sns/PushSnsService.java new file mode 100644 index 00000000..7bd51181 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/push/sns/PushSnsService.java @@ -0,0 +1,114 @@ +package org.gluu.oxauth.service.push.sns; + +import java.io.IOException; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import javax.inject.Inject; + +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.service.common.EncryptionService; +import org.gluu.oxauth.util.ServerUtil; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.oxauth.model.common.User; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.regions.Regions; +import com.amazonaws.services.sns.AmazonSNS; +import com.amazonaws.services.sns.AmazonSNSClientBuilder; +import com.amazonaws.services.sns.model.CreatePlatformEndpointRequest; +import com.amazonaws.services.sns.model.CreatePlatformEndpointResult; +import com.amazonaws.services.sns.model.MessageAttributeValue; +import com.amazonaws.services.sns.model.PublishRequest; +import com.amazonaws.services.sns.model.PublishResult; + +import javax.enterprise.context.ApplicationScoped; + +/** + * Provides operations to send AWS SNS push messages + * + * @author Yuriy Movchan Date: 08/31/2017 + */ +@ApplicationScoped +public class PushSnsService { + + @Inject + private EncryptionService encryptionService; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private PersistenceEntryManager ldapEntryManager; + + public AmazonSNS createSnsClient(String accessKey, String secretKey, String region) { + String decryptedAccessKey = encryptionService.decrypt(accessKey, true); + String decryptedSecretKey = encryptionService.decrypt(secretKey, true); + + BasicAWSCredentials credentials = new BasicAWSCredentials(decryptedAccessKey, decryptedSecretKey); + AmazonSNS snsClient = AmazonSNSClientBuilder.standard().withRegion(Regions.fromName(region)).withCredentials(new AWSStaticCredentialsProvider(credentials)).build(); + + return snsClient; + } + + public String createPlatformArn(AmazonSNS snsClient, String platformApplicationArn, String token, User user) { + CreatePlatformEndpointRequest platformEndpointRequest = new CreatePlatformEndpointRequest(); + platformEndpointRequest.setPlatformApplicationArn(platformApplicationArn); + platformEndpointRequest.setToken(token); + + String customUserData = getCustomUserData(user); + platformEndpointRequest.setCustomUserData(customUserData); + + CreatePlatformEndpointResult platformEndpointResult = snsClient.createPlatformEndpoint(platformEndpointRequest); + + return platformEndpointResult.getEndpointArn(); + } + + public String getCustomUserData(User user) { + String customUserData = String.format("Issuer: %s, user: %s, date: %s", appConfiguration.getIssuer(), user.getUserId(), + ldapEntryManager.encodeTime(user.getDn(), new Date())); + return customUserData; + } + + public PublishResult sendPushMessage(AmazonSNS snsClient, PushPlatform platform, String targetArn, Map customAppMessageMap, Map messageAttributes) throws IOException { + Map appMessageMap = new HashMap(); + + if (platform == PushPlatform.GCM) { + appMessageMap.put("collapse_key", "single"); + appMessageMap.put("delay_while_idle", true); + appMessageMap.put("time_to_live", 30); + appMessageMap.put("dry_run", false); + } + + if (customAppMessageMap != null) { + appMessageMap.putAll(customAppMessageMap); + } + + String message = ServerUtil.asJson(appMessageMap); + + return sendPushMessage(snsClient, platform, targetArn, message, messageAttributes); + } + + public PublishResult sendPushMessage(AmazonSNS snsClient, PushPlatform platform, String targetArn, String message, + Map messageAttributes) throws IOException { + Map messageMap = new HashMap(); + messageMap.put(platform.name(), message); + message = ServerUtil.asJson(messageMap); + + PublishRequest publishRequest = new PublishRequest(); + publishRequest.setMessageStructure("json"); + + if (messageAttributes != null) { + publishRequest.setMessageAttributes(messageAttributes); + } + + publishRequest.setTargetArn(targetArn); + publishRequest.setMessage(message); + + PublishResult publishResult = snsClient.publish(publishRequest); + + return publishResult; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/stat/StatService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/stat/StatService.java new file mode 100644 index 00000000..5da75465 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/stat/StatService.java @@ -0,0 +1,283 @@ +package org.gluu.oxauth.service.stat; + +import net.agkn.hll.HLL; +import org.apache.commons.lang.StringUtils; +import org.gluu.net.InetAddressUtility; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.config.StaticConfiguration; +import org.gluu.oxauth.model.stat.Stat; +import org.gluu.oxauth.model.stat.StatEntry; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.persist.exception.EntryPersistenceException; +import org.gluu.persist.model.base.SimpleBranch; +import org.slf4j.Logger; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import java.text.SimpleDateFormat; +import java.util.Base64; +import java.util.Date; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * @author Yuriy Zabrovarnyy + */ +@ApplicationScoped +public class StatService { + + // January - 202001, December - 202012 + private static final SimpleDateFormat PERIOD_DATE_FORMAT = new SimpleDateFormat("yyyyMM"); + private static final int regwidth = 5; + private static final int log2m = 15; + + public static final String ACCESS_TOKEN_KEY = "access_token"; + public static final String ID_TOKEN_KEY = "id_token"; + public static final String REFRESH_TOKEN_KEY = "refresh_token"; + public static final String UMA_TOKEN_KEY = "uma_token"; + + @Inject + private Logger log; + + @Inject + private PersistenceEntryManager entryManager; + + @Inject + private StaticConfiguration staticConfiguration; + + private String nodeId; + private String monthlyDn; + private StatEntry currentEntry; + private HLL hll; + private ConcurrentMap> tokenCounters; + + private boolean initialized = false; + + @PostConstruct + public void create() { + initialized = false; + } + + public boolean init() { + try { + log.info("Initializing Stat Service"); + initNodeId(); + if (StringUtils.isBlank(nodeId)) { + log.error("Failed to initialize stat service. statNodeId is not set in configuration."); + return false; + } + if (StringUtils.isBlank(getBaseDn())) { + log.error("Failed to initialize stat service. 'stat' base dn is not set in configuration."); + return false; + } + + final Date now = new Date(); + prepareMonthlyBranch(now); + log.trace("Monthly branch created: " + monthlyDn); + + setupCurrentEntry(now); + log.info("Initialized Stat Service"); + initialized = true; + return true; + } catch (Exception e) { + log.error("Failed to initialize Stat Service.", e); + return false; + } + } + + public void updateStat() { + if (!initialized) { + return; + } + + log.trace("Started updateStat ..."); + + Date now = new Date(); + prepareMonthlyBranch(now); + + setupCurrentEntry(now); + + final Stat stat = currentEntry.getStat(); + stat.setTokenCountPerGrantType(tokenCounters); + stat.setLastUpdatedAt(now.getTime()); + + synchronized (hll) { + currentEntry.setUserHllData(Base64.getEncoder().encodeToString(hll.toBytes())); + } + entryManager.merge(currentEntry); + + log.trace("Finished updateStat."); + } + + private void setupCurrentEntry() { + setupCurrentEntry(new Date()); + } + + private void setupCurrentEntry(Date now) { + final String month = PERIOD_DATE_FORMAT.format(now); + String dn = String.format("jansId=%s,%s", nodeId, monthlyDn); // jansId=,ou=yyyyMM,ou=stat,o=gluu + + if (currentEntry != null && month.equals(currentEntry.getStat().getMonth())) { + return; + } + + try { + StatEntry entryFromPersistence = entryManager.find(StatEntry.class, dn); + if (entryFromPersistence != null && month.equals(entryFromPersistence.getStat().getMonth())) { + hll = HLL.fromBytes(Base64.getDecoder().decode(entryFromPersistence.getUserHllData())); + tokenCounters = new ConcurrentHashMap<>(entryFromPersistence.getStat().getTokenCountPerGrantType()); + currentEntry = entryFromPersistence; + log.trace("Stat entry loaded."); + return; + } + } catch (EntryPersistenceException e) { + log.trace("Stat entry is not found in persistence."); + } + + if (currentEntry == null) { + log.trace("Creating stat entry ..."); + hll = newHll(); + tokenCounters = new ConcurrentHashMap<>(); + + currentEntry = new StatEntry(); + currentEntry.setId(nodeId); + currentEntry.setDn(dn); + currentEntry.setUserHllData(Base64.getEncoder().encodeToString(hll.toBytes())); + currentEntry.getStat().setMonth(PERIOD_DATE_FORMAT.format(new Date())); + entryManager.persist(currentEntry); + log.trace("Created stat entry. nodeId:" + nodeId); + } + } + + public HLL newHll() { + return new HLL(log2m, regwidth); + } + + private void initNodeId() { + if (StringUtils.isNotBlank(nodeId)) { + return; + } + + try { + nodeId = InetAddressUtility.getMACAddressOrNull(); + if (StringUtils.isNotBlank(nodeId)) { + log.trace("NodeId created: " + nodeId); + return; + } + + nodeId = UUID.randomUUID().toString(); + log.trace("NodeId created: " + nodeId); + } catch (Exception e) { + log.error("Failed to identify nodeId.", e); + nodeId = UUID.randomUUID().toString(); + } + } + + public String getNodeId() { + return nodeId; + } + + public String getBaseDn() { + return staticConfiguration.getBaseDn().getStat(); + } + + private void prepareMonthlyBranch(Date now) { + final String baseDn = getBaseDn(); + final String month = PERIOD_DATE_FORMAT.format(now); // yyyyMM + monthlyDn = String.format("ou=%s,%s", month, baseDn); // ou=yyyyMM,ou=stat,o=gluu + + if (!entryManager.hasBranchesSupport(baseDn)) { + return; + } + + try { + if (!entryManager.contains(monthlyDn, SimpleBranch.class)) { // Create ou=yyyyMM branch if needed + createBranch(monthlyDn, month); + } + } catch (Exception e) { + log.error("Failed to prepare monthly branch: " + monthlyDn, e); + throw e; + } + } + + public void createBranch(String branchDn, String ou) { + try { + SimpleBranch branch = new SimpleBranch(); + branch.setOrganizationalUnitName(ou); + branch.setDn(branchDn); + + entryManager.persist(branch); + } catch (EntryPersistenceException ex) { + // Check if another process added this branch already + if (!entryManager.contains(branchDn, SimpleBranch.class)) { + throw ex; + } + } + } + + public void reportActiveUser(String id) { + if (!initialized) { + return; + } + + if (StringUtils.isBlank(id)) { + return; + } + + final int hash = id.hashCode(); + try { + setupCurrentEntry(); + synchronized (hll) { + hll.addRaw(hash); + } + } catch (Exception e) { + log.error("Failed to report active user, id: " + id + ", hash: " + hash, e); + } + } + + public void reportAccessToken(GrantType grantType) { + reportToken(grantType, ACCESS_TOKEN_KEY); + } + + public void reportIdToken(GrantType grantType) { + reportToken(grantType, ID_TOKEN_KEY); + } + + public void reportRefreshToken(GrantType grantType) { + reportToken(grantType, REFRESH_TOKEN_KEY); + } + + public void reportUmaToken(GrantType grantType) { + reportToken(grantType, UMA_TOKEN_KEY); + } + + + private void reportToken(GrantType grantType, String tokenKey) { + if (!initialized) { + return; + } + + if (grantType == null || tokenKey == null) { + return; + } + if (tokenCounters == null) { + log.error("Stat service is not initialized."); + return; + } + + Map tokenMap = tokenCounters.computeIfAbsent(grantType.getValue(), k -> new ConcurrentHashMap<>()); + + Long counter = tokenMap.get(tokenKey); + + if (counter == null) { + counter = 1L; + } else { + counter++; + } + + tokenMap.put(tokenKey, counter); + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/stat/StatTimer.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/stat/StatTimer.java new file mode 100644 index 00000000..0dd53381 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/stat/StatTimer.java @@ -0,0 +1,96 @@ +package org.gluu.oxauth.service.stat; + +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.service.cdi.event.StatEvent; +import org.gluu.service.cdi.async.Asynchronous; +import org.gluu.service.cdi.event.Scheduled; +import org.gluu.service.timer.event.TimerEvent; +import org.gluu.service.timer.schedule.TimerSchedule; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Event; +import javax.enterprise.event.Observes; +import javax.inject.Inject; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * @author Yuriy Zabrovarnyy + */ +@ApplicationScoped +public class StatTimer { + + private static final int TIMER_TICK_INTERVAL_IN_SECONDS = 60; // 1 min + private static final int TIMER_INTERVAL_IN_SECONDS = 15 * 60; // 15 min + + @Inject + private Logger log; + + @Inject + private Event timerEvent; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private StatService statService; + + private AtomicBoolean isActive; + private long lastFinishedTime; + + @Asynchronous + public void initTimer() { + log.info("Initializing Stat Service Timer"); + + this.isActive = new AtomicBoolean(false); + + timerEvent.fire(new TimerEvent(new TimerSchedule(TIMER_TICK_INTERVAL_IN_SECONDS, TIMER_TICK_INTERVAL_IN_SECONDS), new StatEvent(), Scheduled.Literal.INSTANCE)); + + this.lastFinishedTime = System.currentTimeMillis(); + log.info("Initialized Stat Service Timer"); + } + + @Asynchronous + public void process(@Observes @Scheduled StatEvent event) { + if (!appConfiguration.getStatEnabled()) { + return; + } + + if (this.isActive.get()) { + return; + } + + if (!this.isActive.compareAndSet(false, true)) { + return; + } + + try { + if (!allowToRun()) { + return; + } + statService.updateStat(); + this.lastFinishedTime = System.currentTimeMillis(); + } catch (Exception ex) { + log.error("Exception happened while updating stat", ex); + } finally { + this.isActive.set(false); + } + } + + private boolean allowToRun() { + int interval = appConfiguration.getStatTimerIntervalInSeconds(); + if (interval < 0) { + log.info("Stat Timer is disabled."); + log.warn("Stat Timer Interval (statTimerIntervalInSeconds in server configuration) is negative which turns OFF statistic on the server. Please set it to positive value if you wish it to run."); + return false; + } + if (interval == 0) + interval = TIMER_INTERVAL_IN_SECONDS; + + long timerInterval = interval * 1000; + + long timeDiff = System.currentTimeMillis() - this.lastFinishedTime; + + return timeDiff >= timerInterval; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/status/ldap/LdapStatusTimer.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/status/ldap/LdapStatusTimer.java new file mode 100644 index 00000000..66d41ab7 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/status/ldap/LdapStatusTimer.java @@ -0,0 +1,117 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service.status.ldap; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Event; +import javax.enterprise.event.Observes; +import javax.inject.Inject; +import javax.inject.Named; + +import org.gluu.oxauth.service.common.ApplicationFactory; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.persist.ldap.operation.LdapOperationService; +import org.gluu.persist.ldap.operation.impl.LdapConnectionProvider; +import org.gluu.persist.operation.PersistenceOperationService; +import org.gluu.service.cdi.async.Asynchronous; +import org.gluu.service.cdi.event.LdapStatusEvent; +import org.gluu.service.cdi.event.Scheduled; +import org.gluu.service.timer.event.TimerEvent; +import org.gluu.service.timer.schedule.TimerSchedule; +import org.slf4j.Logger; + +/** + * @author Yuriy Movchan + * @version 0.1, 11/18/2012 + */ +@ApplicationScoped +public class LdapStatusTimer { + + private final static int DEFAULT_INTERVAL = 60; // 1 minute + + @Inject + private Logger log; + + @Inject + private Event timerEvent; + + @Inject + private PersistenceEntryManager ldapEntryManager; + + @Inject @Named(ApplicationFactory.PERSISTENCE_AUTH_ENTRY_MANAGER_NAME) + private List ldapAuthEntryManagers; + + private AtomicBoolean isActive; + + public void initTimer() { + log.info("Initializing Persistance Layer Status Timer"); + this.isActive = new AtomicBoolean(false); + + timerEvent.fire(new TimerEvent(new TimerSchedule(DEFAULT_INTERVAL, DEFAULT_INTERVAL), new LdapStatusEvent(), + Scheduled.Literal.INSTANCE)); + } + + @Asynchronous + public void process(@Observes @Scheduled LdapStatusEvent ldapStatusEvent) { + if (this.isActive.get()) { + return; + } + + if (!this.isActive.compareAndSet(false, true)) { + return; + } + + try { + processInt(); + } finally { + this.isActive.set(false); + } + } + + private void processInt() { + logConnectionProviderStatistic(ldapEntryManager, "connectionProvider", "bindConnectionProvider"); + + for (int i = 0; i < ldapAuthEntryManagers.size(); i++) { + PersistenceEntryManager ldapAuthEntryManager = ldapAuthEntryManagers.get(i); + logConnectionProviderStatistic(ldapAuthEntryManager, "authConnectionProvider#" + i, "bindAuthConnectionProvider#" + i); + } + } + + public void logConnectionProviderStatistic(PersistenceEntryManager ldapEntryManager, String connectionProviderName, String bindConnectionProviderName) { + PersistenceOperationService persistenceOperationService = ldapEntryManager.getOperationService(); + if (!(persistenceOperationService instanceof LdapOperationService)) { + return; + } + + LdapConnectionProvider ldapConnectionProvider = ((LdapOperationService) persistenceOperationService).getConnectionProvider(); + LdapConnectionProvider bindLdapConnectionProvider = ((LdapOperationService) persistenceOperationService).getBindConnectionProvider(); + + if (ldapConnectionProvider == null) { + log.error("{} is empty", connectionProviderName); + } else { + if (ldapConnectionProvider.getConnectionPool() == null) { + log.error("{} is empty", connectionProviderName); + } else { + log.info("{} statistics: {}", connectionProviderName, ldapConnectionProvider.getConnectionPool().getConnectionPoolStatistics()); + } + } + + if (bindLdapConnectionProvider == null) { + log.error("{} is empty", bindConnectionProviderName); + } else { + if (bindLdapConnectionProvider.getConnectionPool() == null) { + log.error("{} is empty", bindConnectionProviderName); + } else { + log.info("{} statistics: {}", bindConnectionProviderName, bindLdapConnectionProvider.getConnectionPool().getConnectionPoolStatistics()); + } + } + } + +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/service/token/TokenService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/token/TokenService.java new file mode 100644 index 00000000..88afd851 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/service/token/TokenService.java @@ -0,0 +1,120 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.service.token; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.common.AuthorizationGrant; +import org.gluu.oxauth.model.common.AuthorizationGrantList; +import org.gluu.oxauth.model.token.HttpAuthTokenType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.inject.Inject; +import javax.inject.Named; + +/** + * Token specific service methods + * + * @author Yuriy Movchan Date: 10/03/2012 + */ +@Named +public class TokenService { + + @Inject + private AuthorizationGrantList authorizationGrantList; + + public boolean isToken(@Nullable String authorizationParameter, @NotNull HttpAuthTokenType tokenType) { + return StringUtils.startsWithIgnoreCase(authorizationParameter, tokenType.getPrefix()); + } + + @Nullable + public String extractToken(@Nullable String authorizationParameter, @NotNull HttpAuthTokenType tokenType) { + if (isToken(authorizationParameter, tokenType) && authorizationParameter != null) { + return authorizationParameter.substring(tokenType.getPrefix().length()).trim(); + } + return null; + } + + public boolean isBasicAuthToken(@Nullable String authorizationParameter) { + return isToken(authorizationParameter, HttpAuthTokenType.Basic); + } + + public boolean isBearerAuthToken(@Nullable String authorizationParameter) { + return isToken(authorizationParameter, HttpAuthTokenType.Bearer); + } + + public boolean isNegotiateAuthToken(@Nullable String authorizationParameter) { + return isToken(authorizationParameter,HttpAuthTokenType.Negotiate); + } + + @Nullable + public String getBasicToken(@Nullable String authorizationParameter) { + return extractToken(authorizationParameter, HttpAuthTokenType.Basic); + } + + @Nullable + public String getBearerToken(@Nullable String authorizationParameter) { + return extractToken(authorizationParameter, HttpAuthTokenType.Bearer); + } + + @Nullable + public String getToken(@Nullable String authorization) { + return getToken(authorization, HttpAuthTokenType.values()); + } + + @Nullable + public String getToken(@Nullable String authorization, @Nullable HttpAuthTokenType... allowedTokenTypes) { + if (StringUtils.isBlank(authorization) || allowedTokenTypes == null || allowedTokenTypes.length == 0) { + return null; + } + + for (HttpAuthTokenType tokenType : allowedTokenTypes) { + if (tokenType != null && isToken(authorization, tokenType)) { + return extractToken(authorization, tokenType); + } + } + return null; + } + + @Nullable + public AuthorizationGrant getAuthorizationGrant(@Nullable String authorization) { + final String token = getToken(authorization); + if (StringUtils.isNotBlank(token)) { + return authorizationGrantList.getAuthorizationGrantByAccessToken(token); + } + return null; + } + + @Nullable + public AuthorizationGrant getBearerAuthorizationGrant(@Nullable String authorization) { + return getAuthorizationGrant(authorization, HttpAuthTokenType.Bearer); + } + + @Nullable + public AuthorizationGrant getBasicAuthorizationGrant(@Nullable String authorization) { + return getAuthorizationGrant(authorization, HttpAuthTokenType.Basic); + } + + @Nullable + public AuthorizationGrant getAuthorizationGrant(@Nullable String authorization, @Nullable HttpAuthTokenType tokenType) { + final String token = getToken(authorization, tokenType); + if (StringUtils.isNotBlank(token)) { + return authorizationGrantList.getAuthorizationGrantByAccessToken(token); + } + return null; + } + + @NotNull + public String getClientDn(@Nullable String p_authorization) { + final AuthorizationGrant grant = getAuthorizationGrant(p_authorization); + if (grant != null) { + return grant.getClientDn(); + } + return ""; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/servlet/BcFirebaseMessagingSwServlet.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/servlet/BcFirebaseMessagingSwServlet.java new file mode 100644 index 00000000..ae5c309a --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/servlet/BcFirebaseMessagingSwServlet.java @@ -0,0 +1,62 @@ +package org.gluu.oxauth.servlet; + +import org.apache.commons.io.IOUtils; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.util.Util; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +@WebServlet(urlPatterns = "/firebase-messaging-sw.js") +public class BcFirebaseMessagingSwServlet extends HttpServlet { + + private static final long serialVersionUID = 5445488800130871634L; + + @Inject + private Logger log; + + @Inject + private AppConfiguration appConfiguration; + + @Override + protected void doGet(HttpServletRequest httpServletRequest, HttpServletResponse response) + throws ServletException, IOException { + response.setContentType("application/javascript"); + loadFirebaseMessagingSwFile(response); + } + + private void loadFirebaseMessagingSwFile(HttpServletResponse response) { + String baseJavascriptFileConfiguration = "/WEB-INF/firebase-messaging-sw.js"; + try (InputStream in = getServletContext().getResourceAsStream(baseJavascriptFileConfiguration); + OutputStream out = response.getOutputStream()) { + String content = IOUtils.toString(in, StandardCharsets.UTF_8); + + Map publicConfiguration = new HashMap<>(); + publicConfiguration.put("apiKey", appConfiguration.getCibaEndUserNotificationConfig().getApiKey()); + publicConfiguration.put("authDomain", appConfiguration.getCibaEndUserNotificationConfig().getAuthDomain()); + publicConfiguration.put("databaseURL", appConfiguration.getCibaEndUserNotificationConfig().getDatabaseURL()); + publicConfiguration.put("projectId", appConfiguration.getCibaEndUserNotificationConfig().getProjectId()); + publicConfiguration.put("storageBucket", appConfiguration.getCibaEndUserNotificationConfig().getStorageBucket()); + publicConfiguration.put("messagingSenderId", appConfiguration.getCibaEndUserNotificationConfig().getMessagingSenderId()); + publicConfiguration.put("appId", appConfiguration.getCibaEndUserNotificationConfig().getAppId()); + + content = content.replace("'${FIREBASE_CONFIG}'", Util.asJson(publicConfiguration)); + + IOUtils.write(content, out, StandardCharsets.UTF_8); + } catch (IOException e) { + log.debug("Error loading firebase-messaging-sw.js configuration file: " + e.getMessage()); + } + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/servlet/OpenIdConfiguration.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/servlet/OpenIdConfiguration.java new file mode 100644 index 00000000..a68de445 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/servlet/OpenIdConfiguration.java @@ -0,0 +1,480 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.servlet; + +import org.apache.commons.lang.BooleanUtils; +import org.apache.commons.lang.StringUtils; +import org.gluu.model.GluuAttribute; +import org.gluu.oxauth.ciba.CIBAConfigurationService; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseMode; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.common.ScopeType; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.service.AttributeService; +import org.gluu.oxauth.service.LocalResponseCache; +import org.gluu.oxauth.service.ScopeService; +import org.gluu.oxauth.service.external.ExternalAuthenticationService; +import org.gluu.oxauth.service.external.ExternalDynamicScopeService; +import org.gluu.oxauth.util.ServerUtil; +import org.json.JSONArray; +import org.json.JSONObject; +import org.oxauth.persistence.model.Scope; +import org.oxauth.persistence.model.ScopeAttributes; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.*; + +import static org.gluu.oxauth.model.configuration.ConfigurationResponseClaim.*; +import static org.gluu.oxauth.model.util.StringUtils.implode; + +/** + * @author Javier Rojas Blum + * @author Yuriy Movchan Date: 2016/04/26 + * @version August 14, 2019 + */ +@WebServlet(urlPatterns = "/.well-known/openid-configuration", loadOnStartup = 10) +public class OpenIdConfiguration extends HttpServlet { + + private static final long serialVersionUID = -8224898157373678903L; + + @Inject + private Logger log; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private AttributeService attributeService; + + @Inject + private ScopeService scopeService; + + @Inject + private ExternalAuthenticationService externalAuthenticationService; + + @Inject + private ExternalDynamicScopeService externalDynamicScopeService; + + @Inject + private CIBAConfigurationService cibaConfigurationService; + + @Inject + private LocalResponseCache localResponseCache; + + + /** + * Processes requests for both HTTP GET and POST methods. + * + * @param servletRequest servlet request + * @param httpResponse servlet response + * @throws IOException I/O exception + */ + @SuppressWarnings("deprecation") + protected void processRequest(HttpServletRequest servletRequest, HttpServletResponse httpResponse) throws IOException { + if (!(externalAuthenticationService.isLoaded() && externalDynamicScopeService.isLoaded())) { + httpResponse.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); + log.error("oxAuth still starting up!"); + return; + } + + httpResponse.setContentType("application/json"); + try (PrintWriter out = httpResponse.getWriter()) { + final JSONObject cachedResponse = localResponseCache.getDiscoveryResponse(); + if (cachedResponse != null) { + log.trace("Cached discovery response returned."); + out.println(ServerUtil.toPrettyJson(cachedResponse).replace("\\/", "/")); + return; + } + + JSONObject jsonObj = new JSONObject(); + + jsonObj.put(ISSUER, appConfiguration.getIssuer()); + jsonObj.put(AUTHORIZATION_ENDPOINT, appConfiguration.getAuthorizationEndpoint()); + jsonObj.put(TOKEN_ENDPOINT, appConfiguration.getTokenEndpoint()); + jsonObj.put(TOKEN_REVOCATION_ENDPOINT, appConfiguration.getTokenRevocationEndpoint()); // remove this line + // in 5.x + jsonObj.put(REVOCATION_ENDPOINT, appConfiguration.getTokenRevocationEndpoint()); + jsonObj.put(SESSION_REVOCATION_ENDPOINT, endpointUrl("/revoke_session")); + jsonObj.put(USER_INFO_ENDPOINT, appConfiguration.getUserInfoEndpoint()); + jsonObj.put(CLIENT_INFO_ENDPOINT, appConfiguration.getClientInfoEndpoint()); + jsonObj.put(CHECK_SESSION_IFRAME, appConfiguration.getCheckSessionIFrame()); + jsonObj.put(END_SESSION_ENDPOINT, appConfiguration.getEndSessionEndpoint()); + jsonObj.put(JWKS_URI, appConfiguration.getJwksUri()); + jsonObj.put(REGISTRATION_ENDPOINT, appConfiguration.getRegistrationEndpoint()); + jsonObj.put(ID_GENERATION_ENDPOINT, appConfiguration.getIdGenerationEndpoint()); + jsonObj.put(INTROSPECTION_ENDPOINT, appConfiguration.getIntrospectionEndpoint()); + jsonObj.put(DEVICE_AUTHZ_ENDPOINT, appConfiguration.getDeviceAuthzEndpoint()); + + JSONArray responseTypesSupported = new JSONArray(); + for (Set responseTypes : appConfiguration.getResponseTypesSupported()) { + responseTypesSupported.put(implode(responseTypes, " ")); + } + if (responseTypesSupported.length() > 0) { + jsonObj.put(RESPONSE_TYPES_SUPPORTED, responseTypesSupported); + } + + JSONArray responseModesSupported = new JSONArray(); + if (appConfiguration.getResponseModesSupported() != null) { + for (ResponseMode responseMode : appConfiguration.getResponseModesSupported()) { + responseModesSupported.put(responseMode); + } + } + if (responseModesSupported.length() > 0) { + jsonObj.put(RESPONSE_MODES_SUPPORTED, responseModesSupported); + } + + JSONArray grantTypesSupported = new JSONArray(); + for (GrantType grantType : appConfiguration.getGrantTypesSupported()) { + grantTypesSupported.put(grantType); + } + if (grantTypesSupported.length() > 0) { + jsonObj.put(GRANT_TYPES_SUPPORTED, grantTypesSupported); + } + + JSONArray acrValuesSupported = new JSONArray(); + for (String acr : externalAuthenticationService.getAcrValuesList()) { + acrValuesSupported.put(acr); + } + jsonObj.put(ACR_VALUES_SUPPORTED, acrValuesSupported); + jsonObj.put(AUTH_LEVEL_MAPPING, createAuthLevelMapping()); + + JSONArray subjectTypesSupported = new JSONArray(); + for (String subjectType : appConfiguration.getSubjectTypesSupported()) { + subjectTypesSupported.put(subjectType); + } + if (subjectTypesSupported.length() > 0) { + jsonObj.put(SUBJECT_TYPES_SUPPORTED, subjectTypesSupported); + } + + JSONArray userInfoSigningAlgValuesSupported = new JSONArray(); + for (String userInfoSigningAlg : appConfiguration.getUserInfoSigningAlgValuesSupported()) { + userInfoSigningAlgValuesSupported.put(userInfoSigningAlg); + } + if (userInfoSigningAlgValuesSupported.length() > 0) { + jsonObj.put(USER_INFO_SIGNING_ALG_VALUES_SUPPORTED, userInfoSigningAlgValuesSupported); + } + + JSONArray userInfoEncryptionAlgValuesSupported = new JSONArray(); + for (String userInfoEncryptionAlg : appConfiguration.getUserInfoEncryptionAlgValuesSupported()) { + userInfoEncryptionAlgValuesSupported.put(userInfoEncryptionAlg); + } + if (userInfoEncryptionAlgValuesSupported.length() > 0) { + jsonObj.put(USER_INFO_ENCRYPTION_ALG_VALUES_SUPPORTED, userInfoEncryptionAlgValuesSupported); + } + + JSONArray userInfoEncryptionEncValuesSupported = new JSONArray(); + for (String userInfoEncryptionEnc : appConfiguration.getUserInfoEncryptionEncValuesSupported()) { + userInfoEncryptionEncValuesSupported.put(userInfoEncryptionEnc); + } + if (userInfoEncryptionAlgValuesSupported.length() > 0) { + jsonObj.put(USER_INFO_ENCRYPTION_ENC_VALUES_SUPPORTED, userInfoEncryptionAlgValuesSupported); + } + + JSONArray idTokenSigningAlgValuesSupported = new JSONArray(); + for (String idTokenSigningAlg : appConfiguration.getIdTokenSigningAlgValuesSupported()) { + idTokenSigningAlgValuesSupported.put(idTokenSigningAlg); + } + if (idTokenSigningAlgValuesSupported.length() > 0) { + jsonObj.put(ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED, idTokenSigningAlgValuesSupported); + } + + JSONArray idTokenEncryptionAlgValuesSupported = new JSONArray(); + for (String idTokenEncryptionAlg : appConfiguration.getIdTokenEncryptionAlgValuesSupported()) { + idTokenEncryptionAlgValuesSupported.put(idTokenEncryptionAlg); + } + if (idTokenEncryptionAlgValuesSupported.length() > 0) { + jsonObj.put(ID_TOKEN_ENCRYPTION_ALG_VALUES_SUPPORTED, idTokenEncryptionAlgValuesSupported); + } + + JSONArray idTokenEncryptionEncValuesSupported = new JSONArray(); + for (String idTokenEncryptionEnc : appConfiguration.getIdTokenEncryptionEncValuesSupported()) { + idTokenEncryptionEncValuesSupported.put(idTokenEncryptionEnc); + } + if (idTokenEncryptionEncValuesSupported.length() > 0) { + jsonObj.put(ID_TOKEN_ENCRYPTION_ENC_VALUES_SUPPORTED, idTokenEncryptionEncValuesSupported); + } + + JSONArray requestObjectSigningAlgValuesSupported = new JSONArray(); + for (String requestObjectSigningAlg : appConfiguration.getRequestObjectSigningAlgValuesSupported()) { + requestObjectSigningAlgValuesSupported.put(requestObjectSigningAlg); + } + if (requestObjectSigningAlgValuesSupported.length() > 0) { + jsonObj.put(REQUEST_OBJECT_SIGNING_ALG_VALUES_SUPPORTED, requestObjectSigningAlgValuesSupported); + } + + JSONArray requestObjectEncryptionAlgValuesSupported = new JSONArray(); + for (String requestObjectEncryptionAlg : appConfiguration.getRequestObjectEncryptionAlgValuesSupported()) { + requestObjectEncryptionAlgValuesSupported.put(requestObjectEncryptionAlg); + } + if (requestObjectEncryptionAlgValuesSupported.length() > 0) { + jsonObj.put(REQUEST_OBJECT_ENCRYPTION_ALG_VALUES_SUPPORTED, requestObjectEncryptionAlgValuesSupported); + } + + JSONArray requestObjectEncryptionEncValuesSupported = new JSONArray(); + for (String requestObjectEncryptionEnc : appConfiguration.getRequestObjectEncryptionEncValuesSupported()) { + requestObjectEncryptionEncValuesSupported.put(requestObjectEncryptionEnc); + } + if (requestObjectEncryptionEncValuesSupported.length() > 0) { + jsonObj.put(REQUEST_OBJECT_ENCRYPTION_ENC_VALUES_SUPPORTED, requestObjectEncryptionEncValuesSupported); + } + + JSONArray tokenEndpointAuthMethodsSupported = new JSONArray(); + for (String tokenEndpointAuthMethod : appConfiguration.getTokenEndpointAuthMethodsSupported()) { + tokenEndpointAuthMethodsSupported.put(tokenEndpointAuthMethod); + } + if (tokenEndpointAuthMethodsSupported.length() > 0) { + jsonObj.put(TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, tokenEndpointAuthMethodsSupported); + } + + JSONArray tokenEndpointAuthSigningAlgValuesSupported = new JSONArray(); + for (String tokenEndpointAuthSigningAlg : appConfiguration + .getTokenEndpointAuthSigningAlgValuesSupported()) { + tokenEndpointAuthSigningAlgValuesSupported.put(tokenEndpointAuthSigningAlg); + } + if (tokenEndpointAuthSigningAlgValuesSupported.length() > 0) { + jsonObj.put(TOKEN_ENDPOINT_AUTH_SIGNING_ALG_VALUES_SUPPORTED, + tokenEndpointAuthSigningAlgValuesSupported); + } + + JSONArray displayValuesSupported = new JSONArray(); + for (String display : appConfiguration.getDisplayValuesSupported()) { + displayValuesSupported.put(display); + } + if (displayValuesSupported.length() > 0) { + jsonObj.put(DISPLAY_VALUES_SUPPORTED, displayValuesSupported); + } + + JSONArray claimTypesSupported = new JSONArray(); + for (String claimType : appConfiguration.getClaimTypesSupported()) { + claimTypesSupported.put(claimType); + } + if (claimTypesSupported.length() > 0) { + jsonObj.put(CLAIM_TYPES_SUPPORTED, claimTypesSupported); + } + + jsonObj.put(SERVICE_DOCUMENTATION, appConfiguration.getServiceDocumentation()); + + JSONArray idTokenTokenBindingCnfValuesSupported = new JSONArray(); + for (String value : appConfiguration.getIdTokenTokenBindingCnfValuesSupported()) { + idTokenTokenBindingCnfValuesSupported.put(value); + } + jsonObj.put(ID_TOKEN_TOKEN_BINDING_CNF_VALUES_SUPPORTED, idTokenTokenBindingCnfValuesSupported); + + JSONArray claimsLocalesSupported = new JSONArray(); + for (String claimLocale : appConfiguration.getClaimsLocalesSupported()) { + claimsLocalesSupported.put(claimLocale); + } + if (claimsLocalesSupported.length() > 0) { + jsonObj.put(CLAIMS_LOCALES_SUPPORTED, claimsLocalesSupported); + } + + JSONArray uiLocalesSupported = new JSONArray(); + for (String uiLocale : appConfiguration.getUiLocalesSupported()) { + uiLocalesSupported.put(uiLocale); + } + if (uiLocalesSupported.length() > 0) { + jsonObj.put(UI_LOCALES_SUPPORTED, uiLocalesSupported); + } + + JSONArray scopesSupported = new JSONArray(); + JSONArray claimsSupported = new JSONArray(); + JSONArray scopeToClaimsMapping = createScopeToClaimsMapping(scopesSupported, claimsSupported); + if (scopesSupported.length() > 0) { + jsonObj.put(SCOPES_SUPPORTED, scopesSupported); + } + if (claimsSupported.length() > 0) { + jsonObj.put(CLAIMS_SUPPORTED, claimsSupported); + } + jsonObj.put(SCOPE_TO_CLAIMS_MAPPING, scopeToClaimsMapping); + + jsonObj.put(CLAIMS_PARAMETER_SUPPORTED, appConfiguration.getClaimsParameterSupported()); + jsonObj.put(REQUEST_PARAMETER_SUPPORTED, appConfiguration.getRequestParameterSupported()); + jsonObj.put(REQUEST_URI_PARAMETER_SUPPORTED, appConfiguration.getRequestUriParameterSupported()); + jsonObj.put(REQUIRE_REQUEST_URI_REGISTRATION, appConfiguration.getRequireRequestUriRegistration()); + jsonObj.put(OP_POLICY_URI, appConfiguration.getOpPolicyUri()); + jsonObj.put(OP_TOS_URI, appConfiguration.getOpTosUri()); + jsonObj.put(TLS_CLIENT_CERTIFICATE_BOUND_ACCESS_TOKENS, Boolean.TRUE); + jsonObj.put(BACKCHANNEL_LOGOUT_SUPPORTED, Boolean.TRUE); + jsonObj.put(BACKCHANNEL_LOGOUT_SESSION_SUPPORTED, Boolean.TRUE); + jsonObj.put(FRONTCHANNEL_LOGOUT_SUPPORTED, Boolean.TRUE); + jsonObj.put(FRONTCHANNEL_LOGOUT_SESSION_SUPPORTED, Boolean.TRUE); + jsonObj.put(FRONT_CHANNEL_LOGOUT_SESSION_SUPPORTED, + appConfiguration.getFrontChannelLogoutSessionSupported()); + + filterOutKeys(jsonObj, appConfiguration); + + // CIBA Configuration + cibaConfigurationService.processConfiguration(jsonObj); + localResponseCache.putDiscoveryResponse(jsonObj); + + out.println(ServerUtil.toPrettyJson(jsonObj).replace("\\/", "/")); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + + public static void filterOutKeys(JSONObject jsonObj, AppConfiguration appConfiguration) { + if (BooleanUtils.isTrue(appConfiguration.isAllowBlankValuesInDiscoveryResponse())) { + return; + } + + // filter out keys with blank values + for (String key : new HashSet<>(jsonObj.keySet())) { + if (jsonObj.get(key) == null || StringUtils.isBlank(jsonObj.optString(key))) { + jsonObj.remove(key); + } + } + } + + private String endpointUrl(String path) { + return StringUtils.replace(appConfiguration.getEndSessionEndpoint(), "/end_session", path); + } + + /** + * @deprecated theses params: + *
    + *
  • id_generation_endpoint
  • + *
  • introspection_endpoint
  • + *
  • auth_level_mapping
  • + *
  • scope_to_claims_mapping
  • + *
+ * will be moved from /.well-known/openid-configuration to + * /.well-known/gluu-configuration + */ + @Deprecated + private JSONArray createScopeToClaimsMapping(JSONArray scopesSupported, JSONArray claimsSupported) { + final JSONArray scopeToClaimMapping = new JSONArray(); + Set scopes = new HashSet(); + Set claims = new HashSet(); + + try { + for (Scope scope : scopeService.getAllScopesList()) { + if ((scope.getScopeType() == ScopeType.SPONTANEOUS && scope.isDeletable()) + || !(canShowInConfigEndpoint(scope.getAttributes()))) { + continue; + } + + final JSONArray claimsList = new JSONArray(); + final JSONObject mapping = new JSONObject(); + mapping.put(scope.getId(), claimsList); + scopes.add(scope.getId()); + + scopeToClaimMapping.put(mapping); + + if (ScopeType.DYNAMIC.equals(scope.getScopeType())) { + List claimNames = externalDynamicScopeService + .executeExternalGetSupportedClaimsMethods(Arrays.asList(scope)); + for (String claimName : claimNames) { + if (StringUtils.isNotBlank(claimName)) { + claimsList.put(claimName); + claims.add(claimName); + } + } + } else { + final List claimIdList = scope.getOxAuthClaims(); + if (claimIdList != null && !claimIdList.isEmpty()) { + for (String claimDn : claimIdList) { + final GluuAttribute attribute = attributeService.getAttributeByDn(claimDn); + final String claimName = attribute.getOxAuthClaimName(); + if (StringUtils.isNotBlank(claimName)) { + claimsList.put(claimName); + claims.add(claimName); + } + } + } + } + } + + for (String scope : scopes) { + scopesSupported.put(scope); + } + for (String claim : claims) { + claimsSupported.put(claim); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return scopeToClaimMapping; + } + + private boolean canShowInConfigEndpoint(ScopeAttributes scopeAttributes) { + return scopeAttributes.isShowInConfigurationEndpoint(); + } + + /** + * @deprecated theses params: + *
    + *
  • id_generation_endpoint
  • + *
  • introspection_endpoint
  • + *
  • auth_level_mapping
  • + *
  • scope_to_claims_mapping
  • + *
+ * will be moved from /.well-known/openid-configuration to + * /.well-known/gluu-configuration + */ + @Deprecated + private JSONObject createAuthLevelMapping() { + final JSONObject mappings = new JSONObject(); + try { + Map> map = externalAuthenticationService.levelToAcrMapping(); + for (Integer level : map.keySet()) + mappings.put(level.toString(), map.get(level)); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return mappings; + } + + /** + * Handles the HTTP GET method. + * + * @param request + * servlet request + * @param response + * servlet response + * @throws IOException + * if an I/O error occurs + */ + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { + processRequest(request, response); + } + + /** + * Handles the HTTP POST method. + * + * @param request + * servlet request + * @param response + * servlet response + * @throws IOException + * if an I/O error occurs + */ + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { + processRequest(request, response); + } + + /** + * Returns a short description of the servlet. + * + * @return a String containing servlet description + */ + @Override + public String getServletInfo() { + return "OpenID Provider Configuration Information"; + } + +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/servlet/OxAuthFaviconServlet.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/servlet/OxAuthFaviconServlet.java new file mode 100644 index 00000000..d4194d5a --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/servlet/OxAuthFaviconServlet.java @@ -0,0 +1,79 @@ +package org.gluu.oxauth.servlet; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Date; + +import javax.inject.Inject; +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.GluuOrganization; +import org.gluu.oxauth.service.OrganizationService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +@WebServlet(urlPatterns = "/servlet/favicon") +public class OxAuthFaviconServlet extends HttpServlet { + + @Inject + private OrganizationService organizationService; + + private static final long serialVersionUID = 5445488800130871634L; + + private static final Logger log = LoggerFactory.getLogger(OxAuthFaviconServlet.class); + public static final String BASE_OXAUTH_FAVICON_PATH = "/opt/gluu/jetty/oxauth/custom/static/favicon/"; + + @Override + protected void doGet(HttpServletRequest httpServletRequest, HttpServletResponse response) + throws ServletException, IOException { + response.setContentType("image/x-icon"); + response.setDateHeader("Expires", new Date().getTime()+1000L*1800); + GluuOrganization organization = organizationService.getOrganization(); + boolean hasSucceed = readCustomFavicon(response, organization); + if (!hasSucceed) { + readDefaultFavicon(response); + } + } + + private boolean readDefaultFavicon(HttpServletResponse response) { + String defaultFaviconFileName = "/WEB-INF/static/favicon.ico"; + try (InputStream in = getServletContext().getResourceAsStream(defaultFaviconFileName); + OutputStream out = response.getOutputStream()) { + IOUtils.copy(in, out); + return true; + } catch (IOException e) { + log.debug("Error loading default favicon: " + e.getMessage()); + return false; + } + } + + private boolean readCustomFavicon(HttpServletResponse response, GluuOrganization organization) { + if (organization.getOxAuthFaviconPath() == null || StringUtils.isEmpty(organization.getOxAuthFaviconPath())) { + return false; + } + + File directory = new File(BASE_OXAUTH_FAVICON_PATH); + if (!directory.exists()) { + directory.mkdir(); + } + File faviconPath = new File(organization.getOxAuthFaviconPath()); + if (!faviconPath.exists()) { + return false; + } + try (InputStream in = new FileInputStream(faviconPath); OutputStream out = response.getOutputStream()) { + IOUtils.copy(in, out); + return true; + } catch (IOException e) { + log.debug("Error loading custom favicon: " + e.getMessage()); + return false; + } + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/servlet/OxAuthLogoServlet.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/servlet/OxAuthLogoServlet.java new file mode 100644 index 00000000..36a58e45 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/servlet/OxAuthLogoServlet.java @@ -0,0 +1,78 @@ +package org.gluu.oxauth.servlet; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Date; + +import javax.inject.Inject; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.GluuOrganization; +import org.gluu.oxauth.service.OrganizationService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@WebServlet(urlPatterns = "/servlet/logo") +public class OxAuthLogoServlet extends HttpServlet { + + private static final long serialVersionUID = 5445488800130871634L; + + private static final Logger log = LoggerFactory.getLogger(OxAuthLogoServlet.class); + + public static final String BASE_OXAUTH_LOGO_PATH = "/opt/gluu/jetty/oxauth/custom/static/logo/"; + + @Inject + private OrganizationService organizationService; + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) { + response.setContentType("/image/jpg"); + response.setDateHeader("Expires", new Date().getTime()+1000L*1800); + GluuOrganization organization = organizationService.getOrganization(); + boolean hasSucceed = readCustomLogo(response, organization); + if (!hasSucceed) { + readDefaultLogo(response); + } + } + + private boolean readDefaultLogo(HttpServletResponse response) { + String defaultLogoFileName = "/WEB-INF/static/logo.png"; + try (InputStream in = getServletContext().getResourceAsStream(defaultLogoFileName); + OutputStream out = response.getOutputStream()) { + IOUtils.copy(in, out); + return true; + } catch (IOException e) { + log.debug("---------------Error loading default logo: " + e.getMessage()); + return false; + } + } + + private boolean readCustomLogo(HttpServletResponse response, GluuOrganization organization) { + if (organization.getOxAuthLogoPath() == null || StringUtils.isEmpty(organization.getOxAuthLogoPath())) { + return false; + } + File directory = new File(BASE_OXAUTH_LOGO_PATH); + if (!directory.exists()) { + directory.mkdir(); + } + File logoPath = new File(organization.getOxAuthLogoPath()); + if (!logoPath.exists()) { + return false; + } + try (InputStream in = new FileInputStream(logoPath); OutputStream out = response.getOutputStream()) { + IOUtils.copy(in, out); + return true; + } catch (IOException e) { + log.debug("Error loading custom logo: " + e.getMessage()); + return false; + } + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/servlet/SectorIdentifier.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/servlet/SectorIdentifier.java new file mode 100644 index 00000000..de957b10 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/servlet/SectorIdentifier.java @@ -0,0 +1,82 @@ +package org.gluu.oxauth.servlet; + +import org.gluu.oxauth.service.SectorIdentifierService; +import org.json.JSONArray; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; + +/** + * @author Javier Rojas Blum + * @version January 15, 2016 + */ +@WebServlet(urlPatterns = "/sectoridentifier/*") +public class SectorIdentifier extends HttpServlet { + + private static final long serialVersionUID = -1222077047492070618L; + + @Inject + private Logger log; + + @Inject + private SectorIdentifierService sectorIdentifierService; + + protected void processRequest(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + final HttpServletRequest httpRequest = request; + final HttpServletResponse httpResponse = response; + + httpResponse.setContentType("application/json"); + PrintWriter out = httpResponse.getWriter(); + try { + String urlPath = httpRequest.getPathInfo(); + String oxId = urlPath.substring(urlPath.lastIndexOf("/") + 1, urlPath.length()); + + org.oxauth.persistence.model.SectorIdentifier sectorIdentifier = sectorIdentifierService.getSectorIdentifierById(oxId); + + JSONArray jsonArray = new JSONArray(); + + for (String redirectUri : sectorIdentifier.getRedirectUris()) { + jsonArray.put(redirectUri); + } + + out.println(jsonArray.toString(4).replace("\\/", "/")); + } catch (Exception e) { + log.error(e.getMessage(), e); + } finally { + out.close(); + } + } + + /** + * Handles the HTTP + * GET method. + * + * @param request servlet request + * @param response servlet response + * @throws ServletException if a servlet-specific error occurs + * @throws IOException if an I/O error occurs + */ + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + processRequest(request, response); + } + + /** + * Returns a short description of the servlet. + * + * @return a String containing servlet description + */ + @Override + public String getServletInfo() { + return "Sector Identifier"; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/servlet/WebFinger.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/servlet/WebFinger.java new file mode 100644 index 00000000..a7c16f3f --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/servlet/WebFinger.java @@ -0,0 +1,135 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.servlet; + +import static org.gluu.oxauth.model.discovery.WebFingerParam.HREF; +import static org.gluu.oxauth.model.discovery.WebFingerParam.LINKS; +import static org.gluu.oxauth.model.discovery.WebFingerParam.REL; +import static org.gluu.oxauth.model.discovery.WebFingerParam.REL_VALUE; +import static org.gluu.oxauth.model.discovery.WebFingerParam.RESOURCE; +import static org.gluu.oxauth.model.discovery.WebFingerParam.SUBJECT; + +import java.io.IOException; +import java.io.PrintWriter; + +import javax.inject.Inject; +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.discovery.OpenIdConnectDiscoveryParamsValidator; +import org.slf4j.Logger; + +/** + * @author Javier Rojas Blum Date: 01.28.2013 + */ +@WebServlet(urlPatterns = "/.well-known/webfinger") +public class WebFinger extends HttpServlet { + + private static final long serialVersionUID = -4708834950205359151L; + + @Inject + private Logger log; + + @Inject + private AppConfiguration appConfiguration; + + /** + * Processes requests for both HTTP GET and POST methods. + * + * @param request servlet request + * @param response servlet response + * @throws javax.servlet.ServletException if a servlet-specific error occurs + * @throws java.io.IOException if an I/O error occurs + */ + protected void processRequest(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + final HttpServletRequest httpRequest = request; + final HttpServletResponse httpResponse = response; + + httpResponse.setContentType("application/jrd+json"); + PrintWriter out = httpResponse.getWriter(); + + String resource = httpRequest.getParameter(RESOURCE); + String rel = httpRequest.getParameter(REL); + + log.debug("Attempting to request OpenID Connect Discovery: " + resource + ", " + rel + ", Is Secure = " + httpRequest.isSecure()); + + try { + if (OpenIdConnectDiscoveryParamsValidator.validateParams(resource, rel)) { + if (rel == null || rel.equals(REL_VALUE)) { + JSONObject jsonObj = new JSONObject(); + jsonObj.put(SUBJECT, resource); + + JSONArray linksJsonArray = new JSONArray(); + JSONObject linkJsonObject = new JSONObject(); + linkJsonObject.put(REL, REL_VALUE); + linkJsonObject.put(HREF, appConfiguration.getIssuer()); + + linksJsonArray.put(linkJsonObject); + jsonObj.put(LINKS, linksJsonArray); + + out.println(jsonObj.toString(4).replace("\\/", "/")); + } + } + } catch (JSONException e) { + log.error(e.getMessage(), e); + } + + out.close(); + } + + /** + * Handles the HTTP GET method. + * + * @param request servlet request + * @param response servlet response + * @throws javax.servlet.ServletException if a servlet-specific error occurs + * @throws java.io.IOException if an I/O error occurs + */ + @Override + protected void doGet + (HttpServletRequest + request, HttpServletResponse + response) + throws ServletException, IOException { + processRequest(request, response); + } + + /** + * Handles the HTTP POST method. + * + * @param request servlet request + * @param response servlet response + * @throws ServletException if a servlet-specific error occurs + * @throws IOException if an I/O error occurs + */ + @Override + protected void doPost + (HttpServletRequest + request, HttpServletResponse + response) + throws ServletException, IOException { + processRequest(request, response); + } + + /** + * Returns a short description of the servlet. + * + * @return a String containing servlet description + */ + @Override + public String getServletInfo() { + return "OpenID Connect Discovery"; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/session/ws/rs/CheckSessionStatusRestWebServiceImpl.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/session/ws/rs/CheckSessionStatusRestWebServiceImpl.java new file mode 100644 index 00000000..c2b2401d --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/session/ws/rs/CheckSessionStatusRestWebServiceImpl.java @@ -0,0 +1,116 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.session.ws.rs; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.service.CookieService; +import org.gluu.oxauth.service.SessionIdService; +import org.gluu.oxauth.util.ServerUtil; +import org.gluu.util.StringHelper; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; +import java.io.IOException; +import java.util.Date; + +/** + * @author Yuriy Movchan + * @version August 9, 2017 + */ +@Path("/") +public class CheckSessionStatusRestWebServiceImpl { + + @Inject + private Logger log; + + @Inject + private SessionIdService sessionIdService; + + @Inject + private CookieService cookieService; + + @GET + @Path("/session_status") + @Produces({MediaType.APPLICATION_JSON}) + public Response requestCheckSessionStatus(@Context HttpServletRequest httpRequest, @Context HttpServletResponse httpResponse, + @Context SecurityContext securityContext) throws IOException { + String sessionIdCookie = cookieService.getSessionIdFromCookie(httpRequest); + log.debug("Found session '{}' cookie: '{}'", CookieService.SESSION_ID_COOKIE_NAME, sessionIdCookie); + + CheckSessionResponse response = new CheckSessionResponse("unknown", ""); + + SessionId sessionId = sessionIdService.getSessionId(sessionIdCookie); + if (sessionId != null) { + response.setState(sessionId.getState().getValue()); + response.setAuthTime(sessionId.getAuthenticationTime()); + + String sessionCustomState = sessionId.getSessionAttributes().get(SessionIdService.SESSION_CUSTOM_STATE); + if (StringHelper.isNotEmpty(sessionCustomState)) { + response.setCustomState(sessionCustomState); + } + } + + String responseJson = ServerUtil.asJson(response); + log.debug("Check session status response: '{}'", responseJson); + + return Response.ok().type(MediaType.APPLICATION_JSON).entity(responseJson).build(); + } + + class CheckSessionResponse { + + @JsonProperty(value = "state") + String state; + + @JsonProperty(value = "custom_state") + String customState; + + @JsonProperty(value = "auth_time") + Date authTime; + + public CheckSessionResponse(String state, String stateExt) { + this.state = state; + this.customState = stateExt; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public String getCustomState() { + return customState; + } + + public void setCustomState(String customState) { + this.customState = customState; + } + + public Date getAuthTime() { + return authTime; + } + + public void setAuthTime(Date authTime) { + this.authTime = authTime; + } + + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/session/ws/rs/EndSessionRestWebService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/session/ws/rs/EndSessionRestWebService.java new file mode 100644 index 00000000..e2d280b1 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/session/ws/rs/EndSessionRestWebService.java @@ -0,0 +1,41 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.session.ws.rs; + +import org.gluu.oxauth.model.session.EndSessionRequestParam; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; + +/** + * @author Javier Rojas Blum + * @version August 9, 2017 + */ +public interface EndSessionRestWebService { + + @GET + @Path("/end_session") + @Produces({MediaType.TEXT_PLAIN}) + Response requestEndSession(@QueryParam(EndSessionRequestParam.ID_TOKEN_HINT) String idTokenHint, + @QueryParam(EndSessionRequestParam.POST_LOGOUT_REDIRECT_URI) String postLogoutRedirectUri, + @QueryParam(EndSessionRequestParam.STATE) String state, + @QueryParam("session_id") String sessionId, + @QueryParam("sid") String sid, + @QueryParam("client_id") String clientId, + @Context HttpServletRequest httpRequest, + @Context HttpServletResponse httpResponse, + @Context SecurityContext securityContext); + +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/session/ws/rs/EndSessionRestWebServiceImpl.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/session/ws/rs/EndSessionRestWebServiceImpl.java new file mode 100644 index 00000000..16e582ed --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/session/ws/rs/EndSessionRestWebServiceImpl.java @@ -0,0 +1,614 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.session.ws.rs; + +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import org.apache.commons.lang.StringUtils; +import org.gluu.model.security.Identity; +import org.gluu.oxauth.audit.ApplicationAuditLogger; +import org.gluu.oxauth.model.audit.Action; +import org.gluu.oxauth.model.audit.OAuth2AuditLog; +import org.gluu.oxauth.model.authorize.AuthorizeRequestParam; +import org.gluu.oxauth.model.common.AuthorizationGrant; +import org.gluu.oxauth.model.common.AuthorizationGrantList; +import org.gluu.oxauth.model.common.User; +import org.gluu.oxauth.model.config.Constants; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.crypto.AbstractCryptoProvider; +import org.gluu.oxauth.model.error.ErrorHandlingMethod; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.exception.InvalidJwtException; +import org.gluu.oxauth.model.gluu.GluuErrorResponseType; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.session.EndSessionErrorResponseType; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.model.token.JsonWebResponse; +import org.gluu.oxauth.model.util.URLPatternList; +import org.gluu.oxauth.model.util.Util; +import org.gluu.oxauth.service.*; +import org.gluu.oxauth.service.external.ExternalApplicationSessionService; +import org.gluu.oxauth.service.external.ExternalEndSessionService; +import org.gluu.oxauth.service.external.context.EndSessionContext; +import org.gluu.oxauth.util.ServerUtil; +import org.gluu.oxauth.util.TokenHashUtil; +import org.gluu.util.Pair; +import org.gluu.util.StringHelper; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.Path; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; + +import static org.apache.commons.lang.BooleanUtils.isTrue; + +/** + * @author Javier Rojas Blum + * @author Yuriy Movchan + * @author Yuriy Zabrovarnyy + * @version December 8, 2018 + */ +@Path("/") +public class EndSessionRestWebServiceImpl implements EndSessionRestWebService { + + @Inject + private Logger log; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private RedirectionUriService redirectionUriService; + + @Inject + private AuthorizationGrantList authorizationGrantList; + + @Inject + private ExternalApplicationSessionService externalApplicationSessionService; + + @Inject + private ExternalEndSessionService externalEndSessionService; + + @Inject + private SessionIdService sessionIdService; + + @Inject + private CookieService cookieService; + + @Inject + private ClientService clientService; + + @Inject + private GrantService grantService; + + @Inject + private Identity identity; + + @Inject + private ApplicationAuditLogger applicationAuditLogger; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private LogoutTokenFactory logoutTokenFactory; + + @Inject + private AbstractCryptoProvider cryptoProvider; + + @Override + public Response requestEndSession(String idTokenHint, String postLogoutRedirectUri, String state, String sessionId, String sid, String clientId, + HttpServletRequest httpRequest, HttpServletResponse httpResponse, SecurityContext sec) { + try { + log.debug("Attempting to end session, idTokenHint: {}, postLogoutRedirectUri: {}, sessionId: {}, sid: {}, Is Secure = {}, state = {}, client_id = {}", + idTokenHint, postLogoutRedirectUri, sessionId, sid, sec.isSecure(), state, clientId); + + if (StringUtils.isBlank(sid) && StringUtils.isNotBlank(sessionId)) + sid = sessionId; // backward compatibility. WIll be removed in next major release. + + final SessionId sidSession = validateSidRequestParameter(sid, postLogoutRedirectUri, state, clientId); + Jwt validatedIdToken = validateIdTokenHint(idTokenHint, sidSession, postLogoutRedirectUri, state, clientId); + + final Pair pair = getPair(idTokenHint, validatedIdToken, sid, httpRequest); + if (pair.getFirst() == null) { + final String reason = "Failed to identify session by session_id query parameter or by session_id cookie."; + throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_GRANT_AND_SESSION, reason, state, clientId)); + } + + postLogoutRedirectUri = validatePostLogoutRedirectUri(postLogoutRedirectUri, pair, state, clientId); + validateSid(postLogoutRedirectUri, validatedIdToken, pair.getFirst(), state, clientId); + + endSession(pair, httpRequest, httpResponse); + auditLogging(httpRequest, pair); + + Set clients = getSsoClients(pair); + Set frontchannelUris = Sets.newHashSet(); + Map backchannelUris = Maps.newHashMap(); + + for (Client client : clients) { + boolean hasBackchannel = false; + for (String logoutUri : client.getAttributes().getBackchannelLogoutUri()) { + if (Util.isNullOrEmpty(logoutUri)) { + continue; // skip if logout_uri is blank + } + backchannelUris.put(logoutUri, client); + hasBackchannel = true; + } + + if (hasBackchannel) { // client has backchannel_logout_uri + continue; + } + + for (String logoutUri : client.getFrontChannelLogoutUri()) { + if (Util.isNullOrEmpty(logoutUri)) { + continue; // skip if logout_uri is blank + } + + if (client.getFrontChannelLogoutSessionRequired()) { + logoutUri = EndSessionUtils.appendSid(logoutUri, pair.getFirst().getOutsideSid(), appConfiguration.getIssuer()); + } + frontchannelUris.add(logoutUri); + } + } + + backChannel(backchannelUris, pair.getSecond(), pair.getFirst()); + + if (frontchannelUris.isEmpty() && StringUtils.isNotBlank(postLogoutRedirectUri)) { // no front-channel + log.trace("No frontchannel_redirect_uri's found in clients involved in SSO."); + + try { + final String redirectTo = EndSessionUtils.appendState(postLogoutRedirectUri, state); + log.trace("Redirect to postlogout_redirect_uri: {}", redirectTo); + return Response.status(Response.Status.FOUND).location(new URI(redirectTo)).build(); + } catch (URISyntaxException e) { + final String message = "Failed to create URI for " + postLogoutRedirectUri + " postlogout_redirect_uri."; + log.error(message); + return Response.status(Response.Status.BAD_REQUEST).entity(errorResponseFactory.errorAsJson(EndSessionErrorResponseType.INVALID_REQUEST, message)).build(); + } + } + + + return httpBased(frontchannelUris, postLogoutRedirectUri, state, pair, httpRequest); + } catch (WebApplicationException e) { + if (e.getResponse() != null) { + return e.getResponse(); + } + throw e; + } catch (Exception e) { + log.error(e.getMessage(), e); + throw new WebApplicationException(Response + .status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(errorResponseFactory.getJsonErrorResponse(GluuErrorResponseType.SERVER_ERROR)) + .build()); + } + } + + private void validateSid(String postLogoutRedirectUri, Jwt idToken, SessionId session, String state, String clientId) { + if (idToken == null) { + return; + } + final String sid = idToken.getClaims().getClaimAsString("sid"); + if (StringUtils.isNotBlank(sid) && !sid.equals(session.getOutsideSid())) { + log.error("sid in id_token_hint does not match sid of the session. id_token_hint sid: {}, session sid: {}", sid, session.getOutsideSid()); + throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_REQUEST, "sid in id_token_hint does not match sid of the session", state, clientId)); + } + } + + private void backChannel(Map backchannelUris, AuthorizationGrant grant, SessionId session) throws InterruptedException { + if (backchannelUris.isEmpty()) { + return; + } + + log.trace("backchannel_redirect_uri's: " + backchannelUris); + + User user = grant != null ? grant.getUser() : null; + if (user == null) { + user = sessionIdService.getUser(session); + } + + final ExecutorService executorService = EndSessionUtils.getExecutorService(); + for (final Map.Entry entry : backchannelUris.entrySet()) { + final JsonWebResponse logoutToken = logoutTokenFactory.createLogoutToken(entry.getValue(), session.getOutsideSid(), user); + if (logoutToken == null) { + log.error("Failed to create logout_token for client: " + entry.getValue().getClientId()); + return; + } + executorService.execute(() -> EndSessionUtils.callRpWithBackchannelUri(entry.getKey(), logoutToken.toString())); + } + executorService.shutdown(); + executorService.awaitTermination(30, TimeUnit.SECONDS); + log.trace("Finished backchannel calls."); + } + + private Response createErrorResponse(String postLogoutRedirectUri, EndSessionErrorResponseType error, String reason, String state, String clientId) { + log.debug("Creating error response, reason: {}", reason); + try { + if (allowPostLogoutRedirect(postLogoutRedirectUri, clientId)) { + if (ErrorHandlingMethod.REMOTE == appConfiguration.getErrorHandlingMethod()) { + String separator = postLogoutRedirectUri.contains("?") ? "&" : "?"; + postLogoutRedirectUri = postLogoutRedirectUri + separator + errorResponseFactory.getErrorAsQueryString(error, "", reason); + } + final String redirectTo = EndSessionUtils.appendState(postLogoutRedirectUri, state); + log.trace("Redirect error to {}", redirectTo); + return Response.status(Response.Status.FOUND).location(new URI(redirectTo)).build(); + } + } catch (URISyntaxException e) { + log.error("Can't perform redirect", e); + } + + log.trace("Return 400 - error {}, reason {}", error, reason); + return Response.status(Response.Status.BAD_REQUEST).entity(errorResponseFactory.errorAsJson(error, reason)).build(); + } + + /** + * Allow post logout redirect without validation only if: + * allowPostLogoutRedirectWithoutValidation = true and post_logout_redirect_uri is white listed + */ + private boolean allowPostLogoutRedirect(String postLogoutRedirectUri, String clientId) { + if (StringUtils.isBlank(postLogoutRedirectUri)) { + log.trace("Post logout redirect is blank."); + return false; + } + + + final Boolean allowPostLogoutRedirectWithoutValidation = appConfiguration.getAllowPostLogoutRedirectWithoutValidation(); + boolean isOk = allowPostLogoutRedirectWithoutValidation != null && + allowPostLogoutRedirectWithoutValidation && + isUrlWhiteListed(postLogoutRedirectUri); + if (isOk) { + log.trace("Post logout redirect allowed by 'clientWhiteList' {}", appConfiguration.getClientWhiteList()); + return true; + } + + if (StringUtils.isNotBlank(clientId) && StringUtils.isNotBlank(redirectionUriService.validatePostLogoutRedirectUri(clientId, postLogoutRedirectUri))) { + log.trace("Post logout redirect allowed by client_id {}", clientId); + return true; + } + + log.trace("Post logout redirect is denied."); + return false; + + } + + public boolean isUrlWhiteListed(String url) { + final boolean result = new URLPatternList(appConfiguration.getClientWhiteList()).isUrlListed(url); + log.trace("White listed result: {}, url: {}", result, url); + return result; + } + + private SessionId validateSidRequestParameter(String sid, String postLogoutRedirectUri, String state, String clientId) { + // sid is not required but if it is present then we must validate it #831 + if (StringUtils.isNotBlank(sid)) { + SessionId sessionIdObject = sessionIdService.getSessionBySid(sid); + if (sessionIdObject == null) { + final String reason = "sid parameter in request is not valid. Logout is rejected. sid parameter in request can be skipped or otherwise valid value must be provided."; + log.error(reason); + throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_GRANT_AND_SESSION, reason, state, clientId)); + } + return sessionIdObject; + } + return null; + } + + protected Jwt validateIdTokenHint(String idTokenHint, SessionId sidSession, String postLogoutRedirectUri, String state, String clientId) { + final boolean isIdTokenHintRequired = isTrue(appConfiguration.getForceIdTokenHintPrecense()); + if (isIdTokenHintRequired && StringUtils.isBlank(idTokenHint)) { // must be present for logout tests #1279 + final String reason = "id_token_hint is not set"; + log.trace(reason); + throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_REQUEST, reason, state, clientId)); + } + + if (isIdTokenHintRequired && StringUtils.isBlank(idTokenHint)) { // must be present for logout tests #1279 + final String reason = "id_token_hint is not set"; + log.trace(reason); + throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_REQUEST, reason, state, clientId)); + } + + if (StringUtils.isBlank(idTokenHint) && !isIdTokenHintRequired) { + return null; + } + + // id_token_hint is not required but if it is present then we must validate it #831 + if (StringUtils.isNotBlank(idTokenHint) || isIdTokenHintRequired) { + final boolean isRejectEndSessionIfIdTokenExpired = appConfiguration.getRejectEndSessionIfIdTokenExpired(); + final AuthorizationGrant tokenHintGrant = getTokenHintGrant(idTokenHint); + + if (tokenHintGrant == null && isRejectEndSessionIfIdTokenExpired) { + final String reason = "id_token_hint is not valid. Logout is rejected. id_token_hint can be skipped or otherwise valid value must be provided."; + log.trace(reason); + throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_GRANT_AND_SESSION, reason, state, clientId)); + } + try { + final Jwt jwt = Jwt.parse(idTokenHint); + if (jwt == null) { + log.error("Unable to parse id_token_hint as JWT: {}", idTokenHint); + throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_GRANT_AND_SESSION, "Unable to parse id_token_hint as JWT.", state, clientId)); + } + if (tokenHintGrant != null) { // id_token is in db + log.debug("Found id_token in db."); + return jwt; + } + validateIdTokenSignature(sidSession, jwt, postLogoutRedirectUri, state, clientId); + log.debug("id_token is validated successfully."); + return jwt; + } catch (InvalidJwtException e) { + log.error("Unable to parse id_token_hint as JWT.", e); + throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_GRANT_AND_SESSION, "Unable to parse id_token_hint as JWT.", state, clientId)); + } catch (WebApplicationException e) { + throw e; + } catch (Exception e) { + log.error("Unable to validate id_token_hint as JWT.", e); + throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_GRANT_AND_SESSION, "Unable to validate id_token_hint as JWT.", state, clientId)); + } + } + return null; + } + + private void validateIdTokenSignature(SessionId sidSession, Jwt jwt, String postLogoutRedirectUri, String state, String clientId) throws Exception { + // verify jwt signature if we can't find it in db + if (!cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), jwt.getHeader().getKeyId(), + null, null, jwt.getHeader().getSignatureAlgorithm())) { + log.error("id_token signature verification failed."); + throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_GRANT_AND_SESSION, "id_token signature verification failed.", state, clientId)); + } + + if (isTrue(appConfiguration.getAllowEndSessionWithUnmatchedSid())) { + return; + } + final String sidClaim = jwt.getClaims().getClaimAsString("sid"); + if (sidSession != null && StringUtils.equals(sidSession.getOutsideSid(), sidClaim)) { + return; + } + log.error("sid claim from id_token does not match to any valid session on AS."); + throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_GRANT_AND_SESSION, "sid claim from id_token does not match to any valid session on AS.", state, clientId)); + } + + protected AuthorizationGrant getTokenHintGrant(String idTokenHint) { + if (StringUtils.isBlank(idTokenHint)) { + return null; + } + + AuthorizationGrant authorizationGrant = authorizationGrantList.getAuthorizationGrantByIdToken(TokenHashUtil.hash(idTokenHint)); + if (authorizationGrant != null) { + return authorizationGrant; + } + + authorizationGrant = authorizationGrantList.getAuthorizationGrantByIdToken(idTokenHint); + if (authorizationGrant != null) { + return authorizationGrant; + } + + Boolean endSessionWithAccessToken = appConfiguration.getEndSessionWithAccessToken(); + if ((endSessionWithAccessToken != null) && endSessionWithAccessToken) { + return authorizationGrantList.getAuthorizationGrantByAccessToken(idTokenHint); + } + return null; + } + + + public String validatePostLogoutRedirectUri(String postLogoutRedirectUri, Pair pair, String state, String clientId) { + try { + if (StringUtils.isBlank(postLogoutRedirectUri)) { + return ""; + } + if (isTrue(appConfiguration.getAllowPostLogoutRedirectWithoutValidation()) && isUrlWhiteListed(postLogoutRedirectUri)) { + log.trace("Skipped post_logout_redirect_uri validation (because allowPostLogoutRedirectWithoutValidation=true and white listed)"); + return postLogoutRedirectUri; + } + + String result; + if (pair.getSecond() == null) { + result = redirectionUriService.validatePostLogoutRedirectUri(pair.getFirst(), postLogoutRedirectUri); + } else { + result = redirectionUriService.validatePostLogoutRedirectUri(pair.getSecond().getClient().getClientId(), postLogoutRedirectUri); + } + + if (StringUtils.isBlank(result) && StringUtils.isNotBlank(clientId)) { + result = redirectionUriService.validatePostLogoutRedirectUri(clientId, postLogoutRedirectUri); + log.trace("Validated post_logout_redirect_uri: {} against client_id: {}, result: {}" , postLogoutRedirectUri, clientId, result); + } + + if (StringUtils.isBlank(result)) { + log.trace("Failed to validate post_logout_redirect_uri."); + throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.POST_LOGOUT_URI_NOT_ASSOCIATED_WITH_CLIENT, "", state, clientId)); + } + + if (StringUtils.isNotBlank(result)) { + return result; + } + log.trace("Unable to validate post_logout_redirect_uri."); + throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.POST_LOGOUT_URI_NOT_ASSOCIATED_WITH_CLIENT, "", state, clientId)); + } catch (WebApplicationException e) { + if (pair.getFirst() != null) { + log.error(e.getMessage(), e); + throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.POST_LOGOUT_URI_NOT_ASSOCIATED_WITH_CLIENT, "", state, clientId)); + } else { + throw e; + } + } + } + + private Response httpBased(Set frontchannelUris, String postLogoutRedirectUri, String state, Pair pair, HttpServletRequest httpRequest) { + try { + final EndSessionContext context = new EndSessionContext(httpRequest, frontchannelUris, postLogoutRedirectUri, pair.getFirst()); + final String htmlFromScript = externalEndSessionService.getFrontchannelHtml(context); + if (StringUtils.isNotBlank(htmlFromScript)) { + log.debug("HTML from `getFrontchannelHtml` external script: " + htmlFromScript); + return okResponse(htmlFromScript); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + + // default handling + final String html = EndSessionUtils.createFronthannelHtml(frontchannelUris, postLogoutRedirectUri, state); + log.debug("Constructed html logout page: " + html); + return okResponse(html); + } + + private Response okResponse(String html) { + return Response.ok(). + cacheControl(ServerUtil.cacheControl(true, true)). + header("Pragma", "no-cache"). + type(MediaType.TEXT_HTML_TYPE).entity(html). + build(); + } + + private Pair getPair(String idTokenHint, Jwt validatedIdToken, String sid, HttpServletRequest httpRequest) { + AuthorizationGrant authorizationGrant = authorizationGrantList.getAuthorizationGrantByIdToken(idTokenHint); + if (authorizationGrant == null) { + Boolean endSessionWithAccessToken = appConfiguration.getEndSessionWithAccessToken(); + if ((endSessionWithAccessToken != null) && endSessionWithAccessToken) { + authorizationGrant = authorizationGrantList.getAuthorizationGrantByAccessToken(idTokenHint); + } + } + + SessionId sessionId = null; + + try { + String cookieSessionId = cookieService.getSessionIdFromCookie(httpRequest); + if (StringHelper.isNotEmpty(cookieSessionId)) { + sessionId = sessionIdService.getSessionId(cookieSessionId); + } + + if (sessionId == null && StringUtils.isNotBlank(sid)) { + sessionId = sessionIdService.getSessionBySid(sid); + } + + if (sessionId == null && validatedIdToken != null) { + final String sidClaim = validatedIdToken.getClaims().getClaimAsString("sid"); + log.trace("id_token sid value: {}", sidClaim); + + if (StringUtils.isNotBlank(sidClaim)) { + sessionId = sessionIdService.getSessionBySid(sidClaim); + } + } + if (sessionId == null) { + log.trace("Unable to find session for ending."); + } else { + log.trace("Found session for ending successfully."); + } + + } catch (Exception e) { + log.error("Failed to find current session id.", e); + } + return new Pair<>(sessionId, authorizationGrant); + } + + private void endSession(Pair pair, HttpServletRequest httpRequest, HttpServletResponse httpResponse) { + // Clean up authorization session + removeConsentSessionId(httpRequest, httpResponse); + + removeSessionId(pair, httpResponse); + + boolean isExternalLogoutPresent; + boolean externalLogoutResult = false; + + isExternalLogoutPresent = externalApplicationSessionService.isEnabled(); + if (isExternalLogoutPresent) { + String userName = pair.getFirst().getSessionAttributes().get(Constants.AUTHENTICATED_USER); + externalLogoutResult = externalApplicationSessionService.executeExternalEndSessionMethods(httpRequest, pair.getFirst()); + log.info("End session result for '{}': '{}'", userName, externalLogoutResult); + } + + boolean isGrantAndExternalLogoutSuccessful = isExternalLogoutPresent && externalLogoutResult; + if (isExternalLogoutPresent && !isGrantAndExternalLogoutSuccessful) { + throw errorResponseFactory.createWebApplicationException(Response.Status.UNAUTHORIZED, EndSessionErrorResponseType.INVALID_GRANT, "External logout is present but executed external logout script returned failed result."); + } + + grantService.logout(pair.getFirst().getDn()); + + if (identity != null) { + identity.logout(); + } + } + + private Set getSsoClients(Pair pair) { + SessionId sessionId = pair.getFirst(); + AuthorizationGrant authorizationGrant = pair.getSecond(); + if (sessionId == null) { + log.error("session_id is not passed to endpoint (as cookie or manually). Therefore unable to match clients for session_id."); + return Sets.newHashSet(); + } + + final Set clients = sessionId.getPermissionGrantedMap() != null ? + clientService.getClient(sessionId.getPermissionGrantedMap().getClientIds(true), true) : + Sets.newHashSet(); + if (authorizationGrant != null) { + clients.add(authorizationGrant.getClient()); + } + return clients; + } + + private void removeSessionId(Pair pair, HttpServletResponse httpResponse) { + try { + boolean result = sessionIdService.remove(pair.getFirst()); + if (!result) { + log.error("Failed to remove session_id '{}'", pair.getFirst().getId()); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } finally { + cookieService.removeSessionIdCookie(httpResponse); + cookieService.removeOPBrowserStateCookie(httpResponse); + } + } + + private void removeConsentSessionId(HttpServletRequest httpRequest, HttpServletResponse httpResponse) { + try { + String id = cookieService.getConsentSessionIdFromCookie(httpRequest); + + if (StringHelper.isNotEmpty(id)) { + SessionId ldapSessionId = sessionIdService.getSessionId(id); + if (ldapSessionId != null) { + boolean result = sessionIdService.remove(ldapSessionId); + if (!result) { + log.error("Failed to remove consent_session_id '{}'", id); + } + } else { + log.error("Failed to load session by consent_session_id: '{}'", id); + } + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } finally { + cookieService.removeConsentSessionIdCookie(httpResponse); + } + } + + private void auditLogging(HttpServletRequest request, Pair pair) { + SessionId sessionId = pair.getFirst(); + AuthorizationGrant authorizationGrant = pair.getSecond(); + + OAuth2AuditLog oAuth2AuditLog = new OAuth2AuditLog(ServerUtil.getIpAddress(request), Action.SESSION_DESTROYED); + oAuth2AuditLog.setSuccess(true); + + if (authorizationGrant != null) { + oAuth2AuditLog.setClientId(authorizationGrant.getClientId()); + oAuth2AuditLog.setScope(StringUtils.join(authorizationGrant.getScopes(), " ")); + oAuth2AuditLog.setUsername(authorizationGrant.getUserId()); + } else if (sessionId != null) { + oAuth2AuditLog.setClientId(sessionId.getPermissionGrantedMap().getClientIds(true).toString()); + oAuth2AuditLog.setScope(sessionId.getSessionAttributes().get(AuthorizeRequestParam.SCOPE)); + oAuth2AuditLog.setUsername(sessionId.getUserDn()); + } + + applicationAuditLogger.sendMessage(oAuth2AuditLog); + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/session/ws/rs/EndSessionUtils.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/session/ws/rs/EndSessionUtils.java new file mode 100644 index 00000000..2fde98fa --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/session/ws/rs/EndSessionUtils.java @@ -0,0 +1,107 @@ +package org.gluu.oxauth.session.ws.rs; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.client.service.ClientFactory; +import org.gluu.oxauth.model.util.Util; +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Form; +import javax.ws.rs.core.Response; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.gluu.oxauth.util.ServerUtil.daemonThreadFactory; + +/** + * @author Yuriy Zabrovarnyy + */ +public class EndSessionUtils { + + private final static Logger log = LoggerFactory.getLogger(EndSessionUtils.class); + + private EndSessionUtils() { + } + + public static ExecutorService getExecutorService() { + return Executors.newCachedThreadPool(daemonThreadFactory()); + } + + public static void callRpWithBackchannelUri(final String backchannelLogoutUri, String logoutToken) { + javax.ws.rs.client.Client client = ((ResteasyClientBuilder) ResteasyClientBuilder.newBuilder()).httpEngine(ClientFactory.instance().createEngine(true)).build(); + WebTarget target = client.target(backchannelLogoutUri); + + log.debug("Calling RP with backchannel, backchannel_logout_uri: " + backchannelLogoutUri); + try (Response response = target.request().post(Entity.form(new Form("logout_token", logoutToken)))) { + log.debug("Backchannel RP response, status: " + response.getStatus() + ", backchannel_logout_uri" + backchannelLogoutUri); + } catch (Exception e) { + log.error("Failed to call backchannel_logout_uri" + backchannelLogoutUri + ", message: " + e.getMessage(), e); + } + } + + public static String appendSid(String logoutUri, String sid, String issuer) { + if (logoutUri.contains("?")) { + return logoutUri + "&sid=" + sid + "&iss=" + issuer; + } else { + return logoutUri + "?sid=" + sid + "&iss=" + issuer; + } + } + + public static String appendState(String uri, String state) { + if (StringUtils.isBlank(state)) { + return uri; + } + + if (uri.contains("?")) { + if (uri.contains("state=")) { + return uri; + } else { + return uri + "&state=" + state; + } + } else { + return uri + "?state=" + state; + } + } + + public static String createFronthannelHtml(Set logoutUris, String postLogoutUrl, String state) { + String iframes = ""; + for (String logoutUri : logoutUris) { + iframes = iframes + String.format("", logoutUri); + } + + String html = "" + + "" + + ""; + + if (!Util.isNullOrEmpty(postLogoutUrl)) { + + if (!Util.isNullOrEmpty(state)) { + if (postLogoutUrl.contains("?")) { + postLogoutUrl += "&state=" + state; + } else { + postLogoutUrl += "?state=" + state; + } + } + + html += ""; + } + + html += "Your logout was successful" + + "" + + "" + + "Your logout was successful.
" + + iframes + + "" + + ""; + return html; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/session/ws/rs/LogoutTokenFactory.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/session/ws/rs/LogoutTokenFactory.java new file mode 100644 index 00000000..d4ab34a2 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/session/ws/rs/LogoutTokenFactory.java @@ -0,0 +1,88 @@ +package org.gluu.oxauth.session.ws.rs; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.claims.Audience; +import org.gluu.oxauth.model.common.User; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.token.JsonWebResponse; +import org.gluu.oxauth.model.token.JwrService; +import org.gluu.oxauth.service.SectorIdentifierService; +import org.json.JSONObject; +import org.msgpack.core.Preconditions; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; + +import javax.inject.Inject; +import java.util.Calendar; +import java.util.Date; +import java.util.UUID; + +/** + * @author Yuriy Zabrovarnyy + * @version April 10, 2020 + */ +@ApplicationScoped +public class LogoutTokenFactory { + + private static final String EVENTS_KEY = "http://schemas.openid.net/event/backchannel-logout"; + + @Inject + private Logger log; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private JwrService jwrService; + + @Inject + private SectorIdentifierService sectorIdentifierService; + + public JsonWebResponse createLogoutToken(Client rpClient, String outsideSid, User user) { + try { + Preconditions.checkNotNull(rpClient); + + JsonWebResponse jwr = jwrService.createJwr(rpClient); + + fillClaims(jwr, rpClient, outsideSid, user); + + jwrService.encode(jwr, rpClient); + return jwr; + } catch (Exception e) { + log.error("Failed to create logout_token for client:" + rpClient.getClientId()); + return null; + } + } + + private void fillClaims(JsonWebResponse jwr, Client client, String outsideSid, User user) { + int lifeTime = appConfiguration.getIdTokenLifetime(); + Calendar calendar = Calendar.getInstance(); + Date issuedAt = calendar.getTime(); + calendar.add(Calendar.SECOND, lifeTime); + Date expiration = calendar.getTime(); + + jwr.getClaims().setExpirationTime(expiration); + jwr.getClaims().setIssuedAt(issuedAt); + jwr.getClaims().setIssuer(appConfiguration.getIssuer()); + jwr.getClaims().setJwtId(UUID.randomUUID()); + jwr.getClaims().setClaim("events", getLogoutTokenEvents()); + Audience.setAudience(jwr.getClaims(), client); + + if (StringUtils.isNotBlank(outsideSid) && client.getAttributes().getBackchannelLogoutSessionRequired()) { + jwr.getClaims().setClaim("sid", outsideSid); + } + + final String sub = sectorIdentifierService.getSub(client, user, false); + if (StringUtils.isNotBlank(sub)) { + jwr.getClaims().setSubjectIdentifier(sub); + } + } + + private JSONObject getLogoutTokenEvents() { + final JSONObject events = new JSONObject(); + events.put(EVENTS_KEY, new JSONObject()); + return events; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/token/ws/rs/TokenRestWebService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/token/ws/rs/TokenRestWebService.java new file mode 100644 index 00000000..22d62c85 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/token/ws/rs/TokenRestWebService.java @@ -0,0 +1,71 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.token.ws.rs; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.FormParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; + +/** + * Provides interface for token REST web services + * + * @author Javier Rojas Blum + * @author Yuriy Zabrovarnyy + */ +public interface TokenRestWebService { + + @POST + @Path("/token") + @Produces({MediaType.APPLICATION_JSON}) + Response requestAccessToken( + @FormParam("grant_type") + String grantType, + @FormParam("code") + String code, + @FormParam("redirect_uri") + String redirectUri, + @FormParam("username") + String username, + @FormParam("password") + String password, + @FormParam("scope") + String scope, + @FormParam("assertion") + String assertion, + @FormParam("refresh_token") + String refreshToken, + @FormParam("client_id") + String clientId, + @FormParam("client_secret") + String clientSecret, + @FormParam("code_verifier") + String codeVerifier, + @FormParam("ticket") + String ticket, + @FormParam("claim_token") + String claimToken, + @FormParam("claim_token_format") + String claimTokenFormat, + @FormParam("pct") + String pctCode, + @FormParam("rpt") + String rptCode, + @FormParam("auth_req_id") + String authReqId, + @FormParam("device_code") + String deviceCode, + @Context HttpServletRequest request, + @Context HttpServletResponse response, + @Context SecurityContext sec); +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/token/ws/rs/TokenRestWebServiceImpl.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/token/ws/rs/TokenRestWebServiceImpl.java new file mode 100644 index 00000000..11330e1a --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/token/ws/rs/TokenRestWebServiceImpl.java @@ -0,0 +1,752 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.token.ws.rs; + +import com.google.common.base.Function; +import com.google.common.base.Strings; +import com.google.common.collect.Maps; +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.audit.ApplicationAuditLogger; +import org.gluu.oxauth.model.audit.Action; +import org.gluu.oxauth.model.audit.OAuth2AuditLog; +import org.gluu.oxauth.model.authorize.CodeVerifier; +import org.gluu.oxauth.model.common.*; +import org.gluu.oxauth.model.config.Constants; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.crypto.binding.TokenBindingMessage; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.ldap.TokenLdap; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.session.SessionClient; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.model.token.JsonWebResponse; +import org.gluu.oxauth.model.token.JwrService; +import org.gluu.oxauth.model.token.TokenErrorResponseType; +import org.gluu.oxauth.model.token.TokenParamsValidator; +import org.gluu.oxauth.security.Identity; +import org.gluu.oxauth.service.*; +import org.gluu.oxauth.service.ciba.CibaRequestService; +import org.gluu.oxauth.service.external.ExternalResourceOwnerPasswordCredentialsService; +import org.gluu.oxauth.service.external.ExternalUpdateTokenService; +import org.gluu.oxauth.service.external.context.ExternalResourceOwnerPasswordCredentialsContext; +import org.gluu.oxauth.service.external.context.ExternalUpdateTokenContext; +import org.gluu.oxauth.uma.service.UmaTokenService; +import org.gluu.oxauth.util.ServerUtil; +import org.gluu.persist.exception.AuthenticationException; +import org.gluu.util.OxConstants; +import org.gluu.util.StringHelper; +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.Path; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.ResponseBuilder; +import javax.ws.rs.core.SecurityContext; +import java.util.Arrays; +import java.util.Date; +import java.util.concurrent.ConcurrentMap; + +import static org.gluu.oxauth.util.ServerUtil.prepareForLogs; + +/** + * Provides interface for token REST web services + * + * @author Yuriy Zabrovarnyy + * @author Javier Rojas Blum + * @version May 5, 2020 + */ +@Path("/") +public class TokenRestWebServiceImpl implements TokenRestWebService { + + @Inject + private Logger log; + + @Inject + private Identity identity; + + @Inject + private ApplicationAuditLogger applicationAuditLogger; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private AuthorizationGrantList authorizationGrantList; + + @Inject + private UserService userService; + + @Inject + private GrantService grantService; + + @Inject + private AuthenticationFilterService authenticationFilterService; + + @Inject + private AuthenticationService authenticationService; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private UmaTokenService umaTokenService; + + @Inject + private ExternalResourceOwnerPasswordCredentialsService externalResourceOwnerPasswordCredentialsService; + + @Inject + private AttributeService attributeService; + + @Inject + private SessionIdService sessionIdService; + + @Inject + private CibaRequestService cibaRequestService; + + @Inject + private DeviceAuthorizationService deviceAuthorizationService; + + @Inject + private ExternalUpdateTokenService externalUpdateTokenService; + + private final ConcurrentMap refreshTokenLocalLock = Maps.newConcurrentMap(); + + @Override + public Response requestAccessToken(String grantType, String code, + String redirectUri, String username, String password, String scope, + String assertion, String refreshToken, + String clientId, String clientSecret, String codeVerifier, + String ticket, String claimToken, String claimTokenFormat, String pctCode, + String rptCode, String authReqId, String deviceCode, + HttpServletRequest request, HttpServletResponse response, SecurityContext sec) { + log.debug( + "Attempting to request access token: grantType = {}, code = {}, redirectUri = {}, username = {}, refreshToken = {}, " + + "clientId = {}, ExtraParams = {}, isSecure = {}, codeVerifier = {}, ticket = {}", + grantType, code, redirectUri, username, refreshToken, clientId, prepareForLogs(request.getParameterMap()), + sec.isSecure(), codeVerifier, ticket); + + boolean isUma = StringUtils.isNotBlank(ticket); + if (isUma) { + return umaTokenService.requestRpt(grantType, ticket, claimToken, claimTokenFormat, pctCode, rptCode, scope, request, response); + } + + OAuth2AuditLog oAuth2AuditLog = new OAuth2AuditLog(ServerUtil.getIpAddress(request), Action.TOKEN_REQUEST); + oAuth2AuditLog.setClientId(clientId); + oAuth2AuditLog.setUsername(username); + oAuth2AuditLog.setScope(scope); + + String tokenBindingHeader = request.getHeader("Sec-Token-Binding"); + + scope = ServerUtil.urlDecode(scope); // it may be encoded in uma case + ResponseBuilder builder = Response.ok(); + + try { + log.debug("Starting to validate request parameters"); + if (!TokenParamsValidator.validateParams(grantType, code, redirectUri, username, password, + scope, assertion, refreshToken)) { + log.trace("Failed to validate request parameters"); + return response(error(400, TokenErrorResponseType.INVALID_REQUEST, "Failed to validate request parameters"), oAuth2AuditLog); + } + + GrantType gt = GrantType.fromString(grantType); + log.debug("Grant type: '{}'", gt); + + SessionClient sessionClient = identity.getSessionClient(); + Client client = null; + if (sessionClient != null) { + client = sessionClient.getClient(); + log.debug("Get sessionClient: '{}'", sessionClient); + } + + if (client == null) { + return response(error(401, TokenErrorResponseType.INVALID_GRANT, "Unable to find client."), oAuth2AuditLog); + } + + log.debug("Get client from session: '{}'", client.getClientId()); + if (client.isDisabled()) { + return response(error(Response.Status.FORBIDDEN.getStatusCode(), TokenErrorResponseType.DISABLED_CLIENT, "Client is disabled."), oAuth2AuditLog); + } + + final Function idTokenTokingBindingPreprocessing = TokenBindingMessage.createIdTokenTokingBindingPreprocessing( + tokenBindingHeader, client.getIdTokenTokenBindingCnf()); // for all except authorization code grant + final SessionId sessionIdObj = sessionIdService.getSessionId(request); + final Function idTokenPreProcessing = JwrService.wrapWithSidFunction(idTokenTokingBindingPreprocessing, sessionIdObj != null ? sessionIdObj.getOutsideSid() : null); + + final ExecutionContext executionContext = new ExecutionContext(request, response); + executionContext.setClient(client); + executionContext.setAppConfiguration(appConfiguration); + executionContext.setAttributeService(attributeService); + + if (gt == GrantType.AUTHORIZATION_CODE) { + if (!TokenParamsValidator.validateGrantType(gt, client.getGrantTypes(), appConfiguration.getGrantTypesSupported())) { + return response(error(400, TokenErrorResponseType.INVALID_GRANT, "Grant types are invalid."), oAuth2AuditLog); + } + + log.debug("Attempting to find authorizationCodeGrant by clientId: '{}', code: '{}'", client.getClientId(), code); + final AuthorizationCodeGrant authorizationCodeGrant = authorizationGrantList.getAuthorizationCodeGrant(code); + log.trace("AuthorizationCodeGrant : '{}'", authorizationCodeGrant); + + if (authorizationCodeGrant == null) { + log.debug("AuthorizationCodeGrant is empty by clientId: '{}', code: '{}'", client.getClientId(), code); + // if authorization code is not found then code was already used or wrong client provided = remove all grants with this auth code + grantService.removeAllByAuthorizationCode(code); + return response(error(400, TokenErrorResponseType.INVALID_GRANT, "Unable to find grant object for given code."), oAuth2AuditLog); + } + + if (!client.getClientId().equals(authorizationCodeGrant.getClientId())) { + log.debug("AuthorizationCodeGrant is found but belongs to another client. Grant's clientId: '{}', code: '{}'", authorizationCodeGrant.getClientId(), code); + // if authorization code is not found then code was already used or wrong client provided = remove all grants with this auth code + grantService.removeAllByAuthorizationCode(code); + return response(error(400, TokenErrorResponseType.INVALID_GRANT, "Client mismatch."), oAuth2AuditLog); + } + + validatePKCE(authorizationCodeGrant, codeVerifier, oAuth2AuditLog); + + executionContext.setGrant(authorizationCodeGrant); + authorizationCodeGrant.setIsCachedWithNoPersistence(false); + authorizationCodeGrant.save(); + + RefreshToken reToken = null; + if (isRefreshTokenAllowed(client, scope, authorizationCodeGrant)) { + reToken = authorizationCodeGrant.createRefreshToken(executionContext); + } + + if (scope != null && !scope.isEmpty()) { + scope = authorizationCodeGrant.checkScopesPolicy(scope); + } + + AccessToken accToken = authorizationCodeGrant.createAccessToken(request.getHeader("X-ClientCert"), executionContext); // create token after scopes are checked + + IdToken idToken = null; + if (authorizationCodeGrant.getScopes().contains("openid")) { + String nonce = authorizationCodeGrant.getNonce(); + boolean includeIdTokenClaims = Boolean.TRUE.equals( + appConfiguration.getLegacyIdTokenClaims()); + final String idTokenTokenBindingCnf = client.getIdTokenTokenBindingCnf(); + Function authorizationCodePreProcessing = jsonWebResponse -> { + if (StringUtils.isNotBlank(idTokenTokenBindingCnf) && StringUtils.isNotBlank(authorizationCodeGrant.getTokenBindingHash())) { + TokenBindingMessage.setCnfClaim(jsonWebResponse, authorizationCodeGrant.getTokenBindingHash(), idTokenTokenBindingCnf); + } + return null; + }; + + ExternalUpdateTokenContext context = ExternalUpdateTokenContext.of(executionContext); + Function postProcessor = externalUpdateTokenService.buildModifyIdTokenProcessor(context); + + idToken = authorizationCodeGrant.createIdToken( + nonce, authorizationCodeGrant.getAuthorizationCode(), accToken, null, null, + authorizationCodeGrant, includeIdTokenClaims, JwrService.wrapWithSidFunction(authorizationCodePreProcessing, sessionIdObj != null ? sessionIdObj.getOutsideSid() : null), + postProcessor, executionContext); + } + + oAuth2AuditLog.updateOAuth2AuditLog(authorizationCodeGrant, true); + + grantService.removeAuthorizationCode(authorizationCodeGrant.getAuthorizationCode().getCode()); + + final String entity = getJSonResponse(accToken, accToken.getTokenType(), accToken.getExpiresIn(), reToken, scope, idToken); + return response(Response.ok().entity(entity), oAuth2AuditLog); + } + + if (gt == GrantType.REFRESH_TOKEN) { + if (!TokenParamsValidator.validateGrantType(gt, client.getGrantTypes(), appConfiguration.getGrantTypesSupported())) { + return response(error(400, TokenErrorResponseType.INVALID_GRANT, "grant_type is not present in client."), oAuth2AuditLog); + } + + AuthorizationGrant authorizationGrant = authorizationGrantList.getAuthorizationGrantByRefreshToken(client.getClientId(), refreshToken); + + if (authorizationGrant == null) { + log.trace("Grant object is not found by refresh token."); + return response(error(400, TokenErrorResponseType.INVALID_GRANT, "Unable to find grant object by refresh token or otherwise token type or client does not match."), oAuth2AuditLog); + } + + final RefreshToken refreshTokenObject = authorizationGrant.getRefreshToken(refreshToken); + if (refreshTokenObject == null || !refreshTokenObject.isValid()) { + log.trace("Invalid refresh token."); + return response(error(400, TokenErrorResponseType.INVALID_GRANT, "Unable to find refresh token or otherwise token type or client does not match."), oAuth2AuditLog); + } + + checkUser(authorizationGrant, oAuth2AuditLog); + executionContext.setGrant(authorizationGrant); + + // The authorization server MAY issue a new refresh token, in which case + // the client MUST discard the old refresh token and replace it with the new refresh token. + RefreshToken reToken = null; + if (!appConfiguration.getSkipRefreshTokenDuringRefreshing()) { + if (appConfiguration.getRefreshTokenExtendLifetimeOnRotation()) { + reToken = authorizationGrant.createRefreshToken(executionContext); // extend lifetime + } else { + reToken = authorizationGrant.createRefreshToken(executionContext, refreshTokenObject.getExpirationDate()); // do not extend lifetime + } + } + + if (scope != null && !scope.isEmpty()) { + scope = authorizationGrant.checkScopesPolicy(scope); + } else { + scope = authorizationGrant.getScopesAsString(); + } + + AccessToken accToken = authorizationGrant.createAccessToken(request.getHeader("X-ClientCert"), executionContext); // create token after scopes are checked + + IdToken idToken = null; + if (appConfiguration.getOpenidScopeBackwardCompatibility() && authorizationGrant.getScopes().contains("openid")) { + boolean includeIdTokenClaims = Boolean.TRUE.equals( + appConfiguration.getLegacyIdTokenClaims()); + + ExternalUpdateTokenContext context = ExternalUpdateTokenContext.of(executionContext); + Function postProcessor = externalUpdateTokenService.buildModifyIdTokenProcessor(context); + + idToken = authorizationGrant.createIdToken( + null, null, accToken, null, + null, authorizationGrant, includeIdTokenClaims, idTokenPreProcessing, postProcessor, executionContext); + } + + TokenLdap lockedRefreshToken = lockRefreshToken(refreshToken); + if (lockedRefreshToken == null) { + log.trace("Failed to lock refresh token {}", refreshToken); + return response(error(400, TokenErrorResponseType.INVALID_GRANT, "Failed to lock refresh token."), oAuth2AuditLog); + } + + builder.entity(getJSonResponse(accToken, + accToken.getTokenType(), + accToken.getExpiresIn(), + reToken, + scope, + idToken)); + oAuth2AuditLog.updateOAuth2AuditLog(authorizationGrant, true); + } else if (gt == GrantType.CLIENT_CREDENTIALS) { + if (!TokenParamsValidator.validateGrantType(gt, client.getGrantTypes(), appConfiguration.getGrantTypesSupported())) { + return response(error(400, TokenErrorResponseType.INVALID_GRANT, "grant_type is not present in client."), oAuth2AuditLog); + } + + ClientCredentialsGrant clientCredentialsGrant = authorizationGrantList.createClientCredentialsGrant(new User(), client); // TODO: fix the user arg + + if (scope != null && !scope.isEmpty()) { + scope = clientCredentialsGrant.checkScopesPolicy(scope); + } + + executionContext.setGrant(clientCredentialsGrant); + AccessToken accessToken = clientCredentialsGrant.createAccessToken(request.getHeader("X-ClientCert"), executionContext); // create token after scopes are checked + + IdToken idToken = null; + if (appConfiguration.getOpenidScopeBackwardCompatibility() && clientCredentialsGrant.getScopes().contains("openid")) { + boolean includeIdTokenClaims = Boolean.TRUE.equals( + appConfiguration.getLegacyIdTokenClaims()); + + ExternalUpdateTokenContext context = ExternalUpdateTokenContext.of(executionContext); + Function postProcessor = externalUpdateTokenService.buildModifyIdTokenProcessor(context); + + idToken = clientCredentialsGrant.createIdToken( + null, null, null, null, + null, clientCredentialsGrant, includeIdTokenClaims, idTokenPreProcessing, postProcessor, executionContext); + } + + oAuth2AuditLog.updateOAuth2AuditLog(clientCredentialsGrant, true); + builder.entity(getJSonResponse(accessToken, + accessToken.getTokenType(), + accessToken.getExpiresIn(), + null, + scope, + idToken)); + } else if (gt == GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS) { + if (!TokenParamsValidator.validateGrantType(gt, client.getGrantTypes(), appConfiguration.getGrantTypesSupported())) { + return response(error(400, TokenErrorResponseType.INVALID_GRANT, "grant_type is not present in client."), oAuth2AuditLog); + } + + boolean authenticated = false; + User user = null; + if (authenticationFilterService.isEnabled()) { + String userDn = authenticationFilterService.processAuthenticationFilters(request.getParameterMap()); + if (StringHelper.isNotEmpty(userDn)) { + user = userService.getUserByDn(userDn); + authenticated = true; + } + } + + + if (!authenticated) { + if (externalResourceOwnerPasswordCredentialsService.isEnabled()) { + final ExternalResourceOwnerPasswordCredentialsContext context = new ExternalResourceOwnerPasswordCredentialsContext(request, response, appConfiguration, attributeService, userService); + context.setUser(user); + if (externalResourceOwnerPasswordCredentialsService.executeExternalAuthenticate(context)) { + log.trace("RO PC - User is authenticated successfully by external script."); + user = context.getUser(); + } + } else { + try { + authenticated = authenticationService.authenticate(username, password); + if (authenticated) { + user = authenticationService.getAuthenticatedUser(); + } + } catch (AuthenticationException ex) { + log.trace("Failed to authenticate user ", new RuntimeException("User name or password is invalid")); + } + } + } + + if (user != null) { + ResourceOwnerPasswordCredentialsGrant resourceOwnerPasswordCredentialsGrant = authorizationGrantList.createResourceOwnerPasswordCredentialsGrant(user, client); + executionContext.setGrant(resourceOwnerPasswordCredentialsGrant); + SessionId sessionId = identity.getSessionId(); + if (sessionId != null) { + resourceOwnerPasswordCredentialsGrant.setAcrValues(OxConstants.SCRIPT_TYPE_INTERNAL_RESERVED_NAME); + resourceOwnerPasswordCredentialsGrant.setSessionDn(sessionId.getDn()); + resourceOwnerPasswordCredentialsGrant.save(); // call save after object modification!!! + + sessionId.getSessionAttributes().put(Constants.AUTHORIZED_GRANT, gt.getValue()); + boolean updateResult = sessionIdService.updateSessionId(sessionId, false, true, true); + if (!updateResult) { + log.debug("Failed to update session entry: '{}'", sessionId.getId()); + } + } + + + RefreshToken reToken = null; + if (isRefreshTokenAllowed(client, scope, resourceOwnerPasswordCredentialsGrant)) { + reToken = resourceOwnerPasswordCredentialsGrant.createRefreshToken(executionContext); + } + + if (scope != null && !scope.isEmpty()) { + scope = resourceOwnerPasswordCredentialsGrant.checkScopesPolicy(scope); + } + + AccessToken accessToken = resourceOwnerPasswordCredentialsGrant.createAccessToken(request.getHeader("X-ClientCert"), executionContext); // create token after scopes are checked + + IdToken idToken = null; + if (appConfiguration.getOpenidScopeBackwardCompatibility() && resourceOwnerPasswordCredentialsGrant.getScopes().contains("openid")) { + boolean includeIdTokenClaims = Boolean.TRUE.equals( + appConfiguration.getLegacyIdTokenClaims()); + + ExternalUpdateTokenContext context = ExternalUpdateTokenContext.of(executionContext); + Function postProcessor = externalUpdateTokenService.buildModifyIdTokenProcessor(context); + + idToken = resourceOwnerPasswordCredentialsGrant.createIdToken( + null, null, null, null, + null, resourceOwnerPasswordCredentialsGrant, includeIdTokenClaims, idTokenPreProcessing, postProcessor, executionContext); + } + + oAuth2AuditLog.updateOAuth2AuditLog(resourceOwnerPasswordCredentialsGrant, true); + builder.entity(getJSonResponse(accessToken, + accessToken.getTokenType(), + accessToken.getExpiresIn(), + reToken, + scope, + idToken)); + } else { + log.debug("Invalid user", new RuntimeException("User is empty")); + builder = error(401, TokenErrorResponseType.INVALID_CLIENT, "Invalid user."); + } + } else if (gt == GrantType.CIBA) { + if (!appConfiguration.getCibaEnabled()) { + log.warn("Trying to get CIBA token, however CIBA config is disabled."); + return response(error(400, TokenErrorResponseType.INVALID_REQUEST, "Grant types are invalid."), oAuth2AuditLog); + } + + if (!TokenParamsValidator.validateGrantType(gt, client.getGrantTypes(), appConfiguration.getGrantTypesSupported())) { + return response(error(400, TokenErrorResponseType.INVALID_GRANT, "Grant types are invalid."), oAuth2AuditLog); + } + + log.debug("Attempting to find authorizationGrant by authReqId: '{}'", authReqId); + final CIBAGrant cibaGrant = authorizationGrantList.getCIBAGrant(authReqId); + + log.trace("AuthorizationGrant : '{}'", cibaGrant); + + if (cibaGrant != null) { + if (!cibaGrant.getClientId().equals(client.getClientId())) { + builder = error(400, TokenErrorResponseType.INVALID_GRANT, "The client is not authorized."); + return response(builder, oAuth2AuditLog); + } + + executionContext.setGrant(cibaGrant); + if (cibaGrant.getClient().getBackchannelTokenDeliveryMode() == BackchannelTokenDeliveryMode.PING || + cibaGrant.getClient().getBackchannelTokenDeliveryMode() == BackchannelTokenDeliveryMode.POLL) { + if (!cibaGrant.isTokensDelivered()) { + RefreshToken refToken = cibaGrant.createRefreshToken(executionContext); + AccessToken accessToken = cibaGrant.createAccessToken(request.getHeader("X-ClientCert"), executionContext); + + ExternalUpdateTokenContext context = ExternalUpdateTokenContext.of(executionContext); + Function postProcessor = externalUpdateTokenService.buildModifyIdTokenProcessor(context); + + boolean includeIdTokenClaims = Boolean.TRUE.equals(appConfiguration.getLegacyIdTokenClaims()); + IdToken idToken = cibaGrant.createIdToken( + null, null, accessToken, refToken, + null, cibaGrant, includeIdTokenClaims, null, postProcessor, executionContext); + + cibaGrant.setTokensDelivered(true); + cibaGrant.save(); + + RefreshToken reToken = null; + if (isRefreshTokenAllowed(client, scope, cibaGrant)) { + reToken = refToken; + } + + if (scope != null && !scope.isEmpty()) { + scope = cibaGrant.checkScopesPolicy(scope); + } + + builder.entity(getJSonResponse(accessToken, + accessToken.getTokenType(), + accessToken.getExpiresIn(), + reToken, + scope, + idToken)); + + oAuth2AuditLog.updateOAuth2AuditLog(cibaGrant, true); + } else { + builder = error(400, TokenErrorResponseType.INVALID_GRANT, "AuthReqId is no longer available."); + } + } else { + log.debug("Client is not using Poll flow authReqId: '{}'", authReqId); + builder = error(400, TokenErrorResponseType.UNAUTHORIZED_CLIENT, "The client is not authorized as it is configured in Push Mode"); + } + } else { + final CibaRequestCacheControl cibaRequest = cibaRequestService.getCibaRequest(authReqId); + log.trace("Ciba request : '{}'", cibaRequest); + if (cibaRequest != null) { + if (!cibaRequest.getClient().getClientId().equals(client.getClientId())) { + builder = error(400, TokenErrorResponseType.INVALID_GRANT, "The client is not authorized."); + return response(builder, oAuth2AuditLog); + } + long currentTime = new Date().getTime(); + Long lastAccess = cibaRequest.getLastAccessControl(); + if (lastAccess == null) { + lastAccess = currentTime; + } + cibaRequest.setLastAccessControl(currentTime); + cibaRequestService.update(cibaRequest); + + if (cibaRequest.getStatus() == CibaRequestStatus.PENDING) { + int intervalSeconds = appConfiguration.getBackchannelAuthenticationResponseInterval(); + long timeFromLastAccess = currentTime - lastAccess; + + if (timeFromLastAccess > intervalSeconds * 1000) { + log.debug("Access hasn't been granted yet for authReqId: '{}'", authReqId); + builder = error(400, TokenErrorResponseType.AUTHORIZATION_PENDING, "User hasn't answered yet"); + } else { + log.debug("Slow down protection authReqId: '{}'", authReqId); + builder = error(400, TokenErrorResponseType.SLOW_DOWN, "Client is asking too fast the token."); + } + } else if (cibaRequest.getStatus() == CibaRequestStatus.DENIED) { + log.debug("The end-user denied the authorization request for authReqId: '{}'", authReqId); + builder = error(400, TokenErrorResponseType.ACCESS_DENIED, "The end-user denied the authorization request."); + } else if (cibaRequest.getStatus() == CibaRequestStatus.EXPIRED) { + log.debug("The authentication request has expired for authReqId: '{}'", authReqId); + builder = error(400, TokenErrorResponseType.EXPIRED_TOKEN, "The authentication request has expired"); + } + } else { + log.debug("AuthorizationGrant is empty by authReqId: '{}'", authReqId); + builder = error(400, TokenErrorResponseType.EXPIRED_TOKEN, "Unable to find grant object for given auth_req_id."); + } + } + } else if (gt == GrantType.DEVICE_CODE) { + return processDeviceCodeGrantType(gt, client, deviceCode, scope, executionContext, oAuth2AuditLog); + } + } catch (WebApplicationException e) { + throw e; + } catch (Exception e) { + builder = Response.status(500); + log.error(e.getMessage(), e); + } + + return response(builder, oAuth2AuditLog); + } + + private TokenLdap lockRefreshToken(String refreshTokenCode) { + try { + synchronized (refreshTokenLocalLock) { + if (refreshTokenLocalLock.containsKey(refreshTokenCode)) { + log.trace("Refresh token is already used by another request. Refresh token code: {}", refreshTokenCode); + return null; + } + + refreshTokenLocalLock.put(refreshTokenCode, refreshTokenCode); + + final TokenLdap token = grantService.getGrantByCode(refreshTokenCode); + grantService.remove(token); + return token; + } + } catch (Exception e) { + // ignore + log.trace(e.getMessage(), e); + } finally { + refreshTokenLocalLock.remove(refreshTokenCode); + } + return null; + } + + private void checkUser(AuthorizationGrant authorizationGrant, OAuth2AuditLog oAuth2AuditLog) { + if (!appConfiguration.getCheckUserPresenceOnRefreshToken()) { + return; + } + + final User user = authorizationGrant.getUser(); + if (user == null || "inactive".equalsIgnoreCase(user.getStatus())) { + log.trace("The user associated with this grant is not found or otherwise with status=inactive."); + throw new WebApplicationException(response(error(400, TokenErrorResponseType.INVALID_GRANT, "The user associated with this grant is not found or otherwise with status=inactive."), oAuth2AuditLog)); + } + } + + /** + * Processes token request for device code grant type. + * @param grantType Grant type used, should be device code. + * @param client Client in process. + * @param deviceCode Device code generated in device authn request. + * @param scope Scope registered in device authn request. + * @param executionContext ExecutionContext + * @param oAuth2AuditLog OAuth2AuditLog + */ + private Response processDeviceCodeGrantType(final GrantType grantType, final Client client, final String deviceCode, + String scope, final ExecutionContext executionContext, final OAuth2AuditLog oAuth2AuditLog) { + if (!TokenParamsValidator.validateGrantType(grantType, client.getGrantTypes(), appConfiguration.getGrantTypesSupported())) { + return response(error(400, TokenErrorResponseType.INVALID_GRANT, "Grant types are invalid."), oAuth2AuditLog); + } + + log.debug("Attempting to find authorizationGrant by deviceCode: '{}'", deviceCode); + final DeviceCodeGrant deviceCodeGrant = authorizationGrantList.getDeviceCodeGrant(deviceCode); + executionContext.setGrant(deviceCodeGrant); + + log.trace("DeviceCodeGrant : '{}'", deviceCodeGrant); + + if (deviceCodeGrant != null) { + if (!deviceCodeGrant.getClientId().equals(client.getClientId())) { + throw new WebApplicationException(response(error(400, TokenErrorResponseType.INVALID_GRANT, "The client is not authorized."), oAuth2AuditLog)); + } + RefreshToken refToken = deviceCodeGrant.createRefreshToken(executionContext); + AccessToken accessToken = deviceCodeGrant.createAccessToken(executionContext.getHttpRequest().getHeader("X-ClientCert"), executionContext); + + ExternalUpdateTokenContext context = ExternalUpdateTokenContext.of(executionContext); + Function postProcessor = externalUpdateTokenService.buildModifyIdTokenProcessor(context); + + boolean includeIdTokenClaims = Boolean.TRUE.equals(appConfiguration.getLegacyIdTokenClaims()); + IdToken idToken = deviceCodeGrant.createIdToken( + null, null, accessToken, refToken, + null, deviceCodeGrant, includeIdTokenClaims, null, postProcessor, executionContext); + + RefreshToken reToken = null; + if (isRefreshTokenAllowed(client, scope, deviceCodeGrant)) { + reToken = refToken; + } + + if (scope != null && !scope.isEmpty()) { + scope = deviceCodeGrant.checkScopesPolicy(scope); + } + log.info("Device authorization in token endpoint processed and return to the client, device_code: {}", deviceCodeGrant.getDeviceCode()); + + oAuth2AuditLog.updateOAuth2AuditLog(deviceCodeGrant, true); + + grantService.removeByCode(deviceCodeGrant.getDeviceCode()); + + return Response.ok().entity(getJSonResponse(accessToken, accessToken.getTokenType(), + accessToken.getExpiresIn(), reToken, scope, idToken)).build(); + } else { + final DeviceAuthorizationCacheControl cacheData = deviceAuthorizationService.getDeviceAuthzByDeviceCode(deviceCode); + log.trace("DeviceAuthorizationCacheControl data : '{}'", cacheData); + if (cacheData == null) { + log.debug("The authentication request has expired for deviceCode: '{}'", deviceCode); + throw new WebApplicationException(response(error(400, TokenErrorResponseType.EXPIRED_TOKEN, "The authentication request has expired."), oAuth2AuditLog)); + } + if (!cacheData.getClient().getClientId().equals(client.getClientId())) { + throw new WebApplicationException(response(error(400, TokenErrorResponseType.INVALID_GRANT, "The client is not authorized."), oAuth2AuditLog)); + } + long currentTime = new Date().getTime(); + Long lastAccess = cacheData.getLastAccessControl(); + if (lastAccess == null) { + lastAccess = currentTime; + } + cacheData.setLastAccessControl(currentTime); + deviceAuthorizationService.saveInCache(cacheData, true, true); + + if (cacheData.getStatus() == DeviceAuthorizationStatus.PENDING) { + int intervalSeconds = appConfiguration.getBackchannelAuthenticationResponseInterval(); + long timeFromLastAccess = currentTime - lastAccess; + + if (timeFromLastAccess > intervalSeconds * 1000) { + log.debug("Access hasn't been granted yet for deviceCode: '{}'", deviceCode); + throw new WebApplicationException(response(error(400, TokenErrorResponseType.AUTHORIZATION_PENDING, "User hasn't answered yet"), oAuth2AuditLog)); + } else { + log.debug("Slow down protection deviceCode: '{}'", deviceCode); + throw new WebApplicationException(response(error(400, TokenErrorResponseType.SLOW_DOWN, "Client is asking too fast the token."), oAuth2AuditLog)); + } + } + if (cacheData.getStatus() == DeviceAuthorizationStatus.DENIED) { + log.debug("The end-user denied the authorization request for deviceCode: '{}'", deviceCode); + throw new WebApplicationException(response(error(400, TokenErrorResponseType.ACCESS_DENIED, "The end-user denied the authorization request."), oAuth2AuditLog)); + } + log.debug("The authentication request has expired for deviceCode: '{}'", deviceCode); + throw new WebApplicationException(response(error(400, TokenErrorResponseType.EXPIRED_TOKEN, "The authentication request has expired"), oAuth2AuditLog)); + } + } + + private boolean isRefreshTokenAllowed(Client client, String requestedScope, AbstractAuthorizationGrant grant) { + if (appConfiguration.getForceOfflineAccessScopeToEnableRefreshToken() && !grant.getScopes().contains(ScopeConstants.OFFLINE_ACCESS) && !Strings.nullToEmpty(requestedScope).contains(ScopeConstants.OFFLINE_ACCESS)) { + return false; + } + return Arrays.asList(client.getGrantTypes()).contains(GrantType.REFRESH_TOKEN); + } + + private void validatePKCE(AuthorizationCodeGrant grant, String codeVerifier, OAuth2AuditLog oAuth2AuditLog) { + log.trace("PKCE validation, code_verifier: {}, code_challenge: {}, method: {}", + codeVerifier, grant.getCodeChallenge(), grant.getCodeChallengeMethod()); + + if (Strings.isNullOrEmpty(grant.getCodeChallenge()) && Strings.isNullOrEmpty(codeVerifier)) { + return; // if no code challenge then it's valid, no PKCE check + } + + if (!CodeVerifier.matched(grant.getCodeChallenge(), grant.getCodeChallengeMethod(), codeVerifier)) { + log.error("PKCE check fails. Code challenge does not match to request code verifier, " + + "grantId:" + grant.getGrantId() + ", codeVerifier: " + codeVerifier); + throw new WebApplicationException(response(error(401, TokenErrorResponseType.INVALID_GRANT, "PKCE check fails. Code challenge does not match to request code verifier."), oAuth2AuditLog)); + } + } + + private Response response(ResponseBuilder builder, OAuth2AuditLog oAuth2AuditLog) { + builder.cacheControl(ServerUtil.cacheControl(true, false)); + builder.header("Pragma", "no-cache"); + + applicationAuditLogger.sendMessage(oAuth2AuditLog); + + return builder.build(); + } + + private ResponseBuilder error(int p_status, TokenErrorResponseType p_type, String reason) { + return Response.status(p_status).type(MediaType.APPLICATION_JSON_TYPE).entity(errorResponseFactory.errorAsJson(p_type, reason)); + } + + /** + * Builds a JSon String with the structure for token issues. + */ + public String getJSonResponse(AccessToken accessToken, TokenType tokenType, + Integer expiresIn, RefreshToken refreshToken, String scope, + IdToken idToken) { + JSONObject jsonObj = new JSONObject(); + try { + jsonObj.put("access_token", accessToken.getCode()); // Required + jsonObj.put("token_type", tokenType.toString()); // Required + if (expiresIn != null) { // Optional + jsonObj.put("expires_in", expiresIn); + } + if (refreshToken != null) { // Optional + jsonObj.put("refresh_token", refreshToken.getCode()); + } + if (scope != null) { // Optional + jsonObj.put("scope", scope); + } + if (idToken != null) { + jsonObj.put("id_token", idToken.getCode()); + } + } catch (JSONException e) { + log.error(e.getMessage(), e); + } + + return jsonObj.toString(); + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/Claims.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/Claims.java new file mode 100644 index 00000000..c3a3cd56 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/Claims.java @@ -0,0 +1,74 @@ +package org.gluu.oxauth.uma.authorization; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.jwt.Jwt; + +/** + * @author yuriyz on 06/02/2017. + */ +public class Claims { + + private Jwt claimsToken; + private String claimsTokenAsString; + private UmaPCT pct; + private Map claims = new ConcurrentHashMap(); + + public Claims(Jwt claimsToken, UmaPCT pct, String claimsTokenAsString) { + this.claimsToken = claimsToken; + this.pct = pct; + this.claimsTokenAsString = claimsTokenAsString; + } + + public String getClaimsTokenAsString() { + return claimsTokenAsString; + } + + public Set keys() { + return claims.keySet(); + } + + public Object get(String key) { + if (StringUtils.isBlank(key)) { + return null; + } + + if (claims.containsKey(key)) { + return claims.get(key); + } else if (claimsToken != null && claimsToken.getClaims() != null && claimsToken.getClaims().hasClaim(key)) { + return claimsToken.getClaims().getClaim(key); + } else if (pct != null && pct.getClaims() != null && pct.getClaims().hasClaim(key)) { + return pct.getClaims().getClaim(key); + } + return null; + } + + public Object getClaimTokenClaim(String key) { + if (claimsToken != null && claimsToken.getClaims() != null && claimsToken.getClaims().hasClaim(key)) { + return claimsToken.getClaims().getClaim(key); + } + return null; + } + + public Object getPctClaim(String key) { + if (pct != null && pct.getClaims() != null && pct.getClaims().hasClaim(key)) { + return pct.getClaims().getClaim(key); + } + return null; + } + + public boolean has(String key) { + return get(key) != null; + } + + public void put(String key, Object value) { + claims.put(key, value); + } + + public void removeClaim(String key) { + claims.remove(key); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/IPolicyExternalAuthorization.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/IPolicyExternalAuthorization.java new file mode 100644 index 00000000..b6e96d86 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/IPolicyExternalAuthorization.java @@ -0,0 +1,17 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.uma.authorization; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 21/02/2013 + */ + +public interface IPolicyExternalAuthorization { + + public boolean authorize(UmaAuthorizationContext authorizationContext); +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/PolicyExternalAuthorizationEnum.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/PolicyExternalAuthorizationEnum.java new file mode 100644 index 00000000..e74d0abb --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/PolicyExternalAuthorizationEnum.java @@ -0,0 +1,27 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.uma.authorization; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 22/02/2013 + */ + +public enum PolicyExternalAuthorizationEnum implements IPolicyExternalAuthorization { + TRUE(true), FALSE(false); + + private final boolean m_result; + + private PolicyExternalAuthorizationEnum(boolean p_result) { + m_result = p_result; + } + + @Override + public boolean authorize(UmaAuthorizationContext p_authorizationContext) { + return m_result; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/UmaAuthorizationContext.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/UmaAuthorizationContext.java new file mode 100644 index 00000000..d1e17404 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/UmaAuthorizationContext.java @@ -0,0 +1,220 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.uma.authorization; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import org.gluu.model.SimpleCustomProperty; +import org.gluu.oxauth.model.common.User; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.model.uma.persistence.UmaPermission; +import org.gluu.oxauth.model.uma.persistence.UmaResource; +import org.gluu.oxauth.service.AttributeService; +import org.gluu.oxauth.service.common.UserService; +import org.gluu.oxauth.service.external.context.ExternalScriptContext; +import org.gluu.oxauth.uma.service.RedirectParameters; +import org.gluu.oxauth.uma.service.UmaPermissionService; +import org.gluu.oxauth.uma.service.UmaSessionService; +import org.oxauth.persistence.model.Scope; + +import javax.servlet.http.HttpServletRequest; +import java.util.*; + +/** + * @author Yuriy Zabrovarnyy + * @author Javier Rojas Blum + * @author Yuriy Movchan + */ + +public class UmaAuthorizationContext extends ExternalScriptContext { + + private final Claims claims; + private final Map scopes; // scope and boolean, true - if client requested scope and false if it is permission ticket scope + private final Set resources; + private final String scriptDn; + private final Map configurationAttributes; + private final RedirectParameters redirectUserParameters = new RedirectParameters(); + private final AppConfiguration configuration; + + private final AttributeService attributeService; + private final UmaSessionService sessionService; + private final UserService userService; + private final UmaPermissionService permissionService; + private final Client client; + + public UmaAuthorizationContext(AppConfiguration configuration, AttributeService attributeService, Map scopes, + Set resources, Claims claims, String scriptDn, HttpServletRequest httpRequest, + Map configurationAttributes, UmaSessionService sessionService, + UserService userService, UmaPermissionService permissionService, Client client) { + super(httpRequest); + + this.configuration = configuration; + this.attributeService = attributeService; + this.sessionService = sessionService; + this.userService = userService; + this.permissionService = permissionService; + this.client = client; + this.scopes = new HashMap(scopes); + this.resources = resources; + this.claims = claims; + this.scriptDn = scriptDn; + this.configurationAttributes = configurationAttributes != null ? configurationAttributes : new HashMap(); + } + + public String getClaimToken() { + return getClaims().getClaimsTokenAsString(); + } + + public Object getClaimTokenClaim(String key) { + return getClaims().getClaimTokenClaim(key); + } + + public Object getPctClaim(String key) { + return getClaims().getPctClaim(key); + } + + public String getIssuer() { + return configuration.getIssuer(); + } + + public String getScriptDn() { + return scriptDn; + } + + public Map getConfigurationAttributes() { + return configurationAttributes; + } + + public Set getScopes() { + Set result = new HashSet(); + for (Scope scope : getScopeMap().keySet()) { + result.add(scope.getId()); + } + return result; + } + + /** + * @return scopes that are bound to currently executed script + */ + public Set getScriptScopes() { + Set result = new HashSet(); + for (Scope scope : getScopeMap().keySet()) { + if (scope.getUmaAuthorizationPolicies() != null && scope.getUmaAuthorizationPolicies().contains(scriptDn)) { + result.add(scope.getId()); + } + } + return result; + } + + public Map getScopeMap() { + return Maps.newHashMap(scopes); + } + + public Set getResources() { + return resources; + } + + public Set getResourceIds() { + Set result = new HashSet(); + for (UmaResource resource : resources) { + result.add(resource.getId()); + } + return result; + } + + public Claims getClaims() { + return claims; + } + + public Object getClaim(String claimName) { + return claims.get(claimName); + } + + public void putClaim(String claimName, Object claimValue) { + claims.put(claimName, claimValue); + } + + public boolean hasClaim(String claimName) { + return claims.has(claimName); + } + + public void removeClaim(String claimName) { + claims.removeClaim(claimName); + } + + public void addRedirectUserParam(String paramName, String paramValue) { + redirectUserParameters.add(paramName, paramValue); + } + + public void removeRedirectUserParameter(String paramName) { + redirectUserParameters.remove(paramName); + } + + public RedirectParameters getRedirectUserParameters() { + return redirectUserParameters; + } + + public Map> getRedirectUserParametersMap() { + return redirectUserParameters.map(); + } + + public User getUser(String... returnAttributes) { + return sessionService.getUser(httpRequest, returnAttributes); + } + + public boolean isAuthenticated() { + return getUser() != null; + } + + public String getUserDn() { + return sessionService.getUserDn(httpRequest); + } + + public Client getClient() { + return client; + } + + public List getPermissions() { + SessionId session = sessionService.getSession(httpRequest, httpResponse); + if (session == null) { + getLog().trace("No UMA session set."); + return Lists.newArrayList(); + } + return permissionService.getPermissionsByTicket(sessionService.getTicket(session)); + } + + + // public String getClientClaim(String p_claimName) { +// return getEntryAttributeValue(getGrant().getClientDn(), p_claimName); +// } +// +// public String getUserClaim(String p_claimName) { +// GluuAttribute gluuAttribute = attributeService.getByClaimName(p_claimName); +// +// if (gluuAttribute != null) { +// String ldapClaimName = gluuAttribute.getName(); +// return getEntryAttributeValue(getGrant().getUserDn(), ldapClaimName); +// } +// +// return null; +// } +// +// public String getUserClaimByLdapName(String p_ldapName) { +// return getEntryAttributeValue(getGrant().getUserDn(), p_ldapName); +// } +// +// public CustomEntry getUserClaimEntryByLdapName(String ldapName) { +// return getEntryByDn(getGrant().getUserDn(), ldapName); +// } +// +// public CustomEntry getClientClaimEntry(String ldapName) { +// return getEntryByDn(getGrant().getClientDn(), ldapName); +// } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/UmaAuthorizationContextBuilder.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/UmaAuthorizationContextBuilder.java new file mode 100644 index 00000000..e93d4a74 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/UmaAuthorizationContextBuilder.java @@ -0,0 +1,72 @@ +package org.gluu.oxauth.uma.authorization; + +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.uma.persistence.UmaPermission; +import org.gluu.oxauth.model.uma.persistence.UmaResource; +import org.gluu.oxauth.service.AttributeService; +import org.gluu.oxauth.service.common.UserService; +import org.gluu.oxauth.uma.service.UmaPermissionService; +import org.gluu.oxauth.uma.service.UmaResourceService; +import org.gluu.oxauth.uma.service.UmaSessionService; +import org.oxauth.persistence.model.Scope; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author yuriyz on 06/06/2017. + */ +public class UmaAuthorizationContextBuilder { + + private final AttributeService attributeService; + private final UmaResourceService resourceService; + private final List permissions; + private final Map scopes; + private final Claims claims; + private final HttpServletRequest httpRequest; + private final AppConfiguration configuration; + private final UmaSessionService sessionService; + private final UserService userService; + private final UmaPermissionService permissionService; + private final Client client; + + public UmaAuthorizationContextBuilder(AppConfiguration configuration, AttributeService attributeService, UmaResourceService resourceService, + List permissions, Map scopes, + Claims claims, HttpServletRequest httpRequest, + UmaSessionService sessionService, UserService userService, UmaPermissionService permissionService, Client client) { + this.configuration = configuration; + this.attributeService = attributeService; + this.resourceService = resourceService; + this.permissions = permissions; + this.client = client; + this.scopes = scopes; + this.claims = claims; + this.httpRequest = httpRequest; + this.sessionService = sessionService; + this.userService = userService; + this.permissionService = permissionService; + } + + public UmaAuthorizationContext build(CustomScriptConfiguration script) { + return new UmaAuthorizationContext(configuration, attributeService, scopes, getResources(), claims, + script.getCustomScript().getDn(), httpRequest, script.getConfigurationAttributes(), + sessionService, userService, permissionService, client); + } + + public Set getResourceIds() { + Set result = new HashSet(); + for (UmaPermission permission : permissions) { + result.add(permission.getResourceId()); + } + return result; + } + + public Set getResources() { + return resourceService.getResources(getResourceIds()); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/UmaGatherContext.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/UmaGatherContext.java new file mode 100644 index 00000000..2ea077a4 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/UmaGatherContext.java @@ -0,0 +1,205 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2015, Gluu + */ + +package org.gluu.oxauth.uma.authorization; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; + +import org.gluu.jsf2.service.FacesService; +import org.gluu.model.SimpleCustomProperty; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.exception.InvalidJwtException; +import org.gluu.oxauth.model.jwt.JwtClaims; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.model.uma.persistence.UmaPermission; +import org.gluu.oxauth.service.common.UserService; +import org.gluu.oxauth.service.external.context.ExternalScriptContext; +import org.gluu.oxauth.uma.service.RedirectParameters; +import org.gluu.oxauth.uma.service.UmaPctService; +import org.gluu.oxauth.uma.service.UmaPermissionService; +import org.gluu.oxauth.uma.service.UmaSessionService; +import org.gluu.oxauth.uma.ws.rs.UmaMetadataWS; +import org.gluu.oxauth.model.common.User; + +/** + * @author yuriyz + * @version August 9, 2017 + */ +public class UmaGatherContext extends ExternalScriptContext { + + private final UmaSessionService sessionService; + private final UmaPermissionService permissionService; + private final UmaPctService pctService; + private final UserService userService; + private final FacesService facesService; + + private final Map configurationAttributes; + private final AppConfiguration appConfiguration; + private final SessionId session; + private final RedirectParameters redirectUserParameters = new RedirectParameters(); + private final UmaPCT pct; + private final JwtClaims claims; + private final Map pageClaims; + private String redirectToExternalUrl = null; + + public UmaGatherContext(Map configurationAttributes, HttpServletRequest httpRequest, SessionId session, UmaSessionService sessionService, + UmaPermissionService permissionService, UmaPctService pctService, Map pageClaims, + UserService userService, FacesService facesService, AppConfiguration appConfiguration) { + super(httpRequest); + this.configurationAttributes = configurationAttributes; + this.session = session; + this.sessionService = sessionService; + this.permissionService = permissionService; + this.userService = userService; + this.pctService = pctService; + this.facesService = facesService; + this.pct = pctService.getByCode(sessionService.getPct(session)); + this.claims = pct.getClaims(); + this.pageClaims = pageClaims; + this.appConfiguration = appConfiguration; + } + + public Map getConfigurationAttributes() { + return configurationAttributes; + } + + public User getUser(String... returnAttributes) { + return sessionService.getUser(httpRequest, returnAttributes); + } + + public String getUserDn() { + return sessionService.getUserDn(httpRequest); + } + + + public Client getClient() { + return sessionService.getClient(session); + } + + public Map getConnectSessionAttributes() { + SessionId connectSession = sessionService.getConnectSession(httpRequest); + if (connectSession != null) { + return new HashMap(connectSession.getSessionAttributes()); + } + return new HashMap(); + } + + public boolean isAuthenticated() { + return getUser() != null; + } + + public Map getPageClaims() { + return pageClaims; + } + + public Map getRequestParameters() { + return httpRequest.getParameterMap(); + } + + public int getStep() { + return sessionService.getStep(session); + } + + public void setStep(int step) { + sessionService.setStep(step, session); + } + + public void addSessionAttribute(String key, String value) { + session.getSessionAttributes().put(key, value); + } + + public void removeSessionAttribute(String key) { + session.getSessionAttributes().remove(key); + } + + public Map getSessionAttributes() { + return session.getSessionAttributes(); + } + + public void addRedirectUserParam(String paramName, String paramValue) { + redirectUserParameters.add(paramName, paramValue); + } + + public void removeRedirectUserParameter(String paramName) { + redirectUserParameters.remove(paramName); + } + + public RedirectParameters getRedirectUserParameters() { + return redirectUserParameters; + } + + public Map> getRedirectUserParametersMap() { + return redirectUserParameters.map(); + } + + public List getPermissions() { + return permissionService.getPermissionsByTicket(sessionService.getTicket(session)); + } + + public JwtClaims getClaims() { + return claims; + } + + public Object getClaim(String claimName) { + return claims.getClaim(claimName); + } + + public void putClaim(String claimName, Object claimValue) { + claims.setClaimObject(claimName, claimValue, true); + } + + public void removeClaim(String claimName) { + claims.removeClaim(claimName); + } + + public boolean hasClaim(String claimName) { + return getClaim(claimName) != null; + } + + /** + * Must not take any parameters + */ + public void persist() { + try { + pct.setClaims(claims); + } catch (InvalidJwtException e) { + getLog().error("Failed to persist claims", e); + } + + sessionService.persist(session); + pctService.merge(pct); + } + + public void redirectToExternalUrl(String url) { + redirectToExternalUrl = url; + } + + public String getRedirectToExternalUrl() { + return redirectToExternalUrl; + } + + public String getAuthorizationEndpoint() { + return appConfiguration.getAuthorizationEndpoint(); + } + + public String getIssuer() { + return appConfiguration.getIssuer(); + } + + public String getBaseEndpoint() { + return appConfiguration.getBaseEndpoint(); + } + + public String getClaimsGatheringEndpoint() { + return getBaseEndpoint() + UmaMetadataWS.UMA_CLAIMS_GATHERING_PATH; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/UmaPCT.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/UmaPCT.java new file mode 100644 index 00000000..96c6c2c5 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/UmaPCT.java @@ -0,0 +1,86 @@ +package org.gluu.oxauth.uma.authorization; + +import java.util.Date; + +import org.apache.commons.lang.StringUtils; +import org.json.JSONObject; +import org.gluu.oxauth.model.common.AbstractToken; +import org.gluu.oxauth.model.exception.InvalidJwtException; +import org.gluu.oxauth.model.jwt.JwtClaims; +import org.gluu.oxauth.uma.service.UmaPctService; +import org.gluu.persist.annotation.AttributeName; +import org.gluu.persist.annotation.DN; +import org.gluu.persist.annotation.DataEntry; +import org.gluu.persist.annotation.ObjectClass; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author yuriyz on 05/30/2017. + */ +@DataEntry +@ObjectClass(value = "oxAuthUmaPCT") +public class UmaPCT extends AbstractToken { + + private final static Logger log = LoggerFactory.getLogger(UmaPCT.class); + + @DN + private String dn; + @AttributeName(name = "clnId") + private String clientId; + @AttributeName(name = "oxClaimValues") + private String claimValuesAsJson; + + public UmaPCT() { + super(UmaPctService.DEFAULT_PCT_LIFETIME); + } + + public UmaPCT(int lifeTime) { + super(lifeTime); + } + + protected UmaPCT(String code, Date creationDate, Date expirationDate) { + super(code, creationDate, expirationDate); + } + + public String getDn() { + return dn; + } + + public void setDn(String dn) { + this.dn = dn; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getClaimValuesAsJson() { + return claimValuesAsJson; + } + + public void setClaimValuesAsJson(String claimValuesAsJson) { + this.claimValuesAsJson = claimValuesAsJson; + } + + public JwtClaims getClaims() { + try { + return StringUtils.isNotBlank(claimValuesAsJson) ? new JwtClaims(new JSONObject(claimValuesAsJson)) : new JwtClaims(); + } catch (Exception e) { + log.error("Failed to parse PCT claims. " + e.getMessage(), e); + return null; + } + } + + public void setClaims(JwtClaims claims) throws InvalidJwtException { + if (claims != null) { + claimValuesAsJson = claims.toJsonString(); + } else { + claimValuesAsJson = null; + } + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/UmaRPT.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/UmaRPT.java new file mode 100644 index 00000000..4d703fe2 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/UmaRPT.java @@ -0,0 +1,99 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.uma.authorization; + +import org.gluu.oxauth.model.common.AbstractToken; +import org.gluu.persist.annotation.AttributeName; +import org.gluu.persist.annotation.DN; +import org.gluu.persist.annotation.DataEntry; +import org.gluu.persist.annotation.ObjectClass; + +import java.util.Date; +import java.util.List; + +/** + * Requesting Party Token. + * + * @author Yuriy Movchan Date: 10/16/2012 + */ +@DataEntry +@ObjectClass(value = "oxAuthUmaRPT") +public class UmaRPT extends AbstractToken { + + @DN + private String dn; + + private String notHashedCode; + + @AttributeName(name = "usrId") + private String userId; + @AttributeName(name = "clnId") + private String clientId; + @AttributeName(name = "oxUmaPermission") + private List permissions; + + public UmaRPT() { + super(1); + } + + public UmaRPT(String code, Date creationDate, Date expirationDate, String userId, String clientId) { + super(code, creationDate, expirationDate); + this.notHashedCode = getCode(); + this.userId = userId; + this.clientId = clientId; + } + + public String getDn() { + return dn; + } + + public void setDn(String p_dn) { + dn = p_dn; + } + + public void setNotHashedCode(String notHashedCode) { + this.notHashedCode = notHashedCode; + } + + public String getNotHashedCode() { + return notHashedCode; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public List getPermissions() { + return permissions; + } + + public void setPermissions(List p_permissions) { + permissions = p_permissions; + } + + @Override + public String toString() { + return "UmaRPT{" + + "dn='" + dn + '\'' + + ", userId='" + userId + '\'' + + ", clientId='" + clientId + '\'' + + ", permissions=" + permissions + + "} " + super.toString(); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/UmaScriptByScope.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/UmaScriptByScope.java new file mode 100644 index 00000000..53e025d0 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/UmaScriptByScope.java @@ -0,0 +1,46 @@ +package org.gluu.oxauth.uma.authorization; + +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.oxauth.persistence.model.Scope; + +/** + * @author yuriyz + */ +public class UmaScriptByScope { + + private Scope scope; + + private CustomScriptConfiguration script; + + public UmaScriptByScope() { + } + + public UmaScriptByScope(Scope scope, CustomScriptConfiguration script) { + this.scope = scope; + this.script = script; + } + + public Scope getScope() { + return scope; + } + + public void setScope(Scope scope) { + this.scope = scope; + } + + public CustomScriptConfiguration getScript() { + return script; + } + + public void setScript(CustomScriptConfiguration script) { + this.script = script; + } + + @Override + public String toString() { + return "UmaScript{" + + "scope=" + scope + + ", script=" + script + + '}'; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/UmaWebException.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/UmaWebException.java new file mode 100644 index 00000000..d29c1d85 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/authorization/UmaWebException.java @@ -0,0 +1,63 @@ +package org.gluu.oxauth.uma.authorization; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.error.DefaultErrorResponse; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.uma.UmaErrorResponseType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import java.net.URI; +import java.net.URLEncoder; + +import static javax.ws.rs.core.Response.Status.FOUND; +import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; + +/** + * @author yuriyz on 06/06/2017. + */ +public class UmaWebException extends WebApplicationException { + + private final static Logger LOGGER = LoggerFactory.getLogger(UmaWebException.class); + + private UmaWebException() { + } + + public UmaWebException(String redirectUri, ErrorResponseFactory factory, UmaErrorResponseType error, String state) { + super(createRedirectErrorResponse(redirectUri, factory, error, state)); + } + + public static Response createRedirectErrorResponse(String redirectUri, ErrorResponseFactory factory, UmaErrorResponseType errorType, String state) { + return Response + .status(FOUND) + .location(createErrorUri(redirectUri, factory, errorType, state)) + .build(); + } + + public static URI createErrorUri(String redirectUri, ErrorResponseFactory factory, UmaErrorResponseType errorType, String state) { + try { + DefaultErrorResponse error = factory.getErrorResponse(errorType); + if (redirectUri.contains("?")) { + redirectUri += "&"; + } else { + redirectUri += "?"; + } + + redirectUri += "error=" + error.getErrorCode(); + redirectUri += "&error_description=" + URLEncoder.encode(error.getErrorDescription(), "UTF-8"); + if (StringUtils.isNotBlank(error.getErrorUri())) { + redirectUri += "&error_uri=" + URLEncoder.encode(error.getErrorUri(), "UTF-8"); + } + if (StringUtils.isNotBlank(state)) { + redirectUri += "&state=" + state; + } + + return new URI(redirectUri); + } catch (Exception e) { + LOGGER.error("Failed to construct uri: " + redirectUri, e); + throw factory.createWebApplicationException(INTERNAL_SERVER_ERROR, UmaErrorResponseType.SERVER_ERROR, "Failed to construct uri"); + } + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/RedirectParameters.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/RedirectParameters.java new file mode 100644 index 00000000..05cd0507 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/RedirectParameters.java @@ -0,0 +1,65 @@ +package org.gluu.oxauth.uma.service; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author yuriyz on 06/21/2017. + */ +public class RedirectParameters { + + private final static Logger LOGGER = LoggerFactory.getLogger(RedirectParameters.class); + + private final Map> map = new HashMap>(); + + public RedirectParameters() { + } + + public void add(String paramName, String paramValue) { + Set valueSet = map.get(paramName); + if (valueSet != null) { + valueSet.add(paramValue); + } else { + Set value = new HashSet(); + value.add(paramValue); + map.put(paramName, value); + } + } + + public void remove(String paramName) { + map.remove(paramName); + } + + public Map> map() { + return map; + } + + public String buildQueryString() { + String queryString = ""; + for (Map.Entry> param : map.entrySet()) { + Set values = param.getValue(); + if (StringUtils.isNotBlank(param.getKey()) && values != null && !values.isEmpty()) { + for (String value : values) { + if (StringUtils.isNotBlank(value)) { + try { + queryString += param.getKey() + "=" + URLEncoder.encode(value, "UTF-8") + "&"; + } catch (UnsupportedEncodingException e) { + LOGGER.error("Failed to encode value: " + value, e); + } + } + } + } + } + queryString = StringUtils.removeEnd(queryString, "&"); + return queryString; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaExpressionService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaExpressionService.java new file mode 100644 index 00000000..a6e44709 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaExpressionService.java @@ -0,0 +1,173 @@ +package org.gluu.oxauth.uma.service; + +import com.google.common.collect.Lists; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.uma.JsonLogic; +import org.gluu.oxauth.model.uma.JsonLogicNode; +import org.gluu.oxauth.model.uma.JsonLogicNodeParser; +import org.gluu.oxauth.model.uma.UmaErrorResponseType; +import org.gluu.oxauth.model.uma.persistence.UmaPermission; +import org.gluu.oxauth.model.uma.persistence.UmaResource; +import org.gluu.oxauth.model.util.Util; +import org.gluu.oxauth.service.external.ExternalUmaRptPolicyService; +import org.gluu.oxauth.uma.authorization.UmaAuthorizationContext; +import org.gluu.oxauth.uma.authorization.UmaScriptByScope; +import org.gluu.util.StringHelper; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.core.Response; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author yuriyz + */ +@ApplicationScoped +public class UmaExpressionService { + + @Inject + private Logger log; + @Inject + private ExternalUmaRptPolicyService policyService; + @Inject + private ErrorResponseFactory errorResponseFactory; + @Inject + private UmaResourceService resourceService; + @Inject + + private UmaPermissionService permissionService; + + public boolean isExpressionValid(String expression) { + return JsonLogicNodeParser.isNodeValid(expression); + } + + public void evaluate(Map scriptMap, List permissions) { + for (UmaPermission permission : permissions) { + UmaResource resource = resourceService.getResourceById(permission.getResourceId()); + if (StringHelper.isNotEmpty(resource.getScopeExpression())) { + evaluateScopeExpression(scriptMap, permission, resource); + } else { + if (!evaluateByScopes(filterByScopeDns(scriptMap, permission.getScopeDns()))) { + log.trace("Regular evaluation returns false, access FORBIDDEN."); + throw errorResponseFactory.createWebApplicationException(Response.Status.FORBIDDEN, UmaErrorResponseType.FORBIDDEN_BY_POLICY, "Regular evaluation returns false, access FORBIDDEN."); + } + } + } + } + + private boolean evaluateByScopes(Map scriptMap) { + for (Map.Entry entry : scriptMap.entrySet()) { + final boolean result = policyService.authorize(entry.getKey().getScript(), entry.getValue()); + log.trace("Policy script inum: '{}' result: '{}'", entry.getKey().getScript().getInum(), result); + if (!result) { + log.trace("Stop authorization scriptMap execution, current script returns false, script inum: " + entry.getKey().getScript().getInum() + ", scope: " + entry.getKey().getScope()); + return false; + } + } + return true; + } + + private void evaluateScopeExpression(Map scriptMap, UmaPermission permission, UmaResource resource) { + String scopeExpression = resource.getScopeExpression(); + JsonLogicNode node = JsonLogicNodeParser.parseNode(scopeExpression); + if (node != null) { + log.trace("Evaluating scope expression ..."); + + // validate scopes, all must be present + List dataScopes = node.getDataCopy(); + Map scopeIdToDnMap = scopeIdToDnMap(scriptMap, permission.getScopeDns()); + if (dataScopes.size() == scopeIdToDnMap.size()) { + try { + List evaluatedResults = new ArrayList(); + for (String scopeId : dataScopes) { + log.trace("Evaluating scope result for scope: " + scopeId + " ..."); + boolean b = evaluateByScopes(filterByScopeDns(scriptMap, Lists.newArrayList(scopeIdToDnMap.get(scopeId)))); + log.trace("Evaluated scope result: " + b + ", scope: " + scopeId); + evaluatedResults.add(b); + } + + String rule = node.getRule().toString(); + final boolean result; + if (evaluatedResults.isEmpty()) { + result = JsonLogic.apply(rule); + } else { + result = JsonLogic.apply(rule, Util.asJsonSilently(evaluatedResults)); + } + + log.trace("JsonLogic evaluation result: " + result + ", rule: " + rule + ", data:" + Util.asJsonSilently(evaluatedResults)); + if (result) { + // access granted at this point but we have to remove scopes from permissions for which we got 'false' result + removeFalseScopesFromPermission(permission, dataScopes, scopeIdToDnMap, evaluatedResults); + return; // expression returned true; + } + } catch (Exception e) { + log.error("Failed to evaluate jsonlogic expression. Expression: " + scopeExpression + ", resourceDn: " + resource.getDn(), e); + throw errorResponseFactory.createWebApplicationException(Response.Status.FORBIDDEN, UmaErrorResponseType.FORBIDDEN_BY_POLICY, "Failed to evaluate jsonlogic expression."); + } + } else { + log.error("Scope size in JsonLogic object 'data' and in permission differs which is forbidden. Node data: " + node + + ", permissionDns: " + permission.getScopeDns() + ", result scopeIds: " + scopeIdToDnMap); + throw errorResponseFactory.createWebApplicationException(Response.Status.FORBIDDEN, UmaErrorResponseType.FORBIDDEN_BY_POLICY, "Scope size in JsonLogic object 'data' and in permission differs which is forbidden."); + } + } else { + log.error("Failed to parse JsonLogic object, invalid expression: " + scopeExpression); + throw errorResponseFactory.createWebApplicationException(Response.Status.FORBIDDEN, UmaErrorResponseType.FORBIDDEN_BY_POLICY, "Failed to parse JsonLogic object, invalid expression: " + scopeExpression); + } + + throw errorResponseFactory.createWebApplicationException(Response.Status.FORBIDDEN, UmaErrorResponseType.FORBIDDEN_BY_POLICY, "Unknown"); + } + + private void removeFalseScopesFromPermission(UmaPermission permission, List dataScopes, Map scopeIdToDnMap, List evaluatedResults) { + if (!evaluatedResults.isEmpty() && permission.getScopeDns() != null) { + + List newPermissionScopes = new ArrayList(permission.getScopeDns()); + + for (int i = 0; i < evaluatedResults.size(); i++) { + if (!evaluatedResults.get(i)) { + String dnToRemove = scopeIdToDnMap.get(dataScopes.get(i)); + newPermissionScopes.remove(dnToRemove); + } + } + + if (newPermissionScopes.size() < permission.getScopeDns().size()) { + permission.setScopeDns(newPermissionScopes); + + permissionService.mergeSilently(permission); + } + } + } + + private static Map scopeIdToDnMap(Map scriptMap, List scriptDNs) { + Map result = new HashMap(); + for (Map.Entry entry : scriptMap.entrySet()) { + if (scriptDNs.contains(entry.getKey().getScope().getDn())) { + result.put(entry.getKey().getScope().getId(), entry.getKey().getScope().getDn()); + } + } + return result; + } + + private static Map filterByScopeDns(Map scriptMap, List scopeDNs) { + Map result = new HashMap(); + for (Map.Entry entry : scriptMap.entrySet()) { + if (scopeDNs.contains(entry.getKey().getScope().getDn())) { + result.put(entry.getKey(), entry.getValue()); + } + } + return result; + } + + private static Map filterByScopeId(Map scriptMap, String scopeId) { + Map result = new HashMap(); + for (Map.Entry entry : scriptMap.entrySet()) { + if (entry.getKey().getScope().getId().equals(scopeId)) { + result.put(entry.getKey(), entry.getValue()); + } + } + return result; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaGatherer.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaGatherer.java new file mode 100644 index 00000000..b24ca770 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaGatherer.java @@ -0,0 +1,263 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2015, Gluu + */ + +package org.gluu.oxauth.uma.service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.enterprise.context.RequestScoped; +import javax.faces.application.FacesMessage; +import javax.faces.context.ExternalContext; +import javax.faces.context.FacesContext; +import javax.inject.Inject; +import javax.inject.Named; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang.StringUtils; +import org.gluu.jsf2.service.FacesService; +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.oxauth.i18n.LanguageBean; +import org.gluu.oxauth.model.config.Constants; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.model.uma.persistence.UmaPermission; +import org.gluu.oxauth.service.common.UserService; +import org.gluu.oxauth.service.external.ExternalUmaClaimsGatheringService; +import org.gluu.oxauth.uma.authorization.UmaGatherContext; +import org.slf4j.Logger; + +/** + * @author yuriyz + * @version August 9, 2017 + */ +@RequestScoped +@Named(value = "gatherer") +public class UmaGatherer { + + @Inject + private Logger log; + @Inject + private ExternalUmaClaimsGatheringService external; + @Inject + private AppConfiguration appConfiguration; + @Inject + private FacesContext facesContext; + @Inject + private ExternalContext externalContext; + @Inject + private FacesService facesService; + @Inject + private LanguageBean languageBean; + @Inject + private UmaSessionService umaSessionService; + @Inject + private UmaPermissionService umaPermissionService; + @Inject + private UmaPctService umaPctService; + @Inject + private UserService userService; + + private final Map pageClaims = new HashMap(); + + public boolean gather() { + try { + final HttpServletRequest httpRequest = (HttpServletRequest) externalContext.getRequest(); + final HttpServletResponse httpResponse = (HttpServletResponse) externalContext.getResponse(); + final SessionId session = umaSessionService.getSession(httpRequest, httpResponse); + + CustomScriptConfiguration script = getScript(session); + UmaGatherContext context = new UmaGatherContext(script.getConfigurationAttributes(), httpRequest, session, umaSessionService, umaPermissionService, + umaPctService, pageClaims, userService, facesService, appConfiguration); + + int step = umaSessionService.getStep(session); + if (!umaSessionService.isPassedPreviousSteps(session, step)) { + log.error("There are claims-gathering steps not marked as passed. scriptName: '{}', step: '{}'", script.getName(), step); + return false; + } + + boolean gatheredResult = external.gather(script, step, context); + log.debug("Claims-gathering result for script '{}', step: '{}', gatheredResult: '{}'", script.getName(), step, gatheredResult); + + int overridenNextStep = external.getNextStep(script, step, context); + + if (!gatheredResult && overridenNextStep == -1) { + return false; + } + + if (overridenNextStep != -1) { + umaSessionService.resetToStep(session, overridenNextStep, step); + step = overridenNextStep; + } + + int stepsCount = external.getStepsCount(script, context); + + if (step < stepsCount || overridenNextStep != -1) { + int nextStep; + if (overridenNextStep != -1) { + nextStep = overridenNextStep; + } else { + nextStep = step + 1; + umaSessionService.markStep(session, step, true); + } + + umaSessionService.setStep(nextStep, session); + context.persist(); + + String page = external.getPageForStep(script, nextStep, context); + + log.trace("Redirecting to page: '{}'", page); + facesService.redirect(page); + return true; + } + + if (step == stepsCount) { + context.persist(); + onSuccess(session, context); + return true; + } + } catch (Exception e) { + log.error("Exception during gather() method call.", e); + } + + log.error("Failed to perform gather() method successfully."); + return false; + } + + private void onSuccess(SessionId session, UmaGatherContext context) { + List permissions = context.getPermissions(); + String newTicket = umaPermissionService.changeTicket(permissions, permissions.get(0).getAttributes()); + + String url = constructRedirectUri(session, context, newTicket); + if (StringUtils.isNotBlank(url)) { + facesService.redirectToExternalURL(url); + } else { + log.debug("Redirect to claims_redirect_uri is skipped because it was not provided during request."); + } + } + + private String constructRedirectUri(SessionId session, UmaGatherContext context, String newTicket) { + String claimsRedirectUri = umaSessionService.getClaimsRedirectUri(session); + if (StringUtils.isBlank(claimsRedirectUri)) { + log.debug("claims_redirect_uri is blank, session: " + session); + return ""; + } + + claimsRedirectUri = addQueryParameters(claimsRedirectUri, context.getRedirectUserParameters().buildQueryString().trim()); + claimsRedirectUri = addQueryParameter(claimsRedirectUri, "state", umaSessionService.getState(session)); + claimsRedirectUri = addQueryParameter(claimsRedirectUri, "ticket", newTicket); + return claimsRedirectUri; + } + + public static String addQueryParameters(String url, String parameters) { + if (StringUtils.isNotBlank(parameters)) { + if (url.contains("?")) { + url += "&" + parameters; + } else { + url += "?" + parameters; + } + } + return url; + } + + public static String addQueryParameter(String url, String paramName, String paramValue) { + if (StringUtils.isBlank(url)) { + return ""; + } + if (StringUtils.isNotBlank(paramValue)) { + if (url.contains("?")) { + url += "&" + paramName + "=" + paramValue; + } else { + url += "?" + paramName + "=" + paramValue; + } + } + return url; + } + + public String prepareForStep() { + try { + final HttpServletRequest httpRequest = (HttpServletRequest) externalContext.getRequest(); + final HttpServletResponse httpResponse = (HttpServletResponse) externalContext.getResponse(); + final SessionId session = umaSessionService.getSession(httpRequest, httpResponse); + + if (session == null || session.getSessionAttributes().isEmpty()) { + log.error("Invalid session."); + return result(Constants.RESULT_EXPIRED); + } + + CustomScriptConfiguration script = getScript(session); + UmaGatherContext context = new UmaGatherContext(script.getConfigurationAttributes(), httpRequest, session, umaSessionService, umaPermissionService, + umaPctService, pageClaims, userService, facesService, appConfiguration); + + int step = umaSessionService.getStep(session); + if (step < 1) { + log.error("Invalid step: {}", step); + return result(Constants.RESULT_INVALID_STEP); + } + if (script == null) { + log.error("Failed to load script, step: '{}'", step); + return result(Constants.RESULT_FAILURE); + } + + if (!umaSessionService.isPassedPreviousSteps(session, step)) { + log.error("There are claims-gathering steps not marked as passed. scriptName: '{}', step: '{}'", script.getName(), step); + return result(Constants.RESULT_FAILURE); + } + + boolean result = external.prepareForStep(script, step, context); + if (result) { + context.persist(); + return result(Constants.RESULT_SUCCESS); + } else { + String redirectToExternalUrl = context.getRedirectToExternalUrl(); + if (StringUtils.isNotBlank(redirectToExternalUrl)) { + log.debug("Redirect to : " + redirectToExternalUrl); + facesService.redirectToExternalURL(redirectToExternalUrl); + return redirectToExternalUrl; + } + } + } catch (Exception e) { + log.error("Failed to prepareForStep()", e); + } + return result(Constants.RESULT_FAILURE); + } + + private void errorPage(String errorKey) { + addMessage(FacesMessage.SEVERITY_ERROR, errorKey); + facesService.redirect("/error.xhtml"); + } + + public String result(String resultCode) { + if (Constants.RESULT_FAILURE.equals(resultCode)) { + addMessage(FacesMessage.SEVERITY_ERROR, "uma2.gather.failed"); + } else if (Constants.RESULT_INVALID_STEP.equals(resultCode)) { + addMessage(FacesMessage.SEVERITY_ERROR, "uma2.invalid.step"); + } else if (Constants.RESULT_EXPIRED.equals(resultCode)) { + addMessage(FacesMessage.SEVERITY_ERROR, "uma2.invalid.session"); + } + return resultCode; + } + + public void addMessage(FacesMessage.Severity severity, String summary) { + String msg = languageBean.getMessage(summary); + FacesMessage message = new FacesMessage(severity, msg, null); + facesContext.addMessage(null, message); + } + + public Map getPageClaims() { + return pageClaims; + } + + protected CustomScriptConfiguration getScript(final SessionId session) { + String scriptName = umaSessionService.getScriptName(session); + CustomScriptConfiguration script = external.getCustomScriptConfigurationByName(scriptName); + + return script; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaNeedsInfoService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaNeedsInfoService.java new file mode 100644 index 00000000..1bbb5b61 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaNeedsInfoService.java @@ -0,0 +1,148 @@ +package org.gluu.oxauth.uma.service; + +import org.apache.commons.lang.StringUtils; +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.model.uma.ClaimDefinition; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.uma.UmaConstants; +import org.gluu.oxauth.model.uma.UmaNeedInfoResponse; +import org.gluu.oxauth.model.uma.persistence.UmaPermission; +import org.gluu.oxauth.service.AttributeService; +import org.gluu.oxauth.service.common.UserService; +import org.gluu.oxauth.service.external.ExternalUmaRptPolicyService; +import org.gluu.oxauth.uma.authorization.*; +import org.gluu.oxauth.util.ServerUtil; +import org.oxauth.persistence.model.Scope; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import java.util.*; + +/** + * @author yuriyz on 06/16/2017. + */ +@ApplicationScoped +public class UmaNeedsInfoService { + + @Inject + private Logger log; + @Inject + private AppConfiguration appConfiguration; + @Inject + private UmaPermissionService permissionService; + @Inject + private AttributeService attributeService; + @Inject + private UmaResourceService resourceService; + @Inject + private ExternalUmaRptPolicyService policyService; + @Inject + private UmaSessionService sessionService; + @Inject + private UserService userService; + + public Map checkNeedsInfo(Claims claims, Map requestedScopes, + List permissions, UmaPCT pct, HttpServletRequest httpRequest, + Client client) { + + Map scriptMap = new HashMap(); + Map ticketAttributes = new HashMap(); + + List missedClaims = new ArrayList(); + + UmaAuthorizationContextBuilder contextBuilder = new UmaAuthorizationContextBuilder(appConfiguration, + attributeService, resourceService, permissions, requestedScopes, claims, httpRequest, + sessionService, userService, permissionService, client); + + + for (Scope scope : requestedScopes.keySet()) { + List authorizationPolicies = scope.getUmaAuthorizationPolicies(); + if (authorizationPolicies != null && !authorizationPolicies.isEmpty()) { + for (String scriptDN : authorizationPolicies) { //log.trace("Loading UMA script: " + scriptDN + ", scope: " + scope + " ..."); + CustomScriptConfiguration script = policyService.getScriptByDn(scriptDN); + if (script != null) { + UmaAuthorizationContext context = contextBuilder.build(script); + scriptMap.put(new UmaScriptByScope(scope, script), context); + + List requiredClaims = policyService.getRequiredClaims(script, context); + if (requiredClaims != null && !requiredClaims.isEmpty()) { + for (ClaimDefinition definition : requiredClaims) { + if (!claims.has(definition.getName())) { + missedClaims.add(definition); + } + } + } + + String claimsGatheringScriptName = policyService.getClaimsGatheringScriptName(script, context); + if (StringUtils.isNotBlank(claimsGatheringScriptName)) { + ticketAttributes.put(UmaConstants.GATHERING_ID, constructGatheringScriptNameValue(ticketAttributes.get(UmaConstants.GATHERING_ID), claimsGatheringScriptName)); + } else { + log.debug("External 'getClaimsGatheringScriptName' script method return null or blank value, script: " + script.getName()); + } + } else { + log.error("Unable to load UMA script dn: '{}'", scriptDN); + } + } + } else { + log.trace("No policies defined for scope: " + scope.getId() + ", scopeDn: " + scope.getDn()); + } + } + + if (!missedClaims.isEmpty()) { + ticketAttributes.put(UmaPermission.PCT, pct.getCode()); + String newTicket = permissionService.changeTicket(permissions, ticketAttributes); + + UmaNeedInfoResponse needInfoResponse = new UmaNeedInfoResponse(); + needInfoResponse.setTicket(newTicket); + needInfoResponse.setError("need_info"); + needInfoResponse.setRedirectUser(buildClaimsGatheringRedirectUri(scriptMap.values(), client, newTicket)); + needInfoResponse.setRequiredClaims(missedClaims); + + throw new WebApplicationException(Response.status(Response.Status.FORBIDDEN).entity(ServerUtil.asJsonSilently(needInfoResponse)).build()); + } + + return scriptMap; + } + + private String constructGatheringScriptNameValue(String existingValue, String claimsGatheringScriptName) { + if (StringUtils.isBlank(existingValue)) { + return claimsGatheringScriptName; + } + return existingValue + " " + claimsGatheringScriptName; + } + + private String buildClaimsGatheringRedirectUri(Collection contexts, Client client, String newTicket) { + String queryParameters = ""; + + for (UmaAuthorizationContext context : contexts) { + queryParameters += context.getRedirectUserParameters().buildQueryString() + "&"; + } + queryParameters = StringUtils.removeEnd(queryParameters, "&"); + + String result = appConfiguration.getBaseEndpoint() + "/uma/gather_claims"; + if (StringUtils.isNotBlank(queryParameters)) { + result += "?" + queryParameters; + } + result += "&client_id=" + client.getClientId() + "&ticket=" + newTicket; + return result; + } + + public static Set getScriptDNs(List scopes) { + HashSet result = new HashSet(); + + for (Scope scope : scopes) { + List authorizationPolicies = scope.getUmaAuthorizationPolicies(); + if (authorizationPolicies != null) { + result.addAll(authorizationPolicies); + } + } + + return result; + } +} + diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaPctService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaPctService.java new file mode 100644 index 00000000..70b32123 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaPctService.java @@ -0,0 +1,195 @@ +package org.gluu.oxauth.uma.service; + +import java.util.List; +import java.util.UUID; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.inject.Named; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.config.StaticConfiguration; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaims; +import org.gluu.oxauth.model.uma.persistence.UmaPermission; +import org.gluu.oxauth.uma.authorization.UmaPCT; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.persist.model.base.SimpleBranch; +import org.gluu.search.filter.Filter; +import org.slf4j.Logger; + +/** + * @author yuriyz on 05/31/2017. + */ +@ApplicationScoped +public class UmaPctService { + + public static final int DEFAULT_PCT_LIFETIME = 2592000; + + @Inject + private Logger log; + + @Inject + private PersistenceEntryManager ldapEntryManager; + + @Inject + private StaticConfiguration staticConfiguration; + + @Inject + private AppConfiguration appConfiguration; + + public UmaPCT updateClaims(UmaPCT pct, Jwt idToken, String clientId, List permissions) { + try { + String ticketPctCode = permissions.get(0).getAttributes().get("pct"); + UmaPCT ticketPct = StringUtils.isNotBlank(ticketPctCode) ? getByCode(ticketPctCode) : null; + + boolean hasPct = pct != null; + + if (!hasPct) { + if (ticketPct != null) { + pct = ticketPct; + } else { + pct = createPctAndPersist(clientId); + } + } + + // copy claims from pctTicket into normal pct + JwtClaims pctClaims = pct.getClaims(); + if (ticketPct != null && hasPct) { + JwtClaims ticketClaims = ticketPct.getClaims(); + for (String key : ticketClaims.keys()) { + pctClaims.setClaimObject(key, ticketClaims.getClaim(key), false); + } + pct = ticketPct; + } + + if (idToken != null && idToken.getClaims() != null) { + for (String key : idToken.getClaims().keys()) { + pctClaims.setClaimObject(key, idToken.getClaims().getClaim(key), false); + } + } + + pct.setClaims(pctClaims); + log.trace("PCT code: " + pct.getCode() + ", claims: " + pct.getClaimValuesAsJson()); + + pct.resetTtlFromExpirationDate(); + ldapEntryManager.merge(pct); + + return ldapEntryManager.find(UmaPCT.class, pct.getDn()); + } catch (Exception e) { + log.error("Failed to update PCT claims. " + e.getMessage(), e); + } + + return pct; + } + + public UmaPCT getByCode(String pctCode) { + try { + final Filter filter = Filter.createEqualityFilter("tknCde", pctCode); + final List entries = ldapEntryManager.findEntries(branchBaseDn(), UmaPCT.class, filter); + if (entries != null && !entries.isEmpty()) { + return entries.get(0); + } else { + log.error("Failed to find PCT by code: " + pctCode); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return null; + } + + public UmaPCT createPct(String clientId) { + String code = generateCode(); + + UmaPCT pct = new UmaPCT(pctLifetime()); + pct.setCode(code); + pct.setDn(dn(pct.getCode())); + pct.setClientId(clientId); + return pct; + } + + public UmaPCT createPctAndPersist(String clientId) { + UmaPCT pct = createPct(clientId); + persist(pct); + return pct; + } + + public int pctLifetime() { + int lifeTime = appConfiguration.getUmaPctLifetime(); + if (lifeTime <= 0) { + lifeTime = DEFAULT_PCT_LIFETIME; + } + return lifeTime; + } + + public void persist(UmaPCT pct) { + try { + prepareBranch(); + + pct.setDn(dn(pct.getCode())); + ldapEntryManager.persist(pct); + } catch (Exception e) { + log.error("Failed to persist PCT, code: " + pct.getCode() + ". " + e.getMessage(), e); + } + } + + public void remove(UmaPCT umaPCT) { + ldapEntryManager.remove(umaPCT); + } + + public void remove(String pctCode) { + remove(getByCode(pctCode)); + } + + public void remove(List pctList) { + for (UmaPCT pct : pctList) { + remove(pct); + } + } + + private void prepareBranch() { + if (!ldapEntryManager.hasBranchesSupport(branchBaseDn())) { + return; + } + + if (!ldapEntryManager.contains(branchBaseDn(), SimpleBranch.class)) { + addBranch(); + } + } + + public void addBranch() { + SimpleBranch branch = new SimpleBranch(); + branch.setOrganizationalUnitName("pct"); + branch.setDn(branchBaseDn()); + + ldapEntryManager.persist(branch); + } + + private String generateCode() { + String code = UUID.randomUUID().toString(); + + return code; + } + + public String dn(String pctCode) { + if (StringUtils.isBlank(pctCode)) { + throw new IllegalArgumentException("PCT code is null or blank."); + } + return String.format("tknCde=%s,%s", pctCode, branchBaseDn()); + } + + public String branchBaseDn() { + final String umaBaseDn = staticConfiguration.getBaseDn().getUmaBase(); // "ou=uma,o=gluu" + return String.format("ou=pct,%s", umaBaseDn); + } + + public void merge(UmaPCT pct) { + try { + pct.resetTtlFromExpirationDate(); + ldapEntryManager.merge(pct); + } catch (Exception e) { + log.error("Failed to merge PCT, code: " + pct.getCode() + ". " + e.getMessage(), e); + } + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaPermissionService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaPermissionService.java new file mode 100644 index 00000000..b226a24c --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaPermissionService.java @@ -0,0 +1,188 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.uma.service; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.config.StaticConfiguration; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.uma.UmaPermissionList; +import org.gluu.oxauth.model.uma.persistence.UmaPermission; +import org.gluu.oxauth.model.util.Pair; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.persist.model.base.SimpleBranch; +import org.gluu.search.filter.Filter; +import org.gluu.util.INumGenerator; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import java.util.*; + +/** + * Holds permission tokens and permissions + * + * @author Yuriy Zabrovarnyy + */ +@ApplicationScoped +public class UmaPermissionService { + + private static final String ORGUNIT_OF_RESOURCE_PERMISSION = "uma_permission"; + private static final int DEFAULT_TICKET_LIFETIME = 3600; + + @Inject + private Logger log; + + @Inject + private PersistenceEntryManager ldapEntryManager; + + @Inject + private StaticConfiguration staticConfiguration; + + @Inject + private UmaScopeService scopeService; + + @Inject + private AppConfiguration appConfiguration; + + public static String getDn(String clientDn, String ticket) { + return String.format("oxTicket=%s,%s", ticket, getBranchDn(clientDn)); + } + + public static String getBranchDn(String clientDn) { + return String.format("ou=%s,%s", ORGUNIT_OF_RESOURCE_PERMISSION, clientDn); + } + + private List createPermissions(UmaPermissionList permissions, Pair expirationDate) { + final String configurationCode = INumGenerator.generate(8) + "." + System.currentTimeMillis(); + + final String ticket = generateNewTicket(); + List result = new ArrayList(); + for (org.gluu.oxauth.model.uma.UmaPermission permission : permissions) { + UmaPermission p = new UmaPermission(permission.getResourceId(), scopeService.getScopeDNsByIdsAndAddToLdapIfNeeded(permission.getScopes()), ticket, configurationCode, expirationDate); + if (permission.getParams() != null && !permission.getParams().isEmpty()) { + p.getAttributes().putAll(permission.getParams()); + } + result.add(p); + } + + return result; + } + + public String generateNewTicket() { + return UUID.randomUUID().toString(); + } + + public String addPermission(UmaPermissionList permissionList, String clientDn) { + try { + List created = createPermissions(permissionList, ticketExpirationDate()); + for (UmaPermission permission : created) { + addPermission(permission, clientDn); + } + return created.get(0).getTicket(); + } catch (Exception e) { + log.error(e.getMessage(), e); + throw e; + } + } + + public Pair ticketExpirationDate() { + int lifeTime = appConfiguration.getUmaTicketLifetime(); + if (lifeTime <= 0) { + lifeTime = DEFAULT_TICKET_LIFETIME; + } + + final Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.SECOND, lifeTime); + return new Pair<>(calendar.getTime(), lifeTime); + } + + public void addPermission(UmaPermission permission, String clientDn) { + try { + addBranchIfNeeded(clientDn); + permission.setDn(getDn(clientDn, permission.getTicket())); + ldapEntryManager.persist(permission); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + + public void merge(UmaPermission permission) { + permission.resetTtlFromExpirationDate(); + ldapEntryManager.merge(permission); + } + + public void mergeSilently(UmaPermission permission) { + try { + permission.resetTtlFromExpirationDate(); + ldapEntryManager.merge(permission); + } catch (Exception e) { + log.error("Failed to persist permission: " + permission, e); + } + } + + public List getPermissionsByTicket(String ticket) { + try { + final String baseDn = staticConfiguration.getBaseDn().getClients(); + final Filter filter = Filter.createEqualityFilter("oxTicket", ticket); + return ldapEntryManager.findEntries(baseDn, UmaPermission.class, filter); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return null; + } + + public void deletePermission(String ticket) { + try { + final List permissions = getPermissionsByTicket(ticket); + for (UmaPermission p : permissions) { + ldapEntryManager.remove(p); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + + public void addBranch(String clientDn) { + String branchDn = getBranchDn(clientDn); + if (!containsBranch(branchDn)) { + final SimpleBranch branch = new SimpleBranch(); + branch.setOrganizationalUnitName(ORGUNIT_OF_RESOURCE_PERMISSION); + branch.setDn(branchDn); + ldapEntryManager.persist(branch); + } + } + + public void addBranchIfNeeded(String clientDn) { + if (!ldapEntryManager.hasBranchesSupport(clientDn)) { + return; + } + if (!containsBranch(clientDn)) { + addBranch(clientDn); + } + } + + public boolean containsBranch(String clientDn) { + return ldapEntryManager.contains(getBranchDn(clientDn), SimpleBranch.class); + } + + public String changeTicket(List permissions, Map attributes) { + String newTicket = generateNewTicket(); + + for (UmaPermission permission : permissions) { + ldapEntryManager.remove(permission); + + String dn = String.format("oxTicket=%s,%s", newTicket, StringUtils.substringAfter(permission.getDn(), ",")); + permission.setTicket(newTicket); + permission.setDn(dn); + permission.setAttributes(attributes); + ldapEntryManager.persist(permission); + log.trace("New ticket: " + newTicket + ", old permission: " + dn); + } + + return newTicket; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaResourceService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaResourceService.java new file mode 100644 index 00000000..5e149afb --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaResourceService.java @@ -0,0 +1,208 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.uma.service; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Sets; +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.config.StaticConfiguration; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.uma.UmaErrorResponseType; +import org.gluu.oxauth.model.uma.persistence.UmaResource; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.persist.model.base.SimpleBranch; +import org.gluu.search.filter.Filter; +import org.gluu.service.CacheService; +import org.gluu.util.StringHelper; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.core.Response; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Provides operations with resource set descriptions + * + * @author Yuriy Movchan + * @author Yuriy Zabrovarnyy + * Date: 10.05.2012 + */ +@ApplicationScoped +public class UmaResourceService { + + private static final int RESOURCE_CACHE_EXPIRATION_IN_SECONDS = 120; + + @Inject + private Logger log; + + @Inject + private PersistenceEntryManager ldapEntryManager; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private StaticConfiguration staticConfiguration; + + @Inject + private CacheService cacheService; + + public void addBranch() { + SimpleBranch branch = new SimpleBranch(); + branch.setOrganizationalUnitName("resources"); + branch.setDn(getDnForResource(null)); + + ldapEntryManager.persist(branch); + } + + /** + * Add new resource description entry + * + * @param resource resource + */ + public void addResource(UmaResource resource) { + validate(resource); + ldapEntryManager.persist(resource); + } + + public void validate(UmaResource resource) { + Preconditions.checkArgument(StringUtils.isNotBlank(resource.getName()), "Name is required for resource."); + Preconditions.checkArgument(((resource.getScopes() != null && !resource.getScopes().isEmpty()) || StringUtils.isNotBlank(resource.getScopeExpression())), "Scope must be specified for resource."); + Preconditions.checkState(!resource.isExpired(), "UMA Resource expired. It must not be expired."); + prepareBranch(); + } + + public void updateResource(UmaResource resource) { + updateResource(resource, false); + } + + /** + * Update resource description entry + * + * @param resource resource + */ + public void updateResource(UmaResource resource, boolean skipValidation) { + if (!skipValidation) { + validate(resource); + } + cacheService.put(resource.getDn(), resource); + resource.resetTtlFromExpirationDate(); + ldapEntryManager.merge(resource); + } + + /** + * Remove resource description entry + * + * @param resource resource + */ + public void remove(UmaResource resource) { + ldapEntryManager.remove(resource); + } + + /** + * Remove resource description entry by ID. + * + * @param rsid resource ID + */ + public void remove(String rsid) { + ldapEntryManager.remove(getResourceById(rsid)); + } + + public void remove(List resources) { + for (UmaResource resource : resources) { + remove(resource); + } + } + + /** + * Get all resource descriptions + * + * @return List of resource descriptions + */ + public List getResourcesByAssociatedClient(String associatedClientDn) { + try { + prepareBranch(); + + if (StringUtils.isNotBlank(associatedClientDn)) { + final Filter filter = Filter.createEqualityFilter("oxAssociatedClient", associatedClientDn); + return ldapEntryManager.findEntries(getBaseDnForResource(), UmaResource.class, filter); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return Collections.emptyList(); + } + + public Set getResources(Set ids) { + Set result = new HashSet(); + if (ids != null) { + for (String id : ids) { + UmaResource resource = getResourceById(id); + if (resource != null) { + result.add(resource); + } else { + log.error("Failed to find resource by id: " + id); + } + } + } + return result; + } + + public UmaResource getResourceById(String id) { + prepareBranch(); + + try { + final String key = getDnForResource(id); + final UmaResource resource = cacheService.getWithPut(key, () -> ldapEntryManager.find(UmaResource.class, key), RESOURCE_CACHE_EXPIRATION_IN_SECONDS); + if (resource != null) { + return resource; + } + } catch (Exception e) { + log.error("Failed to find resource set with id: " + id, e); + } + log.error("Failed to find resource set with id: " + id); + throw errorResponseFactory.createWebApplicationException(Response.Status.NOT_FOUND, UmaErrorResponseType.NOT_FOUND, "Failed to find resource set with id: " + id); + } + + public Set getResourceScopes(Set resourceIds) { + Set result = Sets.newHashSet(); + for (String resourceId : resourceIds) { + result.addAll(getResourceById(resourceId).getScopes()); + } + return result; + } + + private void prepareBranch() { + if (!ldapEntryManager.hasBranchesSupport(getDnForResource(null))) { + return; + } + + // Create resource description branch if needed + if (!ldapEntryManager.contains(getDnForResource(null), SimpleBranch.class)) { + addBranch(); + } + } + + /** + * Build DN string for resource description + */ + public String getDnForResource(String oxId) { + if (StringHelper.isEmpty(oxId)) { + return getBaseDnForResource(); + } + return String.format("oxId=%s,%s", oxId, getBaseDnForResource()); + } + + public String getBaseDnForResource() { + final String umaBaseDn = staticConfiguration.getBaseDn().getUmaBase(); // "ou=uma,o=gluu" + return String.format("ou=resources,%s", umaBaseDn); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaRptService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaRptService.java new file mode 100644 index 00000000..cac23f0d --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaRptService.java @@ -0,0 +1,317 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.uma.service; + +import com.google.common.base.Preconditions; +import org.apache.commons.lang.ArrayUtils; +import org.gluu.oxauth.claims.Audience; +import org.gluu.oxauth.model.common.ExecutionContext; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.config.StaticConfiguration; +import org.gluu.oxauth.model.config.WebKeysConfiguration; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.token.JwtSigner; +import org.gluu.oxauth.model.uma.persistence.UmaPermission; +import org.gluu.oxauth.model.util.JwtUtil; +import org.gluu.oxauth.service.ClientService; +import org.gluu.oxauth.service.external.ExternalUmaRptClaimsService; +import org.gluu.oxauth.service.external.context.ExternalUmaRptClaimsContext; +import org.gluu.oxauth.service.stat.StatService; +import org.gluu.oxauth.uma.authorization.UmaPCT; +import org.gluu.oxauth.uma.authorization.UmaRPT; +import org.gluu.oxauth.util.ServerUtil; +import org.gluu.oxauth.util.TokenHashUtil; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.persist.model.base.SimpleBranch; +import org.gluu.util.INumGenerator; +import org.gluu.util.StringHelper; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import java.io.IOException; +import java.util.*; + +/** + * RPT manager component + * + * @author Yuriy Zabrovarnyy + * @author Javier Rojas Blum + * @version June 28, 2017 + */ +@ApplicationScoped +public class UmaRptService { + + private static final String ORGUNIT_OF_RPT = "uma_rpt"; + + public static final int DEFAULT_RPT_LIFETIME = 3600; + + @Inject + private Logger log; + + @Inject + private PersistenceEntryManager ldapEntryManager; + + @Inject + private WebKeysConfiguration webKeysConfiguration; + + @Inject + private UmaPctService pctService; + + @Inject + private UmaScopeService umaScopeService; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private StaticConfiguration staticConfiguration; + + @Inject + private ClientService clientService; + + @Inject + private ExternalUmaRptClaimsService externalUmaRptClaimsService; + + @Inject + private StatService statService; + + private boolean containsBranch = false; + + public String createDn(String tokenCode) { + return String.format("tknCde=%s,%s", TokenHashUtil.hash(tokenCode), branchDn()); + } + + public String branchDn() { + return String.format("ou=%s,%s", ORGUNIT_OF_RPT, staticConfiguration.getBaseDn().getTokens()); + } + + public void persist(UmaRPT rpt) { + try { + Preconditions.checkNotNull(rpt.getClientId()); + + addBranchIfNeeded(); + rpt.setDn(createDn(rpt.getNotHashedCode())); + rpt.setCode(TokenHashUtil.hash(rpt.getNotHashedCode())); + ldapEntryManager.persist(rpt); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + + public UmaRPT getRPTByCode(String rptCode) { + try { + final UmaRPT entry = ldapEntryManager.find(UmaRPT.class, createDn(rptCode)); + if (entry != null) { + return entry; + } else { + log.error("Failed to find RPT by code: " + rptCode); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return null; + } + + public void deleteByCode(String rptCode) { + try { + final UmaRPT t = getRPTByCode(rptCode); + if (t != null) { + ldapEntryManager.remove(t); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + + public boolean addPermissionToRPT(UmaRPT rpt, Collection permissions) { + return addPermissionToRPT(rpt, permissions.toArray(new UmaPermission[permissions.size()])); + } + + public boolean addPermissionToRPT(UmaRPT rpt, UmaPermission... permission) { + if (ArrayUtils.isEmpty(permission)) { + return true; + } + + final List permissions = getPermissionDns(Arrays.asList(permission)); + if (rpt.getPermissions() != null) { + permissions.addAll(rpt.getPermissions()); + } + + rpt.setPermissions(permissions); + + try { + rpt.resetTtlFromExpirationDate(); + ldapEntryManager.merge(rpt); + log.trace("Persisted RPT: " + rpt); + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + public static List getPermissionDns(Collection permissions) { + final List result = new ArrayList(); + if (permissions != null) { + for (UmaPermission p : permissions) { + result.add(p.getDn()); + } + } + return result; + } + + public List getRptPermissions(UmaRPT p_rpt) { + final List result = new ArrayList(); + try { + if (p_rpt != null && p_rpt.getPermissions() != null) { + final List permissionDns = p_rpt.getPermissions(); + for (String permissionDn : permissionDns) { + final UmaPermission permissionObject = ldapEntryManager.find(UmaPermission.class, permissionDn); + if (permissionObject != null) { + result.add(permissionObject); + } + } + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return result; + } + + public Date rptExpirationDate() { + int lifeTime = appConfiguration.getUmaRptLifetime(); + if (lifeTime <= 0) { + lifeTime = DEFAULT_RPT_LIFETIME; + } + + final Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.SECOND, lifeTime); + return calendar.getTime(); + } + + public UmaRPT createRPTAndPersist(ExecutionContext executionContext, List permissions) { + try { + final Date creationDate = new Date(); + final Date expirationDate = rptExpirationDate(); + final Client client = executionContext.getClient(); + + final String code; + if (client.isRptAsJwt()) { + code = createRptJwt(executionContext, permissions, creationDate, expirationDate); + } else { + code = UUID.randomUUID().toString() + "_" + INumGenerator.generate(8); + } + + UmaRPT rpt = new UmaRPT(code, creationDate, expirationDate, null, client.getClientId()); + rpt.setPermissions(getPermissionDns(permissions)); + persist(rpt); + statService.reportUmaToken(GrantType.OXAUTH_UMA_TICKET); + return rpt; + } catch (Exception e) { + log.error(e.getMessage(), e); + throw new RuntimeException("Failed to generate RPT, clientId: " + executionContext.getClient().getClientId(), e); + } + } + + public void merge(UmaRPT rpt) { + rpt.resetTtlFromExpirationDate(); + ldapEntryManager.merge(rpt); + } + + private String createRptJwt(ExecutionContext executionContext, List permissions, Date creationDate, Date expirationDate) throws Exception { + Client client = executionContext.getClient(); + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.fromString(appConfiguration.getDefaultSignatureAlgorithm()); + if (client.getAccessTokenSigningAlg() != null && SignatureAlgorithm.fromString(client.getAccessTokenSigningAlg()) != null) { + signatureAlgorithm = SignatureAlgorithm.fromString(client.getAccessTokenSigningAlg()); + } + + final JwtSigner jwtSigner = new JwtSigner(appConfiguration, webKeysConfiguration, signatureAlgorithm, client.getClientId(), clientService.decryptSecret(client.getClientSecret())); + final Jwt jwt = jwtSigner.newJwt(); + jwt.getClaims().setClaim("client_id", client.getClientId()); + jwt.getClaims().setExpirationTime(expirationDate); + jwt.getClaims().setIssuedAt(creationDate); + Audience.setAudience(jwt.getClaims(), client); + + if (permissions != null && !permissions.isEmpty()) { + String pctCode = permissions.iterator().next().getAttributes().get(UmaPermission.PCT); + if (StringHelper.isNotEmpty(pctCode)) { + UmaPCT pct = pctService.getByCode(pctCode); + if (pct != null) { + jwt.getClaims().setClaim("pct_claims", pct.getClaims().toJsonObject()); + } else { + log.error("Failed to find PCT with code: " + pctCode + " which is taken from permission object: " + permissions.iterator().next().getDn()); + } + } + + jwt.getClaims().setClaim("permissions", buildPermissionsJSONObject(permissions)); + } + runScriptAndInjectValuesIntoJwt(jwt, executionContext); + + return jwtSigner.sign().toString(); + } + + private void runScriptAndInjectValuesIntoJwt(Jwt jwt, ExecutionContext executionContext) { + JSONObject responseAsJsonObject = new JSONObject(); + + ExternalUmaRptClaimsContext context = new ExternalUmaRptClaimsContext(executionContext); + if (externalUmaRptClaimsService.externalModify(responseAsJsonObject, context)) { + log.trace("Successfully run external RPT Claim scripts."); + + if (context.isTranferPropertiesIntoJwtClaims()) { + log.trace("Transfering claims into jwt ..."); + JwtUtil.transferIntoJwtClaims(responseAsJsonObject, jwt); + log.trace("Transfered."); + } + } + } + + public JSONArray buildPermissionsJSONObject(List permissions) throws IOException, JSONException { + List result = new ArrayList<>(); + + for (UmaPermission permission : permissions) { + permission.checkExpired(); + permission.isValid(); + if (permission.isValid()) { + final org.gluu.oxauth.model.uma.UmaPermission toAdd = ServerUtil.convert(permission, umaScopeService); + if (toAdd != null) { + result.add(toAdd); + } + } else { + log.debug("Ignore permission, skip it in response because permission is not valid. Permission dn: {}", permission.getDn()); + } + } + + final String json = ServerUtil.asJson(result); + return new JSONArray(json); + } + + public void addBranch() { + final SimpleBranch branch = new SimpleBranch(); + branch.setOrganizationalUnitName(ORGUNIT_OF_RPT); + branch.setDn(branchDn()); + ldapEntryManager.persist(branch); + } + + public void addBranchIfNeeded() { + if (ldapEntryManager.hasBranchesSupport(branchDn()) && !containsBranch() && !containsBranch) { + addBranch(); + } else { + containsBranch = true; + } + } + + public boolean containsBranch() { + return ldapEntryManager.contains(branchDn(), SimpleBranch.class); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaScopeService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaScopeService.java new file mode 100644 index 00000000..294f6e99 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaScopeService.java @@ -0,0 +1,228 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.uma.service; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.common.ScopeType; +import org.gluu.oxauth.model.config.StaticConfiguration; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.uma.UmaErrorResponseType; +import org.gluu.oxauth.service.SpontaneousScopeService; +import org.gluu.oxauth.service.common.InumService; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.search.filter.Filter; +import org.oxauth.persistence.model.Scope; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.core.Response; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import static org.apache.commons.lang3.BooleanUtils.isFalse; + +/** + * @author Yuriy Zabrovarnyy + * @author Yuriy Movchan + * @version 0.9, 22/04/2013 + */ +@ApplicationScoped +public class UmaScopeService { + + @Inject + private Logger log; + + @Inject + private PersistenceEntryManager ldapEntryManager; + + @Inject + private InumService inumService; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private StaticConfiguration staticConfiguration; + + @Inject + private SpontaneousScopeService spontaneousScopeService; + + public Scope getOrCreate(Client client, String scopeId, Set regExps) { + Scope fromLdap = getScope(scopeId); + if (fromLdap != null) { // already exists + return fromLdap; + } + + if (isFalse(appConfiguration.getAllowSpontaneousScopes())) { + return null; + } + + if (isFalse(client.getAttributes().getAllowSpontaneousScopes())) { + return null; + } + + if (!spontaneousScopeService.isAllowedBySpontaneousScopes_(regExps, scopeId)) { + return null; + } + + return spontaneousScopeService.createSpontaneousScopeIfNeeded(regExps, scopeId, client.getClientId()); + } + + public Scope getScope(String scopeId) { + try { + final Filter filter = Filter.createEqualityFilter("oxId", scopeId); + final List entries = ldapEntryManager.findEntries(baseDn(), Scope.class, filter); + if (entries != null && !entries.isEmpty()) { + // if more then one scope then it's problem, non-deterministic behavior, id must be unique + if (entries.size() > 1) { + log.error("Found more then one UMA scope, id: {}", scopeId); + for (Scope s : entries) { + log.error("Scope, Id: {}, dn: {}", s.getId(), s.getDn()); + } + } + return entries.get(0); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return null; + } + + public boolean persist(Scope scope) { + try { + if (StringUtils.isBlank(scope.getDn())) { + scope.setDn(String.format("inum=%s,%s", scope.getInum(), baseDn())); + } + + ldapEntryManager.persist(scope); + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + public List getScopeDNsByIdsAndAddToLdapIfNeeded(List scopeIds) { + List result = new ArrayList(); + for (Scope scope : getScopesByIds(scopeIds)) { + result.add(scope.getDn()); + } + return result; + } + + public List getScopesByDns(List scopeDns) { + final List result = new ArrayList(); + try { + if (scopeDns != null && !scopeDns.isEmpty()) { + for (String dn : scopeDns) { + final Scope scopeDescription = ldapEntryManager.find(Scope.class, dn); + if (scopeDescription != null) { + result.add(scopeDescription); + } else { + log.error("Failed to load UMA scope with dn: {}", dn); + } + } + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return result; + } + + public List getScopeIdsByDns(List scopeDns) { + return getScopeIds(getScopesByDns(scopeDns)); + } + + public List getScopeIds(List scopes) { + final List result = new ArrayList(); + if (scopes != null && !scopes.isEmpty()) { + for (Scope scope : scopes) { + result.add(scope.getId()); + } + } + return result; + } + + public List getScopesByIds(List scopeIds) { + List result = new ArrayList(); + if (scopeIds != null && !scopeIds.isEmpty()) { + List notInLdap = new ArrayList(scopeIds); + + final List entries = ldapEntryManager.findEntries(baseDn(), Scope.class, createAnyFilterByIds(scopeIds)); + if (entries != null) { + result.addAll(entries); + for (Scope scope : entries) { + notInLdap.remove(scope.getId()); + } + } + + if (!notInLdap.isEmpty()) { + for (String scopeId : notInLdap) { + result.add(addScope(scopeId)); + } + } + } + return result; + } + + private Scope addScope(String scopeId) { + final Boolean addAutomatically = appConfiguration.getUmaAddScopesAutomatically(); + if (addAutomatically != null && addAutomatically) { + final String inum = inumService.generateInum(); + final Scope newScope = new Scope(); + newScope.setScopeType(ScopeType.UMA); + newScope.setInum(inum); + newScope.setDisplayName(scopeId); + newScope.setId(scopeId); + newScope.setDeletable(false); + + final boolean persisted = persist(newScope); + if (persisted) { + return newScope; + } else { + log.error("Failed to persist scope, id:{}" + scopeId); + } + } + + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, UmaErrorResponseType.INVALID_SCOPE, "Failed to persist scope."); + } + + private Filter createAnyFilterByIds(List scopeIds) { + if (scopeIds != null && !scopeIds.isEmpty()) { + List filters = new ArrayList(); + for (String url : scopeIds) { + Filter filter = Filter.createEqualityFilter("oxId", url); + filters.add(filter); + } + Filter filter = Filter.createORFilter(filters.toArray(new Filter[0])); + log.trace("Uma scope ids: " + scopeIds + ", ldapFilter: " + filter); + return filter; + } + + return null; + } + + public String baseDn() { + return staticConfiguration.getBaseDn().getScopes(); + } + + public static String asString(Collection scopes) { + String result = ""; + for (Scope scope : scopes) { + result += scope.getId() + " "; + } + return result.trim(); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaSessionService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaSessionService.java new file mode 100644 index 00000000..e002ff33 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaSessionService.java @@ -0,0 +1,225 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2015, Gluu + */ + +package org.gluu.oxauth.uma.service; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.common.User; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.model.uma.persistence.UmaPermission; +import org.gluu.oxauth.model.util.Util; +import org.gluu.oxauth.service.ClientService; +import org.gluu.oxauth.service.CookieService; +import org.gluu.oxauth.service.SessionIdService; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.List; + +/** + * @author yuriyz + * @version December 8, 2018 + */ +@ApplicationScoped +public class UmaSessionService { + + @Inject + private Logger log; + @Inject + private SessionIdService sessionIdService; + @Inject + private ClientService clientService; + @Inject + private CookieService cookieService; + + public SessionId getConnectSession(HttpServletRequest httpRequest) { + String cookieId = cookieService.getSessionIdFromCookie(httpRequest); + log.trace("Cookie - session_id: " + cookieId); + if (StringUtils.isNotBlank(cookieId)) { + return sessionIdService.getSessionId(cookieId); + } + return null; + } + + public SessionId getSession(HttpServletRequest httpRequest, HttpServletResponse httpResponse) { + String cookieId = cookieService.getUmaSessionIdFromCookie(httpRequest); + log.trace("Cookie - uma_session_id: " + cookieId); + + if (StringUtils.isNotBlank(cookieId)) { + SessionId sessionId = sessionIdService.getSessionId(cookieId); + if (sessionId != null) { + log.trace("Loaded uma_session_id from cookie, session: " + sessionId); + return sessionId; + } else { + log.error("Failed to load uma_session_id from cookie: " + cookieId); + } + } else { + log.error("uma_session_id cookie is not set."); + } + + log.trace("Generating new uma_session_id ..."); + SessionId session = sessionIdService.generateAuthenticatedSessionId(httpRequest, "", new HashMap() {{ + put("uma", "true"); + }}); + + cookieService.createSessionIdCookie(session, httpRequest, httpResponse, true); + log.trace("uma_session_id cookie created."); + return session; + } + + public boolean persist(SessionId session) { + try { + + if (sessionIdService.updateSessionId(session, true, true, true)) { + log.trace("Session persisted successfully. Session: " + session); + return true; + } + } catch (Exception e) { + log.error("Failed to persist session, id: " + session.getId(), e); + } + return false; + } + + public int getStep(SessionId session) { + String stepString = session.getSessionAttributes().get("step"); + int step = Util.parseIntSilently(stepString); + if (step == -1) { + step = 1; + setStep(step, session); + } + return step; + } + + public void setStep(int step, SessionId session) { + session.getSessionAttributes().put("step", Integer.toString(step)); + } + + public void configure(SessionId session, String scriptName, Boolean reset, List permissions, + String clientId, String claimRedirectUri, String state) { + setStep(1, session); + setState(session, state); + setClaimsRedirectUri(session, claimRedirectUri); + setTicket(session, permissions.get(0).getTicket()); + setScriptName(session, scriptName); + + String pct = permissions.get(0).getAttributes().get("pct"); + + if (StringUtils.isBlank(pct)) { + log.error("PCT code is null or blank in permission object."); + throw new RuntimeException("PCT code is null or blank in permission object."); + } + + setPct(session, pct); + setClientId(session, clientId); + persist(session); + } + + public boolean isStepPassed(SessionId session, Integer step) { + return Boolean.parseBoolean(session.getSessionAttributes().get(String.format("uma_step_passed_%d", step))); + } + + public boolean isPassedPreviousSteps(SessionId session, Integer step) { + for (int i = 1; i < step; i++) { + if (!isStepPassed(session, i)) { + return false; + } + } + return true; + } + + public void markStep(SessionId session, Integer step, boolean value) { + String key = String.format("uma_step_passed_%d", step); + if (value) { + session.getSessionAttributes().put(key, Boolean.TRUE.toString()); + } else { + session.getSessionAttributes().remove(key); + } + } + + public String getScriptName(SessionId session) { + return session.getSessionAttributes().get("gather_script_name"); + } + + public void setScriptName(SessionId session, String scriptName) { + session.getSessionAttributes().put("gather_script_name", scriptName); + } + + public String getPct(SessionId session) { + return session.getSessionAttributes().get("pct"); + } + + public void setPct(SessionId session, String pct) { + session.getSessionAttributes().put("pct", pct); + } + + public String getClientId(SessionId session) { + return session.getSessionAttributes().get("client_id"); + } + + public void setClientId(SessionId session, String clientId) { + session.getSessionAttributes().put("client_id", clientId); + } + + public String getClaimsRedirectUri(SessionId session) { + return session.getSessionAttributes().get("claims_redirect_uri"); + } + + public void setClaimsRedirectUri(SessionId session, String claimsRedirectUri) { + session.getSessionAttributes().put("claims_redirect_uri", claimsRedirectUri); + } + + public String getState(SessionId session) { + return session.getSessionAttributes().get("state"); + } + + public void setState(SessionId session, String state) { + session.getSessionAttributes().put("state", state); + } + + public String getTicket(SessionId session) { + return session.getSessionAttributes().get("ticket"); + } + + public void setTicket(SessionId session, String ticket) { + session.getSessionAttributes().put("ticket", ticket); + } + + public void resetToStep(SessionId session, int overridenNextStep, int step) { + for (int i = overridenNextStep; i <= step; i++) { + markStep(session, i, false); + } + + setStep(overridenNextStep, session); + } + + public User getUser(HttpServletRequest httpRequest, String... returnAttributes) { + return sessionIdService.getUser(getConnectSession(httpRequest)); + } + + public String getUserDn(HttpServletRequest httpRequest) { + SessionId connectSession = getConnectSession(httpRequest); + if (connectSession != null) { + return connectSession.getUserDn(); + } + + log.trace("No logged in user."); + return null; + } + + public Client getClient(SessionId session) { + String clientId = getClientId(session); + if (StringUtils.isNotBlank(clientId)) { + return clientService.getClient(clientId); + } + log.trace("client_id is not in session."); + return null; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaTokenService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaTokenService.java new file mode 100644 index 00000000..b473e6c6 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaTokenService.java @@ -0,0 +1,150 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.uma.service; + +import org.gluu.oxauth.model.common.ExecutionContext; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.uma.UmaErrorResponseType; +import org.gluu.oxauth.model.uma.UmaTokenResponse; +import org.gluu.oxauth.model.uma.persistence.UmaPermission; +import org.gluu.oxauth.security.Identity; +import org.gluu.oxauth.uma.authorization.*; +import org.gluu.oxauth.util.ServerUtil; +import org.oxauth.persistence.model.Scope; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import java.util.*; + +/** + * UMA Token Service + */ +@ApplicationScoped +public class UmaTokenService { + + @Inject + private Logger log; + @Inject + private Identity identity; + @Inject + private ErrorResponseFactory errorResponseFactory; + @Inject + private UmaRptService rptService; + @Inject + private UmaPctService pctService; + @Inject + private UmaPermissionService permissionService; + @Inject + private UmaValidationService umaValidationService; + @Inject + private AppConfiguration appConfiguration; + @Inject + private UmaNeedsInfoService umaNeedsInfoService; + @Inject + private UmaExpressionService expressionService; + + public Response requestRpt( + String grantType, + String ticket, + String claimToken, + String claimTokenFormat, + String pctCode, + String rptCode, + String scope, + HttpServletRequest httpRequest, + HttpServletResponse httpResponse) { + try { + log.trace("requestRpt grant_type: {}, ticket: {}, claim_token: {}, claim_token_format: {}, pct: {}, rpt: {}, scope: {}" + , grantType, ticket, claimToken, claimTokenFormat, pctCode, rptCode, scope); + + umaValidationService.validateGrantType(grantType); + List permissions = umaValidationService.validateTicket(ticket); + Jwt idToken = umaValidationService.validateClaimToken(claimToken, claimTokenFormat); + UmaPCT pct = umaValidationService.validatePct(pctCode); + UmaRPT rpt = umaValidationService.validateRPT(rptCode); + Client client = umaValidationService.validate(identity.getSessionClient().getClient()); + Map scopes = umaValidationService.validateScopes(scope, permissions, client); + pct = pctService.updateClaims(pct, idToken, client.getClientId(), permissions); // creates new pct if pct is null in request + Claims claims = new Claims(idToken, pct, claimToken); + + Map scriptMap = umaNeedsInfoService.checkNeedsInfo(claims, scopes, permissions, pct, httpRequest, client); + + if (!scriptMap.isEmpty()) { + expressionService.evaluate(scriptMap, permissions); + } else { + log.warn("There are no any policies that protects scopes. Scopes: " + UmaScopeService.asString(scopes.keySet()) + ". Configuration property umaGrantAccessIfNoPolicies: " + appConfiguration.getUmaGrantAccessIfNoPolicies()); + + if (appConfiguration.getUmaGrantAccessIfNoPolicies() != null && appConfiguration.getUmaGrantAccessIfNoPolicies()) { + log.warn("Access granted because there are no any protection. Make sure it is intentional behavior."); + } else { + log.warn("Access denied because there are no any protection. Make sure it is intentional behavior."); + throw errorResponseFactory.createWebApplicationException(Response.Status.FORBIDDEN, UmaErrorResponseType.FORBIDDEN_BY_POLICY, "Access denied because there are no any protection. Make sure it is intentional behavior."); + } + } + + log.trace("Access granted."); + + updatePermissionsWithClientRequestedScope(permissions, scopes); + addPctToPermissions(permissions, pct); + + boolean upgraded = false; + if (rpt == null) { + ExecutionContext executionContext = new ExecutionContext(httpRequest, httpResponse); + executionContext.setClient(client); + rpt = rptService.createRPTAndPersist(executionContext, permissions); + rptCode = rpt.getNotHashedCode(); + } else if (rptService.addPermissionToRPT(rpt, permissions)) { + upgraded = true; + } + + UmaTokenResponse response = new UmaTokenResponse(); + response.setAccessToken(rptCode); + response.setUpgraded(upgraded); + response.setTokenType("Bearer"); + response.setPct(pct.getCode()); + + return Response.ok(ServerUtil.asJson(response)).build(); + } catch (Exception ex) { + log.error("Exception happened", ex); + if (ex instanceof WebApplicationException) { + throw (WebApplicationException) ex; + } + } + + log.error("Failed to handle request to UMA Token Endpoint."); + throw errorResponseFactory.createWebApplicationException(Response.Status.INTERNAL_SERVER_ERROR, UmaErrorResponseType.SERVER_ERROR, "Failed to handle request to UMA Token Endpoint."); + } + + private void addPctToPermissions(List permissions, UmaPCT pct) { + for (UmaPermission p : permissions) { + p.getAttributes().put(UmaPermission.PCT, pct.getCode()); + permissionService.mergeSilently(p); + } + } + + private void updatePermissionsWithClientRequestedScope(List permissions, Map scopes) { + log.trace("Updating permissions with requested scopes ..."); + for (UmaPermission permission : permissions) { + Set scopeDns = new HashSet<>(permission.getScopeDns()); + + for (Map.Entry entry : scopes.entrySet()) { + log.trace("Updating permissions with scope: " + entry.getKey().getId() + ", isRequestedScope: " + entry.getValue() + ", permisson: " + permission.getDn()); + scopeDns.add(entry.getKey().getDn()); + } + + permission.setScopeDns(new ArrayList<>(scopeDns)); + } + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaValidationService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaValidationService.java new file mode 100644 index 00000000..fee3ccf6 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/service/UmaValidationService.java @@ -0,0 +1,493 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.uma.service; + +import com.google.common.base.Joiner; +import com.google.common.collect.Sets; +import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.common.AuthorizationGrant; +import org.gluu.oxauth.model.common.AuthorizationGrantList; +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.config.WebKeysConfiguration; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.crypto.signature.RSAPublicKey; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.jwk.JSONWebKey; +import org.gluu.oxauth.model.jws.RSASigner; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaimName; +import org.gluu.oxauth.model.jwt.JwtHeaderName; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.uma.ClaimTokenFormatType; +import org.gluu.oxauth.model.uma.UmaErrorResponseType; +import org.gluu.oxauth.model.uma.UmaPermissionList; +import org.gluu.oxauth.model.uma.UmaScopeType; +import org.gluu.oxauth.model.uma.persistence.UmaPermission; +import org.gluu.oxauth.model.uma.persistence.UmaResource; +import org.gluu.oxauth.service.ClientService; +import org.gluu.oxauth.service.RedirectionUriService; +import org.gluu.oxauth.service.token.TokenService; +import org.gluu.oxauth.uma.authorization.UmaPCT; +import org.gluu.oxauth.uma.authorization.UmaRPT; +import org.gluu.oxauth.uma.authorization.UmaWebException; +import org.gluu.oxauth.util.ServerUtil; +import org.gluu.persist.exception.EntryPersistenceException; +import org.gluu.util.StringHelper; +import org.oxauth.persistence.model.Scope; +import com.google.common.base.Function; +import com.google.common.collect.Iterables; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.core.Response; +import java.util.*; +import java.util.stream.Collectors; + +import static javax.ws.rs.core.Response.Status.BAD_REQUEST; +import static javax.ws.rs.core.Response.Status.UNAUTHORIZED; +import static org.gluu.oxauth.model.uma.UmaErrorResponseType.*; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 04/02/2013 + */ +@ApplicationScoped +public class UmaValidationService { + + @Inject + private Logger log; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private TokenService tokenService; + + @Inject + private AuthorizationGrantList authorizationGrantList; + + @Inject + private UmaResourceService resourceService; + + @Inject + private UmaScopeService umaScopeService; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private UmaPermissionService permissionService; + + @Inject + private UmaPctService pctService; + + @Inject + private UmaRptService rptService; + + @Inject + private WebKeysConfiguration webKeysConfiguration; + + @Inject + private ClientService clientService; + + @Inject + private UmaExpressionService expressionService; + + public AuthorizationGrant assertHasProtectionScope(String authorization) { + return validateAuthorization(authorization, UmaScopeType.PROTECTION); + } + + private AuthorizationGrant validateAuthorization(String authorization, UmaScopeType umaScopeType) { + log.trace("Validate authorization: {}", authorization); + if (StringHelper.isEmpty(authorization)) { + throw errorResponseFactory.createWebApplicationException(UNAUTHORIZED, UNAUTHORIZED_CLIENT, "Authorization header is blank."); + } + + String token = tokenService.getToken(authorization); + if (StringHelper.isEmpty(token)) { + log.debug("Token is invalid."); + throw errorResponseFactory.createWebApplicationException(UNAUTHORIZED, UNAUTHORIZED_CLIENT, "Token is invalid."); + } + + AuthorizationGrant authorizationGrant = authorizationGrantList.getAuthorizationGrantByAccessToken(token); + if (authorizationGrant == null) { + throw errorResponseFactory.createWebApplicationException(UNAUTHORIZED, ACCESS_DENIED, "Unable to find authorization grant by token."); + } + + Set scopes = authorizationGrant.getScopes(); + if (!scopes.contains(umaScopeType.getValue())) { + throw errorResponseFactory.createWebApplicationException(Response.Status.NOT_ACCEPTABLE, INVALID_CLIENT_SCOPE, "Client does not have scope: " + umaScopeType.getValue()); + } + return authorizationGrant; + } + + public UmaRPT validateRPT(String rptCode) { + if (StringUtils.isNotBlank(rptCode)) { + UmaRPT rpt = rptService.getRPTByCode(rptCode); + if (rpt != null) { + rpt.checkExpired(); + if (rpt.isValid()) { + return rpt; + } else { + log.error("RPT is not valid. Revoked: " + rpt.isRevoked() + ", Expired: " + rpt.isExpired() + ", rptCode: " + rptCode); + throw errorResponseFactory.createWebApplicationException(BAD_REQUEST, INVALID_RPT, "RPT is not valid. Revoked: " + rpt.isRevoked() + ", Expired: " + rpt.isExpired() + ", rptCode: " + rptCode); + } + } else { + log.error("RPT is null, rptCode: " + rptCode); + throw errorResponseFactory.createWebApplicationException(BAD_REQUEST, INVALID_RPT, "RPT is null, rptCode: " + rptCode); + } + } + return null; + } + + public void validatePermissions(List permissions) { + for (UmaPermission permission : permissions) { + validatePermission(permission); + } + } + + public void validatePermission(UmaPermission permission) { + if (permission == null || "invalidated".equalsIgnoreCase(permission.getStatus())) { + log.error("Permission is null or otherwise invalidated. Status: " + (permission != null ? permission.getStatus() : "No permissions.")); + throw errorResponseFactory.createWebApplicationException(BAD_REQUEST, INVALID_TICKET, "Permission is null or otherwise invalidated. Status: " + (permission != null ? permission.getStatus() : "No permissions.")); + } + + permission.checkExpired(); + if (!permission.isValid()) { + log.error("Permission is not valid."); + throw errorResponseFactory.createWebApplicationException(BAD_REQUEST, EXPIRED_TICKET, "Permission is not valid."); + } + } + + public void validatePermissions(UmaPermissionList permissions, Client client) { + for (org.gluu.oxauth.model.uma.UmaPermission permission : permissions) { + validatePermission(permission, client); + } + } + + public void validatePermission(org.gluu.oxauth.model.uma.UmaPermission permission, Client client) { + String resourceId = permission.getResourceId(); + if (StringHelper.isEmpty(resourceId)) { + log.error("Resource id is empty"); + throw errorResponseFactory.createWebApplicationException(BAD_REQUEST, INVALID_RESOURCE_ID, "Resource id is empty"); + } + + try { + UmaResource resource = resourceService.getResourceById(resourceId); + if (resource == null) { + log.error("Resource isn't registered or there are two resources with same Id"); + throw errorResponseFactory.createWebApplicationException(BAD_REQUEST, INVALID_RESOURCE_ID, "Resource is not registered."); + } + + for (String s : permission.getScopes()) { + if (resource.getScopes().contains(s)) { + continue; + } + + final Scope spontaneousScope = umaScopeService.getOrCreate(client, s, Sets.newHashSet(umaScopeService.getScopeIdsByDns(resource.getScopes()))); + if (spontaneousScope == null) { + log.error("Scope isn't registered and is not allowed by spontaneous scopes. Scope: " + s); + throw errorResponseFactory.createWebApplicationException(BAD_REQUEST, INVALID_SCOPE, "At least one of the scopes isn't registered"); + } + } + return; + } catch (EntryPersistenceException ex) { + log.error(ex.getMessage(), ex); + } + + log.error("Resource isn't registered"); + throw errorResponseFactory.createWebApplicationException(BAD_REQUEST, INVALID_RESOURCE_ID, "Resource isn't registered"); + } + + public void validateGrantType(String grantType) { + log.trace("Validate grantType: {}", grantType); + + if (!GrantType.OXAUTH_UMA_TICKET.getValue().equals(grantType)) { + throw errorResponseFactory.createWebApplicationException(BAD_REQUEST, INVALID_RESOURCE_ID, "No required grant_type: " + GrantType.OXAUTH_UMA_TICKET.getValue()); + } + } + + public List validateTicket(String ticket) { + if (StringUtils.isBlank(ticket)) { + log.error("Ticket is null or blank."); + throw errorResponseFactory.createWebApplicationException(BAD_REQUEST, INVALID_TICKET, "Ticket is null or blank."); + } + + List permissions = permissionService.getPermissionsByTicket(ticket); + if (permissions == null || permissions.isEmpty()) { + log.error("Unable to find permissions registered for given ticket:" + ticket); + throw errorResponseFactory.createWebApplicationException(BAD_REQUEST, INVALID_TICKET, "Unable to find permissions registered for given ticket:" + ticket); + } + return permissions; + } + + public List validateTicketWithRedirect(String ticket, String claimsRedirectUri, String state) { + if (StringUtils.isBlank(ticket)) { + log.error("Ticket is null or blank."); + throw new UmaWebException(claimsRedirectUri, errorResponseFactory, INVALID_TICKET, state); + } + + List permissions = permissionService.getPermissionsByTicket(ticket); + if (permissions == null || permissions.isEmpty()) { + log.error("Unable to find permissions registered for given ticket:" + ticket); + throw new UmaWebException(claimsRedirectUri, errorResponseFactory, INVALID_TICKET, state); + } + return permissions; + } + + public Jwt validateClaimToken(String claimToken, String claimTokenFormat) { + if (StringUtils.isNotBlank(claimToken)) { + if (!ClaimTokenFormatType.isValueValid(claimTokenFormat)) { + log.error("claim_token_format is unsupported. Supported format is http://openid.net/specs/openid-connect-core-1_0.html#IDToken"); + throw errorResponseFactory.createWebApplicationException(BAD_REQUEST, INVALID_CLAIM_TOKEN_FORMAT, "claim_token_format is unsupported. Supported format is http://openid.net/specs/openid-connect-core-1_0.html#IDToken"); + } + + try { + final Jwt idToken = Jwt.parse(claimToken); + if (idToken != null) { + if (ServerUtil.isTrue(appConfiguration.getUmaValidateClaimToken()) && !isIdTokenValid(idToken)) { + log.error("claim_token validation failed."); + throw errorResponseFactory.createWebApplicationException(BAD_REQUEST, INVALID_CLAIM_TOKEN, "claim_token validation failed."); + } + return idToken; + } else { + throw errorResponseFactory.createWebApplicationException(BAD_REQUEST, INVALID_CLAIM_TOKEN, "id_tokne is null."); + } + } catch (Exception e) { + log.error("Failed to parse claim_token as valid id_token.", e); + throw errorResponseFactory.createWebApplicationException(BAD_REQUEST, INVALID_CLAIM_TOKEN, "Failed to parse claim_token as valid id_token."); + } + } else if (StringUtils.isNotBlank(claimTokenFormat)) { + log.error("claim_token is blank but claim_token_format is not blank. Both must be blank or both must be not blank"); + throw errorResponseFactory.createWebApplicationException(BAD_REQUEST, INVALID_CLAIM_TOKEN, "claim_token is blank but claim_token_format is not blank. Both must be blank or both must be not blank"); + } + return null; + } + + public boolean isIdTokenValid(Jwt idToken) { + try { + final String issuer = idToken.getClaims().getClaimAsString(JwtClaimName.ISSUER); + //final String nonceFromToken = idToken.getClaims().getClaimAsString(JwtClaimName.NONCE); + //final String audienceFromToken = idToken.getClaims().getClaimAsString(JwtClaimName.AUDIENCE); + + final Date expiresAt = idToken.getClaims().getClaimAsDate(JwtClaimName.EXPIRATION_TIME); + final Date now = new Date(); + if (now.after(expiresAt)) { + log.error("ID Token is expired. (It is after " + now + ")."); + return false; + } + + // 1. validate issuer + if (!issuer.equals(appConfiguration.getIssuer())) { + log.error("ID Token issuer is invalid. Token issuer: " + issuer + ", server issuer: " + appConfiguration.getIssuer()); + return false; + } + + // 2. validate signature + final String kid = idToken.getHeader().getClaimAsString(JwtHeaderName.KEY_ID); + final String algorithm = idToken.getHeader().getClaimAsString(JwtHeaderName.ALGORITHM); + RSAPublicKey publicKey = getPublicKey(kid); + if (publicKey != null) { + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.fromString(algorithm), publicKey); + boolean signature = rsaSigner.validate(idToken); + if (signature) { + log.debug("ID Token is successfully validated."); + return true; + } + log.error("ID Token signature is invalid."); + } else { + log.error("Failed to get RSA public key."); + } + return false; + } catch (Exception e) { + log.error("Failed to validate id_token. Message: " + e.getMessage(), e); + return false; + } + } + + private RSAPublicKey getPublicKey(String kid) { + JSONWebKey key = webKeysConfiguration.getKey(kid); + if (key != null) { + switch (key.getKty()) { + case RSA: + return new RSAPublicKey( + key.getN(), + key.getE()); + } + } + return null; + } + + public UmaPCT validatePct(String pctCode) { + if (StringUtils.isNotBlank(pctCode)) { + UmaPCT pct = pctService.getByCode(pctCode); + + if (pct != null) { + pct.checkExpired(); + if (pct.isValid()) { + log.trace("PCT is validated successfully, pct: " + pctCode); + return pct; + } else { + log.error("PCT is not valid. Revoked: " + pct.isRevoked() + ", Expired: " + pct.isExpired() + ", pctCode: " + pctCode); + throw errorResponseFactory.createWebApplicationException(UNAUTHORIZED, INVALID_PCT, "PCT is not valid. Revoked: " + pct.isRevoked() + ", Expired: " + pct.isExpired() + ", pctCode: " + pctCode); + } + } else { + log.error("Failed to find PCT with pctCode: " + pctCode); + throw errorResponseFactory.createWebApplicationException(UNAUTHORIZED, INVALID_PCT, "Failed to find PCT with pctCode: " + pctCode); + } + } + return null; + } + + /** + * @param scope scope string from token request + * @param permissions permissions + * @return map of loaded scope and boolean, true - if client requested scope and false if it is permission ticket scope + */ + public Map validateScopes(String scope, List permissions, Client client) { + scope = ServerUtil.urlDecode(scope); + final String[] scopesRequested = StringUtils.isNotBlank(scope) ? scope.split(" ") : new String[0]; + + final Map result = new HashMap(); + + if (ArrayUtils.isNotEmpty(scopesRequested)) { + final Set resourceScopes = resourceService.getResourceScopes(permissions.stream().map(UmaPermission::getResourceId).collect(Collectors.toSet())); + for (String scopeId : scopesRequested) { + final Scope ldapScope = umaScopeService.getOrCreate(client, scopeId, resourceScopes); + if (ldapScope != null) { + result.put(ldapScope, true); + } else { + log.trace("Skip requested scope because it's not allowed, scope: " + scopeId); + } + } + } + for (UmaPermission permission : permissions) { + for (Scope s : umaScopeService.getScopesByDns(permission.getScopeDns())) { + result.put(s, false); + } + } + if (result.isEmpty()) { + log.error("There are no any scopes requested in the request."); + throw errorResponseFactory.createWebApplicationException(BAD_REQUEST, UmaErrorResponseType.INVALID_SCOPE, "There are no any scopes requested in give request."); + } + log.trace("CandidateGrantedScopes: " + Joiner.on(", ").join(Iterables.transform(result.keySet(), new Function() { + @Override + public String apply(Scope scope) { + return scope.getId(); + } + }))); + return result; + } + + public void validateScopeExpression(String scopeExpression) { + if (StringUtils.isNotBlank(scopeExpression) && !expressionService.isExpressionValid(scopeExpression)) { + log.error("Scope expression is invalid. Expression: " + scopeExpression); + throw errorResponseFactory.createWebApplicationException(BAD_REQUEST, UmaErrorResponseType.INVALID_SCOPE, "Scope expression is invalid. Expression: " + scopeExpression); + } + } + + public Client validateClientAndClaimsRedirectUri(String clientId, String claimsRedirectUri, String state) { + if (StringUtils.isBlank(clientId)) { + log.error("Invalid clientId: {}", clientId); + throw errorResponseFactory.createWebApplicationException(BAD_REQUEST, UmaErrorResponseType.INVALID_CLIENT_ID, "Invalid clientId: " + clientId); + } + Client client = clientService.getClient(clientId); + if (client == null) { + log.error("Failed to find client with client_id: {}", clientId); + throw errorResponseFactory.createWebApplicationException(BAD_REQUEST, UmaErrorResponseType.INVALID_CLIENT_ID, "Failed to find client with client_id:" + clientId); + } + + if (StringUtils.isNotBlank(claimsRedirectUri)) { + if (ArrayUtils.isEmpty(client.getClaimRedirectUris())) { + log.error("Client does not have claims_redirect_uri specified, clientId: " + clientId); + throw errorResponseFactory.createWebApplicationException(BAD_REQUEST, UmaErrorResponseType.INVALID_CLAIMS_REDIRECT_URI, "Client does not have claims_redirect_uri specified, clientId: " + clientId); + } + + String equalRedirectUri = getEqualRedirectUri(claimsRedirectUri, client.getClaimRedirectUris()); + if (equalRedirectUri != null) { + log.trace("Found match for claims_redirect_uri : " + equalRedirectUri); + return client; + } else { + log.trace("Failed to find match for claims_redirect_uri : " + claimsRedirectUri + ", client claimRedirectUris: " + Arrays.toString(client.getClaimRedirectUris())); + } + } else { + log.trace("claims_redirect_uri is blank"); + if (client.getClaimRedirectUris() != null && client.getClaimRedirectUris().length == 1) { + log.trace("claims_redirect_uri is blank and only one claims_redirect_uri is registered."); + return client; + } + } + + if (StringUtils.isBlank(claimsRedirectUri)) { + log.error("claims_redirect_uri is blank and there is none or more then one registered claims_redirect_uri for clientId: " + clientId); + throw errorResponseFactory.createWebApplicationException(BAD_REQUEST, UmaErrorResponseType.INVALID_CLAIMS_REDIRECT_URI, "claims_redirect_uri is blank and there is none or more then one registered claims_redirect_uri for clientId: " + clientId); + } + + throw new UmaWebException(claimsRedirectUri, errorResponseFactory, INVALID_CLAIMS_REDIRECT_URI, state); + } + + private String getEqualRedirectUri(String redirectUri, String[] clientRedirectUris) { + final String redirectUriWithoutParams = RedirectionUriService.uriWithoutParams(redirectUri); + + for (String uri : clientRedirectUris) { + log.debug("Comparing {} == {}", uri, redirectUri); + if (uri.equals(redirectUri)) { // compare complete uri + return redirectUri; + } + + String uriWithoutParams = RedirectionUriService.uriWithoutParams(uri); + final Map params = RedirectionUriService.getParams(uri); + + if ((uriWithoutParams.equals(redirectUriWithoutParams) && params.size() == 0 && RedirectionUriService.getParams(redirectUri).size() == 0) || + uriWithoutParams.equals(redirectUriWithoutParams) && params.size() > 0 && RedirectionUriService.compareParams(redirectUri, uri)) { + return redirectUri; + } + } + return null; + } + + public String[] validatesGatheringScriptNames(String scriptNamesAsString, String claimsRedirectUri, String state) { + if (StringUtils.isNotBlank(scriptNamesAsString)) { + final String[] scriptNames = scriptNamesAsString.split(" "); + if (ArrayUtils.isNotEmpty(scriptNames)) { + return scriptNames; + } + } + throw new UmaWebException(claimsRedirectUri, errorResponseFactory, INVALID_CLAIMS_GATHERING_SCRIPT_NAME, state); + } + + public void validateRestrictedByClient(String patClientDn, String rsId) { + if (ServerUtil.isTrue(appConfiguration.getUmaRestrictResourceToAssociatedClient())) { + final List clients = resourceService.getResourceById(rsId).getClients(); + if (!clients.contains(patClientDn)) { + log.error("Access to resource is denied because resource associated client does not match PAT client (it can be switched off if set umaRestrictResourceToAssociatedClient oxauth configuration property to false). Associated clients: " + clients + ", PAT client: " + patClientDn); + throw errorResponseFactory.createWebApplicationException(Response.Status.FORBIDDEN, ACCESS_DENIED, "Access to resource is denied because resource associated client does not match PAT client (it can be switched off if set umaRestrictResourceToAssociatedClient oxauth configuration property to false)."); + } + } + } + + public void validateResource(org.gluu.oxauth.model.uma.UmaResource resource) { + validateScopeExpression(resource.getScopeExpression()); + + List scopeDNs = umaScopeService.getScopeDNsByIdsAndAddToLdapIfNeeded(resource.getScopes()); + if (scopeDNs.isEmpty() && StringUtils.isBlank(resource.getScopeExpression()) ) { + log.error("Invalid resource. Both `scope` and `scope_expression` are blank."); + throw errorResponseFactory.createWebApplicationException(BAD_REQUEST, UmaErrorResponseType.INVALID_SCOPE, "Invalid resource. Both `scope` and `scope_expression` are blank."); + } + } + + public Client validate(Client client) { + if (client == null || client.isDisabled()) { + log.debug("Client is not found or otherwise disabled."); + throw errorResponseFactory.createWebApplicationException(Response.Status.FORBIDDEN, UmaErrorResponseType.DISABLED_CLIENT, "Client is disabled."); + } + return client; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/ws/rs/UmaGatheringWS.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/ws/rs/UmaGatheringWS.java new file mode 100644 index 00000000..9b83f2f8 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/ws/rs/UmaGatheringWS.java @@ -0,0 +1,182 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2015, Gluu + */ + +package org.gluu.oxauth.uma.ws.rs; + +import org.apache.commons.lang.StringUtils; +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.model.uma.UmaConstants; +import org.gluu.oxauth.model.uma.UmaErrorResponseType; +import org.gluu.oxauth.model.uma.persistence.UmaPermission; +import org.gluu.oxauth.service.common.UserService; +import org.gluu.oxauth.service.external.ExternalUmaClaimsGatheringService; +import org.gluu.oxauth.uma.authorization.UmaGatherContext; +import org.gluu.oxauth.uma.authorization.UmaWebException; +import org.gluu.oxauth.uma.service.UmaPctService; +import org.gluu.oxauth.uma.service.UmaPermissionService; +import org.gluu.oxauth.uma.service.UmaSessionService; +import org.gluu.oxauth.uma.service.UmaValidationService; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import java.net.URI; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +import static javax.ws.rs.core.Response.Status.BAD_REQUEST; +import static javax.ws.rs.core.Response.Status.FOUND; +import static org.gluu.oxauth.model.uma.UmaErrorResponseType.INVALID_CLAIMS_GATHERING_SCRIPT_NAME; +import static org.gluu.oxauth.model.uma.UmaErrorResponseType.INVALID_SESSION; + +/** + * Claims-Gathering Endpoint. + * + * @author yuriyz + * @version August 9, 2017 + */ +@Path("/uma/gather_claims") +public class UmaGatheringWS { + + @Inject + private Logger log; + @Inject + private ErrorResponseFactory errorResponseFactory; + @Inject + private UmaValidationService validationService; + @Inject + private ExternalUmaClaimsGatheringService external; + @Inject + private UmaSessionService sessionService; + @Inject + private UmaPermissionService permissionService; + @Inject + private UmaPctService pctService; + @Inject + private AppConfiguration appConfiguration; + @Inject + private UserService userService; + + public Response gatherClaims(String clientId, String ticket, String claimRedirectUri, String state, Boolean reset, + Boolean authenticationRedirect, HttpServletRequest httpRequest, HttpServletResponse httpResponse) { + try { + log.trace("gatherClaims client_id: {}, ticket: {}, claims_redirect_uri: {}, state: {}, authenticationRedirect: {}, queryString: {}", + clientId, ticket, claimRedirectUri, state, authenticationRedirect, httpRequest.getQueryString()); + + SessionId session = sessionService.getSession(httpRequest, httpResponse); + + if (authenticationRedirect != null && authenticationRedirect) { // restore parameters from session + log.debug("Authentication redirect, restoring parameters from session ..."); + if (session == null) { + log.error("Session is null however authentication=true. Wrong workflow! Please correct custom Glaims-Gathering Script."); + throw errorResponseFactory.createWebApplicationException(BAD_REQUEST, INVALID_SESSION, "Session is null however authentication=true. Wrong workflow! Please correct custom Glaims-Gathering Script."); + } + clientId = sessionService.getClientId(session); + ticket = sessionService.getTicket(session); + claimRedirectUri = sessionService.getClaimsRedirectUri(session); + state = sessionService.getState(session); + log.debug("Restored parameters from session, clientId: {}, ticket: {}, claims_redirect_uri: {}, state: {}", + clientId, ticket, claimRedirectUri, state); + } + + validationService.validateClientAndClaimsRedirectUri(clientId, claimRedirectUri, state); + List permissions = validationService.validateTicketWithRedirect(ticket, claimRedirectUri, state); + String[] scriptNames = validationService.validatesGatheringScriptNames(getScriptNames(permissions), claimRedirectUri, state); + + CustomScriptConfiguration script = external.determineScript(scriptNames); + if (script == null) { + log.error("Failed to determine claims-gathering script for names: " + Arrays.toString(scriptNames)); + throw new UmaWebException(claimRedirectUri, errorResponseFactory, INVALID_CLAIMS_GATHERING_SCRIPT_NAME, state); + } + + sessionService.configure(session, script.getName(), reset, permissions, clientId, claimRedirectUri, state); + + UmaGatherContext context = new UmaGatherContext(script.getConfigurationAttributes(), httpRequest, session, sessionService, permissionService, + pctService, new HashMap(), userService, null, appConfiguration); + + int step = sessionService.getStep(session); + int stepsCount = external.getStepsCount(script, context); + + if (step < stepsCount) { + String page = external.getPageForStep(script, step, context); + + context.persist(); + + String baseEndpoint = StringUtils.removeEnd(appConfiguration.getBaseEndpoint(), "/"); + baseEndpoint = StringUtils.removeEnd(baseEndpoint, "restv1"); + baseEndpoint = StringUtils.removeEnd(baseEndpoint, "/"); + + String fullUri = baseEndpoint + page; + fullUri = StringUtils.removeEnd(fullUri, ".xhtml") + ".htm"; + log.trace("Redirecting to page: '{}', fullUri: {}", page, fullUri); + return Response.status(FOUND).location(new URI(fullUri)).build(); + } else { + log.error("Step '{}' is more or equal to stepCount: '{}'", stepsCount); + } + } catch (Exception ex) { + log.error("Exception happened", ex); + if (ex instanceof WebApplicationException) { + throw (WebApplicationException) ex; + } + } + + log.error("Failed to handle call to UMA Claims Gathering Endpoint."); + throw errorResponseFactory.createWebApplicationException(Response.Status.INTERNAL_SERVER_ERROR, UmaErrorResponseType.SERVER_ERROR, "Failed to handle call to UMA Claims Gathering Endpoint."); + } + + private static String getScriptNames(List permissions) { + return permissions.get(0).getAttributes().get(UmaConstants.GATHERING_ID); + } + + @GET + @Produces({UmaConstants.JSON_MEDIA_TYPE}) + public Response getGatherClaims( + @QueryParam("client_id") + String clientId, + @QueryParam("ticket") + String ticket, + @QueryParam("claims_redirect_uri") + String claimRedirectUri, + @QueryParam("state") + String state, + @QueryParam("reset") + Boolean reset, + @QueryParam("authentication") + Boolean authenticationRedirect, + @Context HttpServletRequest httpRequest, + @Context HttpServletResponse httpResponse) { + return gatherClaims(clientId, ticket, claimRedirectUri, state, reset, authenticationRedirect, httpRequest, httpResponse); + } + + @POST + @Consumes({UmaConstants.JSON_MEDIA_TYPE}) + @Produces({UmaConstants.JSON_MEDIA_TYPE}) + public Response postGatherClaims( + @FormParam("client_id") + String clientId, + @FormParam("ticket") + String ticket, + @FormParam("claims_redirect_uri") + String claimRedirectUri, + @FormParam("state") + String state, + @FormParam("reset") + Boolean reset, + @FormParam("authentication") + Boolean authenticationRedirect, + @Context HttpServletRequest httpRequest, + @Context HttpServletResponse httpResponse) { + return gatherClaims(clientId, ticket, claimRedirectUri, state, reset, authenticationRedirect, httpRequest, httpResponse); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/ws/rs/UmaMetadataWS.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/ws/rs/UmaMetadataWS.java new file mode 100644 index 00000000..c175c97c --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/ws/rs/UmaMetadataWS.java @@ -0,0 +1,89 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.uma.ws.rs; + +import org.gluu.oxauth.model.common.GrantType; +import org.gluu.oxauth.model.common.ResponseType; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.uma.UmaConstants; +import org.gluu.oxauth.model.uma.UmaErrorResponseType; +import org.gluu.oxauth.model.uma.UmaMetadata; +import org.gluu.oxauth.util.ServerUtil; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +/** + * The endpoint at which the requester can obtain UMA2 metadata. + */ +@Path("/uma2-configuration") +public class UmaMetadataWS { + + public static final String UMA_SCOPES_SUFFIX = "/uma/scopes"; + public static final String UMA_CLAIMS_GATHERING_PATH = "/uma/gather_claims"; + + @Inject + private Logger log; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private AppConfiguration appConfiguration; + + @GET + @Produces({UmaConstants.JSON_MEDIA_TYPE}) + public Response getConfiguration() { + try { + final String baseEndpointUri = appConfiguration.getBaseEndpoint(); + + final UmaMetadata c = new UmaMetadata(); + c.setIssuer(appConfiguration.getIssuer()); + c.setGrantTypesSupported(new String[]{ + GrantType.AUTHORIZATION_CODE.getValue(), + GrantType.IMPLICIT.getValue(), + GrantType.CLIENT_CREDENTIALS.getValue(), + GrantType.OXAUTH_UMA_TICKET.getValue() + }); + c.setResponseTypesSupported(new String[]{ + ResponseType.CODE.getValue(), ResponseType.ID_TOKEN.getValue(), ResponseType.TOKEN.getValue() + }); + c.setTokenEndpointAuthMethodsSupported(appConfiguration.getTokenEndpointAuthMethodsSupported().toArray(new String[appConfiguration.getTokenEndpointAuthMethodsSupported().size()])); + c.setTokenEndpointAuthSigningAlgValuesSupported(appConfiguration.getTokenEndpointAuthSigningAlgValuesSupported().toArray(new String[appConfiguration.getTokenEndpointAuthSigningAlgValuesSupported().size()])); + c.setUiLocalesSupported(appConfiguration.getUiLocalesSupported().toArray(new String[appConfiguration.getUiLocalesSupported().size()])); + c.setOpTosUri(appConfiguration.getOpTosUri()); + c.setOpPolicyUri(appConfiguration.getOpPolicyUri()); + c.setJwksUri(appConfiguration.getJwksUri()); + c.setServiceDocumentation(appConfiguration.getServiceDocumentation()); + + c.setUmaProfilesSupported(new String[0]); + c.setRegistrationEndpoint(appConfiguration.getRegistrationEndpoint()); + c.setTokenEndpoint(appConfiguration.getTokenEndpoint()); + c.setAuthorizationEndpoint(appConfiguration.getAuthorizationEndpoint()); + c.setIntrospectionEndpoint(baseEndpointUri + "/rpt/status"); + c.setResourceRegistrationEndpoint(baseEndpointUri + "/host/rsrc/resource_set"); + c.setPermissionEndpoint(baseEndpointUri + "/host/rsrc_pr"); + c.setScopeEndpoint(baseEndpointUri + UMA_SCOPES_SUFFIX); + c.setClaimsInteractionEndpoint(baseEndpointUri + UMA_CLAIMS_GATHERING_PATH); + + // convert manually to avoid possible conflicts between resteasy providers, e.g. jettison, jackson + final String entity = ServerUtil.asPrettyJson(c); + log.trace("Uma metadata: {}", entity); + + return Response.ok(entity).build(); + } catch (Throwable ex) { + log.error(ex.getMessage(), ex); + throw errorResponseFactory.createWebApplicationException(Response.Status.INTERNAL_SERVER_ERROR, UmaErrorResponseType.SERVER_ERROR, "Internal error."); + } + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/ws/rs/UmaPermissionRegistrationWS.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/ws/rs/UmaPermissionRegistrationWS.java new file mode 100644 index 00000000..b8e4927b --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/ws/rs/UmaPermissionRegistrationWS.java @@ -0,0 +1,117 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.uma.ws.rs; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.gluu.oxauth.model.common.AuthorizationGrant; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.uma.PermissionTicket; +import org.gluu.oxauth.model.uma.UmaConstants; +import org.gluu.oxauth.model.uma.UmaErrorResponseType; +import org.gluu.oxauth.model.uma.UmaPermissionList; +import org.gluu.oxauth.service.token.TokenService; +import org.gluu.oxauth.uma.service.UmaPermissionService; +import org.gluu.oxauth.uma.service.UmaValidationService; +import org.gluu.oxauth.util.ServerUtil; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.io.IOException; + +/** + * The endpoint at which the host registers permissions that it anticipates a + * requester will shortly be asking for from the AM. This AM's endpoint is part + * of resource set registration API. + *

+ * In response to receiving an access request accompanied by an RPT that is + * invalid or has insufficient authorization data, the host SHOULD register a + * permission with the AM that would be sufficient for the type of access + * sought. The AM returns a permission ticket for the host to give to the + * requester in its response. + * + * @author Yuriy Zabrovarnyy + */ +@Path("/host/rsrc_pr") +public class UmaPermissionRegistrationWS { + + @Inject + private Logger log; + + @Inject + private TokenService tokenService; + + @Inject + private UmaPermissionService permissionService; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private UmaValidationService umaValidationService; + + @POST + @Consumes({UmaConstants.JSON_MEDIA_TYPE}) + @Produces({UmaConstants.JSON_MEDIA_TYPE}) + public Response registerPermission(@Context HttpServletRequest request, + @HeaderParam("Authorization") String authorization, + String requestAsString) { + try { + final AuthorizationGrant authorizationGrant = umaValidationService.assertHasProtectionScope(authorization); + + // UMA2 spec defined 2 possible requests, single permission or list of permission. So here we parse manually + UmaPermissionList permissionList = parseRequest(requestAsString); + umaValidationService.validatePermissions(permissionList, authorizationGrant.getClient()); + + String ticket = permissionService.addPermission(permissionList, tokenService.getClientDn(authorization)); + + return Response.status(Response.Status.CREATED). + type(MediaType.APPLICATION_JSON_TYPE). + entity(new PermissionTicket(ticket)). + build(); + } catch (Exception ex) { + if (ex instanceof WebApplicationException) { + throw (WebApplicationException) ex; + } + + log.error("Exception happened", ex); + throw errorResponseFactory.createWebApplicationException(Response.Status.INTERNAL_SERVER_ERROR, UmaErrorResponseType.SERVER_ERROR, "Internal error."); + } + } + + /** + * UMA2 spec (edit 4) defined to possible requests, single permission or list of permission. So here we parse manually + * + * @param requestAsString request as string + * @return uma permission list + */ + private UmaPermissionList parseRequest(String requestAsString) { + final ObjectMapper mapper = ServerUtil.createJsonMapper().configure(SerializationFeature.WRAP_ROOT_VALUE, false); + try { + org.gluu.oxauth.model.uma.UmaPermission permission = mapper.readValue(requestAsString, org.gluu.oxauth.model.uma.UmaPermission.class); + return new UmaPermissionList().addPermission(permission); + } catch (IOException e) { + // ignore + } + + try { + UmaPermissionList permissions = mapper.readValue(requestAsString, org.gluu.oxauth.model.uma.UmaPermissionList.class); + if (!permissions.isEmpty()) { + return permissions; + } + log.error("Permission list is empty."); + } catch (IOException e) { + log.error("Failed to parse uma permission request" + requestAsString, e); + } + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, UmaErrorResponseType.INVALID_PERMISSION_REQUEST, "Failed to parse uma permission request."); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/ws/rs/UmaResourceRegistrationWS.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/ws/rs/UmaResourceRegistrationWS.java new file mode 100644 index 00000000..ef4d45f2 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/ws/rs/UmaResourceRegistrationWS.java @@ -0,0 +1,345 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.uma.ws.rs; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.common.AuthorizationGrant; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.uma.*; +import org.gluu.oxauth.uma.service.UmaResourceService; +import org.gluu.oxauth.uma.service.UmaScopeService; +import org.gluu.oxauth.uma.service.UmaValidationService; +import org.gluu.oxauth.util.ServerUtil; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.ResponseBuilder; +import java.io.IOException; +import java.util.*; + +/** + * The API available at the resource registration endpoint enables the resource server to put resources under + * the protection of an authorization server on behalf of the resource owner and manage them over time. + * Protection of a resource at the authorization server begins on successful registration and ends on successful deregistration. + *

+ * The resource server uses a RESTful API at the authorization server's resource registration endpoint + * to create, read, update, and delete resource descriptions, along with retrieving lists of such descriptions. + * The descriptions consist of JSON documents that are maintained as web resources at the authorization server. + * (Note carefully the similar but distinct senses in which the word "resource" is used in this section.) + * + * @author Yuriy Zabrovarnyy + * @author Yuriy Movchan + * Date: 02/12/2015 + */ +@Path("/host/rsrc/resource_set") +public class UmaResourceRegistrationWS { + + private static final int NOT_ALLOWED_STATUS = 405; + + private static final int DEFAULT_RESOURCE_LIFETIME = 2592000; // 1 month + + @Inject + private Logger log; + + @Inject + private UmaValidationService umaValidationService; + + @Inject + private UmaResourceService resourceService; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private UmaScopeService umaScopeService; + + @Inject + private AppConfiguration appConfiguration; + + @POST + @Consumes({UmaConstants.JSON_MEDIA_TYPE}) + @Produces({UmaConstants.JSON_MEDIA_TYPE}) + public Response createResource( + @HeaderParam("Authorization") + String authorization, + UmaResource resource) { + try { + String id = UUID.randomUUID().toString(); + log.trace("Try to create resource, id: {}", id); + + return putResourceImpl(Response.Status.CREATED, authorization, id, resource); + } catch (Exception ex) { + log.error("Exception during resource creation", ex); + + if (ex instanceof WebApplicationException) { + throw (WebApplicationException) ex; + } + + throw errorResponseFactory.createWebApplicationException(Response.Status.INTERNAL_SERVER_ERROR, UmaErrorResponseType.SERVER_ERROR, ex.getMessage()); + } + } + + @PUT + @Path("{rsid}") + @Consumes({UmaConstants.JSON_MEDIA_TYPE}) + @Produces({UmaConstants.JSON_MEDIA_TYPE}) + public Response updateResource(@HeaderParam("Authorization") String authorization, + @PathParam("rsid") String rsid, + UmaResource resource) { + try { + return putResourceImpl(Response.Status.OK, authorization, rsid, resource); + } catch (Exception ex) { + log.error("Exception during resource update, rsId: " + rsid + ", message: " + ex.getMessage(), ex); + + if (ex instanceof WebApplicationException) { + throw (WebApplicationException) ex; + } + + throw errorResponseFactory.createWebApplicationException(Response.Status.INTERNAL_SERVER_ERROR, UmaErrorResponseType.SERVER_ERROR, ex.getMessage()); + } + } + + @GET + @Path("{rsid}") + @Produces({UmaConstants.JSON_MEDIA_TYPE}) + public Response getResource( + @HeaderParam("Authorization") + String authorization, + @PathParam("rsid") + String rsid) { + try { + final AuthorizationGrant authorizationGrant = umaValidationService.assertHasProtectionScope(authorization); + umaValidationService.validateRestrictedByClient(authorizationGrant.getClientDn(), rsid); + log.debug("Getting resource description: '{}'", rsid); + + final org.gluu.oxauth.model.uma.persistence.UmaResource ldapResource = resourceService.getResourceById(rsid); + + final UmaResourceWithId response = new UmaResourceWithId(); + + response.setId(ldapResource.getId()); + response.setName(ldapResource.getName()); + response.setDescription(ldapResource.getDescription()); + response.setIconUri(ldapResource.getIconUri()); + response.setScopes(umaScopeService.getScopeIdsByDns(ldapResource.getScopes())); + response.setScopeExpression(ldapResource.getScopeExpression()); + response.setType(ldapResource.getType()); + response.setIat(ServerUtil.dateToSeconds(ldapResource.getCreationDate())); + response.setExp(ServerUtil.dateToSeconds(ldapResource.getExpirationDate())); + + final ResponseBuilder builder = Response.ok(); + builder.entity(ServerUtil.asJson(response)); // convert manually to avoid possible conflicts between resteasy providers, e.g. jettison, jackson + + return builder.build(); + } catch (Exception ex) { + log.error("Exception happened", ex); + if (ex instanceof WebApplicationException) { + throw (WebApplicationException) ex; + } + + throw errorResponseFactory.createWebApplicationException(Response.Status.INTERNAL_SERVER_ERROR, UmaErrorResponseType.SERVER_ERROR, ex.getMessage()); + } + } + + /** + * Gets resource set lists. + * ATTENTION: "scope" is parameter added by gluu to have additional filtering. + * There is no such parameter in UMA specification. + * + * @param authorization authorization + * @param scope scope of resource set for additional filtering, can blank string. + * @return resource set ids. + */ + @GET + @Produces({UmaConstants.JSON_MEDIA_TYPE}) + public List getResourceList( + @HeaderParam("Authorization") + String authorization, + @QueryParam("scope") + String scope) { + try { + log.trace("Getting list of resource descriptions."); + + final AuthorizationGrant authorizationGrant = umaValidationService.assertHasProtectionScope(authorization); + final String clientDn = authorizationGrant.getClientDn(); + + final List ldapResources = resourceService + .getResourcesByAssociatedClient(clientDn); + + final List result = new ArrayList(ldapResources.size()); + for (org.gluu.oxauth.model.uma.persistence.UmaResource ldapResource : ldapResources) { + + // if scope parameter is not null then filter by it, otherwise just add to result + if (StringUtils.isNotBlank(scope)) { + final List scopeUrlsByDns = umaScopeService.getScopeIdsByDns(ldapResource.getScopes()); + if (scopeUrlsByDns != null && scopeUrlsByDns.contains(scope)) { + result.add(ldapResource.getId()); + } + } else { + result.add(ldapResource.getId()); + } + } + + return result; + + } catch (Exception ex) { + log.error("Exception happened on getResourceList()", ex); + if (ex instanceof WebApplicationException) { + throw (WebApplicationException) ex; + } else { + throw errorResponseFactory.createWebApplicationException(Response.Status.INTERNAL_SERVER_ERROR, UmaErrorResponseType.SERVER_ERROR, ex.getMessage()); + } + } + } + + @DELETE + @Path("{rsid}") + public Response deleteResource( + @HeaderParam("Authorization") + String authorization, + @PathParam("rsid") + String rsid) { + try { + log.debug("Deleting resource descriptions'"); + + final AuthorizationGrant authorizationGrant = umaValidationService.assertHasProtectionScope(authorization); + umaValidationService.validateRestrictedByClient(authorizationGrant.getClientDn(), rsid); + resourceService.remove(rsid); + + return Response.status(Response.Status.NO_CONTENT).build(); + } catch (Exception ex) { + log.error("Error on DELETE Resource - " + ex.getMessage(), ex); + + if (ex instanceof WebApplicationException) { + throw (WebApplicationException) ex; + } + + throw errorResponseFactory.createWebApplicationException(Response.Status.INTERNAL_SERVER_ERROR, UmaErrorResponseType.SERVER_ERROR, ex.getMessage()); + } + } + + private Response putResourceImpl(Response.Status status, String authorization, String rsid, UmaResource resource) throws IOException { + log.trace("putResourceImpl, rsid: {}, status:", rsid, status.name()); + + AuthorizationGrant authorizationGrant = umaValidationService.assertHasProtectionScope(authorization); + umaValidationService.validateResource(resource); + + String userDn = authorizationGrant.getUserDn(); + String clientDn = authorizationGrant.getClientDn(); + + org.gluu.oxauth.model.uma.persistence.UmaResource ldapUpdatedResource; + + if (status == Response.Status.CREATED) { + ldapUpdatedResource = addResource(rsid, resource, userDn, clientDn); + } else { + umaValidationService.validateRestrictedByClient(clientDn, rsid); + ldapUpdatedResource = updateResource(rsid, resource); + } + + UmaResourceResponse response = new UmaResourceResponse(); + response.setId(ldapUpdatedResource.getId()); + + return Response.status(status). + type(MediaType.APPLICATION_JSON_TYPE). + entity(ServerUtil.asJson(response)). + build(); + } + + private org.gluu.oxauth.model.uma.persistence.UmaResource addResource(String rsid, UmaResource resource, String userDn, String clientDn) { + log.debug("Adding new resource: '{}'", rsid); + + final String resourceDn = resourceService.getDnForResource(rsid); + final List scopeDNs = umaScopeService.getScopeDNsByIdsAndAddToLdapIfNeeded(resource.getScopes()); + + final Calendar calendar = Calendar.getInstance(); + Date iat = calendar.getTime(); + Date exp = getExpirationDate(calendar); + + if (resource.getIat() != null && resource.getIat() > 0) { + iat = new Date(resource.getIat() * 1000L); + } + if (resource.getExp() != null && resource.getExp() > 0) { + exp = new Date(resource.getExp() * 1000L); + } + + final org.gluu.oxauth.model.uma.persistence.UmaResource ldapResource = new org.gluu.oxauth.model.uma.persistence.UmaResource(); + + ldapResource.setName(resource.getName()); + ldapResource.setDescription(resource.getDescription()); + ldapResource.setIconUri(resource.getIconUri()); + ldapResource.setId(rsid); + ldapResource.setRev(1); + ldapResource.setCreator(userDn); + ldapResource.setDn(resourceDn); + ldapResource.setScopes(scopeDNs); + ldapResource.setScopeExpression(resource.getScopeExpression()); + ldapResource.setClients(new ArrayList(Collections.singletonList(clientDn))); + ldapResource.setType(resource.getType()); + ldapResource.setCreationDate(iat); + ldapResource.setExpirationDate(exp); + ldapResource.setTtl(appConfiguration.getUmaResourceLifetime()); + + resourceService.addResource(ldapResource); + + return ldapResource; + } + + private Date getExpirationDate(Calendar creationCalender) { + int lifetime = appConfiguration.getUmaResourceLifetime(); + if (lifetime <= 0) { + lifetime = DEFAULT_RESOURCE_LIFETIME; + } + creationCalender.add(Calendar.SECOND, lifetime); + return creationCalender.getTime(); + } + + private org.gluu.oxauth.model.uma.persistence.UmaResource updateResource(String rsid, UmaResource resource) { + log.debug("Updating resource description: '{}'.", rsid); + + org.gluu.oxauth.model.uma.persistence.UmaResource ldapResource = resourceService.getResourceById(rsid); + if (ldapResource == null) { + return throwNotFoundException(rsid); + } + + ldapResource.setName(resource.getName()); + ldapResource.setDescription(resource.getDescription()); + ldapResource.setIconUri(resource.getIconUri()); + ldapResource.setScopes(umaScopeService.getScopeDNsByIdsAndAddToLdapIfNeeded(resource.getScopes())); + ldapResource.setScopeExpression(resource.getScopeExpression()); + ldapResource.setRev(ldapResource.getRev() + 1); + ldapResource.setType(resource.getType()); + if (resource.getExp() != null && resource.getExp() > 0) { + ldapResource.setExpirationDate(new Date(resource.getExp() * 1000L)); + ldapResource.setTtl(appConfiguration.getUmaResourceLifetime()); + } + + resourceService.updateResource(ldapResource); + + return ldapResource; + } + + private T throwNotFoundException(String rsid) { + log.error("Specified resource description doesn't exist, id: " + rsid); + throw errorResponseFactory.createWebApplicationException(Response.Status.NOT_FOUND, UmaErrorResponseType.NOT_FOUND, "Resource does not exists."); + } + + @HEAD + public Response unsupportedHeadMethod() { + log.error("HEAD method is not allowed"); + throw new WebApplicationException(Response.status(NOT_ALLOWED_STATUS).entity("HEAD Method Not Allowed").build()); + } + + @OPTIONS + public Response unsupportedOptionsMethod() { + log.error("OPTIONS method is not allowed"); + throw new WebApplicationException(Response.status(NOT_ALLOWED_STATUS).entity("OPTIONS Method Not Allowed").build()); + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/ws/rs/UmaRptIntrospectionWS.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/ws/rs/UmaRptIntrospectionWS.java new file mode 100644 index 00000000..37eb8c31 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/ws/rs/UmaRptIntrospectionWS.java @@ -0,0 +1,196 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.uma.ws.rs; + +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.uma.RptIntrospectionResponse; +import org.gluu.oxauth.model.uma.UmaConstants; +import org.gluu.oxauth.model.uma.UmaErrorResponseType; +import org.gluu.oxauth.model.uma.persistence.UmaPermission; +import org.gluu.oxauth.service.ClientService; +import org.gluu.oxauth.service.external.ExternalUmaRptClaimsService; +import org.gluu.oxauth.service.external.context.ExternalUmaRptClaimsContext; +import org.gluu.oxauth.uma.authorization.UmaPCT; +import org.gluu.oxauth.uma.authorization.UmaRPT; +import org.gluu.oxauth.uma.service.UmaPctService; +import org.gluu.oxauth.uma.service.UmaRptService; +import org.gluu.oxauth.uma.service.UmaScopeService; +import org.gluu.oxauth.uma.service.UmaValidationService; +import org.gluu.oxauth.util.ServerUtil; +import org.gluu.util.StringHelper; +import org.json.JSONObject; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.ArrayList; +import java.util.List; + +/** + * The endpoint at which the host requests the status of an RPT presented to it by a requester. + * The endpoint is RPT introspection profile implementation defined by + * http://docs.kantarainitiative.org/uma/draft-uma-core.html#uma-bearer-token-profile + * + * @author Yuriy Zabrovarnyy + */ +@Path("/rpt/status") +public class UmaRptIntrospectionWS { + + @Inject + private Logger log; + @Inject + private ErrorResponseFactory errorResponseFactory; + @Inject + private UmaRptService rptService; + @Inject + private UmaValidationService umaValidationService; + @Inject + private UmaScopeService umaScopeService; + @Inject + private UmaPctService pctService; + @Inject + private ExternalUmaRptClaimsService externalUmaRptClaimsService; + @Inject + private ClientService clientService; + + @GET + @Produces(MediaType.APPLICATION_JSON) + public Response introspectGet(@HeaderParam("Authorization") String authorization, + @QueryParam("token") String token, + @QueryParam("token_type_hint") String tokenTypeHint, + @Context HttpServletRequest httpRequest, + @Context HttpServletResponse httpResponse) { + return introspect(authorization, token, tokenTypeHint, httpRequest, httpResponse); + } + + @POST + @Produces(MediaType.APPLICATION_JSON) + public Response introspectPost(@HeaderParam("Authorization") String authorization, + @FormParam("token") String token, + @FormParam("token_type_hint") String tokenTypeHint, + @Context HttpServletRequest httpRequest, + @Context HttpServletResponse httpResponse) { + return introspect(authorization, token, tokenTypeHint, httpRequest, httpResponse); + } + + private Response introspect(String authorization, String token, String tokenTypeHint, HttpServletRequest httpRequest, HttpServletResponse httpResponse) { + try { + umaValidationService.assertHasProtectionScope(authorization); + + final UmaRPT rpt = rptService.getRPTByCode(token); + + if (!isValid(rpt)) { + return Response.status(Response.Status.OK). + entity(new RptIntrospectionResponse(false)). + cacheControl(ServerUtil.cacheControl(true)). + build(); + } + + final List permissions = buildStatusResponsePermissions(rpt); + + // active status + final RptIntrospectionResponse statusResponse = new RptIntrospectionResponse(); + statusResponse.setActive(true); + statusResponse.setExpiresAt(ServerUtil.dateToSeconds(rpt.getExpirationDate())); + statusResponse.setIssuedAt(ServerUtil.dateToSeconds(rpt.getCreationDate())); + statusResponse.setPermissions(permissions); + statusResponse.setClientId(rpt.getClientId()); + statusResponse.setAud(rpt.getClientId()); + statusResponse.setSub(rpt.getUserId()); + + final List rptPermissions = rptService.getRptPermissions(rpt); + if (!rptPermissions.isEmpty()) { + UmaPermission permission = rptPermissions.iterator().next(); + String pctCode = permission.getAttributes().get(UmaPermission.PCT); + if (StringHelper.isNotEmpty(pctCode)) { + UmaPCT pct = pctService.getByCode(pctCode); + if (pct != null) { + statusResponse.setPctClaims(pct.getClaims().toMap()); + } else { + log.error("Failed to find PCT with code: " + pctCode + " which is taken from permission object: " + permission.getDn()); + } + } else { + log.trace("PCT code is blank for RPT: " + rpt.getCode()); + } + } + + JSONObject rptAsJson = new JSONObject(ServerUtil.asJson(statusResponse)); + + ExternalUmaRptClaimsContext context = new ExternalUmaRptClaimsContext(clientService.getClient(rpt.getClientId()), httpRequest, httpResponse); + if (externalUmaRptClaimsService.externalModify(rptAsJson, context)) { + log.trace("Successfully run external RPT Claims script associated with {}", rpt.getClientId()); + } else { + rptAsJson = new JSONObject(ServerUtil.asJson(statusResponse)); + log.trace("Canceled changes made by external RPT Claims script since method returned `false`."); + } + + return Response.status(Response.Status.OK) + .entity(rptAsJson.toString()) + .type(MediaType.APPLICATION_JSON_TYPE) + .cacheControl(ServerUtil.cacheControl(true)) + .build(); + } catch (Exception ex) { + log.error("Exception happened", ex); + if (ex instanceof WebApplicationException) { + throw (WebApplicationException) ex; + } + + throw errorResponseFactory.createWebApplicationException(Response.Status.INTERNAL_SERVER_ERROR, UmaErrorResponseType.SERVER_ERROR, "Internal error."); + } + } + + private boolean isValid(UmaRPT p_rpt) { + if (p_rpt != null) { + p_rpt.checkExpired(); + return p_rpt.isValid(); + } + return false; + } + + private boolean isValid(UmaPermission permission) { + if (permission != null) { + permission.checkExpired(); + return permission.isValid(); + } + return false; + } + + private List buildStatusResponsePermissions(UmaRPT rpt) { + final List result = new ArrayList(); + if (rpt != null) { + final List rptPermissions = rptService.getRptPermissions(rpt); + if (rptPermissions != null && !rptPermissions.isEmpty()) { + for (UmaPermission permission : rptPermissions) { + if (isValid(permission)) { + final org.gluu.oxauth.model.uma.UmaPermission toAdd = ServerUtil.convert(permission, umaScopeService); + if (toAdd != null) { + result.add(toAdd); + } + } else { + log.debug("Ignore permission, skip it in response because permission is not valid. Permission dn: {}, rpt dn: {}", + permission.getDn(), rpt.getDn()); + } + } + } + } + return result; + } + + @GET + @Consumes({UmaConstants.JSON_MEDIA_TYPE}) + @Produces({UmaConstants.JSON_MEDIA_TYPE}) + public Response requestRptStatusGet(@HeaderParam("Authorization") String authorization, + @FormParam("token") String rpt, + @FormParam("token_type_hint") String tokenTypeHint) { + throw new WebApplicationException(Response.status(405).type(MediaType.APPLICATION_JSON_TYPE).entity("Introspection of RPT is not allowed by GET HTTP method.").build()); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/ws/rs/UmaScopeIconWS.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/ws/rs/UmaScopeIconWS.java new file mode 100644 index 00000000..c95f29c2 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/ws/rs/UmaScopeIconWS.java @@ -0,0 +1,60 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.uma.ws.rs; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.uma.UmaConstants; +import org.gluu.oxauth.model.uma.UmaErrorResponseType; +import org.gluu.oxauth.uma.service.UmaScopeService; +import org.oxauth.persistence.model.Scope; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; +import java.net.URI; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 02/05/2013 + */ + +@Path("/uma/scopes/icons") +public class UmaScopeIconWS { + + @Inject + private Logger log; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private UmaScopeService umaScopeService; + + @GET + @Path("{id}") + @Produces({UmaConstants.JSON_MEDIA_TYPE}) + public Response getScopeDescription(@PathParam("id") String id) { + log.trace("UMA - get scope's icon : id: {}", id); + try { + if (StringUtils.isNotBlank(id)) { + final Scope scope = umaScopeService.getScope(id); + if (scope != null && StringUtils.isNotBlank(scope.getIconUrl())) { + return Response.temporaryRedirect(new URI(scope.getIconUrl())).build(); + } + } + } catch (Exception e) { + log.error(e.getMessage(), e); + throw errorResponseFactory.createWebApplicationException(Response.Status.INTERNAL_SERVER_ERROR, UmaErrorResponseType.SERVER_ERROR, "Internal error."); + } + throw errorResponseFactory.createWebApplicationException(Response.Status.NOT_FOUND, UmaErrorResponseType.NOT_FOUND, "Scope not found."); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/ws/rs/UmaScopeWS.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/ws/rs/UmaScopeWS.java new file mode 100644 index 00000000..b78f633d --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/uma/ws/rs/UmaScopeWS.java @@ -0,0 +1,64 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.uma.ws.rs; + +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.uma.UmaConstants; +import org.gluu.oxauth.model.uma.UmaErrorResponseType; +import org.gluu.oxauth.model.uma.UmaScopeDescription; +import org.gluu.oxauth.uma.service.UmaScopeService; +import org.gluu.oxauth.util.ServerUtil; +import org.oxauth.persistence.model.Scope; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +/** + * @author Yuriy Zabrovarnyy + * @version 0.9, 22/04/2013 + */ +@Path("/uma/scopes") +public class UmaScopeWS { + + @Inject + private Logger log; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private UmaScopeService umaScopeService; + + @GET + @Path("{id}") + @Produces({UmaConstants.JSON_MEDIA_TYPE}) + public Response getScopeDescription(@PathParam("id") String id) { + log.trace("UMA - get scope description: id: {}", id); + try { + if (StringUtils.isNotBlank(id)) { + final Scope scope = umaScopeService.getScope(id); + if (scope != null) { + final UmaScopeDescription jsonScope = new UmaScopeDescription(); + jsonScope.setIconUri(scope.getIconUrl()); + jsonScope.setName(scope.getId()); + jsonScope.setDescription(scope.getDescription()); + return Response.status(Response.Status.OK).entity(ServerUtil.asJson(jsonScope)).build(); + } + } + } catch (Exception e) { + log.error(e.getMessage(), e); + throw errorResponseFactory.createWebApplicationException(Response.Status.INTERNAL_SERVER_ERROR, UmaErrorResponseType.SERVER_ERROR, "Internal error."); + } + throw errorResponseFactory.createWebApplicationException(Response.Status.NOT_FOUND, UmaErrorResponseType.NOT_FOUND, "Not found."); + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/userinfo/ws/rs/UserInfoRestWebService.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/userinfo/ws/rs/UserInfoRestWebService.java new file mode 100644 index 00000000..abf50fe1 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/userinfo/ws/rs/UserInfoRestWebService.java @@ -0,0 +1,41 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.userinfo.ws.rs; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; + +/** + * Provides interface for User Info REST web services + * + * @author Javier Rojas Blum + * @version September 7, 2017 + */ +public interface UserInfoRestWebService { + + @GET + @Path("/userinfo") + @Produces({MediaType.APPLICATION_JSON}) + Response requestUserInfoGet( + @QueryParam("access_token")String accessToken, + @HeaderParam("Authorization") String authorization, + @Context HttpServletRequest request, + @Context SecurityContext securityContext); + + @POST + @Path("/userinfo") + @Produces({MediaType.APPLICATION_JSON}) + Response requestUserInfoPost( + @FormParam("access_token") String accessToken, + @HeaderParam("Authorization") String authorization, + @Context HttpServletRequest request, + @Context SecurityContext securityContext); +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/userinfo/ws/rs/UserInfoRestWebServiceImpl.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/userinfo/ws/rs/UserInfoRestWebServiceImpl.java new file mode 100644 index 00000000..20e7fd0f --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/userinfo/ws/rs/UserInfoRestWebServiceImpl.java @@ -0,0 +1,435 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.userinfo.ws.rs; + +import org.gluu.model.GluuAttribute; +import org.gluu.oxauth.audit.ApplicationAuditLogger; +import org.gluu.oxauth.claims.Audience; +import org.gluu.oxauth.model.audit.Action; +import org.gluu.oxauth.model.audit.OAuth2AuditLog; +import org.gluu.oxauth.model.authorize.Claim; +import org.gluu.oxauth.model.common.*; +import org.gluu.oxauth.model.config.WebKeysConfiguration; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.crypto.AbstractCryptoProvider; +import org.gluu.oxauth.model.crypto.encryption.BlockEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.encryption.KeyEncryptionAlgorithm; +import org.gluu.oxauth.model.crypto.signature.SignatureAlgorithm; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.exception.InvalidJweException; +import org.gluu.oxauth.model.jwe.Jwe; +import org.gluu.oxauth.model.jwe.JweEncrypter; +import org.gluu.oxauth.model.jwe.JweEncrypterImpl; +import org.gluu.oxauth.model.jwk.Algorithm; +import org.gluu.oxauth.model.jwk.JSONWebKeySet; +import org.gluu.oxauth.model.jwk.Use; +import org.gluu.oxauth.model.jwt.Jwt; +import org.gluu.oxauth.model.jwt.JwtClaims; +import org.gluu.oxauth.model.jwt.JwtSubClaimObject; +import org.gluu.oxauth.model.jwt.JwtType; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.token.JsonWebResponse; +import org.gluu.oxauth.model.userinfo.UserInfoErrorResponseType; +import org.gluu.oxauth.model.userinfo.UserInfoParamsValidator; +import org.gluu.oxauth.model.util.Util; +import org.gluu.oxauth.service.*; +import org.gluu.oxauth.service.date.DateFormatterService; +import org.gluu.oxauth.service.external.ExternalDynamicScopeService; +import org.gluu.oxauth.service.external.context.DynamicScopeExternalContext; +import org.gluu.oxauth.service.token.TokenService; +import org.gluu.oxauth.util.ServerUtil; +import org.gluu.persist.exception.EntryPersistenceException; +import org.json.JSONObject; +import org.oxauth.persistence.model.Scope; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.Path; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; +import java.io.Serializable; +import java.security.PublicKey; +import java.util.*; + +/** + * Provides interface for User Info REST web services + * + * @author Javier Rojas Blum + * @version October 14, 2019 + */ +@Path("/") +public class UserInfoRestWebServiceImpl implements UserInfoRestWebService { + + @Inject + private Logger log; + + @Inject + private ApplicationAuditLogger applicationAuditLogger; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private AuthorizationGrantList authorizationGrantList; + + @Inject + private ClientService clientService; + + @Inject + private ScopeService scopeService; + + @Inject + private AttributeService attributeService; + + @Inject + private UserService userService; + + @Inject + private ExternalDynamicScopeService externalDynamicScopeService; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private WebKeysConfiguration webKeysConfiguration; + + @Inject + private AbstractCryptoProvider cryptoProvider; + + @Inject + private TokenService tokenService; + + @Inject + private DateFormatterService dateFormatterService; + + @Override + public Response requestUserInfoGet(String accessToken, String authorization, HttpServletRequest request, SecurityContext securityContext) { + return requestUserInfo(accessToken, authorization, request, securityContext); + } + + @Override + public Response requestUserInfoPost(String accessToken, String authorization, HttpServletRequest request, SecurityContext securityContext) { + return requestUserInfo(accessToken, authorization, request, securityContext); + } + + private Response requestUserInfo(String accessToken, String authorization, HttpServletRequest request, SecurityContext securityContext) { + + if (tokenService.isBearerAuthToken(authorization)) { + accessToken = tokenService.getBearerToken(authorization); + } + + log.debug("Attempting to request User Info, Access token = {}, Is Secure = {}", accessToken, securityContext.isSecure()); + Response.ResponseBuilder builder = Response.ok(); + + OAuth2AuditLog oAuth2AuditLog = new OAuth2AuditLog(ServerUtil.getIpAddress(request), Action.USER_INFO); + + try { + if (!UserInfoParamsValidator.validateParams(accessToken)) { + return response(400, UserInfoErrorResponseType.INVALID_REQUEST, "access token is not valid."); + } + + AuthorizationGrant authorizationGrant = authorizationGrantList.getAuthorizationGrantByAccessToken(accessToken); + + if (authorizationGrant == null) { + log.trace("Failed to find authorization grant by access_token: " + accessToken); + return response(401, UserInfoErrorResponseType.INVALID_TOKEN); + } + oAuth2AuditLog.updateOAuth2AuditLog(authorizationGrant, false); + + final AbstractToken accessTokenObject = authorizationGrant.getAccessToken(accessToken); + if (accessTokenObject == null || !accessTokenObject.isValid()) { + log.trace("Invalid access token object, access_token: {}, isNull: {}, isValid: {}", accessToken, accessTokenObject == null, false); + return response(401, UserInfoErrorResponseType.INVALID_TOKEN); + } + + if (authorizationGrant.getAuthorizationGrantType() == AuthorizationGrantType.CLIENT_CREDENTIALS) { + return response(403, UserInfoErrorResponseType.INSUFFICIENT_SCOPE, "Grant object has client_credentials grant_type which is not valid."); + } + if (appConfiguration.getOpenidScopeBackwardCompatibility() + && !authorizationGrant.getScopes().contains(DefaultScope.OPEN_ID.toString()) + && !authorizationGrant.getScopes().contains(DefaultScope.PROFILE.toString())) { + return response(403, UserInfoErrorResponseType.INSUFFICIENT_SCOPE, "Both openid and profile scopes are not present."); + } + if (!appConfiguration.getOpenidScopeBackwardCompatibility() && !authorizationGrant.getScopes().contains(DefaultScope.OPEN_ID.toString())) { + return response(403, UserInfoErrorResponseType.INSUFFICIENT_SCOPE, "Missed openid scope."); + } + + oAuth2AuditLog.updateOAuth2AuditLog(authorizationGrant, true); + + builder.cacheControl(ServerUtil.cacheControlWithNoStoreTransformAndPrivate()); + builder.header("Pragma", "no-cache"); + + User currentUser = authorizationGrant.getUser(); + try { + currentUser = userService.getUserByDn(authorizationGrant.getUserDn()); + } catch (EntryPersistenceException ex) { + log.warn("Failed to reload user entry: '{}'", authorizationGrant.getUserDn()); + } + + if (authorizationGrant.getClient() != null + && authorizationGrant.getClient().getUserInfoEncryptedResponseAlg() != null + && authorizationGrant.getClient().getUserInfoEncryptedResponseEnc() != null) { + KeyEncryptionAlgorithm keyEncryptionAlgorithm = KeyEncryptionAlgorithm.fromName(authorizationGrant.getClient().getUserInfoEncryptedResponseAlg()); + BlockEncryptionAlgorithm blockEncryptionAlgorithm = BlockEncryptionAlgorithm.fromName(authorizationGrant.getClient().getUserInfoEncryptedResponseEnc()); + builder.type("application/jwt"); + builder.entity(getJweResponse( + keyEncryptionAlgorithm, + blockEncryptionAlgorithm, + currentUser, + authorizationGrant, + authorizationGrant.getScopes())); + } else if (authorizationGrant.getClient() != null + && authorizationGrant.getClient().getUserInfoSignedResponseAlg() != null) { + SignatureAlgorithm algorithm = SignatureAlgorithm.fromString(authorizationGrant.getClient().getUserInfoSignedResponseAlg()); + builder.type("application/jwt"); + builder.entity(getJwtResponse(algorithm, + currentUser, + authorizationGrant, + authorizationGrant.getScopes())); + } else { + builder.type((MediaType.APPLICATION_JSON + ";charset=UTF-8")); + builder.entity(getJSonResponse(currentUser, + authorizationGrant, + authorizationGrant.getScopes())); + } + return builder.build(); + } catch (Exception e) { + log.error(e.getMessage(), e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()).build(); // 500 + } finally { + applicationAuditLogger.sendMessage(oAuth2AuditLog); + } + } + + private Response response(int status, UserInfoErrorResponseType errorResponseType) { + return response(status, errorResponseType, ""); + } + + private Response response(int status, UserInfoErrorResponseType errorResponseType, String reason) { + return Response + .status(status) + .entity(errorResponseFactory.errorAsJson(errorResponseType, reason)) + .type(MediaType.APPLICATION_JSON_TYPE) + .cacheControl(ServerUtil.cacheControlWithNoStoreTransformAndPrivate()) + .build(); + } + + private String getJwtResponse(SignatureAlgorithm signatureAlgorithm, User user, AuthorizationGrant authorizationGrant, + Collection scopes) throws Exception { + log.trace("Building JWT reponse with next scopes {0} for user {1} and user custom attributes {0}", scopes, user.getUserId(), user.getCustomAttributes()); + + Jwt jwt = new Jwt(); + + // Header + jwt.getHeader().setType(JwtType.JWT); + jwt.getHeader().setAlgorithm(signatureAlgorithm); + + String keyId = new ServerCryptoProvider(cryptoProvider).getKeyId(webKeysConfiguration, Algorithm.fromString(signatureAlgorithm.getName()), Use.SIGNATURE); + if (keyId != null) { + jwt.getHeader().setKeyId(keyId); + } + + // Claims + jwt.setClaims(createJwtClaims(user, authorizationGrant, scopes)); + + // Signature + String sharedSecret = clientService.decryptSecret(authorizationGrant.getClient().getClientSecret()); + String signature = cryptoProvider.sign(jwt.getSigningInput(), jwt.getHeader().getKeyId(), sharedSecret, signatureAlgorithm); + jwt.setEncodedSignature(signature); + + return jwt.toString(); + } + + private JwtClaims createJwtClaims(User user, AuthorizationGrant authorizationGrant, Collection scopes) throws Exception { + String claimsString = getJSonResponse(user, authorizationGrant, scopes); + JwtClaims claims = new JwtClaims(new JSONObject(claimsString)); + + claims.setIssuer(appConfiguration.getIssuer()); + Audience.setAudience(claims, authorizationGrant.getClient()); + return claims; + } + + public String getJweResponse( + KeyEncryptionAlgorithm keyEncryptionAlgorithm, BlockEncryptionAlgorithm blockEncryptionAlgorithm, + User user, AuthorizationGrant authorizationGrant, Collection scopes) throws Exception { + log.trace("Building JWE reponse with next scopes {0} for user {1} and user custom attributes {0}", scopes, user.getUserId(), user.getCustomAttributes()); + + Jwe jwe = new Jwe(); + + // Header + jwe.getHeader().setType(JwtType.JWT); + jwe.getHeader().setAlgorithm(keyEncryptionAlgorithm); + jwe.getHeader().setEncryptionMethod(blockEncryptionAlgorithm); + + // Claims + jwe.setClaims(createJwtClaims(user, authorizationGrant, scopes)); + + // Encryption + if (keyEncryptionAlgorithm == KeyEncryptionAlgorithm.RSA_OAEP + || keyEncryptionAlgorithm == KeyEncryptionAlgorithm.RSA1_5) { + JSONObject jsonWebKeys = ServerUtil.getJwks(authorizationGrant.getClient()); + String keyId = new ServerCryptoProvider(cryptoProvider).getKeyId(JSONWebKeySet.fromJSONObject(jsonWebKeys), + Algorithm.fromString(keyEncryptionAlgorithm.getName()), + Use.ENCRYPTION); + PublicKey publicKey = cryptoProvider.getPublicKey(keyId, jsonWebKeys, null); + + if (publicKey != null) { + JweEncrypter jweEncrypter = new JweEncrypterImpl(keyEncryptionAlgorithm, blockEncryptionAlgorithm, publicKey); + jwe = jweEncrypter.encrypt(jwe); + } else { + throw new InvalidJweException("The public key is not valid"); + } + } else if (keyEncryptionAlgorithm == KeyEncryptionAlgorithm.A128KW + || keyEncryptionAlgorithm == KeyEncryptionAlgorithm.A256KW) { + try { + byte[] sharedSymmetricKey = clientService.decryptSecret(authorizationGrant.getClient().getClientSecret()).getBytes(Util.UTF8_STRING_ENCODING); + JweEncrypter jweEncrypter = new JweEncrypterImpl(keyEncryptionAlgorithm, blockEncryptionAlgorithm, sharedSymmetricKey); + jwe = jweEncrypter.encrypt(jwe); + } catch (Exception e) { + throw new InvalidJweException(e); + } + } + + return jwe.toString(); + } + + /** + * Builds a JSon String with the response parameters. + */ + public String getJSonResponse(User user, AuthorizationGrant authorizationGrant, Collection scopes) + throws Exception { + log.trace("Building JSON reponse with next scopes {0} for user {1} and user custom attributes {0}", scopes, user.getUserId(), user.getCustomAttributes()); + + JsonWebResponse jsonWebResponse = new JsonWebResponse(); + + // Claims + List dynamicScopes = new ArrayList(); + for (String scopeName : scopes) { + org.oxauth.persistence.model.Scope scope = scopeService.getScopeById(scopeName); + if ((scope != null) && (org.gluu.oxauth.model.common.ScopeType.DYNAMIC == scope.getScopeType())) { + dynamicScopes.add(scope); + continue; + } + + Map claims = scopeService.getClaims(user, scope); + if (claims == null) { + continue; + } + + if (scope != null && Boolean.TRUE.equals(scope.isOxAuthGroupClaims())) { + JwtSubClaimObject groupClaim = new JwtSubClaimObject(); + groupClaim.setName(scope.getId()); + for (Map.Entry entry : claims.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + if (value instanceof List) { + groupClaim.setClaim(key, (List) value); + } else { + groupClaim.setClaim(key, String.valueOf(value)); + } + } + + jsonWebResponse.getClaims().setClaim(scope.getId(), groupClaim); + } else { + log.info("User Info rest called: {}", claims.entrySet()); + for (Map.Entry entry : claims.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + if (value instanceof List) { + jsonWebResponse.getClaims().setClaim(key, (List) value); + } else if (value instanceof Boolean) { + jsonWebResponse.getClaims().setClaim(key, (Boolean) value); + } else if (value instanceof Date) { + Serializable formattedValue = dateFormatterService.formatClaim((Date) value, key); + jsonWebResponse.getClaims().setClaimObject(key, formattedValue, true); + } else { + jsonWebResponse.getClaims().setClaim(key, String.valueOf(value)); + } + } + } + } + + if (authorizationGrant.getClaims() != null) { + JSONObject claimsObj = new JSONObject(authorizationGrant.getClaims()); + if (claimsObj.has("userinfo")) { + JSONObject userInfoObj = claimsObj.getJSONObject("userinfo"); + for (Iterator it = userInfoObj.keys(); it.hasNext(); ) { + String claimName = it.next(); + boolean optional = true; // ClaimValueType.OPTIONAL.equals(claim.getClaimValue().getClaimValueType()); + GluuAttribute gluuAttribute = attributeService.getByClaimName(claimName); + + if (gluuAttribute != null) { + String ldapClaimName = gluuAttribute.getName(); + + Object attribute = user.getAttribute(ldapClaimName, optional, gluuAttribute.getOxMultiValuedAttribute()); + jsonWebResponse.getClaims().setClaimFromJsonObject(claimName, attribute); + } + } + } + } + + if (authorizationGrant.getJwtAuthorizationRequest() != null + && authorizationGrant.getJwtAuthorizationRequest().getUserInfoMember() != null) { + for (Claim claim : authorizationGrant.getJwtAuthorizationRequest().getUserInfoMember().getClaims()) { + boolean optional = true; // ClaimValueType.OPTIONAL.equals(claim.getClaimValue().getClaimValueType()); + GluuAttribute gluuAttribute = attributeService.getByClaimName(claim.getName()); + + if (gluuAttribute != null) { + Client client = authorizationGrant.getClient(); + + if (validateRequesteClaim(gluuAttribute, client.getClaims(), scopes)) { + String ldapClaimName = gluuAttribute.getName(); + Object attribute = user.getAttribute(ldapClaimName, optional, gluuAttribute.getOxMultiValuedAttribute()); + jsonWebResponse.getClaims().setClaimFromJsonObject(claim.getName(), attribute); + } + } + } + } + + jsonWebResponse.getClaims().setSubjectIdentifier(authorizationGrant.getSub()); + + if ((dynamicScopes.size() > 0) && externalDynamicScopeService.isEnabled()) { + final UnmodifiableAuthorizationGrant unmodifiableAuthorizationGrant = new UnmodifiableAuthorizationGrant(authorizationGrant); + DynamicScopeExternalContext dynamicScopeContext = new DynamicScopeExternalContext(dynamicScopes, jsonWebResponse, unmodifiableAuthorizationGrant); + externalDynamicScopeService.executeExternalUpdateMethods(dynamicScopeContext); + } + + return jsonWebResponse.toString(); + } + + public boolean validateRequesteClaim(GluuAttribute gluuAttribute, String[] clientAllowedClaims, Collection scopes) { + if (gluuAttribute == null) { + log.trace("gluuAttribute is null."); + return false; + } + if (clientAllowedClaims != null) { + for (String clientAllowedClaim : clientAllowedClaims) { + if (gluuAttribute.getDn().equals(clientAllowedClaim)) { + return true; + } + } + } + + for (String scopeName : scopes) { + org.oxauth.persistence.model.Scope scope = scopeService.getScopeById(scopeName); + + if (scope != null && scope.getOxAuthClaims() != null) { + for (String claimDn : scope.getOxAuthClaims()) { + if (gluuAttribute.getDisplayName().equals(attributeService.getAttributeByDn(claimDn).getDisplayName())) { + return true; + } + } + } + } + + return false; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/util/CertUtil.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/util/CertUtil.java new file mode 100644 index 00000000..e83ad165 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/util/CertUtil.java @@ -0,0 +1,72 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.util; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.gluu.oxauth.crypto.cert.CertificateParser; +import org.gluu.util.StringHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * @author Yuriy Movchan + * @version March 11, 2016 + */ +public class CertUtil { + + private final static Logger log = LoggerFactory.getLogger(CertUtil.class); + + private CertUtil() {} + + @SuppressWarnings("unchecked") + public static List loadX509CertificateFromFile(String filePath) { + if (StringHelper.isEmpty(filePath)) { + log.error("X509Certificate file path is empty"); + + return null; + } + + InputStream is; + try { + is = FileUtils.openInputStream(new File(filePath)); + } catch (IOException ex) { + log.error("Failed to read X.509 certificates from file: '" + filePath + "'", ex); + return null; + } + + List certificates = null; + try { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + certificates = (List) cf.generateCertificates(is); + } catch (CertificateException ex) { + log.error("Failed to parse X.509 certificates from file: '" + filePath + "'", ex); + } finally { + IOUtils.closeQuietly(is); + } + + return certificates; + } + + public static X509Certificate parsePem(String pem) { + try { + return CertificateParser.parsePem(pem); + } catch (CertificateException ex) { + log.error("Failed to parse PEM certificate", ex); + } + + return null; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/util/PasswordValidator.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/util/PasswordValidator.java new file mode 100644 index 00000000..b423799c --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/util/PasswordValidator.java @@ -0,0 +1,71 @@ +package org.gluu.oxauth.util; + +import org.gluu.model.attribute.AttributeValidation; +import org.gluu.oxauth.i18n.LanguageBean; +import org.gluu.service.AttributeService; +import org.gluu.service.cdi.util.CdiUtil; + +import javax.enterprise.context.ApplicationScoped; +import javax.faces.application.FacesMessage; +import javax.faces.component.UIComponent; +import javax.faces.component.UIInput; +import javax.faces.context.FacesContext; +import javax.faces.validator.FacesValidator; +import javax.faces.validator.ValidatorException; +import javax.inject.Inject; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@ApplicationScoped +@FacesValidator(value = "gluuPasswordValidator", managed = true) +public class PasswordValidator implements javax.faces.validator.Validator { + + private static final String USER_PASSWORD = "userPassword"; + private String newPassword; + private Pattern pattern; + private Matcher matcher; + private boolean hasValidation = false; + + @Inject + private AttributeService attributeService; + + @Inject + private LanguageBean languageBean; + + @Override + public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException { + // This is workaround unless this bug will be fixed in JSF + if (attributeService == null) { + attributeService = CdiUtil.bean(AttributeService.class); + } + if (languageBean == null) { + languageBean = CdiUtil.bean(LanguageBean.class); + } + + AttributeValidation validation = attributeService.getAttributeByName(USER_PASSWORD).getAttributeValidation(); + if (validation != null) { + String regexp = validation.getRegexp(); + if (regexp != null && !regexp.isEmpty()) { + pattern = Pattern.compile(regexp); + matcher = pattern.matcher(value.toString()); + hasValidation = true; + } + } + if (hasValidation && !matcher.matches()) { + String message = languageBean.getMessage("password.validation.invalid"); + context.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, message, message)); + context.validationFailed(); + ((UIInput) component).setValid(false); + } + + } + + public String getNewPassword() { + return newPassword; + } + + public void setNewPassword(String newPassword) { + this.newPassword = newPassword; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/util/QueryStringDecoder.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/util/QueryStringDecoder.java new file mode 100644 index 00000000..e7f06450 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/util/QueryStringDecoder.java @@ -0,0 +1,66 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.util; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.HashMap; +import java.util.Map; + +/** + * Provides functionality to parse query strings. + * + * @author Javier Rojas Blum + * @version November 24, 2017 + */ +public class QueryStringDecoder { + + /** + * Decodes a query string and returns a map with the parsed query string + * parameters as keys and its values. The parameter values are not + * urldecoded + * + * @param queryString The query string. + * @return A map with the parsed query string parameters and its values. + */ + public static Map decode(String queryString) { + + return decode(queryString,false); + } + + /** + * Decodes a query string and returns a map with the parsed query string + * parameters as keys and its values. + * + * @param queryString The query string. + * @param urlDecode Boolean indicating if the parameter values should be urldecoded + * @return A map with the parsed query string parameters and its values. + */ + public static Map decode(String queryString, boolean urlDecode) { + Map map = new HashMap(); + + if (queryString != null) { + String[] params = queryString.split("&"); + for (String param : params) { + String[] nameValue = param.split("="); + String name = nameValue.length > 0 ? nameValue[0] : ""; + String value = nameValue.length > 1 ? nameValue[1] : ""; + if(urlDecode) { + try { + map.put(name, URLDecoder.decode(value,"UTF-8")); + }catch(UnsupportedEncodingException e) { + map.put(name,value); + } + }else { + map.put(name,value); + } + } + } + + return map; + } +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/util/RedirectUtil.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/util/RedirectUtil.java new file mode 100644 index 00000000..ab85e5f6 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/util/RedirectUtil.java @@ -0,0 +1,72 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.util; + +import org.gluu.oxauth.model.common.ResponseMode; +import org.jboss.resteasy.specimpl.ResponseBuilderImpl; +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.CacheControl; +import javax.ws.rs.core.GenericEntity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.ResponseBuilder; +import java.net.MalformedURLException; +import java.net.URI; + +import static org.gluu.oxauth.client.AuthorizationRequest.NO_REDIRECT_HEADER; + +/** + * @version October 7, 2019 + */ +public class RedirectUtil { + + private final static Logger log = LoggerFactory.getLogger(RedirectUtil.class); + + static String JSON_REDIRECT_PROPNAME = "redirect"; + + static int HTTP_REDIRECT = 302; + + public static ResponseBuilder getRedirectResponseBuilder(RedirectUri redirectUriResponse, HttpServletRequest httpRequest) { + ResponseBuilder builder; + + if (httpRequest != null && httpRequest.getHeader(NO_REDIRECT_HEADER) != null) { + try { + URI redirectURI = URI.create(redirectUriResponse.toString()); + JSONObject jsonObject = new JSONObject(); + jsonObject.put(JSON_REDIRECT_PROPNAME, redirectURI.toURL()); + String jsonResp = jsonObject.toString(); + jsonResp = jsonResp.replace("\\/", "/"); + builder = Response.ok( + new GenericEntity(jsonResp, String.class), + MediaType.APPLICATION_JSON_TYPE + ); + + } catch (MalformedURLException | JSONException e) { + builder = Response.serverError(); + log.debug(e.getMessage(), e); + } + } else if (redirectUriResponse.getResponseMode() == ResponseMode.FORM_POST) { + builder = new ResponseBuilderImpl(); + builder.status(Response.Status.OK); + builder.type(MediaType.TEXT_HTML_TYPE); + builder.cacheControl(CacheControl.valueOf("no-cache, no-store")); + builder.header("Pragma", "no-cache"); + builder.entity(redirectUriResponse.toString()); + } else { + URI redirectURI = URI.create(redirectUriResponse.toString()); + builder = Response.status(HTTP_REDIRECT); + builder.location(redirectURI); + } + + return builder; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/util/ServerUtil.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/util/ServerUtil.java new file mode 100644 index 00000000..47ee2dcc --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/util/ServerUtil.java @@ -0,0 +1,299 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.AnnotationIntrospector; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; +import com.fasterxml.jackson.datatype.jsonorg.JsonOrgModule; +import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector; +import com.google.common.base.Strings; +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.registration.Client; +import org.gluu.oxauth.model.uma.persistence.UmaPermission; +import org.gluu.oxauth.model.util.JwtUtil; +import org.gluu.oxauth.service.common.ApplicationFactory; +import org.gluu.oxauth.uma.service.UmaScopeService; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.persist.model.base.CustomAttribute; +import org.gluu.service.cdi.util.CdiUtil; +import org.gluu.util.ArrayHelper; +import org.gluu.util.Util; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.faces.context.ExternalContext; +import javax.faces.context.FacesContext; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.CacheControl; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLDecoder; +import java.util.*; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; + +/** + * @author Yuriy Zabrovarnyy + * @author Yuriy Movchan + * @version 0.9, 26/12/2012 + */ + +public class ServerUtil { + + private static final Logger log = LoggerFactory.getLogger(ServerUtil.class); + + private ServerUtil() { + } + + public static Map prepareForLogs(Map parameters) { + if (parameters == null || parameters.isEmpty()) { + return new HashMap<>(); + } + + Map result = new HashMap<>(parameters); + if (result.containsKey("client_secret")) { + result.put("client_secret", new String[] {"*****"}); + } + if (result.containsKey("password")) { + result.put("password", new String[] {"*****"}); + } + return result; + } + + public static JSONObject getJwks(Client client) { + return Strings.isNullOrEmpty(client.getJwks()) + ? JwtUtil.getJSONWebKeys(client.getJwksUri()) + : new JSONObject(client.getJwks()); + } + + public static GregorianCalendar now() { + return new GregorianCalendar(TimeZone.getTimeZone("UTC")); + } + + public static int calculateTtl(Date creationDate, Date expirationDate) { + if (creationDate != null && expirationDate != null) { + return (int) ((expirationDate.getTime() - creationDate.getTime()) / 1000L); + } + return 0; + } + + public static String asJsonSilently(Object p_object) { + try { + return asJson(p_object); + } catch (IOException e) { + log.trace(e.getMessage(), e); + return ""; + } + } + + public static ThreadFactory daemonThreadFactory() { + return runnable -> { + Thread thread = new Thread(runnable); + thread.setDaemon(true); + return thread; + }; + } + + public static boolean isTrue(Boolean booleanObject) { + return booleanObject != null && booleanObject; + } + + public static boolean isFalse(Boolean booleanObject) { + return !isTrue(booleanObject); + } + + public static String asPrettyJson(Object p_object) throws IOException { + final ObjectMapper mapper = ServerUtil.createJsonMapper().configure(SerializationFeature.WRAP_ROOT_VALUE, false); + return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(p_object); + } + + public static String asJson(Object p_object) throws IOException { + final ObjectMapper mapper = ServerUtil.createJsonMapper().configure(SerializationFeature.WRAP_ROOT_VALUE, false); + return mapper.writeValueAsString(p_object); + } + + public static CacheControl cacheControl(boolean p_noStore) { + final CacheControl cacheControl = new CacheControl(); + cacheControl.setNoStore(p_noStore); + return cacheControl; + } + + public static CacheControl cacheControl(boolean p_noStore, boolean p_noTransform) { + final CacheControl cacheControl = new CacheControl(); + cacheControl.setNoStore(p_noStore); + cacheControl.setNoTransform(p_noTransform); + return cacheControl; + } + + public static CacheControl cacheControlWithNoStoreTransformAndPrivate() { + final CacheControl cacheControl = cacheControl(true, false); + cacheControl.setPrivate(true); + return cacheControl; + } + + public static ObjectMapper createJsonMapper() { + final AnnotationIntrospector jaxb = new JaxbAnnotationIntrospector(); + final AnnotationIntrospector jackson = new JacksonAnnotationIntrospector(); + + final AnnotationIntrospector pair = AnnotationIntrospector.pair(jackson, jaxb); + + final ObjectMapper mapper = new ObjectMapper(); + mapper.getDeserializationConfig().with(pair); + mapper.getSerializationConfig().with(pair); + return mapper; + } + + public static ObjectMapper jsonMapperWithWrapRoot() { + return createJsonMapper().configure(SerializationFeature.WRAP_ROOT_VALUE, true); + } + + public static ObjectMapper jsonMapperWithUnwrapRoot() { + return createJsonMapper().configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true); + } + + public static String toPrettyJson(JSONObject jsonObject) throws JsonProcessingException { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JsonOrgModule()); + return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonObject); + } + + public static PersistenceEntryManager getLdapManager() { + return CdiUtil.bean(PersistenceEntryManager.class, ApplicationFactory.PERSISTENCE_ENTRY_MANAGER_NAME); + } + + public static CustomAttribute getAttributeByName(List p_list, String p_attributeName) { + if (p_list != null && !p_list.isEmpty() && StringUtils.isNotEmpty(p_attributeName)) { + for (CustomAttribute attr : p_list) { + if (p_attributeName.equals(attr.getName())) { + return attr; + } + } + } + return null; + } + + public static String getAttributeValueByName(List p_list, String p_attributeName) { + final CustomAttribute attr = getAttributeByName(p_list, p_attributeName); + if (attr != null) { + return attr.getValue(); + } + return ""; + } + + public static String urlDecode(String p_str) { + if (StringUtils.isNotBlank(p_str)) { + try { + return URLDecoder.decode(p_str, Util.UTF8); + } catch (UnsupportedEncodingException e) { + log.trace(e.getMessage(), e); + } + } + return p_str; + } + + public static ScheduledExecutorService createExecutor() { + return Executors.newSingleThreadScheduledExecutor(new ThreadFactory() { + public Thread newThread(Runnable p_r) { + Thread thread = new Thread(p_r); + thread.setDaemon(true); + return thread; + } + }); + } + + public static org.gluu.oxauth.model.uma.UmaPermission convert(UmaPermission permission, UmaScopeService umaScopeService) { + if (permission != null) { + final org.gluu.oxauth.model.uma.UmaPermission result = new org.gluu.oxauth.model.uma.UmaPermission(); + result.setResourceId(permission.getResourceId()); + result.setScopes(umaScopeService.getScopeIdsByDns(permission.getScopeDns())); + result.setExpiresAt(dateToSeconds(permission.getExpirationDate())); + return result; + } + return null; + } + + public static String getFirstValue(Map map, String key) { + if (map.containsKey(key)) { + String[] values = map.get(key); + if (ArrayHelper.isNotEmpty(values)) { + return values[0]; + } + } + + return null; + } + + /** + * @param httpRequest interface to provide request information for HTTP servlets. + * @return IP address of client + * @see Getting IP address of client + */ + public static String getIpAddress(HttpServletRequest httpRequest) { + final String[] HEADERS_TO_TRY = { + "X-Forwarded-For", + "Proxy-Client-IP", + "WL-Proxy-Client-IP", + "HTTP_X_FORWARDED_FOR", + "HTTP_X_FORWARDED", + "HTTP_X_CLUSTER_CLIENT_IP", + "HTTP_CLIENT_IP", + "HTTP_FORWARDED_FOR", + "HTTP_FORWARDED", + "HTTP_VIA", + "REMOTE_ADDR" + }; + for (String header : HEADERS_TO_TRY) { + String ip = httpRequest.getHeader(header); + if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) { + return ip; + } + } + return httpRequest.getRemoteAddr(); + } + + /** + * Safe retrieves http request from FacesContext + * + * @return http + */ + public static HttpServletRequest getRequestOrNull() { + FacesContext facesContext = FacesContext.getCurrentInstance(); + if (facesContext == null) + return null; + + ExternalContext externalContext = facesContext.getExternalContext(); + if (externalContext == null) + return null; + Object request = externalContext.getRequest(); + if (request == null || !(request instanceof HttpServletRequest)) + return null; + return (HttpServletRequest) request; + } + + public static boolean isSameRequestPath(String url1, String url2) throws MalformedURLException { + if (StringUtils.isBlank(url1) || StringUtils.isBlank(url2)) { + return false; + } + + URL parsedUrl1 = new URL(url1); + URL parsedUrl2 = new URL(url2); + + return parsedUrl1.getPath().endsWith(parsedUrl2.getPath()); + } + + public static Integer dateToSeconds(Date date) { + return date != null ? (int) (date.getTime() / 1000) : null; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/util/TokenHashUtil.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/util/TokenHashUtil.java new file mode 100644 index 00000000..ebf33eb4 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/util/TokenHashUtil.java @@ -0,0 +1,32 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2014, Gluu + */ + +package org.gluu.oxauth.util; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.lang.StringUtils; + +public class TokenHashUtil { + + public static final String PREFIX = "{sha256Hex}"; + + public static String getHashWithPrefix(String token) { + if (StringUtils.isNotBlank(token) && !token.startsWith(PREFIX)) { + return PREFIX + DigestUtils.sha256Hex(token); + } else { + return token; + } + } + + public static String hash(String hashedToken) { + if (StringUtils.isNotBlank(hashedToken) && hashedToken.startsWith(PREFIX)) { + return hashedToken; + } else { + return DigestUtils.sha256Hex(hashedToken); + } + } + +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/ws/rs/controller/HealthCheckController.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/ws/rs/controller/HealthCheckController.java new file mode 100644 index 00000000..f2a11f2b --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/ws/rs/controller/HealthCheckController.java @@ -0,0 +1,52 @@ +package org.gluu.oxauth.ws.rs.controller; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.gluu.oxauth.service.external.ExternalAuthenticationService; +import org.gluu.oxauth.service.external.ExternalDynamicScopeService; +import org.gluu.persist.PersistenceEntryManager; + +/** + * Health check controller + * + * @author Yuriy Movchan + * @version Jul 24, 2020 + */ +@ApplicationScoped +@Path("/") +public class HealthCheckController { + + @Inject + private PersistenceEntryManager persistenceEntryManager; + + @Inject + private ExternalAuthenticationService externalAuthenticationService; + + @Inject + private ExternalDynamicScopeService externalDynamicScopeService; + + @GET + @POST + @Path("/health-check") + @Produces(MediaType.APPLICATION_JSON) + public String healthCheckController() { + boolean isConnected = persistenceEntryManager.getOperationService().isConnected(); + String dbStatus = isConnected ? "online" : "offline"; + String appStatus = getAppStatus(); + return "{\"status\": \"" + appStatus + "\", \"db_status\":\"" + dbStatus + "\"}"; + } + + public String getAppStatus() { + if (externalAuthenticationService.isLoaded() && externalDynamicScopeService.isLoaded()) { + return "running"; + } else { + return "starting"; + } + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/ws/rs/fido/u2f/U2fAuthenticationWS.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/ws/rs/fido/u2f/U2fAuthenticationWS.java new file mode 100644 index 00000000..d7b0cd1e --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/ws/rs/fido/u2f/U2fAuthenticationWS.java @@ -0,0 +1,210 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2015, Gluu + */ + +package org.gluu.oxauth.ws.rs.fido.u2f; + +import org.gluu.oxauth.exception.fido.u2f.DeviceCompromisedException; +import org.gluu.oxauth.exception.fido.u2f.InvalidKeyHandleDeviceException; +import org.gluu.oxauth.exception.fido.u2f.NoEligableDevicesException; +import org.gluu.oxauth.model.config.Constants; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.fido.u2f.AuthenticateRequestMessageLdap; +import org.gluu.oxauth.model.fido.u2f.DeviceRegistration; +import org.gluu.oxauth.model.fido.u2f.DeviceRegistrationResult; +import org.gluu.oxauth.model.fido.u2f.U2fErrorResponseType; +import org.gluu.oxauth.model.fido.u2f.exception.BadInputException; +import org.gluu.oxauth.model.fido.u2f.protocol.AuthenticateRequestMessage; +import org.gluu.oxauth.model.fido.u2f.protocol.AuthenticateResponse; +import org.gluu.oxauth.model.fido.u2f.protocol.AuthenticateStatus; +import org.gluu.oxauth.model.util.Base64Util; +import org.gluu.oxauth.service.common.UserService; +import org.gluu.oxauth.service.fido.u2f.AuthenticationService; +import org.gluu.oxauth.service.fido.u2f.DeviceRegistrationService; +import org.gluu.oxauth.service.fido.u2f.UserSessionIdService; +import org.gluu.oxauth.service.fido.u2f.ValidationService; +import org.gluu.oxauth.util.ServerUtil; +import org.gluu.util.StringHelper; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.ws.rs.*; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +/** + * The endpoint allows to start and finish U2F authentication process + * + * @author Yuriy Movchan + * @version August 9, 2017 + */ +@Path("/fido/u2f/authentication") +public class U2fAuthenticationWS { + + @Inject + private Logger log; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private UserService userService; + + @Inject + private AuthenticationService u2fAuthenticationService; + + @Inject + private DeviceRegistrationService deviceRegistrationService; + + @Inject + private UserSessionIdService userSessionIdService; + + @Inject + private ValidationService u2fValidationService; + + @GET + @Produces({"application/json"}) + public Response startAuthentication(@QueryParam("username") String userName, @QueryParam("keyhandle") String keyHandle, @QueryParam("application") String appId, @QueryParam("session_id") String sessionId) { + // Parameter username is deprecated. We uses it only to determine is it's one or two step workflow + try { + if (appConfiguration.getDisableU2fEndpoint()) { + return Response.status(Status.FORBIDDEN).build(); + } + + log.debug("Startig authentication with username '{}', keyhandle '{}' for appId '{}' and session_id '{}'", userName, keyHandle, appId, sessionId); + + if (StringHelper.isEmpty(userName) && StringHelper.isEmpty(keyHandle)) { + throw new BadInputException("The request should contains either username or keyhandle"); + } + + String foundUserInum = null; + + boolean twoStep = StringHelper.isNotEmpty(userName); + if (twoStep) { + boolean valid = u2fValidationService.isValidSessionId(userName, sessionId); + if (!valid) { + throw new BadInputException(String.format("session_id '%s' is invalid", sessionId)); + } + + foundUserInum = userService.getUserInum(userName); + } else { + // Convert to non padding URL base64 string + String keyHandleWithoutPading = Base64Util.base64urlencode(Base64Util.base64urldecode(keyHandle)); + + // In one step we expects empty username and not empty keyhandle + foundUserInum = u2fAuthenticationService.getUserInumByKeyHandle(appId, keyHandleWithoutPading); + } + + if (StringHelper.isEmpty(foundUserInum)) { + throw new BadInputException(String.format("Failed to find user by userName '%s' or keyHandle '%s' in LDAP", userName, keyHandle)); + } + + AuthenticateRequestMessage authenticateRequestMessage = u2fAuthenticationService.buildAuthenticateRequestMessage(appId, foundUserInum); + u2fAuthenticationService.storeAuthenticationRequestMessage(authenticateRequestMessage, foundUserInum, sessionId); + + // convert manually to avoid possible conflict between resteasy + // providers, e.g. jettison, jackson + final String entity = ServerUtil.asJson(authenticateRequestMessage); + + return Response.status(Response.Status.OK).entity(entity).cacheControl(ServerUtil.cacheControl(true)).build(); + } catch (Exception ex) { + log.error("Exception happened", ex); + if (ex instanceof WebApplicationException) { + throw (WebApplicationException) ex; + } + + if ((ex instanceof NoEligableDevicesException) || (ex instanceof InvalidKeyHandleDeviceException)) { + throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND) + .entity(errorResponseFactory.getErrorResponse(U2fErrorResponseType.NO_ELIGABLE_DEVICES)).build()); + } + + throw new WebApplicationException(Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(errorResponseFactory.getJsonErrorResponse(U2fErrorResponseType.SERVER_ERROR)).build()); + } + } + + @POST + @Produces({"application/json"}) + public Response finishAuthentication(@FormParam("username") String userName, @FormParam("tokenResponse") String authenticateResponseString) { + String sessionId = null; + try { + if (appConfiguration.getDisableU2fEndpoint()) { + return Response.status(Status.FORBIDDEN).build(); + } + + log.debug("Finishing authentication for username '{}' with response '{}'", userName, authenticateResponseString); + + AuthenticateResponse authenticateResponse = ServerUtil.jsonMapperWithWrapRoot().readValue(authenticateResponseString, AuthenticateResponse.class); + + String requestId = authenticateResponse.getRequestId(); + AuthenticateRequestMessageLdap authenticateRequestMessageLdap = u2fAuthenticationService.getAuthenticationRequestMessageByRequestId(requestId); + if (authenticateRequestMessageLdap == null) { + throw new WebApplicationException(Response.status(Response.Status.FORBIDDEN) + .entity(errorResponseFactory.getJsonErrorResponse(U2fErrorResponseType.SESSION_EXPIRED)).build()); + } + sessionId = authenticateRequestMessageLdap.getSessionId(); + u2fAuthenticationService.removeAuthenticationRequestMessage(authenticateRequestMessageLdap); + + AuthenticateRequestMessage authenticateRequestMessage = authenticateRequestMessageLdap.getAuthenticateRequestMessage(); + + String foundUserInum = authenticateRequestMessageLdap.getUserInum(); + DeviceRegistrationResult deviceRegistrationResult = u2fAuthenticationService.finishAuthentication(authenticateRequestMessage, authenticateResponse, foundUserInum); + + // If sessionId is not empty update session + if (StringHelper.isNotEmpty(sessionId)) { + log.debug("There is session id. Setting session id attributes"); + + boolean oneStep = StringHelper.isEmpty(userName); + userSessionIdService.updateUserSessionIdOnFinishRequest(sessionId, foundUserInum, deviceRegistrationResult, false, oneStep); + } + + AuthenticateStatus authenticationStatus = new AuthenticateStatus(Constants.RESULT_SUCCESS, requestId); + + // convert manually to avoid possible conflict between resteasy + // providers, e.g. jettison, jackson + final String entity = ServerUtil.asJson(authenticationStatus); + + return Response.status(Response.Status.OK).entity(entity).cacheControl(ServerUtil.cacheControl(true)).build(); + } catch (Exception ex) { + log.error("Exception happened", ex); + if (ex instanceof WebApplicationException) { + throw (WebApplicationException) ex; + } + + try { + // If sessionId is not empty update session + if (StringHelper.isNotEmpty(sessionId)) { + log.debug("There is session id. Setting session id status to 'declined'"); + userSessionIdService.updateUserSessionIdOnError(sessionId); + } + } catch (Exception ex2) { + log.error("Failed to update session id status", ex2); + } + + if (ex instanceof BadInputException) { + throw new WebApplicationException(Response.status(Response.Status.FORBIDDEN) + .entity(errorResponseFactory.getErrorResponse(U2fErrorResponseType.INVALID_REQUEST)).build()); + } + + if (ex instanceof DeviceCompromisedException) { + DeviceRegistration deviceRegistration = ((DeviceCompromisedException) ex).getDeviceRegistration(); + try { + deviceRegistrationService.disableUserDeviceRegistration(deviceRegistration); + } catch (Exception ex2) { + log.error("Failed to mark device '{}' as compomised", ex2, deviceRegistration.getId()); + } + throw new WebApplicationException(Response.status(Response.Status.FORBIDDEN) + .entity(errorResponseFactory.getErrorResponse(U2fErrorResponseType.DEVICE_COMPROMISED)).build()); + } + + throw new WebApplicationException(Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(errorResponseFactory.getJsonErrorResponse(U2fErrorResponseType.SERVER_ERROR)).build()); + } + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/ws/rs/fido/u2f/U2fConfigurationWS.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/ws/rs/fido/u2f/U2fConfigurationWS.java new file mode 100644 index 00000000..0ef0b5a9 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/ws/rs/fido/u2f/U2fConfigurationWS.java @@ -0,0 +1,72 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2015, Gluu + */ + +package org.gluu.oxauth.ws.rs.fido.u2f; + +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.fido.u2f.U2fConfiguration; +import org.gluu.oxauth.model.fido.u2f.U2fErrorResponseType; +import org.gluu.oxauth.util.ServerUtil; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +/** + * The endpoint at which the requester can obtain FIDO U2F metadata + * configuration + * + * @author Yuriy Movchan Date: 05/13/2015 + */ +@Path("/fido-configuration") +public class U2fConfigurationWS { + + @Inject + private Logger log; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @GET + @Produces({ "application/json" }) + public Response getConfiguration() { + try { + if (appConfiguration.getDisableU2fEndpoint()) { + return Response.status(Status.FORBIDDEN).build(); + } + + final String baseEndpointUri = appConfiguration.getBaseEndpoint(); + + final U2fConfiguration conf = new U2fConfiguration(); + conf.setVersion("2.1"); + conf.setIssuer(appConfiguration.getIssuer()); + + conf.setRegistrationEndpoint(baseEndpointUri + "/fido/u2f/registration"); + conf.setAuthenticationEndpoint(baseEndpointUri + "/fido/u2f/authentication"); + + // convert manually to avoid possible conflicts between resteasy + // providers, e.g. jettison, jackson + final String entity = ServerUtil.asPrettyJson(conf); + log.trace("FIDO U2F configuration: {}", entity); + + return Response.ok(entity).build(); + } catch (Throwable ex) { + log.error(ex.getMessage(), ex); + throw new WebApplicationException(Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(errorResponseFactory.errorAsJson(U2fErrorResponseType.SERVER_ERROR, "Unknown.")).build()); + } + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/ws/rs/fido/u2f/U2fRegistrationWS.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/ws/rs/fido/u2f/U2fRegistrationWS.java new file mode 100644 index 00000000..01184673 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/ws/rs/fido/u2f/U2fRegistrationWS.java @@ -0,0 +1,247 @@ +/* + * oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2015, Gluu + */ + +package org.gluu.oxauth.ws.rs.fido.u2f; + +import org.gluu.model.custom.script.conf.CustomScriptConfiguration; +import org.gluu.oxauth.model.common.User; +import org.gluu.oxauth.model.config.Constants; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.fido.u2f.*; +import org.gluu.oxauth.model.fido.u2f.exception.BadInputException; +import org.gluu.oxauth.model.fido.u2f.exception.RegistrationNotAllowed; +import org.gluu.oxauth.model.fido.u2f.protocol.RegisterRequestMessage; +import org.gluu.oxauth.model.fido.u2f.protocol.RegisterResponse; +import org.gluu.oxauth.model.fido.u2f.protocol.RegisterStatus; +import org.gluu.oxauth.model.session.SessionId; +import org.gluu.oxauth.service.SessionIdService; +import org.gluu.oxauth.service.common.UserService; +import org.gluu.oxauth.service.external.ExternalAuthenticationService; +import org.gluu.oxauth.service.fido.u2f.DeviceRegistrationService; +import org.gluu.oxauth.service.fido.u2f.RegistrationService; +import org.gluu.oxauth.service.fido.u2f.UserSessionIdService; +import org.gluu.oxauth.service.fido.u2f.ValidationService; +import org.gluu.oxauth.util.ServerUtil; +import org.gluu.util.StringHelper; +import org.slf4j.Logger; + +import javax.inject.Inject; +import javax.ws.rs.*; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import java.util.List; + +/** + * The endpoint allows to start and finish U2F registration process + * + * @author Yuriy Movchan + * @version August 9, 2017 + */ +@Path("/fido/u2f/registration") +public class U2fRegistrationWS { + + @Inject + private Logger log; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private UserService userService; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private RegistrationService u2fRegistrationService; + + @Inject + private DeviceRegistrationService deviceRegistrationService; + + @Inject + private SessionIdService sessionIdService; + + @Inject + private UserSessionIdService userSessionIdService; + + @Inject + private ValidationService u2fValidationService; + + @Inject + private ExternalAuthenticationService service; + + @GET + @Produces({"application/json"}) + public Response startRegistration(@QueryParam("username") String userName, @QueryParam("application") String appId, @QueryParam("session_id") String sessionId, @QueryParam("enrollment_code") String enrollmentCode) { + // Parameter username is deprecated. We uses it only to determine is it's one or two step workflow + try { + if (appConfiguration.getDisableU2fEndpoint()) { + return Response.status(Status.FORBIDDEN).build(); + } + + log.debug("Startig registration with username '{}' for appId '{}'. session_id '{}', enrollment_code '{}'", userName, appId, sessionId, enrollmentCode); + + String userInum = null; + + boolean sessionBasedEnrollment = false; + boolean twoStep = StringHelper.isNotEmpty(userName); + if (twoStep) { + boolean removeEnrollment = false; + if (StringHelper.isNotEmpty(sessionId)) { + boolean valid = u2fValidationService.isValidSessionId(userName, sessionId); + if (!valid) { + throw new BadInputException(String.format("session_id '%s' is invalid", sessionId)); + } + sessionBasedEnrollment = true; + } else if (StringHelper.isNotEmpty(enrollmentCode)) { + boolean valid = u2fValidationService.isValidEnrollmentCode(userName, enrollmentCode); + if (!valid) { + throw new BadInputException(String.format("enrollment_code '%s' is invalid", enrollmentCode)); + } + removeEnrollment = true; + } else { + throw new BadInputException("session_id or enrollment_code is mandatory"); + } + + User user = userService.getUser(userName); + userInum = userService.getUserInum(user); + if (StringHelper.isEmpty(userInum)) { + throw new BadInputException(String.format("Failed to find user '%s' in LDAP", userName)); + } + + if (removeEnrollment) { + // We allow to use enrollment code only one time + user.setAttribute(U2fConstants.U2F_ENROLLMENT_CODE_ATTRIBUTE, ""); + userService.updateUser(user); + } + } + + if (sessionBasedEnrollment) { + List deviceRegistrations = deviceRegistrationService.findUserDeviceRegistrations(userInum, appId); + if (deviceRegistrations.size() > 0 && !isCurrentAuthenticationLevelCorrespondsToU2fLevel(sessionId)) { + throw new RegistrationNotAllowed(String.format("It's not possible to start registration with user_name and session_id because user '%s' has already enrolled device", userName)); + } + } + + RegisterRequestMessage registerRequestMessage = u2fRegistrationService.builRegisterRequestMessage(appId, userInum); + u2fRegistrationService.storeRegisterRequestMessage(registerRequestMessage, userInum, sessionId); + + // Convert manually to avoid possible conflict between resteasy providers, e.g. jettison, jackson + final String entity = ServerUtil.asJson(registerRequestMessage); + + return Response.status(Response.Status.OK).entity(entity).cacheControl(ServerUtil.cacheControl(true)).build(); + } catch (Exception ex) { + log.error("Exception happened", ex); + if (ex instanceof WebApplicationException) { + throw (WebApplicationException) ex; + } + + if (ex instanceof RegistrationNotAllowed) { + throw new WebApplicationException(Response.status(Response.Status.NOT_ACCEPTABLE) + .entity(errorResponseFactory.getErrorResponse(U2fErrorResponseType.REGISTRATION_NOT_ALLOWED)).build()); + } + + throw new WebApplicationException(Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(errorResponseFactory.getJsonErrorResponse(U2fErrorResponseType.SERVER_ERROR)).build()); + } + } + + @POST + @Produces({"application/json"}) + public Response finishRegistration(@FormParam("username") String userName, @FormParam("tokenResponse") String registerResponseString) { + String sessionId = null; + try { + if (appConfiguration.getDisableU2fEndpoint()) { + return Response.status(Status.FORBIDDEN).build(); + } + + log.debug("Finishing registration for username '{}' with response '{}'", userName, registerResponseString); + + RegisterResponse registerResponse = ServerUtil.jsonMapperWithWrapRoot().readValue(registerResponseString, RegisterResponse.class); + + String requestId = registerResponse.getRequestId(); + RegisterRequestMessageLdap registerRequestMessageLdap = u2fRegistrationService.getRegisterRequestMessageByRequestId(requestId); + if (registerRequestMessageLdap == null) { + throw new WebApplicationException(Response.status(Response.Status.FORBIDDEN) + .entity(errorResponseFactory.getJsonErrorResponse(U2fErrorResponseType.SESSION_EXPIRED)).build()); + } + u2fRegistrationService.removeRegisterRequestMessage(registerRequestMessageLdap); + + String foundUserInum = registerRequestMessageLdap.getUserInum(); + + RegisterRequestMessage registerRequestMessage = registerRequestMessageLdap.getRegisterRequestMessage(); + DeviceRegistrationResult deviceRegistrationResult = u2fRegistrationService.finishRegistration(registerRequestMessage, registerResponse, foundUserInum); + + // If sessionId is not empty update session + sessionId = registerRequestMessageLdap.getSessionId(); + if (StringHelper.isNotEmpty(sessionId)) { + log.debug("There is session id. Setting session id attributes"); + + boolean oneStep = StringHelper.isEmpty(foundUserInum); + userSessionIdService.updateUserSessionIdOnFinishRequest(sessionId, foundUserInum, deviceRegistrationResult, true, oneStep); + } + + RegisterStatus registerStatus = new RegisterStatus(Constants.RESULT_SUCCESS, requestId); + + // Convert manually to avoid possible conflict between resteasy providers, e.g. jettison, jackson + final String entity = ServerUtil.asJson(registerStatus); + + return Response.status(Response.Status.OK).entity(entity).cacheControl(ServerUtil.cacheControl(true)).build(); + } catch (Exception ex) { + log.error("Exception happened", ex); + + try { + // If sessionId is not empty update session + if (StringHelper.isNotEmpty(sessionId)) { + log.debug("There is session id. Setting session id status to 'declined'"); + userSessionIdService.updateUserSessionIdOnError(sessionId); + } + } catch (Exception ex2) { + log.error("Failed to update session id status", ex2); + } + + if (ex instanceof WebApplicationException) { + throw (WebApplicationException) ex; + } + + if (ex instanceof BadInputException) { + throw new WebApplicationException(Response.status(Response.Status.FORBIDDEN) + .entity(errorResponseFactory.getErrorResponse(U2fErrorResponseType.INVALID_REQUEST)).build()); + } + + throw new WebApplicationException(Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(errorResponseFactory.getJsonErrorResponse(U2fErrorResponseType.SERVER_ERROR)).build()); + } + } + + private boolean isCurrentAuthenticationLevelCorrespondsToU2fLevel(String session) { + SessionId sessionId = sessionIdService.getSessionId(session); + if (sessionId == null) + return false; + + String acrValuesStr = sessionIdService.getAcr(sessionId); + if (acrValuesStr == null) + return false; + + CustomScriptConfiguration u2fScriptConfiguration = service.getCustomScriptConfigurationByName("u2f"); + if (u2fScriptConfiguration == null) + return false; + + String[] acrValuesArray = acrValuesStr.split(" "); + for (String acrValue : acrValuesArray) { + CustomScriptConfiguration currentScriptConfiguration = service.getCustomScriptConfigurationByName(acrValue); + if (currentScriptConfiguration == null) + continue; + + if (currentScriptConfiguration.getLevel() >= u2fScriptConfiguration.getLevel()) + return true; + } + + return false; + } + +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/ws/rs/stat/StatResponse.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/ws/rs/stat/StatResponse.java new file mode 100644 index 00000000..8f8bb85a --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/ws/rs/stat/StatResponse.java @@ -0,0 +1,35 @@ +package org.gluu.oxauth.ws.rs.stat; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.jboss.resteasy.annotations.providers.jaxb.IgnoreMediaTypes; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Yuriy Zabrovarnyy + */ +@IgnoreMediaTypes("application/*+json") +@JsonIgnoreProperties(ignoreUnknown = true) +public class StatResponse { + + @JsonProperty(value = "response") // month to stat item + private Map response = new HashMap<>(); + + public Map getResponse() { + if (response == null) response = new HashMap<>(); + return response; + } + + public void setResponse(Map response) { + this.response = response; + } + + @Override + public String toString() { + return "StatResponse{" + + "response=" + response + + '}'; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/ws/rs/stat/StatResponseItem.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/ws/rs/stat/StatResponseItem.java new file mode 100644 index 00000000..b4c52e86 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/ws/rs/stat/StatResponseItem.java @@ -0,0 +1,43 @@ +package org.gluu.oxauth.ws.rs.stat; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Yuriy Zabrovarnyy + */ +public class StatResponseItem { + + @JsonProperty(value = "monthly_active_users") + private long monthlyActiveUsers; + + @JsonProperty("token_count_per_granttype") + private Map> tokenCountPerGrantType; + + public long getMonthlyActiveUsers() { + return monthlyActiveUsers; + } + + public void setMonthlyActiveUsers(long monthlyActiveUsers) { + this.monthlyActiveUsers = monthlyActiveUsers; + } + + public Map> getTokenCountPerGrantType() { + if (tokenCountPerGrantType == null) tokenCountPerGrantType = new HashMap<>(); + return tokenCountPerGrantType; + } + + public void setTokenCountPerGrantType(Map> tokenCountPerGrantType) { + this.tokenCountPerGrantType = tokenCountPerGrantType; + } + + @Override + public String toString() { + return "StatResponseItem{" + + "monthlyActiveUsers=" + monthlyActiveUsers + + ", tokenCountPerGrantType=" + tokenCountPerGrantType + + '}'; + } +} diff --git a/oxAuth/Server/src/main/java/org/gluu/oxauth/ws/rs/stat/StatWS.java b/oxAuth/Server/src/main/java/org/gluu/oxauth/ws/rs/stat/StatWS.java new file mode 100644 index 00000000..9492eba6 --- /dev/null +++ b/oxAuth/Server/src/main/java/org/gluu/oxauth/ws/rs/stat/StatWS.java @@ -0,0 +1,340 @@ +package org.gluu.oxauth.ws.rs.stat; + +import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.Counter; +import io.prometheus.client.exporter.common.TextFormat; +import net.agkn.hll.HLL; +import org.apache.commons.lang.StringUtils; +import org.gluu.oxauth.model.common.AbstractToken; +import org.gluu.oxauth.model.common.AuthorizationGrant; +import org.gluu.oxauth.model.configuration.AppConfiguration; +import org.gluu.oxauth.model.error.ErrorResponseFactory; +import org.gluu.oxauth.model.stat.StatEntry; +import org.gluu.oxauth.model.token.TokenErrorResponseType; +import org.gluu.oxauth.security.Identity; +import org.gluu.oxauth.service.stat.StatService; +import org.gluu.oxauth.service.token.TokenService; +import org.gluu.oxauth.util.ServerUtil; +import org.gluu.persist.PersistenceEntryManager; +import org.gluu.search.filter.Filter; +import org.slf4j.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.Map; + +/** + * Provides server with basic statistic. + *

+ * https://github.com/GluuFederation/oxAuth/issues/1512 + * https://github.com/GluuFederation/oxAuth/issues/1321 + * + * @author Yuriy Zabrovarnyy + */ +@ApplicationScoped +@Path("/internal/stat") +public class StatWS { + + private static final int DEFAULT_WS_INTERVAL_LIMIT_IN_SECONDS = 60; + + @Inject + private Logger log; + + @Inject + private PersistenceEntryManager entryManager; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private Identity identity; + + @Inject + private StatService statService; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private TokenService tokenService; + + private long lastProcessedAt; + + @GET + @Produces(MediaType.APPLICATION_JSON) + public Response statGet(@HeaderParam("Authorization") String authorization, @QueryParam("month") String month, @QueryParam("format") String format) { + return stat(authorization, month, format); + } + + @POST + @Produces(MediaType.APPLICATION_JSON) + public Response statPost(@HeaderParam("Authorization") String authorization, @FormParam("month") String month, @FormParam("format") String format) { + return stat(authorization, month, format); + } + + public Response stat(String authorization, String month, String format) { + log.debug("Attempting to request stat, month: " + month + ", format: " + format); + + validateAuthorization(authorization); + final List months = validateMonth(month); + + if (!allowToRun()) { + log.trace("Interval request limit exceeded. Request is rejected. Current interval limit: " + appConfiguration.getStatWebServiceIntervalLimitInSeconds() + " (or 60 seconds if not set)."); + throw errorResponseFactory.createWebApplicationException(Response.Status.FORBIDDEN, TokenErrorResponseType.ACCESS_DENIED, "Interval request limit exceeded."); + } + + lastProcessedAt = System.currentTimeMillis(); + + try { + log.trace("Recognized months: " + months); + final StatResponse statResponse = buildResponse(months); + + final String responseAsStr; + if ("openmetrics".equalsIgnoreCase(format)) { + responseAsStr = createOpenMetricsResponse(statResponse); + } else { + responseAsStr = ServerUtil.asJson(statResponse); + } + + log.trace("Stat: " + responseAsStr); + return Response.ok().entity(responseAsStr).build(); + } catch (WebApplicationException e) { + log.error(e.getMessage(), e); + throw e; + } catch (Exception e) { + log.error(e.getMessage(), e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).type(MediaType.APPLICATION_JSON_TYPE).build(); + } + } + + private StatResponse buildResponse(List months) { + StatResponse response = new StatResponse(); + for (String month : months) { + final StatResponseItem responseItem = buildItem(month); + if (responseItem != null) { + response.getResponse().put(month, responseItem); + } + } + + return response; + } + + private StatResponseItem buildItem(String month) { + try { + String monthlyDn = String.format("ou=%s,%s", month, statService.getBaseDn()); + + final List entries = entryManager.findEntries(monthlyDn, StatEntry.class, Filter.createPresenceFilter("jansId")); + if (entries == null || entries.isEmpty()) { + log.trace("Can't find stat entries for month: " + monthlyDn); + return null; + } + + final StatResponseItem responseItem = new StatResponseItem(); + responseItem.setMonthlyActiveUsers(userCardinality(entries)); + + unionTokenMapIntoResponseItem(entries, responseItem); + + return responseItem; + } catch (Exception e) { + log.error(e.getMessage(), e); + return null; + } + } + + private void unionTokenMapIntoResponseItem(List entries, StatResponseItem responseItem) { + for (StatEntry entry : entries) { + for (Map.Entry> en : entry.getStat().getTokenCountPerGrantType().entrySet()) { + if (en.getValue() == null) { + continue; + } + + final Map tokenMap = responseItem.getTokenCountPerGrantType().get(en.getKey()); + if (tokenMap == null) { + responseItem.getTokenCountPerGrantType().put(en.getKey(), en.getValue()); + continue; + } + + for (Map.Entry tokenEntry : en.getValue().entrySet()) { + final Long counter = tokenMap.get(tokenEntry.getKey()); + if (counter == null) { + tokenMap.put(tokenEntry.getKey(), tokenEntry.getValue()); + continue; + } + + tokenMap.put(tokenEntry.getKey(), counter + tokenEntry.getValue()); + } + } + } + } + + private long userCardinality(List entries) { + HLL hll = decodeHll(entries.get(0)); + + // Union hll + if (entries.size() > 1) { + for (int i = 1; i < entries.size(); i++) { + hll.union(decodeHll(entries.get(i))); + } + } + return hll.cardinality(); + } + + private HLL decodeHll(StatEntry entry) { + try { + return HLL.fromBytes(Base64.getDecoder().decode(entry.getUserHllData())); + } catch (Exception e) { + log.error("Failed to decode HLL data, entry dn: " + entry.getDn() + ", data: " + entry.getUserHllData()); + return statService.newHll(); + } + } + + private void validateAuthorization(String authorization) { + log.trace("Validating authorization: " + authorization); + + AuthorizationGrant grant = tokenService.getAuthorizationGrant(authorization); + if (grant == null) { + log.trace("Unable to find token by authorization: " + authorization); + throw errorResponseFactory.createWebApplicationException(Response.Status.UNAUTHORIZED, TokenErrorResponseType.ACCESS_DENIED, "Can't find grant for authorization."); + } + + final AbstractToken accessToken = grant.getAccessToken(tokenService.getToken(authorization)); + if (accessToken == null) { + log.trace("Unable to find token by authorization: " + authorization); + throw errorResponseFactory.createWebApplicationException(Response.Status.UNAUTHORIZED, TokenErrorResponseType.ACCESS_DENIED, "Can't find access token."); + } + + if (accessToken.isExpired()) { + log.trace("Access Token is expired: " + accessToken.getCode()); + throw errorResponseFactory.createWebApplicationException(Response.Status.UNAUTHORIZED, TokenErrorResponseType.ACCESS_DENIED, "Token expired."); + } + + if (!grant.getScopesAsString().contains(appConfiguration.getStatAuthorizationScope())) { + log.trace("Access Token does NOT have '" + appConfiguration.getStatAuthorizationScope() + "' scope which is required to call Statistic Endpoint."); + throw errorResponseFactory.createWebApplicationException(Response.Status.UNAUTHORIZED, TokenErrorResponseType.ACCESS_DENIED, appConfiguration.getStatAuthorizationScope() + " scope is required for token."); + } + } + + private List validateMonth(String month) { + if (StringUtils.isBlank(month)) { + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, TokenErrorResponseType.INVALID_REQUEST, "`month` parameter can't be blank and should be in format yyyyMM (e.g. 202012)"); + } + + month = ServerUtil.urlDecode(month); + + List months = new ArrayList<>(); + for (String m : month.split(" ")) { + m = m.trim(); + if (m.length() == 6) { + months.add(m); + } + } + + if (months.isEmpty()) { + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, TokenErrorResponseType.INVALID_REQUEST, "`month` parameter can't be blank and should be in format yyyyMM (e.g. 202012)"); + } + + return months; + } + + private boolean allowToRun() { + int interval = appConfiguration.getStatWebServiceIntervalLimitInSeconds(); + if (interval <= 0) { + interval = DEFAULT_WS_INTERVAL_LIMIT_IN_SECONDS; + } + + long timerInterval = interval * 1000L; + + long timeDiff = System.currentTimeMillis() - lastProcessedAt; + + return timeDiff >= timerInterval; + } + + public static String createOpenMetricsResponse(StatResponse statResponse) throws IOException { + Writer writer = new StringWriter(); + CollectorRegistry registry = new CollectorRegistry(); + + final Counter usersCounter = Counter.build() + .name("monthly_active_users") + .labelNames("month") + .help("Monthly active users") + .register(registry); + + final Counter accessTokenCounter = Counter.build() + .name(StatService.ACCESS_TOKEN_KEY) + .labelNames("month", "grantType") + .help("Access Token") + .register(registry); + + final Counter idTokenCounter = Counter.build() + .name(StatService.ID_TOKEN_KEY) + .labelNames("month", "grantType") + .help("Id Token") + .register(registry); + + final Counter refreshTokenCounter = Counter.build() + .name(StatService.REFRESH_TOKEN_KEY) + .labelNames("month", "grantType") + .help("Refresh Token") + .register(registry); + + final Counter umaTokenCounter = Counter.build() + .name(StatService.UMA_TOKEN_KEY) + .labelNames("month", "grantType") + .help("UMA Token") + .register(registry); + + for (Map.Entry entry : statResponse.getResponse().entrySet()) { + final String month = entry.getKey(); + final StatResponseItem item = entry.getValue(); + + usersCounter + .labels(month) + .inc(item.getMonthlyActiveUsers()); + + for (Map.Entry> tokenEntry : item.getTokenCountPerGrantType().entrySet()) { + final String grantType = tokenEntry.getKey(); + final Map tokenMap = tokenEntry.getValue(); + + accessTokenCounter + .labels(month, grantType) + .inc(getToken(tokenMap, StatService.ACCESS_TOKEN_KEY)); + + idTokenCounter + .labels(month, grantType) + .inc(getToken(tokenMap, StatService.ID_TOKEN_KEY)); + + refreshTokenCounter + .labels(month, grantType) + .inc(getToken(tokenMap, StatService.REFRESH_TOKEN_KEY)); + + umaTokenCounter + .labels(month, grantType) + .inc(getToken(tokenMap, StatService.UMA_TOKEN_KEY)); + } + } + + TextFormat.write004(writer, registry.metricFamilySamples()); + return writer.toString(); + } + + private static long getToken(Map map, String key) { + Long v = map.get(key); + return v != null ? v : 0; + } +} diff --git a/oxAuth/Server/src/main/resources/META-INF/beans.xml b/oxAuth/Server/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000..fac30a29 --- /dev/null +++ b/oxAuth/Server/src/main/resources/META-INF/beans.xml @@ -0,0 +1,7 @@ + + + diff --git a/oxAuth/Server/src/main/resources/META-INF/navigation/cas2.navigation.xml b/oxAuth/Server/src/main/resources/META-INF/navigation/cas2.navigation.xml new file mode 100644 index 00000000..035a7034 --- /dev/null +++ b/oxAuth/Server/src/main/resources/META-INF/navigation/cas2.navigation.xml @@ -0,0 +1,41 @@ + + + + + + /auth/cas2/cas2login.xhtml + + + #{authenticator.prepareAuthenticationForStep} + no_permissions + /error.xhtml + + + + #{authenticator.prepareAuthenticationForStep} + failure + /error.xhtml + + + + + + /auth/cas2/cas2postlogin.xhtml + + + #{authenticator.prepareAuthenticationForStep} + no_permissions + /error.xhtml + + + + #{authenticator.prepareAuthenticationForStep} + failure + /error.xhtml + + + + + \ No newline at end of file diff --git a/oxAuth/Server/src/main/resources/META-INF/navigation/cert.navigation.xml b/oxAuth/Server/src/main/resources/META-INF/navigation/cert.navigation.xml new file mode 100644 index 00000000..0d6d38e8 --- /dev/null +++ b/oxAuth/Server/src/main/resources/META-INF/navigation/cert.navigation.xml @@ -0,0 +1,53 @@ + + + + + + /auth/cert/cert-login.xhtml + + + #{authenticator.prepareAuthenticationForStep} + success + /postlogin.xhtml + + + + #{authenticator.prepareAuthenticationForStep} + no_permissions + /error.xhtml + + + + #{authenticator.prepareAuthenticationForStep} + failure + /error.xhtml + + + + #{authenticator.prepareAuthenticationForStep} + expired + /error.xhtml + + + + + + /auth/cert/login.xhtml + + + #{authenticator.prepareAuthenticationForStep} + no_permissions + /error.xhtml + + + + #{authenticator.prepareAuthenticationForStep} + failure + /error.xhtml + + + + + \ No newline at end of file diff --git a/oxAuth/Server/src/main/resources/META-INF/navigation/duo.navigation.xml b/oxAuth/Server/src/main/resources/META-INF/navigation/duo.navigation.xml new file mode 100644 index 00000000..1024fba6 --- /dev/null +++ b/oxAuth/Server/src/main/resources/META-INF/navigation/duo.navigation.xml @@ -0,0 +1,24 @@ + + + + + + /auth/duo/duologin.xhtml + + + #{authenticator.prepareAuthenticationForStep} + no_permissions + /error.xhtml + + + + #{authenticator.prepareAuthenticationForStep} + failure + /error.xhtml + + + + + \ No newline at end of file diff --git a/oxAuth/Server/src/main/resources/META-INF/navigation/gplus.navigation.xml b/oxAuth/Server/src/main/resources/META-INF/navigation/gplus.navigation.xml new file mode 100644 index 00000000..08609e71 --- /dev/null +++ b/oxAuth/Server/src/main/resources/META-INF/navigation/gplus.navigation.xml @@ -0,0 +1,41 @@ + + + + + + /auth/gplus/gpluslogin.xhtml + + + #{authenticator.prepareAuthenticationForStep} + no_permissions + /error.xhtml + + + + #{authenticator.prepareAuthenticationForStep} + failure + /error.xhtml + + + + + + /auth/gplus/gpluspostlogin.xhtml + + + #{authenticator.prepareAuthenticationForStep} + no_permissions + /error.xhtml + + + + #{authenticator.prepareAuthenticationForStep} + failure + /error.xhtml + + + + + \ No newline at end of file diff --git a/oxAuth/Server/src/main/resources/META-INF/navigation/otp.navigation.xml b/oxAuth/Server/src/main/resources/META-INF/navigation/otp.navigation.xml new file mode 100644 index 00000000..6e5df22d --- /dev/null +++ b/oxAuth/Server/src/main/resources/META-INF/navigation/otp.navigation.xml @@ -0,0 +1,42 @@ + + + + + + /auth/otp/enroll.xhtml + + + #{authenticator.prepareAuthenticationForStep} + no_permissions + /error.xhtml + + + + #{authenticator.prepareAuthenticationForStep} + failure + /error.xhtml + + + + + + /auth/otp/otplogin.xhtml + + + #{authenticator.prepareAuthenticationForStep} + no_permissions + /error.xhtml + + + + #{authenticator.prepareAuthenticationForStep} + failure + /error.xhtml + + + + + + \ No newline at end of file diff --git a/oxAuth/Server/src/main/resources/META-INF/navigation/oxpush.navigation.xml b/oxAuth/Server/src/main/resources/META-INF/navigation/oxpush.navigation.xml new file mode 100644 index 00000000..51e20c0b --- /dev/null +++ b/oxAuth/Server/src/main/resources/META-INF/navigation/oxpush.navigation.xml @@ -0,0 +1,58 @@ + + + + + + /auth/oxpush/oxauthenticate.xhtml + + + #{authenticator.prepareAuthenticationForStep} + no_permissions + /error.xhtml + + + + #{authenticator.prepareAuthenticationForStep} + failure + /error.xhtml + + + + + + /auth/oxpush/oxlogin.xhtml + + + #{authenticator.prepareAuthenticationForStep} + no_permissions + /error.xhtml + + + + #{authenticator.prepareAuthenticationForStep} + failure + /error.xhtml + + + + + + /auth/oxpush/oxpair.xhtml + + + #{authenticator.prepareAuthenticationForStep} + no_permissions + /error.xhtml + + + + #{authenticator.prepareAuthenticationForStep} + failure + /error.xhtml + + + + + \ No newline at end of file diff --git a/oxAuth/Server/src/main/resources/META-INF/navigation/passport.navigation.xml b/oxAuth/Server/src/main/resources/META-INF/navigation/passport.navigation.xml new file mode 100644 index 00000000..a12902b0 --- /dev/null +++ b/oxAuth/Server/src/main/resources/META-INF/navigation/passport.navigation.xml @@ -0,0 +1,47 @@ + + + + + + /auth/passport/passportlogin.xhtml + + + #{authenticator.prepareAuthenticationForStep} + no_permissions + /error.xhtml + + + + #{authenticator.prepareAuthenticationForStep} + failure + /error.xhtml + + + + #{authenticator.prepareAuthenticationForStep} + expired + /error.xhtml + + + + + + /auth/passport/passportpostlogin.xhtml + + + #{authenticator.prepareAuthenticationForStep} + no_permissions + /error.xhtml + + + + #{authenticator.prepareAuthenticationForStep} + failure + /error.xhtml + + + + + \ No newline at end of file diff --git a/oxAuth/Server/src/main/resources/META-INF/navigation/saml.navigation.xml b/oxAuth/Server/src/main/resources/META-INF/navigation/saml.navigation.xml new file mode 100644 index 00000000..d38d9f46 --- /dev/null +++ b/oxAuth/Server/src/main/resources/META-INF/navigation/saml.navigation.xml @@ -0,0 +1,41 @@ + + + + + + /auth/saml/samllogin.xhtml + + + #{authenticator.prepareAuthenticationForStep} + no_permissions + /error.xhtml + + + + #{authenticator.prepareAuthenticationForStep} + failure + /error.xhtml + + + + + + /auth/saml/samlpostlogin.xhtml + + + #{authenticator.prepareAuthenticationForStep} + no_permissions + /error.xhtml + + + + #{authenticator.prepareAuthenticationForStep} + failure + /error.xhtml + + + + + \ No newline at end of file diff --git a/oxAuth/Server/src/main/resources/META-INF/navigation/super-gluu.navigation.xml b/oxAuth/Server/src/main/resources/META-INF/navigation/super-gluu.navigation.xml new file mode 100644 index 00000000..d207d5d8 --- /dev/null +++ b/oxAuth/Server/src/main/resources/META-INF/navigation/super-gluu.navigation.xml @@ -0,0 +1,24 @@ + + + + + + /auth/super-gluu/login.xhtml + + + #{authenticator.prepareAuthenticationForStep} + no_permissions + /error.xhtml + + + + #{authenticator.prepareAuthenticationForStep} + failure + /error.xhtml + + + + + \ No newline at end of file diff --git a/oxAuth/Server/src/main/resources/META-INF/navigation/u2f.navigation.xml b/oxAuth/Server/src/main/resources/META-INF/navigation/u2f.navigation.xml new file mode 100644 index 00000000..140ff23e --- /dev/null +++ b/oxAuth/Server/src/main/resources/META-INF/navigation/u2f.navigation.xml @@ -0,0 +1,24 @@ + + + + + + /auth/u2f/login.xhtml + + + #{authenticator.prepareAuthenticationForStep} + no_permissions + /error.xhtml + + + + #{authenticator.prepareAuthenticationForStep} + failure + /error.xhtml + + + + + \ No newline at end of file diff --git a/oxAuth/Server/src/main/resources/META-INF/navigation/uaf.navigation.xml b/oxAuth/Server/src/main/resources/META-INF/navigation/uaf.navigation.xml new file mode 100644 index 00000000..b60f6660 --- /dev/null +++ b/oxAuth/Server/src/main/resources/META-INF/navigation/uaf.navigation.xml @@ -0,0 +1,24 @@ + + + + + + /auth/uaf/login.xhtml + + + #{authenticator.prepareAuthenticationForStep} + no_permissions + /error.xhtml + + + + #{authenticator.prepareAuthenticationForStep} + failure + /error.xhtml + + + + + \ No newline at end of file diff --git a/oxAuth/Server/src/main/resources/META-INF/navigation/uma2.sample.navigation.xml b/oxAuth/Server/src/main/resources/META-INF/navigation/uma2.sample.navigation.xml new file mode 100644 index 00000000..79298ab0 --- /dev/null +++ b/oxAuth/Server/src/main/resources/META-INF/navigation/uma2.sample.navigation.xml @@ -0,0 +1,40 @@ + + + + + /uma2/sample/country.xhtml + + + #{gatherer.prepareForStep} + invalid_step + /error.xhtml + + + + #{gatherer.prepareForStep} + failure + /error.xhtml + + + + + + /uma2/sample/city.xhtml + + + #{gatherer.prepareForStep} + invalid_step + /error.xhtml + + + + #{gatherer.prepareForStep} + failure + /error.xhtml + + + + + \ No newline at end of file diff --git a/oxAuth/Server/src/main/resources/META-INF/services/javax.faces.application.ApplicationConfigurationPopulator b/oxAuth/Server/src/main/resources/META-INF/services/javax.faces.application.ApplicationConfigurationPopulator new file mode 100644 index 00000000..0daa9b90 --- /dev/null +++ b/oxAuth/Server/src/main/resources/META-INF/services/javax.faces.application.ApplicationConfigurationPopulator @@ -0,0 +1 @@ +org.gluu.oxauth.i18n.ApplicationFacesLocalizationConfigPopulator \ No newline at end of file diff --git a/oxAuth/Server/src/main/resources/ehcache.xml b/oxAuth/Server/src/main/resources/ehcache.xml new file mode 100644 index 00000000..ae7513b3 --- /dev/null +++ b/oxAuth/Server/src/main/resources/ehcache.xml @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/oxAuth/Server/src/main/resources/faces-config.xml b/oxAuth/Server/src/main/resources/faces-config.xml new file mode 100644 index 00000000..e052d20a --- /dev/null +++ b/oxAuth/Server/src/main/resources/faces-config.xml @@ -0,0 +1,7 @@ + + + diff --git a/oxAuth/Server/src/main/resources/log4j2.xml b/oxAuth/Server/src/main/resources/log4j2.xml new file mode 100644 index 00000000..53d9e924 --- /dev/null +++ b/oxAuth/Server/src/main/resources/log4j2.xml @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/oxAuth/Server/src/main/resources/oxauth.properties b/oxAuth/Server/src/main/resources/oxauth.properties new file mode 100644 index 00000000..0602480c --- /dev/null +++ b/oxAuth/Server/src/main/resources/oxauth.properties @@ -0,0 +1,410 @@ +common.copyright=Powered by +common.allRightsReserved=Free and open source access management. +common.agreePolicy=By proceeding, you agree with the {0} +common.privacyPolicy=Privacy Policy +common.pleaseReadTheTos=Please read the +common.termsOfService=Terms of Service +common.gluuInc=Gluu, Inc +common.caution=Use subject to MIT LICENSE + +login.pageTitle=oxAuth - Login +login.login=Login +login.loginAsAnother=Login as another user +login.selectAccount=Select Account +login.register=Register +login.pleaseLoginHere=Please login here +login.username=Username +login.password=Password +login.rememberMe=Remember me +login.errorMessage=Incorrect username or password. +login.errorSessionInvalidMessage=Failed to authenticate. Authentication session has expired. Please navigate back to the original page and try again. +login.failedToAuthenticate=Failed to authenticate. +login.failedToGrantPermission=Failed to grant permission. +login.failedToDeny=Failed to deny authorization. +login.userAlreadyAuthenticated=User is already authenticated. Re-send authorization request (must be handled by RP). +login.youDontHavePermission=You don't have permissions. +login.forgotYourPassword = Forgot your password? + +fido2.touch.verification.usedevice=Use Touch ID on your Apple device to sign in to your Gluu account. +fido2.touch.verification.insertkey = Place your finger on the Touch ID. +fido2.touch.verification.useit=Click Ok to enable the Touch ID. +login.use.touchID=OK +selectAccount.pageTitle=Select Account + +logout.missingParameters=Invalid Request. The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed. +logout.failedToProceed=Failed to process logout + +authorize.pageTitle=oxAuth - Authorize +authorize.requestForPermission=Request for Permission +authorize.requestingPermissionForScopes={0} is requesting permission to do the following: +authorize.viewIndividualClaim=View {0} claim. +authorize.allow=Allow +authorize.doNotAllow=Don't Allow + +uma2.gather.failed=Failed to gather claims. +uma2.invalid.step=Invalid step provided during claims-gathering flow. +uma2.invalid.session=Failed to gather claims. Session has expired. + +consent.gather.failed=Failed to gather claims. +consent.gather.invalid.step=Invalid step provided during claims-gathering flow. +consent.gather.invalid.session=Failed to gather claims. Session has expired. +consent.gather.declined=Authorization declined + +up=\u2191 +down=\u2193 +left=\u2039 +right=\u203A + +validator.assertFalse=validation failed +validator.assertTrue=validation failed +validator.future=must be a future date +validator.length=length must be between {min} and {max} +validator.max=must be less than or equal to {value} +validator.min=must be greater than or equal to {value} +validator.notNull=may not be null +validator.past=must be a past date +validator.pattern=must match "{regex}" +validator.range=must be between {min} and {max} +validator.size=size must be between {min} and {max} +validator.email=must be a well-formed email address + +org.jboss.seam.loginFailed=Login failed +org.jboss.seam.loginSuccessful=Welcome, #0! + +org.jboss.seam.TransactionFailed=Transaction failed +org.jboss.seam.NoConversation=The conversation ended, timed out or was processing another request +org.jboss.seam.IllegalNavigation=Illegal navigation +org.jboss.seam.ProcessEnded=Process #0 already ended +org.jboss.seam.ProcessNotFound=Process #0 not found +org.jboss.seam.TaskEnded=Task #0 already ended +org.jboss.seam.TaskNotFound=Task #0 not found +org.jboss.seam.NotLoggedIn=Please log in first + +javax.faces.validator.BeanValidator.MESSAGE={0} +javax.faces.component.UIInput.CONVERSION=value could not be converted to the expected type +javax.faces.component.UIInput.REQUIRED={0} is required +javax.faces.component.UIInput.UPDATE=an error occurred when processing your submitted information +javax.faces.component.UISelectOne.INVALID=value is not valid +javax.faces.component.UISelectMany.INVALID=value is not valid + +javax.faces.converter.BigDecimalConverter.DECIMAL=value must be a number +javax.faces.converter.BigDecimalConverter.DECIMAL_detail=value must be a signed decimal number consisting of zero or more digits, optionally followed by a decimal point and fraction, eg. {1} +javax.faces.converter.BigIntegerConverter.BIGINTEGER=value must be an integer +javax.faces.converter.BigIntegerConverter.BIGINTEGER_detail=value must be a signed integer number consisting of zero or more digits +javax.faces.converter.BooleanConverter.BOOLEAN=value must be true or false +javax.faces.converter.BooleanConverter.BOOLEAN_detail=value must be true or false (any value other than true will evaluate to false) +javax.faces.converter.ByteConverter.BYTE=value must be a number between 0 and 255 +javax.faces.converter.ByteConverter.BYTE_detail=value must be a number between 0 and 255 +javax.faces.converter.CharacterConverter.CHARACTER=value must be a character +javax.faces.converter.CharacterConverter.CHARACTER_detail=value must be a valid ASCII character +javax.faces.converter.DateTimeConverter.DATE=value must be a date +javax.faces.converter.DateTimeConverter.DATE_detail=value must be a date, eg. {1} +javax.faces.converter.DateTimeConverter.TIME=value must be a time +javax.faces.converter.DateTimeConverter.TIME_detail=value must be a time, eg. {1} +javax.faces.converter.DateTimeConverter.DATETIME=value must be a date and time +javax.faces.converter.DateTimeConverter.DATETIME_detail=value must be a date and time, eg. {1} +javax.faces.converter.DateTimeConverter.PATTERN_TYPE=a pattern or type attribute must be specified to convert the value +javax.faces.converter.DoubleConverter.DOUBLE=value must be a number +javax.faces.converter.DoubleConverter.DOUBLE_detail=value must be a number between 4.9E-324 and 1.7976931348623157E308 +javax.faces.converter.EnumConverter.ENUM=value must be convertible to an enum +javax.faces.converter.EnumConverter.ENUM_detail=value must be convertible to an enum or from the enum that contains the constant {1} +javax.faces.converter.EnumConverter.ENUM_NO_CLASS=value must be convertible to an enum or from the enum, but no enum class provided +javax.faces.converter.EnumConverter.ENUM_NO_CLASS_detail=value must be convertible to an enum or from the enum, but no enum class provided +javax.faces.converter.FloatConverter.FLOAT=value must be a number +javax.faces.converter.FloatConverter.FLOAT_detail=value must be a number between 1.4E-45 and 3.4028235E38 +javax.faces.converter.IntegerConverter.INTEGER=value must be an integer +javax.faces.converter.IntegerConverter.INTEGER_detail=value must be an integer number between -2147483648 and 2147483647 +javax.faces.converter.LongConverter.LONG=value must be an integer +javax.faces.converter.LongConverter.LONG_detail=value must be an integer number between -9223372036854775808 and 9223372036854775807 +javax.faces.converter.NumberConverter.CURRENCY=value must be a currency amount +javax.faces.converter.NumberConverter.CURRENCY_detail=value must be a currency amount, eg. {1} +javax.faces.converter.NumberConverter.PERCENT=value must be a percentage amount +javax.faces.converter.NumberConverter.PERCENT_detail=value must be a percentage amount, eg. {1} +javax.faces.converter.NumberConverter.NUMBER=value must be a number +javax.faces.converter.NumberConverter.NUMBER_detail=value must be a number +javax.faces.converter.NumberConverter.PATTERN=value must be a number +javax.faces.converter.NumberConverter.PATTERN_detail=value must be a number +javax.faces.converter.ShortConverter.SHORT=value must be an integer +javax.faces.converter.ShortConverter.SHORT_detail=value must be an integer number between -32768 and 32767 + +javax.faces.validator.DoubleRangeValidator.MAXIMUM=value must be less than or equal to {0} +javax.faces.validator.DoubleRangeValidator.MINIMUM=value must be greater than or equal to {0} +javax.faces.validator.DoubleRangeValidator.NOT_IN_RANGE=value must be between {0} and {1} +javax.faces.validator.DoubleRangeValidator.TYPE=value is not of the correct type +javax.faces.validator.LengthValidator.MAXIMUM=value must be shorter than or equal to {0} characters +javax.faces.validator.LengthValidator.MINIMUM=value must be longer than or equal to {0} characters +javax.faces.validator.LongRangeValidator.MAXIMUM=value must be less than or equal to {0} +javax.faces.validator.LongRangeValidator.MINIMUM=value must be greater than or equal to {0} +javax.faces.validator.LongRangeValidator.NOT_IN_RANGE=value must be between {0} and {1} +javax.faces.validator.LongRangeValidator.TYPE=value is not of the correct type + +javax.faces.validator.NOT_IN_RANGE=value must be between {0} and {1} +javax.faces.converter.STRING=value could not be converted to a string + +cas2login.pageTitle = oxAuth - Login +cas2login.loginHeader = Login (second step) +cas2login.pleaseLoginHere = Please login here +cas2login.username = Username +cas2login.password = Password +cas2login.rememberMe = Remember me +cas2login.termsPrivace = Terms & Privacy + +error.errorEncountered = Error Encountered +error.unexpectedError = An unexpected error has occurred at {0} + +error.invalidSessionState = Invalid session state or session limitation rule has occured at {0} + +cert.failedToCheckCertificate = Failed to check certificate +cert.thisCanHappen = This can happen in the following cases: +cert.youHaveExpired = You have expired certificate.
Please, registered with the system and get new certificate. +cert.youSelectedInvalid = You selected invalid certificate.
Please, restart your browser and try to log in again. +cert.noteInternetExplorer = Note: Internet Explorer and Mozilla Firefox allow to clear the SSL state without restarting the browser: +cert.ifYouAreUsing = If you are using Internet Explorer, you can use the "Clear SSL" button in the settings section: Contents (click Tools \u2192 Internet Options). +cert.ifYouUseFireFox = If you use FireFox, you can use the menu: Options \u2192 Privacy \u2192 Clear all current history \u2192 select \u201cActive Logins\u201d \u2192 press button Clear Now. +cert.youFailedToInstall = You failed to install the personal certificate into the browser
Please, install the certificate from the backup file (.PFX), restart your browser and try to log in again. +cert.youHaveAlreadyOpened = You have already opened the page in this browser but haven't chosen any certificate for further usage. The browser now remembers this choice.
Please, restart your browser, log in again and choose the appropriate certificate.
If you use Internet Explorer, click "Clear SSL state" at Tools/Internet Options/Content. +cert.ifYouUseMacOS = If you use MacOS X and updated the operation system to version 10.5.3 please see http://support.apple.com/kb/HT1679?viewlocale=en_US to resolve the issue. + +duologin.title = oxAuth - DUO Login +duologin.login = DUO Login (second step) +duologin.termsPrivacy = Terms & Privacy + +gpluslogin.title = oxAuth - Google+ Login +gpluslogin.pageTitle = oxAuth - Login +gpluslogin.login = Login (second step) +gpluslogin.pleaseLoginHere = Please login here +gpluslogin.username = Username +gpluslogin.password = Password +gpluslogin.rememberMe = Remember Me +gpluslogin.termsPrivacy = Terms & Privacy + +otp.pageTitle = oxAuth - OTP Login +otp.otpCode = OTP code +otp.scanQRCode = Scan QR code using OTP authenticator and press finish button +otp.finish = Finish + +oxpush.pageTitle = oxAuth oxPush - Login +oxpush.login = oxPush Login (Authentication request) +oxpush.loginPairingRequest = oxPush Login (Pairing request) +oxpush.sendingAuthenticationRequest = Sending authentication request... +oxpush.termsPrivacy = Terms & Privacy +oxpush.title = oxAuth - Login +oxpush.loginLabel = Login +oxpush.pleaseLoginHere = Please login here +oxpush.username = Username +oxpush.password = Password +oxpush.rememberMe = Remember me +oxpush.beforeLogIn = Before log in make sure that you installed oxPush mobile application +oxpush.androidMobileApplication = Android mobile application +oxpush.byProceeding = By proceeding, you agree with the +oxpush.privacyPolicy = Privacy Policy +oxpush.pleaseRead = Please read the +oxpush.termsOfService = Terms of Service +oxpush.checkingPairing = Checking pairing status... +oxpush.waitingForUser = Waiting for user approval... +oxpush.pairingQRCode = Pairing QR code: +oxpush.pairingCode = Pairing code: + +passport.pleaseLoginHere = Please login here +passport.email = Email +passport.termsPrivacy = Terms & Privacy +passport.oxAuthPassportLogin = oxAuth - Passport Login +passport.needAGluuAccount = Need a Gluu account? +passport.forgotYourPassword = Forgot your password? +passport.useExternalAuthentication = Use External Authentication +passport.orUseExternal=OR use an external service to sign in: +passport.registerNewUser = Register new user +passport.javascriptRequired=This pages requires javascript enabled to work properly +passport.fillMissingData=Please provide the following to complete authentication: +passport.invalidMailWarn=Please provide a valid email + +saml.pageTitle = oxAuth - Login +saml.login = Login (second step) +saml.username = Username +saml.password = Password +saml.rememberMe = Remember me +saml.termsPrivacy = Terms & Privacy + +supergluu.scanQRCode = Scan QR code using Super-Gluu + +# Used by twilio and smpp sms 2FA +otp_sms.pageTitle = SMS - Login +otp_sms.verification=2 Step Verification +otp_sms.usedevice=Use your device to sign in to your Gluu account. +otp_sms.verificationcode=Enter a verification code +otp_sms.codesent=A text message with a verification code
\ was sent to +otp_sms.login=Done +otp_sms.termsPrivacy = Terms & Privacy + +uaf.pageTitle = oxAuth - UAF Login +uaf.scanQRCode = Scan QR code using UAF mobile authenticator + +supergluu.enroll.website.url= https://super.gluu.org +supergluu.enroll.Docs.url= https://gluu.org/docs/supergluu +supergluu.enroll.supergluuisfreesecure= Super Gluu is a free and secure two-factor authentication mobile application for all the people in your school, department, organization, or enterprise. +supergluu.enroll.downloadsupergluu= Download Super Gluu: +supergluu.enroll.scanqrcode= Scan the QR code with your Super Gluu app to enroll your device +supergluu.enroll.enrollyourdevice= Enroll your device +supergluu.enroll.Website= Website +supergluu.enroll.Docs=Docs + +u2f.verification.stepverification=2 Step Verification +u2f.verification.usedevice=Use your U2F device to sign in to your Gluu account. +u2f.verification.insertkey = Insert your U2F security key. +u2f.verification.useit=If your U2F key has a button, tap it. Otherwise you can remove it and re-insert it. + +fido2.verification.stepverification=2 Step Verification +fido2.verification.usedevice=Use your fido2 device to sign in to your Gluu account. +fido2.verification.insertkey = Insert your fido2 security key. +fido2.verification.useit=If your fido2 key has a button, tap it. Otherwise you can remove it and re-insert it. + +fido2.touch.verification.usedevice=Use Touch ID on your Apple device to sign in to your Gluu account. +fido2.touch.verification.insertkey = Place your finger on the Touch ID. +fido2.touch.verification.useit=Click Ok to enable the Touch ID. + + +otp.login=Done +otp.verification=2 Step Verification +otp.entercode=Enter a verification code +otp.usedevice=Use your device to sign in to your Gluu account. +otp.getcode=Get a verification code from your OTP mobile app. + +password.validation.invalid = The password provided is not strong enough + +# Gluu Casa +casa.login.title=Casa login +casa.login.panel_title=Welcome +casa.cancel=Cancel +casa.close=Close +casa.proceed=Proceed +casa.alternative=Try another way to sign in +casa.snd_step=2-step verification +casa.enter_code=Enter code + +casa.cert.title=User certificate +casa.cert.text=Use a digital certificate issued to you by a certification authority +casa.cert.precontinue=Before you continue... +casa.cert.hint_1=If you haven't done so, import the certificate issued to you into your web browser +casa.cert.hint_2=When clicking on the proceed button, you will be prompted to select a certificate. Choose carefully +casa.cert.hint_3=If presented, uncheck the option related to remembering your choice +casa.cert.hint_4=If no prompt is shown, your browser is caching a choice made recently. To clear the SSL cache, close the browser window and try again +casa.cert.error.not_valid=Your certificate is not valid +casa.cert.error.unparsable=Your certificate couldn't be processed by the server +casa.cert.error.not_selected=You did not select any certificate +casa.cert.error.cert_enrolled_other_user=The certificate presented is registered to a different account +casa.cert.error.cert_not_recognized=The certificate presented has not been enrolled yet in Casa +casa.cert.error.unknown_user=Inexisting user + +casa.u2f.title=Security key +casa.u2f.text=Use your security key to sign in + +casa.fido2.title=Security key +casa.fido2.text=Use your security key to sign in +casa.fido2.abort_error=Operation cancelled. Use the links below to use a different alternative or to try again +casa.fido2.invalid_state_error=The presented key is not registered for this account. Please try again with a different key +casa.fido2.generic_error=An error occurred. Use the links below to use a different alternative or to try again +casa.fido2.retry_key=Retry security key + + +casa.securitykey.insert=Insert your security key +casa.securitykey.tap=If your key has a button, tap it. Otherwise you can remove it and re-insert it. + +casa.touchid.use=Use the Touch ID on your Apple device. +casa.touchid.tap=Activate and use Touch ID by clicking on the link below. +casa.activate.touchid=Retry security key / Activate Touch ID + +casa.twilio_sms.title=Passcode sent via SMS +casa.twilio_sms.text=We'll send you a one-time passcode to validate your identity + +casa.smpp.title=Passcode sent via SMS +casa.smpp.text=We'll send you a one-time passcode to validate your identity + +casa.sms.enter=Enter the code sent via SMS +casa.sms.choose=Choose a number to send an SMS to: +casa.sms.send=Send + +casa.super_gluu.title=Super Gluu +casa.super_gluu.text=Get a push notification sent to your Super Gluu device +casa.super_gluu.push_approve=A push notification has been sent to your Super Gluu device. Approve to login +casa.super_gluu.not_received_1=Didn't receive the push? +casa.super_gluu.not_received_2=Scan a QR code instead + +casa.otp.title=OTP token +casa.otp.text=Get a verification code from your OTP mobile app or hardware token. + + +#casa plugin- DUO +casa.duo.title=DUO credentials +casa.duo.text=Use your registered DUO credential like SMS passcodes, push notifications or U2F token. + +#casa plugin-BioID +casa.bioid.title=BioID authentication +casa.bioid.text=BioID's face recognition technology for detecting user presence and preventing fraud. + +#casa plugin- Stytch +casa.stytch.title=Stytch Phone credentials +casa.stytch.text=Stytch protects your applications by using a phone OTP to verify user identity before granting access. + +# CIBA +ciba.bindingMessage=Binding Message + +device.authorization.pageTitle=Device Authorization +device.authorization.title=Device Authorization +device.authorization.subtitle=A device is processing permission to connect with your account +device.authorization.code.inputbox.label=Code displayed on your device +device.authorization.confirm.button=Continue +device.authorization.invalid.user.code=Invalid code, please review the code and retry +device.authorization.access.denied.msg=The authorization has been denied, please retry from the device +device.authorization.expired.code.msg=Code expired, please retry from the device +device.authorization.init.new.request.msg=Init new request +device.authorization.access.granted.title=Device authorization granted +device.authorization.authorization.completed.msg=Successful! Required permissions have been granted to the device. +device.authorization.brute.forcing.msg=Too many failed attemps... please init again from device after an hour. + +# Passwordless +pwdless.pageTitle=Max-Security Profile Login +pwdless.username=Enter your username +pwdless.choose=Choose an account: +pwdless.other=Use another account + +# Stytch +stytch.pageTitle = Stytch 2FA Login + +#casa plugin - email otp +casa.email_2fa.title= Email OTP +casa.email_2fa.text=The Email OTP method enables you to authenticate using the one-time password (OTP) that is sent to the registered email address. +casa.email.enter=Enter the code sent via Email +casa.email.choose=Choose an email-id to send an OTP to: +casa.email.send=Send + +#whispeak +whispeak.auth.alternative.instruction=To loggin without using your voice. +whispeak.auth.error.instruction=Try another authentification option. +whispeak.auth.instructions.passport.choose.provider=Please used a previous configured method to authentify yourself. +whispeak.auth.instructions.passport.fallback=You have chosen to use your voice. You can change this choice on next authentification. +whispeak.auth.instructions.passport.verif=First, we need to verify your identity. +whispeak.auth.instructions.record=Please click on mic button (🎙) to start authentication record. +whispeak.auth.instructions.validate=Validate (→) when your reading of the following sentece is finished. +whispeak.modo=Simple, fast et secure. +whispeak.msg2=Please enter your e-mail address. +whispeak.use.voice.auth=Do you want to use authentication by voice? +whispeak.use.voice.auth.question=Do you want to use authentication by voice? +whispeak.welcome=Welcome to WHISPEAK. +whispeak.revocation.title=Instructions to revoke your voice signature: +whispeak.revocation.instruction=Keep note of this information so that you can destroy your voice signature at any time: +whispeak.revocation.revocation_ui_link=Access this link to destroy the voice signature: +whispeak.revocation.revocation_pwd=Confirmation code for destruction: +whispeak.revocation.copytext=Copy +whispeak.revocation.ok=Understood +whispeak.login.signatureDoesNotExist=Your voice signature is invalid or does not exist. Please enroll again. +whispeak.login.2fa.passwordMismatch=Incorrect password. Please try again. +whispeak.login.2fa.password=Please use your password or create it if not having it yet. +whispeak.apiError.voiceAuthFailed=Voice authentication failed, please try again. +whispeak.apiError.badRequest=Technical error: incomplete request. +whispeak.apiError.unauthorized=Technical error: Unauthorized action. +whispeak.apiError.invalidCredential=Server error: Unauthorized action. +whispeak.apiError.signatureNotFound=Unknown voice signature: Please register again. +whispeak.apiError.unsupportedAudioFile=Technical error. Unsupported audio file. +whispeak.apiError.audioConstraintsFailed=The audio clip of your voice is too short or incomplete. +whispeak.apiError.voiceMismatch=Your voice does not match this user's voice. +whispeak.apiError.invalidEnrollSignature=Invalid voice signature: Please enroll again. \ No newline at end of file diff --git a/oxAuth/Server/src/main/resources/oxauth_bg.properties b/oxAuth/Server/src/main/resources/oxauth_bg.properties new file mode 100644 index 00000000..18316af5 --- /dev/null +++ b/oxAuth/Server/src/main/resources/oxauth_bg.properties @@ -0,0 +1,204 @@ +login.pageTitle=oxAuth - Inicio de sesi\u00F3n +login.login=Inicio de sesi\u00F3n +login.pleaseLoginHere=Ingrese los datos de su cuenta para iniciar sesi\u00F3n +login.username=Nombre de usuario +login.password=Contrase\u00F1a +login.rememberMe=Recordar mis datos +login.errorMessage=Ingrese un nombre de usuario y password validos +login.failedToAuthenticate=Failed to authenticate. +login.youDontHavePermission=You don't have permissions. +login.forgotYourPassword = Forgot your password? + +logout.missingParameters=Invalid Request. The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed. +logout.failedToProceed=Failed to process logout + +down = \u2193 + +javax.faces.component.UIInput.CONVERSION = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u043D\u0435 \u043C\u043E\u0436\u0435 \u0434\u0430 \u0431\u044A\u0434\u0435 \u043F\u0440\u0435\u043E\u0431\u0440\u0430\u0437\u0443\u0432\u0430\u043D\u0430 \u043A\u044A\u043C \u043E\u0447\u0430\u043A\u0432\u0430\u043D\u0438\u044F \u0442\u0438\u043F +javax.faces.component.UIInput.REQUIRED = \u043F\u043E\u043B\u0435\u0442\u043E \u0435 \u0437\u0430\u0434\u044A\u043B\u0436\u0438\u0442\u0435\u043B\u043D\u043E +javax.faces.component.UIInput.UPDATE = \u0432\u044A\u0437\u043D\u0438\u043A\u043D\u0430\u043B\u0430 \u0435 \u0433\u0440\u0435\u0448\u043A\u0430 \u043F\u0440\u0438 \u043E\u0431\u0440\u0430\u0431\u043E\u0442\u043A\u0430 \u043D\u0430 \u0438\u0437\u043F\u0440\u0430\u0442\u0435\u043D\u0430\u0442\u0430 \u0438\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u044F +javax.faces.component.UISelectMany.INVALID = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0435 \u043D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u0430 +javax.faces.component.UISelectOne.INVALID = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0435 \u043D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u0430 +javax.faces.converter.BigDecimalConverter.DECIMAL = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0447\u0438\u0441\u043B\u043E +javax.faces.converter.BigDecimalConverter.DECIMAL_detail = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0447\u0438\u0441\u043B\u043E \u0441\u044A\u0441 \u0437\u043D\u0430\u043A, \u0441\u044A\u0441\u0442\u043E\u044F\u0449\u043E \u0441\u0435 \u043E\u0442 \u043D\u0443\u043B\u0430 \u0438\u043B\u0438 \u043F\u043E\u0432\u0435\u0447\u0435 \u0446\u0438\u0444\u0440\u0438, \u0441\u043B\u0435\u0434\u0432\u0430\u043D\u043E \u043F\u043E \u0438\u0437\u0431\u043E\u0440 \u043E\u0442 \u0434\u0435\u0441\u0435\u0442\u0438\u0447\u043D\u0430 \u0437\u0430\u043F\u0435\u0442\u0430\u044F \u0438 \u0434\u0440\u043E\u0431\u043D\u0430 \u0447\u0430\u0441\u0442, \u043D\u0430\u043F\u0440. {1} +javax.faces.converter.BigIntegerConverter.BIGINTEGER = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0446\u044F\u043B\u043E \u0447\u0438\u0441\u043B\u043E +javax.faces.converter.BigIntegerConverter.BIGINTEGER_detail = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0446\u044F\u043B\u043E \u0447\u0438\u0441\u043B\u043E \u0441\u044A\u0441 \u0437\u043D\u0430\u043A, \u0441\u044A\u0441\u0442\u043E\u044F\u0449\u043E \u0441\u0435 \u043E\u0442 \u043D\u0443\u043B\u0430 \u0438\u043B\u0438 \u043F\u043E\u0432\u0435\u0447\u0435 \u0446\u0438\u0444\u0440\u0438 +javax.faces.converter.BooleanConverter.BOOLEAN = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 'true' \u0438\u043B\u0438 'false' +javax.faces.converter.BooleanConverter.BOOLEAN_detail = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 'true' \u0438\u043B\u0438 'false' (\u0432\u0441\u044F\u043A\u0430 \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442 \u0440\u0430\u0437\u043B\u0438\u0447\u043D\u0430 \u043E\u0442 'true' \u0449\u0435 \u0441\u0435 \u0441\u043C\u044F\u0442\u0430 \u0437\u0430 'false') +javax.faces.converter.ByteConverter.BYTE = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0447\u0438\u0441\u043B\u043E \u043C\u0435\u0434\u0436\u0443 0 \u0438 255 +javax.faces.converter.ByteConverter.BYTE_detail = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0447\u0438\u0441\u043B\u043E \u043C\u0435\u0434\u0436\u0443 0 \u0438 255 +javax.faces.converter.CharacterConverter.CHARACTER = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0437\u043D\u0430\u043A +javax.faces.converter.CharacterConverter.CHARACTER_detail = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0432\u0430\u043B\u0438\u0434\u0435\u043D ASCII \u0437\u043D\u0430\u043A +javax.faces.converter.DateTimeConverter.DATE = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0434\u0430\u0442\u0430 +javax.faces.converter.DateTimeConverter.DATETIME = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0434\u0430\u0442\u0430 \u0438 \u0447\u0430\u0441 +javax.faces.converter.DateTimeConverter.DATETIME_detail = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0434\u0430\u0442\u0430 \u0438 \u0447\u0430\u0441, \u043D\u0430\u043F\u0440. {1} +javax.faces.converter.DateTimeConverter.DATE_detail = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0434\u0430\u0442\u0430, \u043D\u0430\u043F\u0440. {1} +javax.faces.converter.DateTimeConverter.PATTERN_TYPE = \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0437\u0430\u0434\u0430\u0434\u0435\u043D \u043E\u0431\u0440\u0430\u0437\u0435\u0446 \u0438\u043B\u0438 \u0430\u0442\u0440\u0438\u0431\u0443\u0442 type, \u0437\u0430 \u0434\u0430 \u0441\u0435 \u043F\u0440\u0435\u043E\u0431\u0440\u0430\u0437\u0443\u0432\u0430 \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 +javax.faces.converter.DateTimeConverter.TIME = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u043E\u0442 \u0442\u0438\u043F \u0447\u0430\u0441 +javax.faces.converter.DateTimeConverter.TIME_detail = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u043E\u0442 \u0442\u0438\u043F \u0447\u0430\u0441, \u043D\u0430\u043F\u0440. {1} +javax.faces.converter.DoubleConverter.DOUBLE = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0447\u0438\u0441\u043B\u043E +javax.faces.converter.DoubleConverter.DOUBLE_detail = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0447\u0438\u0441\u043B\u043E \u043C\u0435\u0436\u0434\u0443 4.9E-324 \u0438 1.7976931348623157E308 +javax.faces.converter.EnumConverter.ENUM = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u043C\u043E\u0436\u0435 \u0434\u0430 \u0441\u0435 \u043F\u0440\u0435\u043E\u0431\u0440\u0430\u0437\u0443\u0432\u0430 \u0434\u043E \u0438\u0437\u0431\u0440\u043E\u0435\u043D \u0442\u0438\u043F +javax.faces.converter.EnumConverter.ENUM_NO_CLASS = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u043C\u043E\u0436\u0435 \u0434\u0430 \u0441\u0435 \u043F\u0440\u0435\u043E\u0431\u0440\u0430\u0437\u0443\u0432\u0430 \u043E\u0442 \u0438\u043B\u0438 \u0434\u043E \u0438\u0437\u0431\u0440\u043E\u0435\u043D \u0442\u0438\u043F, \u043D\u043E \u043D\u0435 \u0435 \u043F\u043E\u0434\u0430\u0434\u0435\u043D \u043A\u043B\u0430\u0441 \u043E\u0442 \u0442\u0430\u043A\u044A\u0432 \u0442\u0438\u043F +javax.faces.converter.EnumConverter.ENUM_NO_CLASS_detail = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u043C\u043E\u0436\u0435 \u0434\u0430 \u0441\u0435 \u043F\u0440\u0435\u043E\u0431\u0440\u0430\u0437\u0443\u0432\u0430 \u043E\u0442 \u0438\u043B\u0438 \u0434\u043E \u0438\u0437\u0431\u0440\u043E\u0435\u043D \u0442\u0438\u043F, \u043D\u043E \u043D\u0435 \u0435 \u043F\u043E\u0434\u0430\u0434\u0435\u043D \u043A\u043B\u0430\u0441 \u043E\u0442 \u0442\u0430\u043A\u044A\u0432 \u0442\u0438\u043F +javax.faces.converter.EnumConverter.ENUM_detail = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u043C\u043E\u0436\u0435 \u0434\u0430 \u0441\u0435 \u043F\u0440\u0435\u043E\u0431\u0440\u0430\u0437\u0443\u0432\u0430 \u043E\u0442 \u0438\u043B\u0438 \u0434\u043E \u0438\u0437\u0431\u0440\u043E\u0435\u043D \u0442\u0438\u043F, \u043A\u043E\u0439\u0442\u043E \u0441\u044A\u0434\u044A\u0440\u0436\u0430 \u043A\u043E\u043D\u0441\u0442\u0430\u043D\u0442\u0430\u0442\u0430 {1} +javax.faces.converter.FloatConverter.FLOAT = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0447\u0438\u0441\u043B\u043E +javax.faces.converter.FloatConverter.FLOAT_detail = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0447\u0438\u0441\u043B\u043E \u043C\u0435\u0436\u0434\u0443 1.4E-45 \u0438 3.4028235E38 +javax.faces.converter.IntegerConverter.INTEGER = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0446\u044F\u043B\u043E \u0447\u0438\u0441\u043B\u043E +javax.faces.converter.IntegerConverter.INTEGER_detail = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0446\u044F\u043B\u043E \u0447\u0438\u0441\u043B\u043E \u043C\u0435\u0436\u0434\u0443 -2147483648 \u0438 2147483647 +javax.faces.converter.LongConverter.LONG = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0446\u044F\u043B\u043E \u0447\u0438\u0441\u043B\u043E +javax.faces.converter.LongConverter.LONG_detail = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0446\u044F\u043B\u043E \u0447\u0438\u0441\u043B\u043E \u043C\u0435\u0436\u0434\u0443 -9223372036854775808 \u0438 9223372036854775807 +javax.faces.converter.NumberConverter.CURRENCY = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0441\u0443\u043C\u0430 \u0432\u044A\u0432 \u0432\u0430\u043B\u0443\u0442\u0430 +javax.faces.converter.NumberConverter.CURRENCY_detail = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0441\u0443\u043C\u0430 \u0432\u044A\u0432 \u0432\u0430\u043B\u0443\u0442\u0430, \u043D\u0430\u043F\u0440. {1} +javax.faces.converter.NumberConverter.NUMBER = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0447\u0438\u0441\u043B\u043E +javax.faces.converter.NumberConverter.NUMBER_detail = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0447\u0438\u0441\u043B\u043E +javax.faces.converter.NumberConverter.PATTERN = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0447\u0438\u0441\u043B\u043E +javax.faces.converter.NumberConverter.PATTERN_detail = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0447\u0438\u0441\u043B\u043E +javax.faces.converter.NumberConverter.PERCENT = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u043F\u0440\u043E\u0446\u0435\u043D\u0442 +javax.faces.converter.NumberConverter.PERCENT_detail = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u043F\u0440\u043E\u0446\u0435\u043D\u0442, \u043D\u0430\u043F\u0440. {1} +javax.faces.converter.STRING = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u043D\u0435 \u043C\u043E\u0436\u0435 \u0434\u0430 \u0431\u044A\u0434\u0435 \u043F\u0440\u0435\u0432\u044A\u0440\u043D\u0430\u0442\u0430 \u0432 \u043D\u0438\u0437 \u043E\u0442 \u0441\u0438\u043C\u0432\u043E\u043B\u0438 +javax.faces.converter.ShortConverter.SHORT = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0446\u044F\u043B\u043E \u0447\u0438\u0441\u043B\u043E +javax.faces.converter.ShortConverter.SHORT_detail = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0446\u044F\u043B\u043E \u0447\u0438\u0441\u043B\u043E \u043C\u0435\u0436\u0434\u0443 -32768 \u0438 32767 +javax.faces.validator.DoubleRangeValidator.MAXIMUM = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u043F\u043E-\u043C\u0430\u043B\u043A\u0430 \u0438\u043B\u0438 \u0440\u0430\u0432\u043D\u0430 \u043D\u0430 {0} +javax.faces.validator.DoubleRangeValidator.MINIMUM = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u043F\u043E-\u0433\u043E\u043B\u044F\u043C\u0430 \u0438\u043B\u0438 \u0440\u0430\u0432\u043D\u0430 \u043D\u0430 {0} +javax.faces.validator.DoubleRangeValidator.NOT_IN_RANGE = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u043C\u0435\u0436\u0434\u0443 {0} \u0438 {1} +javax.faces.validator.DoubleRangeValidator.TYPE = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u043D\u0435 \u0435 \u043E\u0442 \u043F\u0440\u0430\u0432\u0438\u043B\u043D\u0438\u044F \u0432\u0438\u0434 +javax.faces.validator.LengthValidator.MAXIMUM = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u043D\u0435 \u043F\u043E-\u0434\u044A\u043B\u0433\u0430 \u043E\u0442 {0} \u0437\u043D\u0430\u043A\u0430 +javax.faces.validator.LengthValidator.MINIMUM = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u043F\u043E-\u0434\u044A\u043B\u0433\u0430 \u043E\u0442 {0} \u0437\u043D\u0430\u043A\u0430 +javax.faces.validator.LongRangeValidator.MAXIMUM = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u043F\u043E-\u043C\u0430\u043B\u043A\u0430 \u0438\u043B\u0438 \u0440\u0430\u0432\u043D\u0430 \u043D\u0430 {0} +javax.faces.validator.LongRangeValidator.MINIMUM = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u043F\u043E-\u0433\u043E\u043B\u044F\u043C\u0430 \u0438\u043B\u0438 \u0440\u0430\u0432\u043D\u0430 \u043D\u0430 {0} +javax.faces.validator.LongRangeValidator.NOT_IN_RANGE = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u043C\u0435\u0436\u0434\u0443 {0} \u0438 {1} +javax.faces.validator.LongRangeValidator.TYPE = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u043D\u0435 \u0435 \u043E\u0442 \u043F\u0440\u0430\u0432\u0438\u043B\u043D\u0438\u044F \u0432\u0438\u0434 +javax.faces.validator.NOT_IN_RANGE = \u0441\u0442\u043E\u0439\u043D\u043E\u0441\u0442\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u043C\u0435\u0436\u0434\u0443 {0} \u0438 {1} + +left = \u2039 + +org.jboss.seam.IllegalNavigation = \u041D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u0430 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044F +org.jboss.seam.NoConversation = \u0420\u0430\u0437\u0433\u043E\u0432\u043E\u0440\u044A\u0442 \u0435 \u043F\u0440\u0438\u043A\u043B\u044E\u0447\u0438\u043B, \u0438\u0437\u0442\u0435\u043A\u043B\u043E \u043C\u0443 \u0435 \u0432\u0440\u0435\u043C\u0435\u0442\u043E \u0438\u043B\u0438 \u043E\u0431\u0440\u0430\u0431\u043E\u0442\u0432\u0430 \u0434\u0440\u0443\u0433\u0430 \u0437\u0430\u044F\u0432\u043A\u0430 +org.jboss.seam.ProcessEnded = \u041F\u0440\u043E\u0446\u0435\u0441 #0 \u0435 \u043F\u0440\u0438\u043A\u043B\u044E\u0447\u0438\u043B +org.jboss.seam.ProcessNotFound = \u041F\u0440\u043E\u0446\u0435\u0441 #0 \u043D\u0435 \u0435 \u043D\u0430\u043C\u0435\u0440\u0435\u043D +org.jboss.seam.TaskEnded = \u0417\u0430\u0434\u0430\u0447\u0430 #0 \u0435 \u043F\u0440\u0438\u043A\u043B\u044E\u0447\u0438\u043B\u0430 +org.jboss.seam.TaskNotFound = \u0417\u0430\u0434\u0430\u0447\u0430 #0 \u043D\u0435 \u0435 \u043D\u0430\u043C\u0435\u0440\u0435\u043D\u0430 +org.jboss.seam.TransactionFailed = \u0422\u0440\u0430\u043D\u0437\u0430\u043A\u0446\u0438\u044F \u043D\u0435 \u0435 \u0443\u0441\u043F\u044F\u043B\u0430 +org.jboss.seam.NotLoggedIn = \u041C\u043E\u043B\u044F \u043F\u044A\u0440\u0432\u043E \u0441\u0435 \u0438\u0434\u0435\u043D\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u0430\u0439\u0442\u0435 + +org.jboss.seam.loginFailed = \u041D\u0435\u0443\u0441\u043F\u0435\u0448\u0435\u043D \u0432\u0445\u043E\u0434 +org.jboss.seam.loginSuccessful = \u0417\u0434\u0440\u0430\u0432\u0435\u0439, #0 + +right = \u203A + +up = \u2191 + +validator.assertFalse = \u0433\u0440\u0435\u0448\u043A\u0438 \u043F\u0440\u0438 \u0432\u0430\u043B\u0438\u0434\u0430\u0446\u0438\u044F +validator.assertTrue = \u0433\u0440\u0435\u0448\u043A\u0438 \u043F\u0440\u0438 \u0432\u0430\u043B\u0438\u0434\u0430\u0446\u0438\u044F +validator.email = \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0432\u0430\u043B\u0438\u0434\u0435\u043D e-mail \u0430\u0434\u0440\u0435\u0441 +validator.future = \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0431\u044A\u0434\u0435\u0449\u0430 \u0434\u0430\u0442\u0430 +validator.length = \u0434\u044A\u043B\u0436\u0438\u043D\u0430\u0442\u0430 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u043C\u0435\u0436\u0434\u0443 {min} \u0438 {max} +validator.max = \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u043F\u043E-\u043C\u0430\u043B\u043A\u043E \u0438\u043B\u0438 \u0440\u0430\u0432\u043D\u043E \u043D\u0430 {value} +validator.min = \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u043F\u043E-\u0433\u043E\u043B\u044F\u043C\u043E \u0438\u043B\u0438 \u0440\u0430\u0432\u043D\u043E \u043D\u0430 {value} +validator.notNull = \u0435 \u0437\u0430\u0434\u044A\u043B\u0436\u0438\u0442\u0435\u043B\u043D\u043E +validator.past = \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0435 \u043C\u0438\u043D\u0430\u043B\u0430 \u0434\u0430\u0442\u0430\u0442\u0430 +validator.pattern = \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0441\u044A\u043E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0430 \u043D\u0430 "{regex}" +validator.range = \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0435 \u043C\u0435\u0436\u0434\u0443 {min} \u0438 {max} +validator.size = \u0440\u0430\u0437\u043C\u0435\u0440\u044A\u0442 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0435 \u043C\u0435\u0436\u0434\u0443 {min} \u0438 {max} + +cas2login.pageTitle = oxAuth - Login +cas2login.loginHeader = Login (second step) +cas2login.pleaseLoginHere = Please login here +cas2login.username = Username +cas2login.password = Password +cas2login.rememberMe = Remember me +cas2login.termsPrivace = Terms & Privacy + +error.errorEncountered = Error Encountered +error.unexpectedError = An unexpected error has occurred at {0} + +cert.failedToCheckCertificate = Failed to check certificate +cert.thisCanHappen = This can happen in the following cases: +cert.youHaveExpired = You have expired certificate.
Please, registered with the system and get new certificate. +cert.youSelectedInvalid = You selected invalid certificate.
Please, restart your browser and try to log in again. +cert.noteInternetExplorer = Note: Internet Explorer and Mozilla Firefox allow to clear the SSL state without restarting the browser: +cert.ifYouAreUsing = If you are using Internet Explorer, you can use the "Clear SSL" button in the settings section: Contents (click Tools \u2192 Internet Options). +cert.ifYouUseFireFox = If you use FireFox, you can use the menu: Options \u2192 Privacy \u2192 Clear all current history \u2192 select \u201cActive Logins\u201d \u2192 press button Clear Now. +cert.youFailedToInstall = You failed to install the personal certificate into the browser
Please, install the certificate from the backup file (.PFX), restart your browser and try to log in again. +cert.youHaveAlreadyOpened = You have already opened the page in this browser but haven't chosen any certificate for further usage. The browser now remembers this choice.
Please, restart your browser, log in again and choose the appropriate certificate.
If you use Internet Explorer, click "Clear SSL state" at Tools/Internet Options/Content. +cert.ifYouUseMacOS = If you use MacOS X and updated the operation system to version 10.5.3 please see http://support.apple.com/kb/HT1679?viewlocale=en_US to resolve the issue. + +duologin.title = oxAuth - DUO Login +duologin.login = DUO Login (second step) +duologin.termsPrivacy = Terms & Privacy + +gpluslogin.title = oxAuth - Google+ Login +gpluslogin.pageTitle = oxAuth - Login +gpluslogin.login = Login (second step) +gpluslogin.pleaseLoginHere = Please login here +gpluslogin.username = Username +gpluslogin.password = Password +gpluslogin.rememberMe = Remember Me +gpluslogin.termsPrivacy = Terms & Privacy + +otp.pageTitle = oxAuth - OTP Login +otp.otpCode = OTP code +otp.scanQRCode = Scan QR code using OTP authenticator and press finish button +otp.finish = Finish + +oxpush.pageTitle = oxAuth oxPush - Login +oxpush.login = oxPush Login (Authentication request) +oxpush.loginPairingRequest = oxPush Login (Pairing request) +oxpush.sendingAuthenticationRequest = Sending authentication request... +oxpush.termsPrivacy = Terms & Privacy +oxpush.title = oxAuth - Login +oxpush.loginLabel = Login +oxpush.pleaseLoginHere = Please login here +oxpush.username = Username +oxpush.password = Password +oxpush.rememberMe = Remember me +oxpush.beforeLogIn = Before log in make sure that you installed oxPush mobile application +oxpush.androidMobileApplication = Android mobile application +oxpush.byProceeding = By proceeding, you agree with the +oxpush.privacyPolicy = Privacy Policy +oxpush.pleaseRead = Please read the +oxpush.termsOfService = Terms of Service +oxpush.checkingPairing = Checking pairing status... +oxpush.waitingForUser = Waiting for user approval... +oxpush.pairingQRCode = Pairing QR code: +oxpush.pairingCode = Pairing code: + +passport.pageTitle = Passport - PostLogin +passport.loginSecondStep = Login (second step) +passport.pleaseLoginHere = Please login here +passport.email = Email +passport.termsPrivacy = Terms & Privacy +passport.oxAuthPassportLogin = oxAuth - Passport Login +passport.needAGluuAccount = Need a Gluu account? +passport.forgotYourPassword = Forgot your password? +passport.useExternalAuthentication = Use External Authentification +passport.registerNewUser = Register new user + +saml.pageTitle = oxAuth - Login +saml.login = Login (second step) +saml.username = Username +saml.password = Password +saml.rememberMe = Remember me +saml.termsPrivacy = Terms & Privacy + +supergluu.scanQRCode = Scan QR code using Super-Gluu + +uaf.pageTitle = oxAuth - UAF Login +uaf.scanQRCode = Scan QR code using UAF mobile authenticator + +# CIBA +ciba.bindingMessage=Binding Message + +device.authorization.pageTitle=\u0423\u043f\u044a\u043b\u043d\u043e\u043c\u043e\u0449\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e +device.authorization.title=\u0423\u043f\u044a\u043b\u043d\u043e\u043c\u043e\u0449\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e +device.authorization.subtitle=\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0432\u0430 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u0437\u0430 \u0432\u0440\u044a\u0437\u043a\u0430 \u0441 \u0432\u0430\u0448\u0438\u044f \u0430\u043a\u0430\u0443\u043d\u0442 +device.authorization.code.inputbox.label=\u041a\u043e\u0434\u002c \u043f\u043e\u043a\u0430\u0437\u0430\u043d \u043d\u0430 \u0432\u0430\u0448\u0435\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e +device.authorization.confirm.button=\u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438 +device.authorization.invalid.user.code=\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u043a\u043e\u0434\u002c \u043c\u043e\u043b\u044f\u002c \u043f\u0440\u0435\u0433\u043b\u0435\u0434\u0430\u0439\u0442\u0435 \u043a\u043e\u0434\u0430 \u0438 \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e +device.authorization.access.denied.msg=\u041e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f\u0442\u0430 \u0435 \u043e\u0442\u043a\u0430\u0437\u0430\u043d\u0430\u002c \u043c\u043e\u043b\u044f\u002c \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043e\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e +device.authorization.expired.code.msg=\u041a\u043e\u0434\u044a\u0442 \u0438\u0437\u0442\u0435\u0447\u0435\u002c \u043c\u043e\u043b\u044f\u002c \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043e\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e +device.authorization.init.new.request.msg=\u0418\u0437\u043f\u0440\u0430\u0442\u0435\u0442\u0435 \u043d\u043e\u0432\u0430 \u0437\u0430\u044f\u0432\u043a\u0430 +device.authorization.access.granted.title=\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0438\u0435\u0442\u043e \u0437\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0435\u043d\u043e +device.authorization.authorization.completed.msg=\u0423\u0441\u043f\u0435\u0448\u0435\u043d\u0021 \u041d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0441\u0430 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0435\u043d\u0438 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0438\u0442\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0438\u044f\u002e +device.authorization.brute.forcing.msg=\u0422\u0432\u044a\u0440\u0434\u0435 \u043c\u043d\u043e\u0433\u043e \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0438 \u043e\u043f\u0438\u0442\u0438 \u002e\u002e\u002e \u043c\u043e\u043b\u044f\u002c \u0438\u043d\u0438\u0446\u0438\u0438\u0440\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043e\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0441\u043b\u0435\u0434 \u0447\u0430\u0441\u002e \ No newline at end of file diff --git a/oxAuth/Server/src/main/resources/oxauth_de.properties b/oxAuth/Server/src/main/resources/oxauth_de.properties new file mode 100644 index 00000000..5dffd2d3 --- /dev/null +++ b/oxAuth/Server/src/main/resources/oxauth_de.properties @@ -0,0 +1,210 @@ +up=\u2191 +down=\u2193 +left=\u2039 +right=\u203A + +login.pageTitle=oxAuth - Anmeldung +login.login=Anmelden +login.pleaseLoginHere=Bitte hier anmelden +login.username=Benutzername +login.password=Passwort +login.rememberMe=Anmeldedaten speichern +login.errorMessage=Falscher Benutzername oder falsches Passwort. +login.failedToAuthenticate=Authentifizierung fehlgeschlagen +login.youDontHavePermission=Sie haben keine Berechtigung. +login.forgotYourPassword=Passwort vergessen? + +logout.missingParameters=Ung\u00FCltige Anfrage. Ein erforderlicher Parameter fehlt, ist nicht unterstg\u00FCtzt oder die Anfrage ist anderweitig fehlerhaft. +logout.failedToProceed=Abmeldung kann nicht verarbeitet werden + +validator.assertFalse=Validierung fehlgeschlagen +validator.assertTrue=Validierung fehlgeschlagen +validator.future=muss einem zuk\u00FCnfitigen Datum entsprechen +validator.length=die L\u00E4nge muss zwischen {min} und {max} liegen +validator.max=muss kleiner oder gleich {value} sein +validator.min=muss gr\u00F6\u00DFer oder gleich {value} sein +validator.notNull=darf nicht leer sein +validator.past=muss einem vergangenen Datum entsprechen +validator.pattern=muss dem regul\u00E4ren Ausdruck "{regex}" entsprechen +validator.range=muss im Wertebereich von {min} bis {max} liegen +validator.size=die Gr\u00F6\u00DFe muss zischen {min} und {max} liegen +validator.email=muss einer wohlgeformten E-Mailadresse entsprechen + +org.jboss.seam.loginFailed=Anmeldung fehlgeschlagen +org.jboss.seam.loginSuccessful=Willkommen, #0! + +org.jboss.seam.TransactionFailed=Transaktion fehlgeschlagen +org.jboss.seam.NoConversation=Der Vorgang wurde bereits beendet, verarbeitet eine andere Abfrage oder ergab eine Zeit\u00FCberschreitung +org.jboss.seam.IllegalNavigation=Unzul\u00E4ssige Navigation +org.jboss.seam.ProcessEnded=Prozess #0 wurde bereits beendet +org.jboss.seam.ProcessNotFound=Prozess #0 nicht gefunden +org.jboss.seam.TaskEnded=Funktion #0 wurde bereits beendet +org.jboss.seam.TaskNotFound=Funktion #0 nicht gefunden +org.jboss.seam.NotLoggedIn=Bitte melden Sie sich zun\u00E4chst an + +javax.faces.component.UIInput.CONVERSION=Wert konnte nicht in den erwarteten Typ umgewandelt werden +javax.faces.component.UIInput.REQUIRED=Wert erforderlich +javax.faces.component.UIInput.UPDATE=ein Fehler ist bei der Verarbeitung der von Ihnen gesendeten Daten aufgetreten +javax.faces.component.UISelectOne.INVALID=Wert ung\u00FCltig +javax.faces.component.UISelectMany.INVALID=Wert ung\u00FCltig + +javax.faces.converter.BigDecimalConverter.DECIMAL = ''{0}'' muss eine Dezimalzahl sein. +javax.faces.converter.BigDecimalConverter.DECIMAL_detail = ''{0}'' muss eine Dezimalzahl aus keinem oder mehr Zeichen gefolgt von einem optionalen Punkt und den Nachkommastellen sein. Beispiel: {1} +javax.faces.converter.BigIntegerConverter.BIGINTEGER = ''{0}'' muss eine Zahl aus ein oder mehr Ziffern sein. +javax.faces.converter.BigIntegerConverter.BIGINTEGER_detail = ''{0}'' muss eine Zahl aus ein oder mehr Ziffern sein. Beispiel: {1} +javax.faces.converter.BooleanConverter.BOOLEAN = ''{0}'' muss 'true' oder 'false' sein. +javax.faces.converter.BooleanConverter.BOOLEAN_detail = ''{0}'' muss 'true' oder 'false' sein. Jeder Wert au\u00DFer 'true' wird als 'false' interpretiert. +javax.faces.converter.ByteConverter.BYTE = ''{0}'' muss eine Zahl zwischen 0 und 255 sein. +javax.faces.converter.ByteConverter.BYTE_detail = ''{0}'' muss eine Zahl zwischen 0 und 255 sein. Beispiel: {1} +javax.faces.converter.CharacterConverter.CHARACTER = ''{0}'' muss ein g\u00FCltiges Zeichen sein. +javax.faces.converter.CharacterConverter.CHARACTER_detail = ''{0}'' muss ein g\u00FCltiges ASCII Zeichen sein. +javax.faces.converter.DateTimeConverter.DATE = ''{0}'' konnte nicht als Datum erkannt werden. +javax.faces.converter.DateTimeConverter.DATE_detail = ''{0}'' konnte nicht als Datum erkannt werden. Beispiel: {1} +javax.faces.converter.DateTimeConverter.TIME = ''{0}'' konnte nicht als Zeit erkannt werden. +javax.faces.converter.DateTimeConverter.TIME_detail = ''{0}'' konnte nicht als Zeit erkannt werden. Beispiel: {1} +javax.faces.converter.DateTimeConverter.DATETIME = ''{0}'' konnte nicht als Datum und Zeit erkannt werden. +javax.faces.converter.DateTimeConverter.DATETIME_detail = ''{0}'' konnte nicht als Datum und Zeit erkannt werden. Beispiel: {1} +javax.faces.converter.DateTimeConverter.PATTERN_TYPE = Ein 'pattern' oder 'type' Attribut muss angegeben werden um den Wert ''{0}'' zu konvertieren. +javax.faces.converter.DoubleConverter.DOUBLE = ''{0}'' muss eine Zahl aus ein oder mehr Ziffern sein. +javax.faces.converter.DoubleConverter.DOUBLE_detail = ''{0}'' muss eine Zahl zwischen 4.9E-324 und 1.7976931348623157E308 sein. Beispiel: {1} +javax.faces.converter.EnumConverter.ENUM = ''{0}'' muss in eine Enumeration konvertierbar sein. +javax.faces.converter.EnumConverter.ENUM_detail = ''{0}'' muss in eine Enumeration konvertierbar sein, welche die Konstante ''{1}'' enth\u00E4lt. +javax.faces.converter.EnumConverter.ENUM_NO_CLASS = ''{0}'' muss in eine Enumeration konvertierbar sein, aber es wurde keine Klasse des Typs Enum bereitgestellt. +javax.faces.converter.EnumConverter.ENUM_NO_CLASS_detail = ''{0}'' muss in eine Enumeration konvertierbar sein, aber es wurde keine Klasse des Typs Enum bereitgestellt. +javax.faces.converter.FloatConverter.FLOAT = ''{0}'' muss eine Zahl aus ein oder mehr Ziffern sein. +javax.faces.converter.FloatConverter.FLOAT_detail = ''{0}'' muss eine Zahl zwischen 1.4E-45 und 3.4028235E38 sein. Beispiel: {1} +javax.faces.converter.IntegerConverter.INTEGER = ''{0}'' muss eine Zahl aus ein oder mehr Ziffern sein. +javax.faces.converter.IntegerConverter.INTEGER_detail = ''{0}'' muss eine Zahl zwischen -2147483648 und 2147483647 sein. Beispiel: {1} +javax.faces.converter.LongConverter.LONG = ''{0}'' muss eine Zahl aus ein oder mehr Ziffern sein. +javax.faces.converter.LongConverter.LONG_detail = ''{0}'' muss eine Zahl zwischen -9223372036854775808 und 9223372036854775807 sein. Beispiel: {1} +javax.faces.converter.NumberConverter.CURRENCY = ''{0}'' konnte nicht als ein Geldbetrag erkannt werden. +javax.faces.converter.NumberConverter.CURRENCY_detail = ''{0}'' konnte nicht als ein Geldbetrag erkannt werden. Beispiel: {1} +javax.faces.converter.NumberConverter.PERCENT = ''{0}'' konnte nicht als ein Prozentanteil erkannt werden. +javax.faces.converter.NumberConverter.PERCENT_detail = ''{0}'' konnte nicht als ein Prozentanteil erkannt werden. Beispiel: {1} +javax.faces.converter.NumberConverter.NUMBER = ''{0}'' ist keine Zahl. +javax.faces.converter.NumberConverter.NUMBER_detail = ''{0}'' ist keine Zahl. Beispiel: {1} +javax.faces.converter.NumberConverter.PATTERN = ''{0}'' ist kein Zahlmuster. +javax.faces.converter.NumberConverter.PATTERN_detail = ''{0}'' ist kein Zahlmuster. Beispiel: {1} +javax.faces.converter.ShortConverter.SHORT = ''{0}'' muss eine Zahl aus ein oder mehr Ziffern sein. +javax.faces.converter.ShortConverter.SHORT_detail = ''{0}'' muss eine Zahl zwischen -32768 und 32767 sein. Beispiel: {1} +javax.faces.converter.STRING = Konnte ''{0}'' nicht in eine Zeichenkette konvertieren. + +javax.faces.validator.NOT_IN_RANGE = Validierungsfehler: Der Wert liegt nicht im erwarteten Wertebereich von {0} bis {1}. +javax.faces.validator.DoubleRangeValidator.MAXIMUM = Validierungsfehler: Der Wert ist gr\u00F6\u00DFer als das erlaubte Maximum von ''{0}'' +javax.faces.validator.DoubleRangeValidator.MINIMUM = Validierungsfehler: Der Wert ist kleiner als das erlaubte Minimum von ''{0}'' +javax.faces.validator.DoubleRangeValidator.NOT_IN_RANGE = Validierungsfehler: Der Wert ist nicht zwischen den erwarteten Werten von {0} und {1} +javax.faces.validator.DoubleRangeValidator.TYPE = Validierungsfehler: Der Wert ist nicht vom korrekten Typ +javax.faces.validator.LengthValidator.MAXIMUM = Validierungsfehler: Der Wert ist gr\u00F6\u00DFer als das erlaubte Maximum von ''{0}'' +javax.faces.validator.LengthValidator.MINIMUM = Validierungsfehler: Der Wert ist kleiner als das erlaubte Minimum von ''{0}'' +javax.faces.validator.LongRangeValidator.MAXIMUM = Validierungsfehler: Der Wert ist gr\u00F6\u00DFer als das erlaubte Maximum von ''{0}'' +javax.faces.validator.LongRangeValidator.MINIMUM = Validation Error: Der Wert ist kleiner als das erlaubte Minimum von ''{0}'' +javax.faces.validator.LongRangeValidator.NOT_IN_RANGE = Validierungsfehler: Der Wert ist nicht zwischen den erwarteten Werten von {0} und {1}. +javax.faces.validator.LongRangeValidator.TYPE = Validierungsfehler: Der Wert ist nicht vom korrekten Typ. + +cas2login.pageTitle = oxAuth - Login +cas2login.loginHeader = Login (second step) +cas2login.pleaseLoginHere = Please login here +cas2login.username = Username +cas2login.password = Password +cas2login.rememberMe = Remember me +cas2login.termsPrivace = Terms & Privacy + +error.errorEncountered = Error Encountered +error.unexpectedError = An unexpected error has occurred at {0} + +cert.failedToCheckCertificate = Failed to check certificate +cert.thisCanHappen = This can happen in the following cases: +cert.youHaveExpired = You have expired certificate.
Please, registered with the system and get new certificate. +cert.youSelectedInvalid = You selected invalid certificate.
Please, restart your browser and try to log in again. +cert.noteInternetExplorer = Note: Internet Explorer and Mozilla Firefox allow to clear the SSL state without restarting the browser: +cert.ifYouAreUsing = If you are using Internet Explorer, you can use the "Clear SSL" button in the settings section: Contents (click Tools \u2192 Internet Options). +cert.ifYouUseFireFox = If you use FireFox, you can use the menu: Options \u2192 Privacy \u2192 Clear all current history \u2192 select \u201cActive Logins\u201d \u2192 press button Clear Now. +cert.youFailedToInstall = You failed to install the personal certificate into the browser
Please, install the certificate from the backup file (.PFX), restart your browser and try to log in again. +cert.youHaveAlreadyOpened = You have already opened the page in this browser but haven't chosen any certificate for further usage. The browser now remembers this choice.
Please, restart your browser, log in again and choose the appropriate certificate.
If you use Internet Explorer, click "Clear SSL state" at Tools/Internet Options/Content. +cert.ifYouUseMacOS = If you use MacOS X and updated the operation system to version 10.5.3 please see http://support.apple.com/kb/HT1679?viewlocale=en_US to resolve the issue. + +duologin.title = oxAuth - DUO Login +duologin.login = DUO Login (second step) +duologin.termsPrivacy = Terms & Privacy + +gpluslogin.title = oxAuth - Google+ Login +gpluslogin.pageTitle = oxAuth - Login +gpluslogin.login = Login (second step) +gpluslogin.pleaseLoginHere = Please login here +gpluslogin.username = Username +gpluslogin.password = Password +gpluslogin.rememberMe = Remember Me +gpluslogin.termsPrivacy = Terms & Privacy + +otp.pageTitle = oxAuth - OTP Login +otp.otpCode = OTP code +otp.scanQRCode = Scan QR code using OTP authenticator and press finish button +otp.finish = Finish + +oxpush.pageTitle = oxAuth oxPush - Login +oxpush.login = oxPush Login (Authentication request) +oxpush.loginPairingRequest = oxPush Login (Pairing request) +oxpush.sendingAuthenticationRequest = Sending authentication request... +oxpush.termsPrivacy = Terms & Privacy +oxpush.title = oxAuth - Login +oxpush.loginLabel = Login +oxpush.pleaseLoginHere = Please login here +oxpush.username = Username +oxpush.password = Password +oxpush.rememberMe = Remember me +oxpush.beforeLogIn = Before log in make sure that you installed oxPush mobile application +oxpush.androidMobileApplication = Android mobile application +oxpush.byProceeding = By proceeding, you agree with the +oxpush.privacyPolicy = Privacy Policy +oxpush.pleaseRead = Please read the +oxpush.termsOfService = Terms of Service +oxpush.checkingPairing = Checking pairing status... +oxpush.waitingForUser = Waiting for user approval... +oxpush.pairingQRCode = Pairing QR code: +oxpush.pairingCode = Pairing code: + +passport.pageTitle = Passport - PostLogin +passport.loginSecondStep = Login (second step) +passport.pleaseLoginHere = Please login here +passport.email = Email +passport.termsPrivacy = Terms & Privacy +passport.oxAuthPassportLogin = oxAuth - Passport Login +passport.needAGluuAccount = Need a Gluu account? +passport.forgotYourPassword = Forgot your password? +passport.useExternalAuthentication = Use External Authentification +passport.registerNewUser = Register new user + +saml.pageTitle = oxAuth - Login +saml.login = Login (second step) +saml.username = Username +saml.password = Password +saml.rememberMe = Remember me +saml.termsPrivacy = Terms & Privacy + +supergluu.scanQRCode = Scan QR code using Super-Gluu + +fido2.verification.stepverification=2 Step Verification +fido2.verification.usedevice=Use your fido2 device to sign in to your Gluu account. +fido2.verification.insertkey = Insert your fido2 security key. +fido2.verification.useit=If your fido2 key has a button, tap it. Otherwise you can remove it and re-insert it. + +uaf.pageTitle = oxAuth - UAF Login +uaf.scanQRCode = Scan QR code using UAF mobile authenticator + +password.validation.invalid = The password provide is not strong enough + +# CIBA +ciba.bindingMessage=Binding Message + +device.authorization.pageTitle=Ger\u00E4teberechtigung +device.authorization.title=Ger\u00E4teberechtigung +device.authorization.subtitle=Ein Ger\u00E4t verarbeitet die Berechtigung zum Herstellen einer Verbindung mit Ihrem Konto +device.authorization.code.inputbox.label=Auf Ihrem Ger\u00E4t angezeigter Code +device.authorization.confirm.button=Fortsetzen +device.authorization.invalid.user.code=Ung\u00FCltiger Code, \u00DCberpr\u00FCfen Sie den Code und versuchen Sie es erneut +device.authorization.access.denied.msg=Die Autorisierung wurde verweigert. Bitte versuchen Sie es erneut vom Ger\u00E4t +device.authorization.expired.code.msg=Code abgelaufen, bitte versuchen Sie es erneut vom Ger\u00E4t +device.authorization.init.new.request.msg=Neue Anfrage initialisieren +device.authorization.access.granted.title=Ger\u00E4teberechtigung erteilt +device.authorization.authorization.completed.msg=Erfolgreich! Erforderliche Berechtigungen wurden dem Ger\u00E4t erteilt. +device.authorization.brute.forcing.msg=Zu viele fehlgeschlagene Versuche ... bitte nach einer Stunde erneut vom Ger\u00e4t aus starten. \ No newline at end of file diff --git a/oxAuth/Server/src/main/resources/oxauth_en.properties b/oxAuth/Server/src/main/resources/oxauth_en.properties new file mode 100644 index 00000000..0770f8d1 --- /dev/null +++ b/oxAuth/Server/src/main/resources/oxauth_en.properties @@ -0,0 +1,341 @@ +common.copyright=Powered by +common.allRightsReserved=Free and open source access management. +common.agreePolicy=By proceeding, you agree with the {0} +common.privacyPolicy=Privacy Policy +common.pleaseReadTheTos=Please read the +common.termsOfService=Terms of Service +common.gluuInc=Gluu, Inc +common.caution=Use subject to MIT LICENSE + +login.pageTitle=oxAuth - Login +login.login=Login +login.loginAsAnother=Login as another user +login.selectAccount=Select Account +login.register=Register +login.pleaseLoginHere=Please login here +login.username=Username +login.password=Password +login.rememberMe=Remember me +login.errorMessage=Incorrect username or password. +login.errorSessionInvalidMessage=Failed to authenticate. Authentication session has expired. Please navigate back to the original page and try again. +login.failedToAuthenticate=Failed to authenticate. +login.failedToGrantPermission=Failed to grant permission. +login.failedToDeny=Failed to deny authorization. +login.userAlreadyAuthenticated=User is already authenticated. Re-send authorization request (must be handled by RP). +login.youDontHavePermission=You don't have permissions. +login.forgotYourPassword = Forgot your password? + +selectAccount.pageTitle=Select Account + +logout.missingParameters=Invalid Request. The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed. +logout.failedToProceed=Failed to process logout + +authorize.pageTitle=oxAuth - Authorize +authorize.requestForPermission=Request for Permission +authorize.requestingPermissionForScopes={0} is requesting permission to do the following: +authorize.viewIndividualClaim=View {0} claim. +authorize.allow=Allow +authorize.doNotAllow=Don't Allow + +uma2.gather.failed=Failed to gather claims. +uma2.invalid.step=Invalid step provided during claims-gathering flow. +uma2.invalid.session=Failed to gather claims. Session has expired. + +consent.gather.failed=Failed to gather claims. +consent.gather.invalid.step=Invalid step provided during claims-gathering flow. +consent.gather.invalid.session=Failed to gather claims. Session has expired. +consent.gather.declined=Authorization declined + +up=\u2191 +down=\u2193 +left=\u2039 +right=\u203A + +validator.assertFalse=validation failed +validator.assertTrue=validation failed +validator.future=must be a future date +validator.length=length must be between {min} and {max} +validator.max=must be less than or equal to {value} +validator.min=must be greater than or equal to {value} +validator.notNull=may not be null +validator.past=must be a past date +validator.pattern=must match "{regex}" +validator.range=must be between {min} and {max} +validator.size=size must be between {min} and {max} +validator.email=must be a well-formed email address + +org.jboss.seam.loginFailed=Login failed +org.jboss.seam.loginSuccessful=Welcome, #0! + +org.jboss.seam.TransactionFailed=Transaction failed +org.jboss.seam.NoConversation=The conversation ended, timed out or was processing another request +org.jboss.seam.IllegalNavigation=Illegal navigation +org.jboss.seam.ProcessEnded=Process #0 already ended +org.jboss.seam.ProcessNotFound=Process #0 not found +org.jboss.seam.TaskEnded=Task #0 already ended +org.jboss.seam.TaskNotFound=Task #0 not found +org.jboss.seam.NotLoggedIn=Please log in first + +javax.faces.validator.BeanValidator.MESSAGE={0} +javax.faces.component.UIInput.CONVERSION=value could not be converted to the expected type +javax.faces.component.UIInput.REQUIRED={0} is required +javax.faces.component.UIInput.UPDATE=an error occurred when processing your submitted information +javax.faces.component.UISelectOne.INVALID=value is not valid +javax.faces.component.UISelectMany.INVALID=value is not valid + +javax.faces.converter.BigDecimalConverter.DECIMAL=value must be a number +javax.faces.converter.BigDecimalConverter.DECIMAL_detail=value must be a signed decimal number consisting of zero or more digits, optionally followed by a decimal point and fraction, eg. {1} +javax.faces.converter.BigIntegerConverter.BIGINTEGER=value must be an integer +javax.faces.converter.BigIntegerConverter.BIGINTEGER_detail=value must be a signed integer number consisting of zero or more digits +javax.faces.converter.BooleanConverter.BOOLEAN=value must be true or false +javax.faces.converter.BooleanConverter.BOOLEAN_detail=value must be true or false (any value other than true will evaluate to false) +javax.faces.converter.ByteConverter.BYTE=value must be a number between 0 and 255 +javax.faces.converter.ByteConverter.BYTE_detail=value must be a number between 0 and 255 +javax.faces.converter.CharacterConverter.CHARACTER=value must be a character +javax.faces.converter.CharacterConverter.CHARACTER_detail=value must be a valid ASCII character +javax.faces.converter.DateTimeConverter.DATE=value must be a date +javax.faces.converter.DateTimeConverter.DATE_detail=value must be a date, eg. {1} +javax.faces.converter.DateTimeConverter.TIME=value must be a time +javax.faces.converter.DateTimeConverter.TIME_detail=value must be a time, eg. {1} +javax.faces.converter.DateTimeConverter.DATETIME=value must be a date and time +javax.faces.converter.DateTimeConverter.DATETIME_detail=value must be a date and time, eg. {1} +javax.faces.converter.DateTimeConverter.PATTERN_TYPE=a pattern or type attribute must be specified to convert the value +javax.faces.converter.DoubleConverter.DOUBLE=value must be a number +javax.faces.converter.DoubleConverter.DOUBLE_detail=value must be a number between 4.9E-324 and 1.7976931348623157E308 +javax.faces.converter.EnumConverter.ENUM=value must be convertible to an enum +javax.faces.converter.EnumConverter.ENUM_detail=value must be convertible to an enum or from the enum that contains the constant {1} +javax.faces.converter.EnumConverter.ENUM_NO_CLASS=value must be convertible to an enum or from the enum, but no enum class provided +javax.faces.converter.EnumConverter.ENUM_NO_CLASS_detail=value must be convertible to an enum or from the enum, but no enum class provided +javax.faces.converter.FloatConverter.FLOAT=value must be a number +javax.faces.converter.FloatConverter.FLOAT_detail=value must be a number between 1.4E-45 and 3.4028235E38 +javax.faces.converter.IntegerConverter.INTEGER=value must be an integer +javax.faces.converter.IntegerConverter.INTEGER_detail=value must be an integer number between -2147483648 and 2147483647 +javax.faces.converter.LongConverter.LONG=value must be an integer +javax.faces.converter.LongConverter.LONG_detail=value must be an integer number between -9223372036854775808 and 9223372036854775807 +javax.faces.converter.NumberConverter.CURRENCY=value must be a currency amount +javax.faces.converter.NumberConverter.CURRENCY_detail=value must be a currency amount, eg. {1} +javax.faces.converter.NumberConverter.PERCENT=value must be a percentage amount +javax.faces.converter.NumberConverter.PERCENT_detail=value must be a percentage amount, eg. {1} +javax.faces.converter.NumberConverter.NUMBER=value must be a number +javax.faces.converter.NumberConverter.NUMBER_detail=value must be a number +javax.faces.converter.NumberConverter.PATTERN=value must be a number +javax.faces.converter.NumberConverter.PATTERN_detail=value must be a number +javax.faces.converter.ShortConverter.SHORT=value must be an integer +javax.faces.converter.ShortConverter.SHORT_detail=value must be an integer number between -32768 and 32767 + +javax.faces.validator.DoubleRangeValidator.MAXIMUM=value must be less than or equal to {0} +javax.faces.validator.DoubleRangeValidator.MINIMUM=value must be greater than or equal to {0} +javax.faces.validator.DoubleRangeValidator.NOT_IN_RANGE=value must be between {0} and {1} +javax.faces.validator.DoubleRangeValidator.TYPE=value is not of the correct type +javax.faces.validator.LengthValidator.MAXIMUM=value must be shorter than or equal to {0} characters +javax.faces.validator.LengthValidator.MINIMUM=value must be longer than or equal to {0} characters +javax.faces.validator.LongRangeValidator.MAXIMUM=value must be less than or equal to {0} +javax.faces.validator.LongRangeValidator.MINIMUM=value must be greater than or equal to {0} +javax.faces.validator.LongRangeValidator.NOT_IN_RANGE=value must be between {0} and {1} +javax.faces.validator.LongRangeValidator.TYPE=value is not of the correct type + +javax.faces.validator.NOT_IN_RANGE=value must be between {0} and {1} +javax.faces.converter.STRING=value could not be converted to a string + +cas2login.pageTitle = oxAuth - Login +cas2login.loginHeader = Login (second step) +cas2login.pleaseLoginHere = Please login here +cas2login.username = Username +cas2login.password = Password +cas2login.rememberMe = Remember me +cas2login.termsPrivace = Terms & Privacy + +error.errorEncountered = Error Encountered +error.unexpectedError = An unexpected error has occurred at {0} + +error.invalidSessionState = Invalid session state or session limitation rule has occured at {0} + +cert.failedToCheckCertificate = Failed to check certificate +cert.thisCanHappen = This can happen in the following cases: +cert.youHaveExpired = You have expired certificate.
Please, registered with the system and get new certificate. +cert.youSelectedInvalid = You selected invalid certificate.
Please, restart your browser and try to log in again. +cert.noteInternetExplorer = Note: Internet Explorer and Mozilla Firefox allow to clear the SSL state without restarting the browser: +cert.ifYouAreUsing = If you are using Internet Explorer, you can use the "Clear SSL" button in the settings section: Contents (click Tools \u2192 Internet Options). +cert.ifYouUseFireFox = If you use FireFox, you can use the menu: Options \u2192 Privacy \u2192 Clear all current history \u2192 select \u201cActive Logins\u201d \u2192 press button Clear Now. +cert.youFailedToInstall = You failed to install the personal certificate into the browser
Please, install the certificate from the backup file (.PFX), restart your browser and try to log in again. +cert.youHaveAlreadyOpened = You have already opened the page in this browser but haven't chosen any certificate for further usage. The browser now remembers this choice.
Please, restart your browser, log in again and choose the appropriate certificate.
If you use Internet Explorer, click "Clear SSL state" at Tools/Internet Options/Content. +cert.ifYouUseMacOS = If you use MacOS X and updated the operation system to version 10.5.3 please see http://support.apple.com/kb/HT1679?viewlocale=en_US to resolve the issue. + +duologin.title = oxAuth - DUO Login +duologin.login = DUO Login (second step) +duologin.termsPrivacy = Terms & Privacy + +gpluslogin.title = oxAuth - Google+ Login +gpluslogin.pageTitle = oxAuth - Login +gpluslogin.login = Login (second step) +gpluslogin.pleaseLoginHere = Please login here +gpluslogin.username = Username +gpluslogin.password = Password +gpluslogin.rememberMe = Remember Me +gpluslogin.termsPrivacy = Terms & Privacy + +otp.pageTitle = oxAuth - OTP Login +otp.otpCode = OTP code +otp.scanQRCode = Scan QR code using OTP authenticator and press finish button +otp.finish = Finish + +oxpush.pageTitle = oxAuth oxPush - Login +oxpush.login = oxPush Login (Authentication request) +oxpush.loginPairingRequest = oxPush Login (Pairing request) +oxpush.sendingAuthenticationRequest = Sending authentication request... +oxpush.termsPrivacy = Terms & Privacy +oxpush.title = oxAuth - Login +oxpush.loginLabel = Login +oxpush.pleaseLoginHere = Please login here +oxpush.username = Username +oxpush.password = Password +oxpush.rememberMe = Remember me +oxpush.beforeLogIn = Before log in make sure that you installed oxPush mobile application +oxpush.androidMobileApplication = Android mobile application +oxpush.byProceeding = By proceeding, you agree with the +oxpush.privacyPolicy = Privacy Policy +oxpush.pleaseRead = Please read the +oxpush.termsOfService = Terms of Service +oxpush.checkingPairing = Checking pairing status... +oxpush.waitingForUser = Waiting for user approval... +oxpush.pairingQRCode = Pairing QR code: +oxpush.pairingCode = Pairing code: + +passport.pleaseLoginHere = Please login here +passport.email = Email +passport.termsPrivacy = Terms & Privacy +passport.oxAuthPassportLogin = oxAuth - Passport Login +passport.needAGluuAccount = Need a Gluu account? +passport.forgotYourPassword = Forgot your password? +passport.useExternalAuthentication = Use External Authentication +passport.orUseExternal=OR use an external service to sign in: +passport.registerNewUser = Register new user +passport.javascriptRequired=This pages requires javascript enabled to work properly +passport.fillMissingData=Please provide the following to complete authentication: +passport.invalidMailWarn=Please provide a valid email + +saml.pageTitle = oxAuth - Login +saml.login = Login (second step) +saml.username = Username +saml.password = Password +saml.rememberMe = Remember me +saml.termsPrivacy = Terms & Privacy + +supergluu.scanQRCode = Scan QR code using Super-Gluu + +# Used by twilio and smpp sms 2FA +otp_sms.pageTitle = SMS - Login +otp_sms.verification=2 Step Verification +otp_sms.usedevice=Use your device to sign in to your Gluu account. +otp_sms.verificationcode=Enter a verification code +otp_sms.codesent=A text message with a verification code
\ was sent to +otp_sms.login=Done +otp_sms.termsPrivacy = Terms & Privacy + +uaf.pageTitle = oxAuth - UAF Login +uaf.scanQRCode = Scan QR code using UAF mobile authenticator + +supergluu.enroll.website.url= https://super.gluu.org +supergluu.enroll.Docs.url= https://gluu.org/docs/supergluu +supergluu.enroll.supergluuisfreesecure= Super Gluu is a free and secure two-factor authentication mobile application for all the people in your school, department, organization, or enterprise. +supergluu.enroll.downloadsupergluu= Download Super Gluu: +supergluu.enroll.scanqrcode= Scan the QR code with your Super Gluu app to enroll your device +supergluu.enroll.enrollyourdevice= Enroll your device +supergluu.enroll.Website= Website +supergluu.enroll.Docs=Docs + +u2f.verification.stepverification=2 Step Verification +u2f.verification.usedevice=Use your U2F device to sign in to your Gluu account. +u2f.verification.insertkey = Insert your U2F security key. +u2f.verification.useit=If your U2F key has a button, tap it. Otherwise you can remove it and re-insert it. + +fido2.verification.stepverification=2 Step Verification +fido2.verification.usedevice=Use your fido2 device to sign in to your Gluu account. +fido2.verification.insertkey = Insert your fido2 security key. +fido2.verification.useit=If your fido2 key has a button, tap it. Otherwise you can remove it and re-insert it. + +otp.login=Done +otp.verification=2 Step Verification +otp.entercode=Enter a verification code +otp.usedevice=Use your device to sign in to your Gluu account. +otp.getcode=Get a verification code from your OTP mobile app. + +password.validation.invalid = The password provided is not strong enough + +# Gluu Casa +casa.login.title=Casa login +casa.login.panel_title=Welcome +casa.cancel=Cancel +casa.close=Close +casa.proceed=Proceed +casa.alternative=Try another way to sign in +casa.snd_step=2-step verification +casa.enter_code=Enter code + +casa.cert.title=User certificate +casa.cert.text=Use a digital certificate issued to you by a certification authority +casa.cert.precontinue=Before you continue... +casa.cert.hint_1=If you haven't done so, import the certificate issued to you into your web browser +casa.cert.hint_2=When clicking on the proceed button, you will be prompted to select a certificate. Choose carefully +casa.cert.hint_3=If presented, uncheck the option related to remembering your choice +casa.cert.hint_4=If no prompt is shown, your browser is caching a choice made recently. To clear the SSL cache, close the browser window and try again +casa.cert.error.not_valid=Your certificate is not valid +casa.cert.error.unparsable=Your certificate couldn't be processed by the server +casa.cert.error.not_selected=You did not select any certificate +casa.cert.error.cert_enrolled_other_user=The certificate presented is registered to a different account +casa.cert.error.cert_not_recognized=The certificate presented has not been enrolled yet in Casa +casa.cert.error.unknown_user=Inexisting user + +casa.u2f.title=Security key +casa.u2f.text=Use your security key to sign in + +casa.fido2.title=Security key +casa.fido2.text=Use your security key to sign in +casa.fido2.abort_error=Operation cancelled. Use the links below to use a different alternative or to try again +casa.fido2.invalid_state_error=The presented key is not registered for this account. Please try again with a different key +casa.fido2.generic_error=An error occurred. Use the links below to use a different alternative or to try again +casa.fido2.retry_key=Retry security key + +casa.securitykey.insert=Insert your security key +casa.securitykey.tap=If your key has a button, tap it. Otherwise you can remove it and re-insert it. + +casa.twilio_sms.title=Passcode sent via SMS +casa.twilio_sms.text=We'll send you a one-time passcode to validate your identity + +casa.smpp.title=Passcode sent via SMS +casa.smpp.text=We'll send you a one-time passcode to validate your identity + +casa.sms.enter=Enter the code sent via SMS +casa.sms.choose=Choose a number to send an SMS to: +casa.sms.send=Send + +casa.super_gluu.title=Super Gluu +casa.super_gluu.text=Get a push notification sent to your Super Gluu device +casa.super_gluu.push_approve=A push notification has been sent to your Super Gluu device. Approve to login +casa.super_gluu.not_received_1=Didn't receive the push? +casa.super_gluu.not_received_2=Scan a QR code instead + +casa.otp.title=OTP token +casa.otp.text=Get a verification code from your OTP mobile app or hardware token. + +# CIBA +ciba.bindingMessage=Binding Message + +device.authorization.pageTitle=Device Authorization +device.authorization.title=Device Authorization +device.authorization.subtitle=A device is processing permission to connect with your account +device.authorization.code.inputbox.label=Code displayed on your device +device.authorization.confirm.button=Continue +device.authorization.invalid.user.code=Invalid code, please review the code and retry +device.authorization.access.denied.msg=The authorization has been denied, please retry from the device +device.authorization.expired.code.msg=Code expired, please retry from the device +device.authorization.init.new.request.msg=Init new request +device.authorization.access.granted.title=Device authorization granted +device.authorization.authorization.completed.msg=Successful! Required permissions have been granted to the device. +device.authorization.brute.forcing.msg=Too many failed attemps... please init again from device after an hour. + +# Passwordless +pwdless.pageTitle=Max-Security Profile Login +pwdless.username=Enter your username +pwdless.choose=Choose an account: +pwdless.other=Use another account diff --git a/oxAuth/Server/src/main/resources/oxauth_es.properties b/oxAuth/Server/src/main/resources/oxauth_es.properties new file mode 100644 index 00000000..13108b70 --- /dev/null +++ b/oxAuth/Server/src/main/resources/oxauth_es.properties @@ -0,0 +1,153 @@ +common.copyright=Desarrollado por +common.allRightsReserved=Gesti\u00F3n de acceso de c\u00F3digo abierto. +common.agreePolicy=Aseg\u00FArese de leer y aceptar las +common.privacyPolicy=Pol\u00EDticas de privacidad +common.pleaseReadTheTos=Por favor lea los +common.termsOfService=T\u00E9rminos de Servicio + +login.pageTitle=oxAuth - Inicio de sesi\u00F3n +login.login=Inicio de sesi\u00F3n +login.pleaseLoginHere=Ingrese los datos de su cuenta para iniciar sesi\u00F3n +login.username=Nombre de usuario +login.password=Contrase\u00F1a +login.rememberMe=Recordar mis datos +login.errorMessage=Ingrese un nombre de usuario y password validos +login.failedToAuthenticate=Error al autenticarse. +login.youDontHavePermission=No tienes permisos. +login.forgotYourPassword = ¿Olvidaste tu contrase\u00F1a? + +logout.missingParameters=Solicitud no v\u00E1lida. A la solicitud le falta un par\u00E1metro requerido, incluye un valor de par\u00E1metro no soportado o est\u00E1 malformado. +logout.failedToProceed=Error al procesar el cierre de sesi\u00F3n + + +authorize.pageTitle=oxAuth - Autorizaci\u00F3n +authorize.requestForPermission=Solicitud de permiso +authorize.requestingPermissionForScopes={0} esta solicitando permiso para lo siguiente: +authorize.viewIndividualClaim=Ver claim {0}. +authorize.allow=Permitir +authorize.doNotAllow=No permitir + +cas2login.pageTitle = oxAuth - Inicio de sesi\u00F3n +cas2login.loginHeader = Inicio de sesi\u00F3n (paso dos) +cas2login.pleaseLoginHere = Por favor inicie sesi\u00F3n aqu\u00ED +cas2login.username = Nombre de usuario +cas2login.password = Contrase\u00F1a +cas2login.rememberMe = Recordar mis datos +cas2login.termsPrivace = T\u00E9rminos & Privacidad + +error.errorEncountered = Se encontr\u00F3 un error +error.unexpectedError = Ocurri\u00F3 un error inesperado en {0} + +cert.failedToCheckCertificate = Error al verificar el certificado +cert.thisCanHappen = Esto puedo suceder en los siguientes casos: +cert.youHaveExpired = El certificado ha expirado.
Por favor, regístrese en el sistema y obtenga un nuevo certificado. +cert.youSelectedInvalid = El certificado seleccionado no es v\u00E1lido.
Por favor, reinicie su navegador e intente iniciar sesi\u00F3n de nuevo. +cert.noteInternetExplorer = Nota: Internet Explorer y Mozilla Firefox permiten borrar el estado SSL sin reiniciar el navegador: +cert.ifYouAreUsing = Si está utilizando Internet Explorer, puede usar el botón "Borrar SSL" en la sección de configuración: Contenido (haga clic en Herramientas \u2192 Opciones de Internet). +cert.ifYouUseFireFox = Si usa FireFox, puede usar el menú: Opciones \u2192 Privacidad \u2192 Borrar todo el historial actual \u2192 seleccione \u201cInicio de sesión activo\u201d \u2192 presione el botón Borrar ahora. +cert.youFailedToInstall = Error al instalar el certificado personal en el navegador
Por favor, instale el certificado desde el archivo de copia de seguridad (.PFX), reinicie su navegador e intente iniciar sesión de nuevo. +cert.youHaveAlreadyOpened = Ya ha abierto la página en este navegador, pero no ha elegido ningún certificado para utilizar. El navegador recordar\u00E1 esta elección.
Por favor, reinicie su navegador, inicie sesión nuevamente y elija el certificado apropiado.
Si usa Internet Explorer, haga clic en "Borrar estado SSL" en Herramientas/Opciones de Internet/Contenido. +cert.ifYouUseMacOS = Si usa MacOS X y actualizó el sistema operativo a la versión 10.5.3, consulte http://support.apple.com/kb/HT1679?viewlocale=en_US para resolver el problema. + +duologin.title = oxAuth - Inicio de sesi\u00F3n DUO +duologin.login = Inicio de sesi\u00F3n DUO (paso dos) +duologin.termsPrivacy = T\u00E9rminos & Privacidad + +gpluslogin.title = oxAuth - Inicio de sesi\u00F3n Google+ +gpluslogin.pageTitle = oxAuth - Inicio de sesi\u00F3n +gpluslogin.login = Inicio de sesi\u00F3n (paso dos) +gpluslogin.pleaseLoginHere = Por favor inicie sesi\u00F3n aqu\u00ED +gpluslogin.username = Nombre de usuario +gpluslogin.password = Contrase\u00F1a +gpluslogin.rememberMe = Recordar mis datos +gpluslogin.termsPrivacy = T\u00E9rminos & Privacidad + +otp.pageTitle = oxAuth - Inicio de sesi\u00F3n OTP +otp.otpCode = c\u00F3digo OTP +otp.scanQRCode = Escanee el código QR usando el autenticador OTP y presione el botón de finalizar +otp.finish = Finalizar + +oxpush.pageTitle = oxAuth oxPush - Inicio de sesi\u00F3n +oxpush.login = Inicio de sesi\u00F3n oxPush (Solicitud de autenticaci\u00F3n) +oxpush.loginPairingRequest = Inicio de sesi\u00F3n oxPush (Solicitud de sincronizaci\u00F3n) +oxpush.sendingAuthenticationRequest = Enviando solicitud de autenticaci\u00F3n... +oxpush.termsPrivacy = T\u00E9rminos & Privacidad +oxpush.title = oxAuth - Inicio de sesi\u00F3n +oxpush.loginLabel = Inicio de sesi\u00F3n +oxpush.pleaseLoginHere = Por favor inicie sesi\u00F3n aqu\u00ED +oxpush.username = Nombre de usuario +oxpush.password = Contrase\u00F1a +oxpush.rememberMe = Recordar mis datos +oxpush.beforeLogIn = Antes de iniciar sesi\u00F3n, aseg\u00FArese de haber instalado la aplicaci\u00F3n m\u00F3vil oxPush +oxpush.androidMobileApplication = Aplicaci\u00F3n m\u00F3vil Android +oxpush.byProceeding = Al proceder, usted está de acuerdo con la +oxpush.privacyPolicy = Pol\u00EDtica de privacidad +oxpush.pleaseRead = Por favor lea los +oxpush.termsOfService = T\u00E9rminos de servicio +oxpush.checkingPairing = Verificando el estado de emparejamiento... +oxpush.waitingForUser = Esperando la aprobaci\u00F3n del usuario... +oxpush.pairingQRCode = Emparejar c\u00F3digo QR: +oxpush.pairingCode = Emparejar c\u00F3digo: + +passport.pageTitle = Passport - PostLogin +passport.loginSecondStep = Inicio de sesi\u00F3n (paso dos) +passport.pleaseLoginHere = Por favor inicie sesi\u00F3n aqu\u00ED +passport.email = Correo electr\u00F3nico +passport.termsPrivacy = T\u00E9rminos & Privacidad +passport.oxAuthPassportLogin = oxAuth - Inicio de sesi\u00F3n Passport +passport.needAGluuAccount = ¿Necesitas una cuenta Gluu? +passport.forgotYourPassword = ¿Olvidaste tu contrase\u00F1a? +passport.useExternalAuthentication = Usar autenticaci\u00F3n externa +passport.registerNewUser = Registrar un nuevo usuario + +saml.pageTitle = oxAuth - Inicio de sesi\u00F3n +saml.login = Inicio de sesi\u00F3n (paso dos) +saml.username = Nombre de usuario +saml.password = Contrase\u00F1a +saml.rememberMe = Recordar mis datos +saml.termsPrivacy = T\u00E9rminos & Privacidad + +supergluu.scanQRCode = Escanear el c\u00F3digo QR usando Super-Gluu + +# Used by twilio and smpp sms 2FA +otp_sms.pageTitle = SMS - Inicio de sesi\u00f3n +otp_sms.verification=2 Step Verification +otp_sms.usedevice=Use your device to sign in to your Gluu account. +otp_sms.verificationcode=Ingrese el c\u00f3digo que recibi\u00f3 por SMS +otp_sms.codesent=A text message with a verification code
\ was sent to +otp_sms.login=Inicio de sesi\u00f3n SMS (paso dos) +otp_sms.termsPrivacy = T\u00e9rminos & Privacidad + +uaf.pageTitle = oxAuth - Inicio de sesi\u00F3n UAF +uaf.scanQRCode = Escanear el c\u00F3digo QR usando el autenticador m\u00F3vil UAF + +# Gluu Casa - general +casa.alternative=¿No tienes tu credencial preferida a la mano?. Ingresa con una alternativa: +casa.button.u2f=Llave de seguridad +casa.button.twilio_sms=C\u00F3digo a trav\u00E9s de SMS +casa.button.smpp=C\u00f3digo a trav\u00e9s de SMS +casa.button.super_gluu=Super Gluu +casa.button.otp=Token OTP + +# Gluu Casa - OTP +otp.getcode2=Obt\u00E9n un c\u00F3digo de verificaci\u00F3n con tu aplicaci\u00F3n m\u00F3vil o token f\u00EDsico. + +# Gluu Casa - OTP SMS +otp_sms.choose=Escoge un n\u00FAmero al cual enviar un mensaje de texto: +otp_sms.send=Enviar + +# CIBA +ciba.bindingMessage=Mensaje Vinculante + +device.authorization.pageTitle=Autorizaci\u00F3n de Dispositivos +device.authorization.title=Autorizaci\u00F3n de Dispositivos +device.authorization.subtitle=Un dispositivo esta procesando permiso para conectarse con tu cuenta +device.authorization.code.inputbox.label=C\u00F3digo mostrado en el dispositivo +device.authorization.confirm.button=Continuar +device.authorization.invalid.user.code=Codigo invalido, por favor revisa el codigo y reintentalo. +device.authorization.access.denied.msg=La authorizacion ha sido denegada, por favor reintenta desde el dispositivo. +device.authorization.expired.code.msg=Codigo expirado, por favor reintenta desde el dispositivo. +device.authorization.init.new.request.msg=Iniciar nueva solicitud +device.authorization.access.granted.title=Autorizacion de dispositivo concedida +device.authorization.authorization.completed.msg=Exitoso! Permisos requeridos han sido asignados al dispositivo. +device.authorization.brute.forcing.msg=Demasiados intentos fallidos... por favor inicia nuevamente desde el dispositivo despues de una hora. \ No newline at end of file diff --git a/oxAuth/Server/src/main/resources/oxauth_fr.properties b/oxAuth/Server/src/main/resources/oxauth_fr.properties new file mode 100644 index 00000000..6bc372b1 --- /dev/null +++ b/oxAuth/Server/src/main/resources/oxauth_fr.properties @@ -0,0 +1,270 @@ +common.copyright=Powered by +common.allRightsReserved=Free and open source access management. +common.agreePolicy=By proceeding, you agree with the {0} +common.privacyPolicy=Privacy Policy +common.pleaseReadTheTos=Please read the +common.termsOfService=Terms of Service +common.gluuInc=Gluu, Inc +common.caution=Use subject to MIT LICENSE + + +login.pageTitle=oxAuth - Connexion +login.login=Connexion +login.register=Cr\u00e9er un compte +login.pleaseLoginHere=Se connecter +login.username=Nom utilisateur +login.password=Mot de passe +login.rememberMe=Se souvenir de moi +login.errorMessage=Email ou mot de passe incorrect. +login.errorSessionInvalidMessage=Erreur d'authentification. La session a expir\u00e9. + +login.failedToAuthenticate=Erreur d'authentification. +login.youDontHavePermission=Vous n'avez pas les permissions. +login.forgotYourPassword = Forgot your password? + +authorize.pageTitle=oxAuth - Autorisation +authorize.requestForPermission=Demande de permission +authorize.requestingPermissionForScopes={0} demande l'autorisation de faire ce qui suit: +authorize.viewIndividualClaim=View {0} claim. +authorize.allow=Autoris\u00e9 +authorize.doNotAllow=Non autoris\u00e9 + +logout.missingParameters=Requ\u00eate Invalide. La requ\u00eate manque d'un param\u00e8tre requis, comprend une valeur de param\u00e8tre non prise en charge ou est autrement mal form\u00e9e. +logout.failedToProceed=Erreur pendant le processus de d\u00e9connexion + +up=\u2191 +down=\u2193 +left=\u2039 +right=\u203A + +javax.persistence.EntityNotFoundException = Entit\ufffd introuvable +javax.persistence.OptimisticLockException = Un autre utilisateur a modifi\ufffd les m\ufffdmes donn\ufffdes. Veuillez essayer de nouveau. + +validator.assertFalse = La validation a \ufffdchou\ufffd +validator.assertTrue = La validation a \ufffdchou\ufffd +validator.future = doit \ufffdtre une date \ufffd venir. +validator.length = la longueur doit \ufffdtre comprise entre {min} et {max} +validator.max = doit \ufffdtre inf\ufffdrieur(e) ou \ufffdgal(e) \ufffd {value} +validator.min = doit \ufffdtre sup\ufffdrieur(e) ou \ufffdgal(e) \ufffd {value} +validator.notNull = ne peut \ufffdtre nul(le) +validator.past = doit \ufffdtre une date pass\ufffde +validator.pattern = doit correspondre \ufffd "{regex}" +validator.range = doit \ufffdtre compris(e) entre {min} et {max} +validator.size = La taille doit \ufffdtre comprise entre {min} et {max} +validator.email = doit \ufffdtre une adresse email valide + +org.jboss.seam.framework.EntityNotFoundException = Entit\ufffd introuvable +org.jboss.seam.security.AuthorizationException = Vous n'avez pas les autorisations n\ufffdcessaires pour effectuer ceci +org.jboss.seam.security.NotLoggedInException = Veuillez d'abord vous connecter +org.jboss.seam.unexpected.error = Erreur inattendue. Veuillez essayez de nouveau. + +org.jboss.seam.loginFailed = La connexion a \ufffdchou\ufffd. +org.jboss.seam.loginSuccessful = Bienvenue, {0}! + +org.jboss.seam.TransactionFailed = La transaction a \ufffdchou\ufffd. +org.jboss.seam.NoConversation = La conversation s'est interrompue, a d\ufffdpass\ufffd les d\ufffdlais ou traite une autre requ\ufffdte. +org.jboss.seam.IllegalNavigation = Navigation ill\ufffdgale +org.jboss.seam.ProcessEnded = Processus #0 d\ufffdj\ufffd achev\ufffd +org.jboss.seam.ProcessNotFound = Le processus #0 n'a pas \ufffdt\ufffd trouv\ufffd. +org.jboss.seam.TaskEnded = T\ufffdche #0 d\ufffdj\ufffd achev\ufffde +org.jboss.seam.TaskNotFound = La t\ufffdche #0 n'a pas \ufffdt\ufffd trouv\ufffde. +org.jboss.seam.NotLoggedIn = Veuillez d'abord vous connecter. + +javax.faces.component.UIInput.CONVERSION = La valeur ne peut \ufffdtre convertie dans le type attendu +javax.faces.component.UIInput.REQUIRED = Une valeur est requise. +javax.faces.component.UIInput.UPDATE = Une erreur est survenue lors du traitement de l'information que vous avez fournie +javax.faces.component.UISelectMany.INVALID = La valeur n'est pas valide. +javax.faces.component.UISelectOne.INVALID = La valeur n'est pas valide. + +javax.faces.converter.BigDecimalConverter.DECIMAL = la valeur doit \ufffdtre un nombre +javax.faces.converter.BigDecimalConverter.DECIMAL_detail = la valeur doit \ufffdtre un nombre d\ufffdcimal sign\ufffd comprenant z\ufffdro chiffre ou plus, \ufffdventuellement suivi par une virgule d\ufffdcimale et une faction, par ex. {1} +javax.faces.converter.BigIntegerConverter.BIGINTEGER = la valeur doit \ufffdtre un nombre +javax.faces.converter.BigIntegerConverter.BIGINTEGER_detail = la valeur doit \ufffdtre un entier sign\ufffd comprenant z\ufffdro chiffre ou plus +javax.faces.converter.BooleanConverter.BOOLEAN = la valeur doit \ufffdtre "vrai" ou "faux" +javax.faces.converter.BooleanConverter.BOOLEAN_detail = la valeur doit \ufffdtre "vrai" ou "faux" (toute valeur autre que "vrai" est \ufffdvalu\ufffde \ufffd "faux") +javax.faces.converter.ByteConverter.BYTE = la valeur doit \ufffdtre un nombre entre 0 et 255 +javax.faces.converter.ByteConverter.BYTE_detail = la valeur doit \ufffdtre un nombre entre 0 et 255 +javax.faces.converter.CharacterConverter.CHARACTER = la valeur doit \ufffdtre un caract\ufffdre +javax.faces.converter.CharacterConverter.CHARACTER_detail = la valeur doit \ufffdtre un caract\ufffdre ASCII valide +javax.faces.converter.DateTimeConverter.DATE = la valeur doit \ufffdtre une date +javax.faces.converter.DateTimeConverter.DATE_detail = la valeur doit \ufffdtre une date, par ex. {1} +javax.faces.converter.DateTimeConverter.TIME = la valeur doit \ufffdtre une heure +javax.faces.converter.DateTimeConverter.TIME_detail = la valeur doit \ufffdtre une heure, par ex. {1} +javax.faces.converter.DateTimeConverter.DATETIME = la valeur doit \ufffdtre une date et une heure +javax.faces.converter.DateTimeConverter.DATETIME_detail = la valeur doit \ufffdtre une date et une heure, par ex. {1} +javax.faces.converter.DateTimeConverter.PATTERN_TYPE = un attribut doit indiquer le mod\ufffdle ou le type pour convertir la valeur +javax.faces.converter.DoubleConverter.DOUBLE = la valeur doit \ufffdtre un nombre +javax.faces.converter.DoubleConverter.DOUBLE_detail = la valeur doit \ufffdtre un nombre entre 4,9E-324 et 17976931348623157E308 +javax.faces.converter.EnumConverter.ENUM = la valeur doit \ufffdtre convertible en une \ufffdnum\ufffdration +javax.faces.converter.EnumConverter.ENUM_detail = la valeur doit \ufffdtre convertible en une \ufffdnum\ufffdration ou depuis l''\ufffdnum\ufffdration qui contient la constante {1} +javax.faces.converter.EnumConverter.ENUM_NO_CLASS = la valeur doit \ufffdtre convertible en une \ufffdnum\ufffdration ou depuis l'\ufffdnum\ufffdration, mais aucune classe d'\ufffdnum\ufffdration n'est fournie +javax.faces.converter.EnumConverter.ENUM_NO_CLASS_detail = la valeur doit \ufffdtre convertible en une \ufffdnum\ufffdration ou depuis l'\ufffdnum\ufffdration, mais aucune classe d'\ufffdnum\ufffdration n'est fournie +javax.faces.converter.FloatConverter.FLOAT = la valeur doit \ufffdtre un nombre +javax.faces.converter.FloatConverter.FLOAT_detail = la valeur doit \ufffdtre un nombre entre 1,4E-45 et 3,4028235E38 +javax.faces.converter.IntegerConverter.INTEGER = la valeur doit \ufffdtre un nombre +javax.faces.converter.IntegerConverter.INTEGER_detail = la valeur doit \ufffdtre un nombre entre -2147483648 et 2147483647 +javax.faces.converter.LongConverter.LONG = la valeur doit \ufffdtre un nombre +javax.faces.converter.LongConverter.LONG_detail = la valeur doit \ufffdtre un nombre entre -9223372036854775808 et 9223372036854775807 +javax.faces.converter.NumberConverter.CURRENCY = la valeur doit \ufffdtre un montant mon\ufffdtaire +javax.faces.converter.NumberConverter.CURRENCY_detail = la valeur doit \ufffdtre un montant mon\ufffdtaire, par ex. {1} +javax.faces.converter.NumberConverter.NUMBER = la valeur doit \ufffdtre un nombre +javax.faces.converter.NumberConverter.NUMBER_detail = la valeur doit \ufffdtre un nombre +javax.faces.converter.NumberConverter.PATTERN = la valeur doit \ufffdtre un nombre +javax.faces.converter.NumberConverter.PATTERN_detail = la valeur doit \ufffdtre un nombre +javax.faces.converter.NumberConverter.PERCENT = la valeur doit \ufffdtre un pourcentage +javax.faces.converter.NumberConverter.PERCENT_detail = la valeur doit \ufffdtre un pourcentage, par ex. {1} +javax.faces.converter.ShortConverter.SHORT = la valeur doit \ufffdtre un nombre +javax.faces.converter.ShortConverter.SHORT_detail = la valeur doit \ufffdtre comprise entre -32768 et 32767 + +javax.faces.validator.DoubleRangeValidator.MAXIMUM = La valeur doit \ufffdtre inf\ufffdrieure ou \ufffdgale \ufffd {0} +javax.faces.validator.DoubleRangeValidator.MINIMUM = La valeur doit \ufffdtre sup\ufffdrieure ou \ufffdgale \ufffd {0} +javax.faces.validator.DoubleRangeValidator.NOT_IN_RANGE = La valeur doit \ufffdtre comprise entre {0} et {1} +javax.faces.validator.DoubleRangeValidator.TYPE = La valeur n'est pas du type correct +javax.faces.validator.LengthValidator.MAXIMUM = La valeur doit comporter {0} caract\ufffdres ou moins +javax.faces.validator.LengthValidator.MINIMUM = La valeur doit comporter {0} caract\ufffdres ou plus +javax.faces.validator.LongRangeValidator.MAXIMUM = La valeur doit \ufffdtre inf\ufffdrieure ou \ufffdgale \ufffd {0} +javax.faces.validator.LongRangeValidator.MINIMUM = La valeur doit \ufffdtre sup\ufffdrieure ou \ufffdgale \ufffd {0} +javax.faces.validator.LongRangeValidator.NOT_IN_RANGE = La valeur doit \ufffdtre comprise entre {0} et {1} +javax.faces.validator.LongRangeValidator.TYPE = La valeur n'est pas du type correct + +javax.faces.validator.NOT_IN_RANGE = La valeur doit \ufffdtre comprise entre {0} et {1} +javax.faces.converter.STRING = La valeur ne peut \ufffdtre convertie en cha\ufffdne de caract\ufffdres + +cas2login.pageTitle = oxAuth - Login +cas2login.loginHeader = Login (second step) +cas2login.pleaseLoginHere = Please login here +cas2login.username = Username +cas2login.password = Password +cas2login.rememberMe = Remember me +cas2login.termsPrivace = Terms & Privacy + +error.errorEncountered = Error Encountered +error.unexpectedError = An unexpected error has occurred at {0} + +cert.failedToCheckCertificate = Failed to check certificate +cert.thisCanHappen = This can happen in the following cases: +cert.youHaveExpired = You have expired certificate.
Please, registered with the system and get new certificate. +cert.youSelectedInvalid = You selected invalid certificate.
Please, restart your browser and try to log in again. +cert.noteInternetExplorer = Note: Internet Explorer and Mozilla Firefox allow to clear the SSL state without restarting the browser: +cert.ifYouAreUsing = If you are using Internet Explorer, you can use the "Clear SSL" button in the settings section: Contents (click Tools \u2192 Internet Options). +cert.ifYouUseFireFox = If you use FireFox, you can use the menu: Options \u2192 Privacy \u2192 Clear all current history \u2192 select \u201cActive Logins\u201d \u2192 press button Clear Now. +cert.youFailedToInstall = You failed to install the personal certificate into the browser
Please, install the certificate from the backup file (.PFX), restart your browser and try to log in again. +cert.youHaveAlreadyOpened = You have already opened the page in this browser but haven't chosen any certificate for further usage. The browser now remembers this choice.
Please, restart your browser, log in again and choose the appropriate certificate.
If you use Internet Explorer, click "Clear SSL state" at Tools/Internet Options/Content. +cert.ifYouUseMacOS = If you use MacOS X and updated the operation system to version 10.5.3 please see http://support.apple.com/kb/HT1679?viewlocale=en_US to resolve the issue. + +duologin.title = oxAuth - DUO Login +duologin.login = DUO Login (second step) +duologin.termsPrivacy = Terms & Privacy + +gpluslogin.title = oxAuth - Google+ Login +gpluslogin.pageTitle = oxAuth - Login +gpluslogin.login = Login (second step) +gpluslogin.pleaseLoginHere = Please login here +gpluslogin.username = Username +gpluslogin.password = Password +gpluslogin.rememberMe = Remember Me +gpluslogin.termsPrivacy = Terms & Privacy + +otp.pageTitle = oxAuth - OTP Login +otp.otpCode = OTP code +otp.scanQRCode = Scan QR code using OTP authenticator and press finish button +otp.finish = Finish + +oxpush.pageTitle = oxAuth oxPush - Login +oxpush.login = oxPush Login (Authentication request) +oxpush.loginPairingRequest = oxPush Login (Pairing request) +oxpush.sendingAuthenticationRequest = Sending authentication request... +oxpush.termsPrivacy = Terms & Privacy +oxpush.title = oxAuth - Login +oxpush.loginLabel = Login +oxpush.pleaseLoginHere = Please login here +oxpush.username = Username +oxpush.password = Password +oxpush.rememberMe = Remember me +oxpush.beforeLogIn = Before log in make sure that you installed oxPush mobile application +oxpush.androidMobileApplication = Android mobile application +oxpush.byProceeding = By proceeding, you agree with the +oxpush.privacyPolicy = Privacy Policy +oxpush.pleaseRead = Please read the +oxpush.termsOfService = Terms of Service +oxpush.checkingPairing = Checking pairing status... +oxpush.waitingForUser = Waiting for user approval... +oxpush.pairingQRCode = Pairing QR code: +oxpush.pairingCode = Pairing code: + +passport.pageTitle = Passport - PostLogin +passport.loginSecondStep = Login (second step) +passport.pleaseLoginHere = Please login here +passport.email = Email +passport.termsPrivacy = Terms & Privacy +passport.oxAuthPassportLogin = oxAuth - Passport Login +passport.needAGluuAccount = Need a Gluu account? +passport.forgotYourPassword = Forgot your password? +passport.useExternalAuthentication = Use External Authentification +passport.registerNewUser = Register new user + +saml.pageTitle = oxAuth - Login +saml.login = Login (second step) +saml.username = Username +saml.password = Password +saml.rememberMe = Remember me +saml.termsPrivacy = Terms & Privacy + +supergluu.scanQRCode = Scan QR code using Super-Gluu + +fido2.verification.stepverification=2 Step Verification +fido2.verification.usedevice=Use your fido2 device to sign in to your Gluu account. +fido2.verification.insertkey = Insert your fido2 security key. +fido2.verification.useit=If your fido2 key has a button, tap it. Otherwise you can remove it and re-insert it. + +uaf.pageTitle = oxAuth - UAF Login +uaf.scanQRCode = Scan QR code using UAF mobile authenticator + +password.validation.invalid = The password provide is not strong enough + +# CIBA +ciba.bindingMessage=Binding Message + +device.authorization.pageTitle=Autorisation de p\u00E9riph\u00E9rique +device.authorization.title=Autorisation de p\u00E9riph\u00E9rique +device.authorization.subtitle=Un appareil traite l'autorisation de se connecter \u00E0 votre compte +device.authorization.code.inputbox.label=Code affich\u00E9 sur votre appareil +device.authorization.confirm.button=Continuer +device.authorization.invalid.user.code=Code non valide, veuillez verifier le code et reessayer +device.authorization.access.denied.msg=L'autorisation a ete refusee, veuillez reessayer depuis l'appareil +device.authorization.expired.code.msg=Le code a expire, veuillez reessayer depuis l'appareil +device.authorization.init.new.request.msg=Init nouvelle demande +device.authorization.access.granted.title=Autorisation d'appareil accordee +device.authorization.authorization.completed.msg=Reussi! Les autorisations requises ont ete accordees a l'appareil. +device.authorization.brute.forcing.msg=Trop de tentatives infructueuses ... veuillez recommencer depuis l'appareil apres une heure. + +# person authentication script - whispeak +whispeak.auth.alternative.instruction=Pour vous connecter sans utiliser votre voix. +whispeak.auth.error.instruction=Essayer un autre moyen d\'authentification. +whispeak.auth.instructions.passport.choose.provider=Veuillez vous authentifier \u00E0 l\'aide d\'une m\u00E9thode d\u00E9j\u00E0 configur\u00E9e. +whispeak.auth.instructions.passport.fallback=Vous avez choisi de ne pas utiliser votre voix, vous pouvez changer ce choix \u00E0 votre prochaine authentification. +whispeak.auth.instructions.passport.verif=Nous devons d\'abord v\u00E9rifier votre identit\u00E9. +whispeak.auth.instructions.record=Veuillez cliquer sur le bouton Micro (\ud83c\udf99) pour commencer l\'authentification. +whispeak.auth.instructions.validate=Validez (→) quand vous aurez termin\u00E9 votre lecture. +whispeak.modo=Simple, rapide et s\u00E9curis\u00E9. +whispeak.msg2=Veuillez entrer votre adresse e-mail. +whispeak.use.voice.auth=Souhaitez-vous utiliser l\'authentification vocale ? +whispeak.use.voice.auth.question=Souhaitez-vous utiliser l\'authentification vocale ? +whispeak.welcome=Bienvenue chez WHISPEAK. +whispeak.revocation.title=Instructions pour révoquer votre signature vocale : +whispeak.revocation.instruction=Notez précieusement ces informations afin de pouvoir détruire votre signature vocale à tout moment : +whispeak.revocation.revocation_ui_link=Accédez à ce lien pour détruire la signature vocale : +whispeak.revocation.revocation_pwd=Code de confirmation pour la destruction : +whispeak.revocation.copytext=Copier +whispeak.revocation.ok=Compris +whispeak.login.signatureDoesNotExist=Votre signature vocale est invalide ou inexistante. Veuillez vous inscrire en faisant une nouvelle signature. +whispeak.login.2fa.passwordMismatch=Mot de passe incorrect. Veuillez tenter encore une fois. +whispeak.login.2fa.password=Veuillez renseigner votre mot de passe ou en créer un si vous n\'en avez pas encore. +whispeak.apiError.badRequest=Erreur technique: erreur de communication. +whispeak.apiError.unauthorized=Erreur technique: Action non possible. +whispeak.apiError.invalidCredential=Erreur serveur: Action non possible. +whispeak.apiError.signatureNotFound=Signature vocale inconnue: veuillez vous inscrire une autre fois. +whispeak.apiError.unsupportedAudioFile=Erreur technique. Fichier audio non valide. +whispeak.apiError.audioConstraintsFailed=Extrait audio trop court ou incomplet. +whispeak.apiError.voiceMismatch=Votre voix ne correspond pas pour cet utilisateur. +whispeak.apiError.invalidEnrollSignature=Signature vocale invalide: veuillez vous inscrire une autre fois. \ No newline at end of file diff --git a/oxAuth/Server/src/main/resources/oxauth_it.properties b/oxAuth/Server/src/main/resources/oxauth_it.properties new file mode 100644 index 00000000..69f4f566 --- /dev/null +++ b/oxAuth/Server/src/main/resources/oxauth_it.properties @@ -0,0 +1,204 @@ +login.pageTitle=oxAuth - Inicio de sesi\u00F3n +login.login=Inicio de sesi\u00F3n +login.pleaseLoginHere=Ingrese los datos de su cuenta para iniciar sesi\u00F3n +login.username=Nombre de usuario +login.password=Contrase\u00F1a +login.rememberMe=Recordar mis datos +login.errorMessage=Ingrese un nombre de usuario y password validos +login.failedToAuthenticate=Failed to authenticate. +login.youDontHavePermission=You don't have permissions. +login.forgotYourPassword = Forgot your password? + +logout.missingParameters=Invalid Request. The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed. +logout.failedToProceed=Failed to process logout + +up=\u2191 +down=\u2193 +left=\u2039 +right=\u203A + +validator.assertFalse=validazione fallita +validator.assertTrue=validazione corretta +validator.future=deve essere una data futura +validator.length=la lunghezza deve essere tra {min} e {max} +validator.max=deve essere minore o uguale a {value} +validator.min=deve essere maggiore o uguale a {value} +validator.notNull=non pu\u00F2 essere nullo +validator.past=deve essere una data passata +validator.pattern=deve corrispondere a "{regex}" +validator.range=deve essere tra {min} e {max} +validator.size=la grandezza deve essere tra {min} e {max} +validator.email=deve essere un indirizzo di posta elettronica + +org.jboss.seam.loginFailed=Autenticazione fallita +org.jboss.seam.loginSuccessful=Benvenuto/a, #0 + +org.jboss.seam.TransactionFailed=Transazione fallita +org.jboss.seam.NoConversation=La conversazione \u00E8 terminata, scaduta oppure \u00E8 stata processata un'altra richiesta +org.jboss.seam.IllegalNavigation=Navigazione illegale +org.jboss.seam.ProcessEnded=Il processo \#0 \u00E8 gi\u00E0\u00A0 terminato +org.jboss.seam.ProcessNotFound=Il processo \#0 non \u00E8 stato trovato +org.jboss.seam.TaskEnded=Il task \#0 \u00E8 gi\u00E0 terminato +org.jboss.seam.TaskNotFound=Il task \#0 non \u00E8 stato trovato +org.jboss.seam.NotLoggedIn=Per favore, eseguire la login + +javax.faces.component.UIInput.CONVERSION=il valore non pu\u00F2 essere convertito +javax.faces.component.UIInput.REQUIRED=\u00C8 richiesto un valore +javax.faces.component.UIInput.UPDATE=Si \u00E8 verificato un errore nell'elaborazione delle informazioni inviate +javax.faces.component.UISelectOne.INVALID=il valore non \u00E8 valido +javax.faces.component.UISelectMany.INVALID=il valore non \u00E8 valido + +javax.faces.converter.BigDecimalConverter.DECIMAL=il valore deve essere un numero +javax.faces.converter.BigDecimalConverter.DECIMAL_detail=il valore deve essere un numero decimale con zero o pi\u00F9 cifre, opzionalmente seguito da un punto e una frazione, es. {1} +javax.faces.converter.BigIntegerConverter.BIGINTEGER=deve essere un intero +javax.faces.converter.BigIntegerConverter.BIGINTEGER_detail=il valore deve essere un numero intero con zero o pi\u00F9 cifre +javax.faces.converter.BooleanConverter.BOOLEAN=deve essere vero o falso +javax.faces.converter.BooleanConverter.BOOLEAN_detail=il valore deve essere vero o falso (qualsiasi valore diverso da vero \u00E8 considerato falso) +javax.faces.converter.ByteConverter.BYTE=il valore deve essere un numero compreso tra 0 e 255 +javax.faces.converter.ByteConverter.BYTE_detail=il valore deve essere un numero compreso tra 0 e 255 +javax.faces.converter.CharacterConverter.CHARACTER=deve essere un carattere +javax.faces.converter.CharacterConverter.CHARACTER_detail=il valore deve essere un carattere ASCII valido +javax.faces.converter.DateTimeConverter.DATE=deve essere una data +javax.faces.converter.DateTimeConverter.DATE_detail=il valore deve essere una data, es. {1} +javax.faces.converter.DateTimeConverter.TIME=deve essere un orario +javax.faces.converter.DateTimeConverter.TIME_detail=il valore deve essere un orario, es. {1} +javax.faces.converter.DateTimeConverter.DATETIME=deve essere una data ed un orario +javax.faces.converter.DateTimeConverter.DATETIME_detail=il valore deve essere una data e un orario, es. {1} +javax.faces.converter.DateTimeConverter.PATTERN_TYPE=per convetire il valore devono essere specificati un pattern o un attributo tipo +javax.faces.converter.DoubleConverter.DOUBLE=deve essere un numero +javax.faces.converter.DoubleConverter.DOUBLE_detail=il valore deve essere un numero compreso tra 4.9E-324 e 1.7976931348623157E308 +javax.faces.converter.EnumConverter.ENUM=il valore deve essere convertibile in una enum +javax.faces.converter.EnumConverter.ENUM_detail=il valore deve essere convertibile in una enum o da enum che contiene la costante {1} +javax.faces.converter.EnumConverter.ENUM_NO_CLASS=il valore deve essere convertibile in una enum o da enum, ma non \u00E8 stata fornita nessuna classe enum +javax.faces.converter.EnumConverter.ENUM_NO_CLASS_detail=il valore deve essere convertibile in una enum o da enum, ma non \u00E8 stata fornita nessuna classe enum +javax.faces.converter.FloatConverter.FLOAT=deve essere un numero +javax.faces.converter.FloatConverter.FLOAT_detail=il valore deve essere un numero compreso tra 1.4E-45 e 3.4028235E38 +javax.faces.converter.IntegerConverter.INTEGER=deve essere un numero intero +javax.faces.converter.IntegerConverter.INTEGER_detail=il valore deve essere un numero intero compreso tra -2147483648 e 2147483647 +javax.faces.converter.LongConverter.LONG=deve essere un numero intero +javax.faces.converter.LongConverter.LONG_detail=il valore deve essere un numero intero compreso tra -9223372036854775808 e 9223372036854775807 +javax.faces.converter.NumberConverter.CURRENCY=il valore deve essere una valuta +javax.faces.converter.NumberConverter.CURRENCY_detail=il valore deve essere una valuta, es. {1} +javax.faces.converter.NumberConverter.PERCENT=il valore deve essere una percentuale +javax.faces.converter.NumberConverter.PERCENT_detail=il valore deve essere una percentuale, es. {1} +javax.faces.converter.NumberConverter.NUMBER=deve essere un numero +javax.faces.converter.NumberConverter.NUMBER_detail=deve essere un numero +javax.faces.converter.NumberConverter.PATTERN=deve essere un numero +javax.faces.converter.NumberConverter.PATTERN_detail=deve essere un numero +javax.faces.converter.ShortConverter.SHORT=deve essere un numero intero +javax.faces.converter.ShortConverter.SHORT_detail=il valore deve essere un numero intero compreso -32768 e 32767 + +javax.faces.validator.DoubleRangeValidator.MAXIMUM=il valore deve essere minore o uguale a {0} +javax.faces.validator.DoubleRangeValidator.MINIMUM=il valore deve essere maggiore o uguale a {0} +javax.faces.validator.DoubleRangeValidator.NOT_IN_RANGE=il valore deve essere compreso tra {0} e {1} +javax.faces.validator.DoubleRangeValidator.TYPE=il valore non \u00E8 del tipo corretto +javax.faces.validator.LengthValidator.MAXIMUM=il valore deve essere inferiore o uguale {0} caratteri +javax.faces.validator.LengthValidator.MINIMUM=il valore deve essere superiore o uguale {0} caratteri +javax.faces.validator.LongRangeValidator.MAXIMUM=il valore deve essere minore o uguale a {0} +javax.faces.validator.LongRangeValidator.MINIMUM=il valore deve essere superiore o uguale a {0} +javax.faces.validator.LongRangeValidator.NOT_IN_RANGE=il valore deve essere compreso tra {0} e {1} +javax.faces.validator.LongRangeValidator.TYPE=il valore non \u00E8 del tipo corretto + +javax.faces.validator.NOT_IN_RANGE=il valore deve essere compreso tra {0} e {1} +javax.faces.converter.STRING=il valore non pu\u00F2 essere convertito a stringa + +cas2login.pageTitle = oxAuth - Login +cas2login.loginHeader = Login (second step) +cas2login.pleaseLoginHere = Please login here +cas2login.username = Username +cas2login.password = Password +cas2login.rememberMe = Remember me +cas2login.termsPrivace = Terms & Privacy + +error.errorEncountered = Error Encountered +error.unexpectedError = An unexpected error has occurred at {0} + +cert.failedToCheckCertificate = Failed to check certificate +cert.thisCanHappen = This can happen in the following cases: +cert.youHaveExpired = You have expired certificate.
Please, registered with the system and get new certificate. +cert.youSelectedInvalid = You selected invalid certificate.
Please, restart your browser and try to log in again. +cert.noteInternetExplorer = Note: Internet Explorer and Mozilla Firefox allow to clear the SSL state without restarting the browser: +cert.ifYouAreUsing = If you are using Internet Explorer, you can use the "Clear SSL" button in the settings section: Contents (click Tools \u2192 Internet Options). +cert.ifYouUseFireFox = If you use FireFox, you can use the menu: Options \u2192 Privacy \u2192 Clear all current history \u2192 select \u201cActive Logins\u201d \u2192 press button Clear Now. +cert.youFailedToInstall = You failed to install the personal certificate into the browser
Please, install the certificate from the backup file (.PFX), restart your browser and try to log in again. +cert.youHaveAlreadyOpened = You have already opened the page in this browser but haven't chosen any certificate for further usage. The browser now remembers this choice.
Please, restart your browser, log in again and choose the appropriate certificate.
If you use Internet Explorer, click "Clear SSL state" at Tools/Internet Options/Content. +cert.ifYouUseMacOS = If you use MacOS X and updated the operation system to version 10.5.3 please see http://support.apple.com/kb/HT1679?viewlocale=en_US to resolve the issue. + +duologin.title = oxAuth - DUO Login +duologin.login = DUO Login (second step) +duologin.termsPrivacy = Terms & Privacy + +gpluslogin.title = oxAuth - Google+ Login +gpluslogin.pageTitle = oxAuth - Login +gpluslogin.login = Login (second step) +gpluslogin.pleaseLoginHere = Please login here +gpluslogin.username = Username +gpluslogin.password = Password +gpluslogin.rememberMe = Remember Me +gpluslogin.termsPrivacy = Terms & Privacy + +otp.pageTitle = oxAuth - OTP Login +otp.otpCode = OTP code +otp.scanQRCode = Scan QR code using OTP authenticator and press finish button +otp.finish = Finish + +oxpush.pageTitle = oxAuth oxPush - Login +oxpush.login = oxPush Login (Authentication request) +oxpush.loginPairingRequest = oxPush Login (Pairing request) +oxpush.sendingAuthenticationRequest = Sending authentication request... +oxpush.termsPrivacy = Terms & Privacy +oxpush.title = oxAuth - Login +oxpush.loginLabel = Login +oxpush.pleaseLoginHere = Please login here +oxpush.username = Username +oxpush.password = Password +oxpush.rememberMe = Remember me +oxpush.beforeLogIn = Before log in make sure that you installed oxPush mobile application +oxpush.androidMobileApplication = Android mobile application +oxpush.byProceeding = By proceeding, you agree with the +oxpush.privacyPolicy = Privacy Policy +oxpush.pleaseRead = Please read the +oxpush.termsOfService = Terms of Service +oxpush.checkingPairing = Checking pairing status... +oxpush.waitingForUser = Waiting for user approval... +oxpush.pairingQRCode = Pairing QR code: +oxpush.pairingCode = Pairing code: + +passport.pageTitle = Passport - PostLogin +passport.loginSecondStep = Login (second step) +passport.pleaseLoginHere = Please login here +passport.email = Email +passport.termsPrivacy = Terms & Privacy +passport.oxAuthPassportLogin = oxAuth - Passport Login +passport.needAGluuAccount = Need a Gluu account? +passport.forgotYourPassword = Forgot your password? +passport.useExternalAuthentication = Use External Authentification +passport.registerNewUser = Register new user + +saml.pageTitle = oxAuth - Login +saml.login = Login (second step) +saml.username = Username +saml.password = Password +saml.rememberMe = Remember me +saml.termsPrivacy = Terms & Privacy + +supergluu.scanQRCode = Scan QR code using Super-Gluu + +uaf.pageTitle = oxAuth - UAF Login +uaf.scanQRCode = Scan QR code using UAF mobile authenticator + +# CIBA +ciba.bindingMessage=Binding Message + +device.authorization.pageTitle=Autorizzazione del dispositivo +device.authorization.title=Autorizzazione del dispositivo +device.authorization.subtitle=Un dispositivo sta elaborando l'autorizzazione per connettersi con il tuo account +device.authorization.code.inputbox.label=Codice visualizzato sul dispositivo +device.authorization.confirm.button=Continua +device.authorization.invalid.user.code=Codice non valido, rivedi il codice e riprova +device.authorization.access.denied.msg=L'autorizzazione e stata negata, riprovare dal dispositivo +device.authorization.expired.code.msg=Codice scaduto, riprovare dal dispositivo +device.authorization.init.new.request.msg=Init nuova richiesta +device.authorization.access.granted.title=Autorizzazione del dispositivo concessa +device.authorization.authorization.completed.msg=Riuscito! Le autorizzazioni necessarie sono state concesse al dispositivo. +device.authorization.brute.forcing.msg=Troppi tentativi falliti ... ricominciare dal dispositivo dopo un'ora. \ No newline at end of file diff --git a/oxAuth/Server/src/main/resources/oxauth_ru.properties b/oxAuth/Server/src/main/resources/oxauth_ru.properties new file mode 100644 index 00000000..dd105eb9 --- /dev/null +++ b/oxAuth/Server/src/main/resources/oxauth_ru.properties @@ -0,0 +1,227 @@ +common.copyright=Работает на +common.allRightsReserved=Free and open source access management. +common.agreePolicy=ПродолжаÑ, вы ÑоглашаетеÑÑŒ Ñ #{client.getClientName()} +common.privacyPolicy=Политика конфиденциальноÑти +common.pleaseReadTheTos=ПожалуйÑта, прочитайте +common.termsOfService=УÑÐ»Ð¾Ð²Ð¸Ñ Ð¸ÑÐ¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ð½Ð¸Ñ +common.gluuInc=Gluu, Inc +common.caution=ИÑпользовать в ÑоответÑтвии Ñ Ð»Ð¸Ñ†ÐµÐ½Ð·Ð¸ÐµÐ¹ MIT LICENSE + +login.pageTitle=oxAuth - Вход +login.login=Вход +login.register=РегиÑÑ‚Ñ€Ð°Ñ†Ð¸Ñ +login.pleaseLoginHere=ПожалуйÑта, войдите здеÑÑŒ +login.username=Ð˜Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ +login.password=Пароль +login.rememberMe=Помнить Ð¼ÐµÐ½Ñ +login.errorMessage=Ðеверное Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð¸Ð»Ð¸ пароль. +login.errorSessionInvalidMessage=\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438. \u0421\u0435\u0441\u0441\u0438\u044f \u0438\u0441\u0442\u0435\u043a\u043b\u0430. +login.failedToAuthenticate=Ðе удалоÑÑŒ выполнить проверку подлинноÑти. +login.youDontHavePermission=Ðе доÑтаточно прав. +login.forgotYourPassword = Forgot your password? + +logout.failedToProceed=Ошибка при выполнении выхода +logout.missingParameters=Invalid Request. The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed. + +authorize.pageTitle=oxAuth - Authorize +authorize.requestForPermission=Request for Permission +authorize.requestingPermissionForScopes={0} is requesting permission to do the following: +authorize.viewIndividualClaim=View {0} claim. +authorize.allow=Разрешить +authorize.doNotAllow=Don't Allow + +up=\u2191 +down=\u2193 +left=\u2039 +right=\u203A + +validator.assertFalse=validation failed +validator.assertTrue=validation failed +validator.future=must be a future date +validator.length=length must be between {min} and {max} +validator.max=must be less than or equal to {value} +validator.min=must be greater than or equal to {value} +validator.notNull=may not be null +validator.past=must be a past date +validator.pattern=must match "{regex}" +validator.range=must be between {min} and {max} +validator.size=size must be between {min} and {max} +validator.email=must be a well-formed email address + +org.jboss.seam.loginFailed=Login failed +org.jboss.seam.loginSuccessful=Welcome, #0! + +org.jboss.seam.TransactionFailed=Transaction failed +org.jboss.seam.NoConversation=The conversation ended, timed out or was processing another request +org.jboss.seam.IllegalNavigation=Illegal navigation +org.jboss.seam.ProcessEnded=Process #0 already ended +org.jboss.seam.ProcessNotFound=Process #0 not found +org.jboss.seam.TaskEnded=Task #0 already ended +org.jboss.seam.TaskNotFound=Task #0 not found +org.jboss.seam.NotLoggedIn=Please log in first + +javax.faces.component.UIInput.CONVERSION=value could not be converted to the expected type +javax.faces.component.UIInput.REQUIRED=value is required +javax.faces.component.UIInput.UPDATE=an error occurred when processing your submitted information +javax.faces.component.UISelectOne.INVALID=value is not valid +javax.faces.component.UISelectMany.INVALID=value is not valid + +javax.faces.converter.BigDecimalConverter.DECIMAL=value must be a number +javax.faces.converter.BigDecimalConverter.DECIMAL_detail=value must be a signed decimal number consisting of zero or more digits, optionally followed by a decimal point and fraction, eg. {1} +javax.faces.converter.BigIntegerConverter.BIGINTEGER=value must be an integer +javax.faces.converter.BigIntegerConverter.BIGINTEGER_detail=value must be a signed integer number consisting of zero or more digits +javax.faces.converter.BooleanConverter.BOOLEAN=value must be true or false +javax.faces.converter.BooleanConverter.BOOLEAN_detail=value must be true or false (any value other than true will evaluate to false) +javax.faces.converter.ByteConverter.BYTE=value must be a number between 0 and 255 +javax.faces.converter.ByteConverter.BYTE_detail=value must be a number between 0 and 255 +javax.faces.converter.CharacterConverter.CHARACTER=value must be a character +javax.faces.converter.CharacterConverter.CHARACTER_detail=value must be a valid ASCII character +javax.faces.converter.DateTimeConverter.DATE=value must be a date +javax.faces.converter.DateTimeConverter.DATE_detail=value must be a date, eg. {1} +javax.faces.converter.DateTimeConverter.TIME=value must be a time +javax.faces.converter.DateTimeConverter.TIME_detail=value must be a time, eg. {1} +javax.faces.converter.DateTimeConverter.DATETIME=value must be a date and time +javax.faces.converter.DateTimeConverter.DATETIME_detail=value must be a date and time, eg. {1} +javax.faces.converter.DateTimeConverter.PATTERN_TYPE=a pattern or type attribute must be specified to convert the value +javax.faces.converter.DoubleConverter.DOUBLE=value must be a number +javax.faces.converter.DoubleConverter.DOUBLE_detail=value must be a number between 4.9E-324 and 1.7976931348623157E308 +javax.faces.converter.EnumConverter.ENUM=value must be convertible to an enum +javax.faces.converter.EnumConverter.ENUM_detail=value must be convertible to an enum or from the enum that contains the constant {1} +javax.faces.converter.EnumConverter.ENUM_NO_CLASS=value must be convertible to an enum or from the enum, but no enum class provided +javax.faces.converter.EnumConverter.ENUM_NO_CLASS_detail=value must be convertible to an enum or from the enum, but no enum class provided +javax.faces.converter.FloatConverter.FLOAT=value must be a number +javax.faces.converter.FloatConverter.FLOAT_detail=value must be a number between 1.4E-45 and 3.4028235E38 +javax.faces.converter.IntegerConverter.INTEGER=value must be an integer +javax.faces.converter.IntegerConverter.INTEGER_detail=value must be an integer number between -2147483648 and 2147483647 +javax.faces.converter.LongConverter.LONG=value must be an integer +javax.faces.converter.LongConverter.LONG_detail=value must be an integer number between -9223372036854775808 and 9223372036854775807 +javax.faces.converter.NumberConverter.CURRENCY=value must be a currency amount +javax.faces.converter.NumberConverter.CURRENCY_detail=value must be a currency amount, eg. {1} +javax.faces.converter.NumberConverter.PERCENT=value must be a percentage amount +javax.faces.converter.NumberConverter.PERCENT_detail=value must be a percentage amount, eg. {1} +javax.faces.converter.NumberConverter.NUMBER=value must be a number +javax.faces.converter.NumberConverter.NUMBER_detail=value must be a number +javax.faces.converter.NumberConverter.PATTERN=value must be a number +javax.faces.converter.NumberConverter.PATTERN_detail=value must be a number +javax.faces.converter.ShortConverter.SHORT=value must be an integer +javax.faces.converter.ShortConverter.SHORT_detail=value must be an integer number between -32768 and 32767 + +javax.faces.validator.DoubleRangeValidator.MAXIMUM=value must be less than or equal to {0} +javax.faces.validator.DoubleRangeValidator.MINIMUM=value must be greater than or equal to {0} +javax.faces.validator.DoubleRangeValidator.NOT_IN_RANGE=value must be between {0} and {1} +javax.faces.validator.DoubleRangeValidator.TYPE=value is not of the correct type +javax.faces.validator.LengthValidator.MAXIMUM=value must be shorter than or equal to {0} characters +javax.faces.validator.LengthValidator.MINIMUM=value must be longer than or equal to {0} characters +javax.faces.validator.LongRangeValidator.MAXIMUM=value must be less than or equal to {0} +javax.faces.validator.LongRangeValidator.MINIMUM=value must be greater than or equal to {0} +javax.faces.validator.LongRangeValidator.NOT_IN_RANGE=value must be between {0} and {1} +javax.faces.validator.LongRangeValidator.TYPE=value is not of the correct type + +javax.faces.validator.NOT_IN_RANGE=value must be between {0} and {1} +javax.faces.converter.STRING=value could not be converted to a string + +cas2login.pageTitle = oxAuth - Login +cas2login.loginHeader = Login (second step) +cas2login.pleaseLoginHere = Please login here +cas2login.username = Username +cas2login.password = Password +cas2login.rememberMe = Remember me +cas2login.termsPrivace = Terms & Privacy + +error.errorEncountered = Error Encountered +error.unexpectedError = An unexpected error has occurred at {0} + +cert.failedToCheckCertificate = Failed to check certificate +cert.thisCanHappen = This can happen in the following cases: +cert.youHaveExpired = You have expired certificate.
Please, registered with the system and get new certificate. +cert.youSelectedInvalid = You selected invalid certificate.
Please, restart your browser and try to log in again. +cert.noteInternetExplorer = Note: Internet Explorer and Mozilla Firefox allow to clear the SSL state without restarting the browser: +cert.ifYouAreUsing = If you are using Internet Explorer, you can use the "Clear SSL" button in the settings section: Contents (click Tools \u2192 Internet Options). +cert.ifYouUseFireFox = If you use FireFox, you can use the menu: Options \u2192 Privacy \u2192 Clear all current history \u2192 select \u201cActive Logins\u201d \u2192 press button Clear Now. +cert.youFailedToInstall = You failed to install the personal certificate into the browser
Please, install the certificate from the backup file (.PFX), restart your browser and try to log in again. +cert.youHaveAlreadyOpened = You have already opened the page in this browser but haven't chosen any certificate for further usage. The browser now remembers this choice.
Please, restart your browser, log in again and choose the appropriate certificate.
If you use Internet Explorer, click "Clear SSL state" at Tools/Internet Options/Content. +cert.ifYouUseMacOS = If you use MacOS X and updated the operation system to version 10.5.3 please see http://support.apple.com/kb/HT1679?viewlocale=en_US to resolve the issue. + +duologin.title = oxAuth - DUO Login +duologin.login = DUO Login (second step) +duologin.termsPrivacy = Terms & Privacy + +gpluslogin.title = oxAuth - Google+ Login +gpluslogin.pageTitle = oxAuth - Login +gpluslogin.login = Login (second step) +gpluslogin.pleaseLoginHere = Please login here +gpluslogin.username = Username +gpluslogin.password = Password +gpluslogin.rememberMe = Remember Me +gpluslogin.termsPrivacy = Terms & Privacy + +otp.pageTitle = oxAuth - OTP Login +otp.otpCode = OTP code +otp.scanQRCode = Scan QR code using OTP authenticator and press finish button +otp.finish = Finish + +oxpush.pageTitle = oxAuth oxPush - Login +oxpush.login = oxPush Login (Authentication request) +oxpush.loginPairingRequest = oxPush Login (Pairing request) +oxpush.sendingAuthenticationRequest = Sending authentication request... +oxpush.termsPrivacy = Terms & Privacy +oxpush.title = oxAuth - Login +oxpush.loginLabel = Login +oxpush.pleaseLoginHere = Please login here +oxpush.username = Username +oxpush.password = Password +oxpush.rememberMe = Remember me +oxpush.beforeLogIn = Before log in make sure that you installed oxPush mobile application +oxpush.androidMobileApplication = Android mobile application +oxpush.byProceeding = By proceeding, you agree with the +oxpush.privacyPolicy = Privacy Policy +oxpush.pleaseRead = Please read the +oxpush.termsOfService = Terms of Service +oxpush.checkingPairing = Checking pairing status... +oxpush.waitingForUser = Waiting for user approval... +oxpush.pairingQRCode = Pairing QR code: +oxpush.pairingCode = Pairing code: + +passport.pageTitle = Passport - PostLogin +passport.loginSecondStep = Login (second step) +passport.pleaseLoginHere = Please login here +passport.email = Email +passport.termsPrivacy = Terms & Privacy +passport.oxAuthPassportLogin = oxAuth - Passport Login +passport.needAGluuAccount = Need a Gluu account? +passport.forgotYourPassword = Forgot your password? +passport.useExternalAuthentication = Use External Authentification +passport.registerNewUser = Register new user + +saml.pageTitle = oxAuth - Login +saml.login = Login (second step) +saml.username = Username +saml.password = Password +saml.rememberMe = Remember me +saml.termsPrivacy = Terms & Privacy + +supergluu.scanQRCode = Scan QR code using Super-Gluu + +fido2.verification.stepverification=2 Step Verification +fido2.verification.usedevice=Use your fido2 device to sign in to your Gluu account. +fido2.verification.insertkey = Insert your fido2 security key. +fido2.verification.useit=If your fido2 key has a button, tap it. Otherwise you can remove it and re-insert it. + +uaf.pageTitle = oxAuth - UAF Login +uaf.scanQRCode = Scan QR code using UAF mobile authenticator + +# CIBA +ciba.bindingMessage=Binding Message + +device.authorization.pageTitle=ÐÐ²Ñ‚Ð¾Ñ€Ð¸Ð·Ð°Ñ†Ð¸Ñ ÑƒÑтройÑтва +device.authorization.title=ÐÐ²Ñ‚Ð¾Ñ€Ð¸Ð·Ð°Ñ†Ð¸Ñ ÑƒÑтройÑтва +device.authorization.subtitle=УÑтройÑтво обрабатывает разрешение на подключение к вашей учетной запиÑи +device.authorization.code.inputbox.label=Код отображаетÑÑ Ð½Ð° вашем уÑтройÑтве +device.authorization.confirm.button=Продолжать +device.authorization.invalid.user.code=Ðеверный код, пожалуйÑта, проверьте код и повторите попытку +device.authorization.access.denied.msg=Ð’ авторизации отказано, повторите попытку Ñ ÑƒÑтройÑтва +device.authorization.expired.code.msg=Код иÑтек, повторите попытку Ñ ÑƒÑтройÑтва +device.authorization.init.new.request.msg=Инициировать новый Ð·Ð°Ð¿Ñ€Ð¾Ñ +device.authorization.access.granted.title=ÐÐ²Ñ‚Ð¾Ñ€Ð¸Ð·Ð°Ñ†Ð¸Ñ ÑƒÑтройÑтва предоÑтавлена +device.authorization.authorization.completed.msg=УÑпешный! Требуемые Ñ€Ð°Ð·Ñ€ÐµÑˆÐµÐ½Ð¸Ñ Ð±Ñ‹Ð»Ð¸ предоÑтавлены уÑтройÑтву. +device.authorization.brute.forcing.msg=Слишком много неудачных попыток ... пожалуйÑта, повторите инициализацию Ñ ÑƒÑтройÑтва через чаÑ. \ No newline at end of file diff --git a/oxAuth/Server/src/main/resources/oxauth_tr.properties b/oxAuth/Server/src/main/resources/oxauth_tr.properties new file mode 100644 index 00000000..81d4029f --- /dev/null +++ b/oxAuth/Server/src/main/resources/oxauth_tr.properties @@ -0,0 +1,206 @@ +login.pageTitle=oxAuth - Login +login.login=Login +login.register=Register +login.pleaseLoginHere=Please login here +login.username=Username +login.password=Password +login.rememberMe=Remember me +login.errorMessage=Incorrect email or password. +login.errorSessionInvalidMessage=Failed to authenticate. Authentication session has expired +login.failedToAuthenticate=Failed to authenticate. +login.youDontHavePermission=You don't have permissions. +login.forgotYourPassword = Forgot your password? + +logout.missingParameters=Invalid Request. The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed. +logout.failedToProceed=Failed to process logout + +up=\u2191 +down=\u2193 +left=\u2039 +right=\u203a + +validator.assertFalse=do\u011frulanamad\u0131 +validator.assertTrue=do\u011frulanamad\u0131 +validator.future=gelecek bir tarih olmal\u0131d\u0131r +validator.length=girilen de\u011ferin uzunlu\u011fu {min} ile {max} aras\u0131nda olmal\u0131d\u0131r +validator.max=girilen de\u011fer {value} ya da daha k\u00fc\u00e7\u00fck olmal\u0131d\u0131r +validator.min=girilen de\u011fer {value} ya da daha b\u00fcy\u00fck olmal\u0131d\u0131r +validator.notNull=bir de\u011fer girilmesi gereklidir +validator.past=ge\u00e7mi\u015f bir tarih girilmelidir +validator.pattern=girilen de\u011fer "{regex}" tan\u0131mlamas\u0131na uymal\u0131d\u0131r +validator.range=girilen de\u011ferin b\u00fcy\u00fckl\u00fc\u011f\u00fc {min} ile {max} aras\u0131nda olmal\u0131d\u0131r +validator.size=girilen de\u011ferin boyut {min} ile {max} aras\u0131nda olmal\u0131d\u0131r +validator.email=girilen de\u011fer e-posta adresi format\u0131na uygun olmal\u0131d\u0131r + +org.jboss.seam.loginFailed=Oturum a\u00e7\u0131lamad\u0131 +org.jboss.seam.loginSuccessful=Ho\u015fgeldiniz, #0! + +org.jboss.seam.TransactionFailed=\u0130\u015flem tamamlanamad\u0131 +org.jboss.seam.NoConversation=Konu\u015fma sonland\u0131, zaman a\u015f\u0131m\u0131na u\u011frad\u0131 ya da ba\u015fka bir talebi i\u015fliyordu +org.jboss.seam.IllegalNavigation=Ge\u00e7ersiz y\u00f6nleme(navigasyon) +org.jboss.seam.ProcessEnded=#0 s\u00fcreci sonlanm\u0131\u015f +org.jboss.seam.ProcessNotFound=#0 s\u00fcreci bulunamad\u0131 +org.jboss.seam.TaskEnded=#0 g\u00f6revi sonlanm\u0131\u015f +org.jboss.seam.TaskNotFound=#0 g\u00f6revi bulunamad\u0131 +org.jboss.seam.NotLoggedIn=L\u00fctfen \u00f6ncelikle oturum a\u00e7\u0131n + +javax.faces.component.UIInput.CONVERSION=girilen de\u011fer, beklenen tipe d\u00f6n\u00fc\u015ft\u00fcr\u00fclemedi +javax.faces.component.UIInput.REQUIRED=bir de\u011fer girilmesi gereklidir +javax.faces.component.UIInput.UPDATE=talebiniz i\u015flenirken bir hata olu\u015ftu +javax.faces.component.UISelectOne.INVALID=ge\u00e7erli bir de\u011fer girilmelidir +javax.faces.component.UISelectMany.INVALID=ge\u00e7erli bir de\u011fer girilmelidir + +javax.faces.converter.BigDecimalConverter.DECIMAL=bir say\u0131 girilmelidir +javax.faces.converter.BigDecimalConverter.DECIMAL_detail=girilen de\u011fer, s\u0131f\u0131r ya da daha fazla basamak i\u00e7eren, ondal\u0131k ayrac\u0131 ve ondal\u0131k basamak i\u00e7erebilen i\u015faretli bir ondal\u0131k say\u0131 olmal\u0131d\u0131r, \u00f6rn. {1} +javax.faces.converter.BigIntegerConverter.BIGINTEGER=bir tamsay\u0131 girilmelidir +javax.faces.converter.BigIntegerConverter.BIGINTEGER_detail=girilen de\u011fer bir ya da daha fazla basamak i\u00e7eren bir tamsay\u0131 olmal\u0131d\u0131r +javax.faces.converter.BooleanConverter.BOOLEAN=girilen de\u011fer true ya da false olmal\u0131d\u0131r +javax.faces.converter.BooleanConverter.BOOLEAN_detail=girilen de\u011fer true ya da false olmal\u0131d\u0131r (true olmayan her de\u011fer false olarak de\u011ferlendirilecektir) +javax.faces.converter.ByteConverter.BYTE=girilen de\u011fer 0 ile 255 aras\u0131nda bir say\u0131 olmal\u0131d\u0131r +javax.faces.converter.ByteConverter.BYTE_detail=girilen de\u011fer 0 ile 255 aras\u0131nda bir say\u0131 olmal\u0131d\u0131r +javax.faces.converter.CharacterConverter.CHARACTER=girilen de\u011fer bir karakter olmal\u0131d\u0131r +javax.faces.converter.CharacterConverter.CHARACTER_detail=ge\u00e7erli bir ASCII karakter girilmelidir +javax.faces.converter.DateTimeConverter.DATE=girilen de\u011fer bir tarih olmal\u0131d\u0131r +javax.faces.converter.DateTimeConverter.DATE_detail=girilen de\u011fer bir tarih olmal\u0131d\u0131r, \u00f6rn. {1} +javax.faces.converter.DateTimeConverter.TIME=girilen de\u011fer bir zaman olmal\u0131d\u0131r +javax.faces.converter.DateTimeConverter.TIME_detail=girilen de\u011fer bir zaman olmal\u0131d\u0131r, \u00f6rn. {1} +javax.faces.converter.DateTimeConverter.DATETIME=girilen de\u011fer tarih ve zaman i\u00e7ermelidir +javax.faces.converter.DateTimeConverter.DATETIME_detail=girilen de\u011fer tarih ve zaman i\u00e7ermelidir, \u00f6rn. {1} +javax.faces.converter.DateTimeConverter.PATTERN_TYPE=girilen de\u011feri d\u00f6n\u00fc\u015ft\u00fcrebilmek i\u00e7in bir desen(pattern) ya da tip \u00f6zniteli\u011fi(attribute) tan\u0131mlanmal\u0131d\u0131r. +javax.faces.converter.DoubleConverter.DOUBLE=bir say\u0131 girilmelidir +javax.faces.converter.DoubleConverter.DOUBLE_detail=4.9E-324 ile 1.7976931348623157E308 aras\u0131nda bir say\u0131 girilmelidir +javax.faces.converter.EnumConverter.ENUM=girilen de\u011fer enum tipine d\u00f6n\u00fc\u015ft\u00fcr\u00fclebilmelidir +javax.faces.converter.EnumConverter.ENUM_detail=girilen de\u011fer sabit i\u00e7eren bir enum tipinden enum tipine d\u00f6n\u00fc\u015ft\u00fcr\u00fclebilmelidir {1} +javax.faces.converter.EnumConverter.ENUM_NO_CLASS=girilen de\u011fer enum tipinden enum tipine d\u00f6n\u00fc\u015ft\u00fcr\u00fclebilmelidir. Ancak enum s\u0131n\u0131f\u0131(class) bulunamad\u0131 +javax.faces.converter.EnumConverter.ENUM_NO_CLASS_detail=girilen de\u011fer enum tipinden enum tipine d\u00f6n\u00fc\u015ft\u00fcr\u00fclebilmelidir. Ancak enum s\u0131n\u0131f\u0131(class) bulunamad\u0131 +javax.faces.converter.FloatConverter.FLOAT=bir say\u0131 girilmelidir +javax.faces.converter.FloatConverter.FLOAT_detail=1.4E-45 ile 3.4028235E38 aras\u0131nda bir say\u0131 girilmelidir +javax.faces.converter.IntegerConverter.INTEGER=bir tamsay\u0131 girilmelidir +javax.faces.converter.IntegerConverter.INTEGER_detail=-2147483648 ile 2147483647 aras\u0131nda bir tamsay\u0131 girilmelidir +javax.faces.converter.LongConverter.LONG=bir tamsay\u0131 girilmelidir +javax.faces.converter.LongConverter.LONG_detail=-9223372036854775808 ile 9223372036854775807 aras\u0131nda bir tamsay\u0131 girilmelidir +javax.faces.converter.NumberConverter.CURRENCY=para birimi tipinden bir de\u011fer girilmelidir +javax.faces.converter.NumberConverter.CURRENCY_detail=para birimi tipinden bir de\u011fer girilmelidir, \u00f6rn. {1} +javax.faces.converter.NumberConverter.PERCENT=y\u00fczdelik tipinden bir de\u011fer girilmelidir +javax.faces.converter.NumberConverter.PERCENT_detail=y\u00fczdelik tipinden bir de\u011fer girilmelidir, \u00f6rn. {1} +javax.faces.converter.NumberConverter.NUMBER=bir say\u0131 girilmelidir +javax.faces.converter.NumberConverter.NUMBER_detail=bir say\u0131 girilmelidir +javax.faces.converter.NumberConverter.PATTERN=bir say\u0131 girilmelidir +javax.faces.converter.NumberConverter.PATTERN_detail=bir say\u0131 girilmelidir +javax.faces.converter.ShortConverter.SHORT=bir tamsay\u0131 girilmelidir +javax.faces.converter.ShortConverter.SHORT_detail=-32768 ile 32767 aras\u0131nda bir tamsay\u0131 girilmelidir + +javax.faces.validator.DoubleRangeValidator.MAXIMUM=girilen de\u011fer {0} ya da daha k\u00fc\u00e7\u00fck olmal\u0131d\u0131r +javax.faces.validator.DoubleRangeValidator.MINIMUM=girilen de\u011fer {0} ya da daha b\u00fcy\u00fck olmal\u0131d\u0131r +javax.faces.validator.DoubleRangeValidator.NOT_IN_RANGE=girilen de\u011fer {0} ile {1} aras\u0131nda olmal\u0131d\u0131r +javax.faces.validator.DoubleRangeValidator.TYPE=girilen de\u011ferin tipi yanl\u0131\u015f +javax.faces.validator.LengthValidator.MAXIMUM=girilen de\u011fer {0} ya da daha az karakter i\u00e7ermelidir +javax.faces.validator.LengthValidator.MINIMUM=girilen de\u011fer {0} ya da fazla karakter i\u00e7ermelidir +javax.faces.validator.LongRangeValidator.MAXIMUM=girilen de\u011fer {0} ya da daha k\u00fc\u00e7\u00fck olmal\u0131d\u0131r +javax.faces.validator.LongRangeValidator.MINIMUM=girilen de\u011fer {0} ya da daha b\u00fcy\u00fck olmal\u0131d\u0131r +javax.faces.validator.LongRangeValidator.NOT_IN_RANGE=girilen de\u011fer {0} ile {1} aral\u0131\u011f\u0131nda olmal\u0131d\u0131r +javax.faces.validator.LongRangeValidator.TYPE=yanl\u0131\u015f tipte bir de\u011fer girildi + +javax.faces.validator.NOT_IN_RANGE=girilen de\u011fer {0} ile {1} aral\u0131\u011f\u0131nda olmal\u0131d\u0131r +javax.faces.converter.STRING=girilen de\u011fer bir dizgiye(string) d\u00f6n\u00fc\u015ft\u00fcr\u00fclemedi + +cas2login.pageTitle = oxAuth - Login +cas2login.loginHeader = Login (second step) +cas2login.pleaseLoginHere = Please login here +cas2login.username = Username +cas2login.password = Password +cas2login.rememberMe = Remember me +cas2login.termsPrivace = Terms & Privacy + +error.errorEncountered = Error Encountered +error.unexpectedError = An unexpected error has occurred at {0} + +cert.failedToCheckCertificate = Failed to check certificate +cert.thisCanHappen = This can happen in the following cases: +cert.youHaveExpired = You have expired certificate.
Please, registered with the system and get new certificate. +cert.youSelectedInvalid = You selected invalid certificate.
Please, restart your browser and try to log in again. +cert.noteInternetExplorer = Note: Internet Explorer and Mozilla Firefox allow to clear the SSL state without restarting the browser: +cert.ifYouAreUsing = If you are using Internet Explorer, you can use the "Clear SSL" button in the settings section: Contents (click Tools \u2192 Internet Options). +cert.ifYouUseFireFox = If you use FireFox, you can use the menu: Options \u2192 Privacy \u2192 Clear all current history \u2192 select \u201cActive Logins\u201d \u2192 press button Clear Now. +cert.youFailedToInstall = You failed to install the personal certificate into the browser
Please, install the certificate from the backup file (.PFX), restart your browser and try to log in again. +cert.youHaveAlreadyOpened = You have already opened the page in this browser but haven't chosen any certificate for further usage. The browser now remembers this choice.
Please, restart your browser, log in again and choose the appropriate certificate.
If you use Internet Explorer, click "Clear SSL state" at Tools/Internet Options/Content. +cert.ifYouUseMacOS = If you use MacOS X and updated the operation system to version 10.5.3 please see http://support.apple.com/kb/HT1679?viewlocale=en_US to resolve the issue. + +duologin.title = oxAuth - DUO Login +duologin.login = DUO Login (second step) +duologin.termsPrivacy = Terms & Privacy + +gpluslogin.title = oxAuth - Google+ Login +gpluslogin.pageTitle = oxAuth - Login +gpluslogin.login = Login (second step) +gpluslogin.pleaseLoginHere = Please login here +gpluslogin.username = Username +gpluslogin.password = Password +gpluslogin.rememberMe = Remember Me +gpluslogin.termsPrivacy = Terms & Privacy + +otp.pageTitle = oxAuth - OTP Login +otp.otpCode = OTP code +otp.scanQRCode = Scan QR code using OTP authenticator and press finish button +otp.finish = Finish + +oxpush.pageTitle = oxAuth oxPush - Login +oxpush.login = oxPush Login (Authentication request) +oxpush.loginPairingRequest = oxPush Login (Pairing request) +oxpush.sendingAuthenticationRequest = Sending authentication request... +oxpush.termsPrivacy = Terms & Privacy +oxpush.title = oxAuth - Login +oxpush.loginLabel = Login +oxpush.pleaseLoginHere = Please login here +oxpush.username = Username +oxpush.password = Password +oxpush.rememberMe = Remember me +oxpush.beforeLogIn = Before log in make sure that you installed oxPush mobile application +oxpush.androidMobileApplication = Android mobile application +oxpush.byProceeding = By proceeding, you agree with the +oxpush.privacyPolicy = Privacy Policy +oxpush.pleaseRead = Please read the +oxpush.termsOfService = Terms of Service +oxpush.checkingPairing = Checking pairing status... +oxpush.waitingForUser = Waiting for user approval... +oxpush.pairingQRCode = Pairing QR code: +oxpush.pairingCode = Pairing code: + +passport.pageTitle = Passport - PostLogin +passport.loginSecondStep = Login (second step) +passport.pleaseLoginHere = Please login here +passport.email = Email +passport.termsPrivacy = Terms & Privacy +passport.oxAuthPassportLogin = oxAuth - Passport Login +passport.needAGluuAccount = Need a Gluu account? +passport.forgotYourPassword = Forgot your password? +passport.useExternalAuthentication = Use External Authentification +passport.registerNewUser = Register new user + +saml.pageTitle = oxAuth - Login +saml.login = Login (second step) +saml.username = Username +saml.password = Password +saml.rememberMe = Remember me +saml.termsPrivacy = Terms & Privacy + +supergluu.scanQRCode = Scan QR code using Super-Gluu + +uaf.pageTitle = oxAuth - UAF Login +uaf.scanQRCode = Scan QR code using UAF mobile authenticator + +# CIBA +ciba.bindingMessage=Binding Message + +device.authorization.pageTitle=Cihaz Yetkilendirme +device.authorization.title=Cihaz Yetkilendirme +device.authorization.subtitle=Bir cihaz, hesabınızla ba\u011flantı kurma iznini i\u015fliyor +device.authorization.code.inputbox.label=Cihazınızda g\u00f6r\u00fcnt\u00fclenen kod +device.authorization.confirm.button=Devam et +device.authorization.invalid.user.code=Ge\u00e7ersiz kod, l\u00fctfen kodu inceleyin ve tekrar deneyin +device.authorization.access.denied.msg=Yetkilendirme reddedildi, l\u00fctfen cihazdan tekrar deneyin +device.authorization.expired.code.msg=Kodun s\u00fcresi doldu, l\u00fctfen cihazdan tekrar deneyin +device.authorization.init.new.request.msg=Yeni istek ba\u015flat +device.authorization.access.granted.title=Cihaz yetkilendirmesi verildi +device.authorization.authorization.completed.msg=Ba\u015farılı! Cihaza gerekli izinler verildi. +device.authorization.brute.forcing.msg=\u00c7\u006f\u006b \u0066\u0061\u007a\u006c\u0061 \u0062\u0061\u015f\u0061\u0072\u0131\u0073\u0131\u007a \u0064\u0065\u006e\u0065\u006d\u0065 \u002e\u002e\u002e \u004c\u00fc\u0074\u0066\u0065\u006e \u0062\u0069\u0072 \u0073\u0061\u0061\u0074 \u0073\u006f\u006e\u0072\u0061 \u0063\u0069\u0068\u0061\u007a\u0064\u0061\u006e \u0074\u0065\u006b\u0072\u0061\u0072 \u0062\u0061\u015f\u006c\u0061\u0074\u0131\u006e\u002e \ No newline at end of file diff --git a/oxAuth/Server/src/main/resources/quartz.properties b/oxAuth/Server/src/main/resources/quartz.properties new file mode 100644 index 00000000..618df94d --- /dev/null +++ b/oxAuth/Server/src/main/resources/quartz.properties @@ -0,0 +1,4 @@ +org.quartz.scheduler.instanceName=oxAuthScheduler +org.quartz.threadPool.threadCount=5 +org.quartz.jobStore.class=org.quartz.simpl.RAMJobStore +org.quartz.scheduler.skipUpdateCheck=true diff --git a/oxAuth/Server/src/main/resources/validation_messages.properties b/oxAuth/Server/src/main/resources/validation_messages.properties new file mode 100644 index 00000000..f5e33e7b --- /dev/null +++ b/oxAuth/Server/src/main/resources/validation_messages.properties @@ -0,0 +1,131 @@ +# Copyright 2004 The Apache Software Foundation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# standard messages (Spec. 2.5.2.4) + +# components +javax.faces.component.UIInput.CONVERSION = {0}: Conversion error occurred. +javax.faces.component.UIInput.REQUIRED = Value is required. +javax.faces.component.UIInput.UPDATE = {0}: An error occurred when processing your submitted information. + +javax.faces.component.UISelectOne.INVALID = {0}: Validation Error: Value is not valid +javax.faces.component.UISelectMany.INVALID = {0}: Validation Error: Value is not valid + +# converters +javax.faces.converter.BigDecimalConverter.DECIMAL = {2}: ''{0}'' must be a signed decimal number. +javax.faces.converter.BigDecimalConverter.DECIMAL_detail = {2}: ''{0}'' must be a signed decimal number consisting of zero or more digits, that may be followed by a decimal point and fraction. Example: {1} + +javax.faces.converter.BigIntegerConverter.BIGINTEGER = {2}: ''{0}'' must be a number consisting of one or more digits. +javax.faces.converter.BigIntegerConverter.BIGINTEGER_detail = {2}: ''{0}'' must be a number consisting of one or more digits. Example: {1} + +javax.faces.converter.BooleanConverter.BOOLEAN = {1}: ''{0}'' must be ''true'' or ''false''. +javax.faces.converter.BooleanConverter.BOOLEAN_detail = {1}: ''{0}'' must be ''true'' or ''false''. Any value other than ''true'' will evaluate to ''false''. + +javax.faces.converter.ByteConverter.BYTE = {2}: ''{0}'' must be a number between 0 and 255. +javax.faces.converter.ByteConverter.BYTE_detail = {2}: ''{0}'' must be a number between 0 and 255. Example: {1} + +javax.faces.converter.CharacterConverter.CHARACTER = {1}: ''{0}'' must be a valid character. +javax.faces.converter.CharacterConverter.CHARACTER_detail = {1}: ''{0}'' must be a valid ASCII character. + +javax.faces.converter.DateTimeConverter.DATE = {2}: ''{0}'' could not be understood as a date. +javax.faces.converter.DateTimeConverter.DATE_detail = {2}: ''{0}'' could not be understood as a date. Example: {1} +javax.faces.converter.DateTimeConverter.TIME = {2}: ''{0}'' could not be understood as a time. +javax.faces.converter.DateTimeConverter.TIME_detail = {2}: ''{0}'' could not be understood as a time. Example: {1} +javax.faces.converter.DateTimeConverter.DATETIME = {2}: ''{0}'' could not be understood as a date and time. +javax.faces.converter.DateTimeConverter.DATETIME_detail = {2}: ''{0}'' could not be understood as a date and time. Example: {1} +javax.faces.converter.DateTimeConverter.PATTERN_TYPE = {1}: A ''pattern'' or ''type'' attribute must be specified to convert the value ''{0}''. + +javax.faces.converter.DoubleConverter.DOUBLE = {2}: ''{0}'' must be a number consisting of one or more digits. +javax.faces.converter.DoubleConverter.DOUBLE_detail = {2}: ''{0}'' must be a number between 4.9E-324 and 1.7976931348623157E308 Example: {1} + +javax.faces.converter.EnumConverter.ENUM = {2}: ''{0}'' must be convertible to an enum. +javax.faces.converter.EnumConverter.ENUM_detail = {2}: ''{0}'' must be convertible to an enum from the enum that contains the constant ''{1}''. +javax.faces.converter.EnumConverter.ENUM_NO_CLASS = {1}: ''{0}'' must be convertible to an enum from the enum, but no enum class provided. +javax.faces.converter.EnumConverter.ENUM_NO_CLASS_detail = {1}: ''{0}'' must be convertible to an enum from the enum, but no enum class provided. + +javax.faces.converter.FloatConverter.FLOAT = {2}: ''{0}'' must be a number consisting of one or more digits. +javax.faces.converter.FloatConverter.FLOAT_detail = {2}: ''{0}'' must be a number between 1.4E-45 and 3.4028235E38 Example: {1} + +javax.faces.converter.IntegerConverter.INTEGER = {2}: ''{0}'' must be a number consisting of one or more digits. +javax.faces.converter.IntegerConverter.INTEGER_detail = {2}: ''{0}'' must be a number between -2147483648 and 2147483647 Example: {1} + +javax.faces.converter.LongConverter.LONG = {2}: ''{0}'' must be a number consisting of one or more digits. +javax.faces.converter.LongConverter.LONG_detail = {2}: ''{0}'' must be a number between -9223372036854775808 to 9223372036854775807 Example: {1} + +javax.faces.converter.NumberConverter.CURRENCY = {2}: ''{0}'' could not be understood as a currency value. +javax.faces.converter.NumberConverter.CURRENCY_detail = {2}: ''{0}'' could not be understood as a currency value. Example: {1} +javax.faces.converter.NumberConverter.PERCENT = {2}: ''{0}'' could not be understood as a percentage. +javax.faces.converter.NumberConverter.PERCENT_detail = {2}: ''{0}'' could not be understood as a percentage. Example: {1} +javax.faces.converter.NumberConverter.NUMBER = {2}: ''{0}'' is not a number. +javax.faces.converter.NumberConverter.NUMBER_detail = {2}: ''{0}'' is not a number. Example: {1} +javax.faces.converter.NumberConverter.PATTERN = {2}: ''{0}'' is not a number pattern. +javax.faces.converter.NumberConverter.PATTERN_detail = {2}: ''{0}'' is not a number pattern. Example: {1} + +javax.faces.converter.ShortConverter.SHORT = {2}: ''{0}'' must be a number consisting of one or more digits. +javax.faces.converter.ShortConverter.SHORT_detail = {2}: ''{0}'' must be a number between -32768 and 32767 Example: {1} + +javax.faces.converter.STRING = {1}: Could not convert ''{0}'' to a string. + +# validators +javax.faces.validator.NOT_IN_RANGE = Validation Error: Specified attribute is not between the expected values of {0} and {1}. + +javax.faces.validator.DoubleRangeValidator.MAXIMUM = {1}: Validation Error: Value is greater than allowable maximum of ''{0}'' +javax.faces.validator.DoubleRangeValidator.MINIMUM = {1}: Validation Error: Value is less than allowable minimum of ''{0}'' +javax.faces.validator.DoubleRangeValidator.NOT_IN_RANGE = {2}: Validation Error: Specified attribute is not between the expected values of {0} and {1}. +javax.faces.validator.DoubleRangeValidator.TYPE = {0}: Validation Error: Value is not of the correct type + +javax.faces.validator.LengthValidator.MAXIMUM = {1}: Validation Error: Length is greater than allowable maximum of ''{0}'' +javax.faces.validator.LengthValidator.MINIMUM = {1}: Validation Error: Length is less than allowable minimum of ''{0}'' + +javax.faces.validator.LongRangeValidator.MAXIMUM = {1}: Validation Error: Value is greater than allowable maximum of ''{0}'' +javax.faces.validator.LongRangeValidator.MINIMUM = {1}: Validation Error: Value is less than allowable minimum of ''{0}'' +javax.faces.validator.LongRangeValidator.NOT_IN_RANGE = {2}: Validation Error: Specified attribute is not between the expected values of {0} and {1}. +javax.faces.validator.LongRangeValidator.TYPE = {0}: Validation Error: Value is not of the correct type. + +javax.faces.validator.RegexValidator.NOT_MATCHED = {1}: Validation Error: Value not according to pattern ''{0}'' +javax.faces.validator.RegexValidator.PATTERN_NOT_SET = A pattern must be set for validate. +javax.faces.validator.RegexValidator.MATCH_EXCEPTION = The pattern is not a valid regular expression. + +javax.faces.validator.BeanValidator.MESSAGE = {1}: {0} + +# myfaces specific messages +org.apache.myfaces.renderkit.html.HtmlMessagesRenderer.IN_FIELD = \u0020in {0} +org.apache.myfaces.Email.INVALID = Validation Error +org.apache.myfaces.Email.INVALID_detail =The given value ({0}) is not a correct email-address. + +org.apache.myfaces.Equal.INVALID = Validation Error +org.apache.myfaces.Equal.INVALID_detail =The given value ({0}) is not equal with value of "{1}". + +org.apache.myfaces.Creditcard.INVALID = Validation Error +org.apache.myfaces.Creditcard.INVALID_detail =The given value ({0}) is not a correct creditcard + +org.apache.myfaces.Regexpr.INVALID=Validation Error +org.apache.myfaces.Regexpr.INVALID_detail=The given value ({0}) is not valid. + +org.apache.myfaces.Date.INVALID = Validation Error +org.apache.myfaces.Date.INVALID_detail =The given value ({0}) is not a correct date + +org.apache.myfaces.ticker.NOCONNECTION = No Connection: +org.apache.myfaces.ticker.NOCONNECTION_detail = Maybe you are behind a firewall? + +org.apache.myfaces.ISBN.INVALID = Validation Error +org.apache.myfaces.ISBN.INVALID_detail =The given value ({0}) is not a correct isbn code. + +org.apache.myfaces.tree2.MISSING_NODE = Missing Node +org.apache.myfaces.tree2.MISSING_NODE_detail = The requested node "{0}" does not exist. + +org.apache.myfaces.calendar.CONVERSION = Conversion Error +org.apache.myfaces.calendar.CONVERSION_detail = "{0}": The given value "{1}" could not be converted to a date. + +org.apache.myfaces.FileUpload.SIZE_LIMIT = "{0}": The uploaded file exceeded the maximum size of {1} bytes. diff --git a/oxAuth/Server/src/main/webapp-jetty/WEB-INF/jetty-env.xml b/oxAuth/Server/src/main/webapp-jetty/WEB-INF/jetty-env.xml new file mode 100644 index 00000000..5a0fe810 --- /dev/null +++ b/oxAuth/Server/src/main/webapp-jetty/WEB-INF/jetty-env.xml @@ -0,0 +1,21 @@ + + + + + + + + + + BeanManager + + + + javax.enterprise.inject.spi.BeanManager + org.jboss.weld.resources.ManagerObjectFactory + + + + + + diff --git a/oxAuth/Server/src/main/webapp-jetty/WEB-INF/web.xml b/oxAuth/Server/src/main/webapp-jetty/WEB-INF/web.xml new file mode 100644 index 00000000..0c8e5de9 --- /dev/null +++ b/oxAuth/Server/src/main/webapp-jetty/WEB-INF/web.xml @@ -0,0 +1,101 @@ + + + + + oxAuth Server + + + + org.eclipse.jetty.servlet.Default.dirAllowed + false + + + + + org.jboss.weld.development + ${weld.debug} + + + + + javax.faces.PROJECT_STAGE + Production + + + javax.faces.DEFAULT_SUFFIX + .xhtml + + + javax.faces.FACELETS_RESOURCE_RESOLVER + org.gluu.service.ExternalResourceHandler + + + + javax.faces.FACELETS_REFRESH_PERIOD + ${javax.faces.FACELETS_REFRESH_PERIOD} + + + javax.faces.STATE_SAVING_METHOD + client + + + + + org.richfaces.SKIN + glassX + + + + + org.richfaces.CONTROL_SKINNING + disable + + + org.richfaces.CONTROL_SKINNING_CLASSES + disable + + + + + org.jboss.weld.environment.servlet.Listener + + + + + com.sun.faces.config.ConfigureListener + + + + Faces Servlet + javax.faces.webapp.FacesServlet + 1 + + + + Faces Servlet + *.htm + + + + 1 + COOKIE + + + + Restrict raw XHTML Documents + + XHTML + *.xhtml + + + + + + + Object factory for the CDI Bean Manager + BeanManager + javax.enterprise.inject.spi.BeanManager + + + diff --git a/oxAuth/Server/src/main/webapp-tomcat/META-INF/context.xml b/oxAuth/Server/src/main/webapp-tomcat/META-INF/context.xml new file mode 100644 index 00000000..930c04ed --- /dev/null +++ b/oxAuth/Server/src/main/webapp-tomcat/META-INF/context.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + diff --git a/oxAuth/Server/src/main/webapp/META-INF/MANIFEST.MF b/oxAuth/Server/src/main/webapp/META-INF/MANIFEST.MF new file mode 100644 index 00000000..254272e1 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Class-Path: + diff --git a/oxAuth/Server/src/main/webapp/WEB-INF/faces-config.xml b/oxAuth/Server/src/main/webapp/WEB-INF/faces-config.xml new file mode 100644 index 00000000..de304332 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/WEB-INF/faces-config.xml @@ -0,0 +1,23 @@ + + + + + + en + + + + org.gluu.oxauth.i18n.CustomResourceBundle + msgs + + + + + + org.gluu.oxauth.exception.GlobalExceptionHandlerFactory + + + + diff --git a/oxAuth/Server/src/main/webapp/WEB-INF/firebase-messaging-sw.js b/oxAuth/Server/src/main/webapp/WEB-INF/firebase-messaging-sw.js new file mode 100644 index 00000000..9ac94f86 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/WEB-INF/firebase-messaging-sw.js @@ -0,0 +1,51 @@ +// Import and configure the Firebase SDK +// These scripts are made available when the app is served or deployed on Firebase Hosting +// If you do not serve/host your project using Firebase Hosting see https://firebase.google.com/docs/web/setup +importScripts('https://www.gstatic.com/firebasejs/7.6.1/firebase-app.js'); +importScripts('https://www.gstatic.com/firebasejs/7.6.1/firebase-messaging.js'); + +// Your web app's Firebase configuration +var firebaseConfig = '${FIREBASE_CONFIG}'; +// Initialize Firebase +firebase.initializeApp(firebaseConfig); + +var messaging = firebase.messaging(); + +/** + * Here is is the code snippet to initialize Firebase Messaging in the Service + * Worker when your app is not hosted on Firebase Hosting. + // [START initialize_firebase_in_sw] + // Give the service worker access to Firebase Messaging. + // Note that you can only use Firebase Messaging here, other Firebase libraries + // are not available in the service worker. + importScripts('https://www.gstatic.com/firebasejs/4.8.1/firebase-app.js'); + importScripts('https://www.gstatic.com/firebasejs/4.8.1/firebase-messaging.js'); + // Initialize the Firebase app in the service worker by passing in the + // messagingSenderId. + firebase.initializeApp({ + 'messagingSenderId': 'YOUR-SENDER-ID' + }); + // Retrieve an instance of Firebase Messaging so that it can handle background + // messages. + const messaging = firebase.messaging(); + // [END initialize_firebase_in_sw] + **/ + + +// If you would like to customize notifications that are received in the +// background (Web app is closed or not in browser focus) then you should +// implement this optional method. +// [START background_handler] +messaging.setBackgroundMessageHandler(function(payload) { + console.log('[firebase-messaging-sw.js] Received background message ', payload); + // Customize notification here + var notificationTitle = 'Background Message Title'; + var notificationOptions = { + body: 'Background Message body.', + icon: '/firebase-logo.png' + }; + + return self.registration.showNotification(notificationTitle, + notificationOptions); +}); +// [END background_handler] \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/WEB-INF/incl/layout/authorize-extended-template.xhtml b/oxAuth/Server/src/main/webapp/WEB-INF/incl/layout/authorize-extended-template.xhtml new file mode 100644 index 00000000..27520b9e --- /dev/null +++ b/oxAuth/Server/src/main/webapp/WEB-INF/incl/layout/authorize-extended-template.xhtml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + +

+
+
+
+ + + Logo + + +
+
+
+
+

+ +

+ + + + + +

+ + + +

+ +
    + + +
  • + + + + + +
    + + + + + +
    +
  • +
    +
    +
+ + +

+ + + + +

+
+ + +

+ + + + +

+
+ +
+ +
+
+
+ #{msgs['common.copyright']} + Gluu. #{msgs['common.allRightsReserved']} +
+
+ + + \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/WEB-INF/incl/layout/ciba-authorize-template.xhtml b/oxAuth/Server/src/main/webapp/WEB-INF/incl/layout/ciba-authorize-template.xhtml new file mode 100644 index 00000000..90022703 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/WEB-INF/incl/layout/ciba-authorize-template.xhtml @@ -0,0 +1,55 @@ + + + + + + oxAuth + + + + + + + + + + + + + +
+ + + + + +
+
+
+ \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/WEB-INF/incl/layout/login-extended-template.xhtml b/oxAuth/Server/src/main/webapp/WEB-INF/incl/layout/login-extended-template.xhtml new file mode 100644 index 00000000..0dd82f81 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/WEB-INF/incl/layout/login-extended-template.xhtml @@ -0,0 +1,133 @@ + + + + + + + <ui:insert name="pageTitle" /> + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ +
+
+
+
+
+
+

+ © | | +

+
+
+
+
+ +
+ \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/WEB-INF/incl/layout/login-template.xhtml b/oxAuth/Server/src/main/webapp/WEB-INF/incl/layout/login-template.xhtml new file mode 100644 index 00000000..8e70e86b --- /dev/null +++ b/oxAuth/Server/src/main/webapp/WEB-INF/incl/layout/login-template.xhtml @@ -0,0 +1,301 @@ + + + + + + + <ui:insert name="pageTitle" /> + + + + + + + + + \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/WEB-INF/incl/layout/template.xhtml b/oxAuth/Server/src/main/webapp/WEB-INF/incl/layout/template.xhtml new file mode 100644 index 00000000..3075338f --- /dev/null +++ b/oxAuth/Server/src/main/webapp/WEB-INF/incl/layout/template.xhtml @@ -0,0 +1,48 @@ + + + + + + oxAuth + + + + + + +
+ + + + + +
+
+
+ \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/WEB-INF/incl/layout/whispeak-open-template.xhtml b/oxAuth/Server/src/main/webapp/WEB-INF/incl/layout/whispeak-open-template.xhtml new file mode 100644 index 00000000..90b1968e --- /dev/null +++ b/oxAuth/Server/src/main/webapp/WEB-INF/incl/layout/whispeak-open-template.xhtml @@ -0,0 +1,37 @@ + + + + + + + + + <ui:insert name="pageTitle" /> + + + + + + + + + +
+ + + + + +
+ +
+
+
+
+ + \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/WEB-INF/static/favicon.ico b/oxAuth/Server/src/main/webapp/WEB-INF/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..04c571577fa16929129e10958f3009d9f331735d GIT binary patch literal 19608 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 QZ@;oyVPlqMvVP0|03u`X3;+NC literal 0 HcmV?d00001 diff --git a/oxAuth/Server/src/main/webapp/WEB-INF/static/logo.png b/oxAuth/Server/src/main/webapp/WEB-INF/static/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..539f9940bde74e4c817f3c9814c02b9d3051cde3 GIT binary patch literal 3544 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 S%`NQ!0000SI1-k literal 0 HcmV?d00001 diff --git a/oxAuth/Server/src/main/webapp/auth/bioid/bioid.xhtml b/oxAuth/Server/src/main/webapp/auth/bioid/bioid.xhtml new file mode 100644 index 00000000..bf9b964e --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/bioid/bioid.xhtml @@ -0,0 +1,1045 @@ + + + + + + + + + + + + + + + BWS Unified User Interface + + + + +
+
+
+
+ +
+ + + + + + + +
+
\ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/auth/bioid/css/uui.css b/oxAuth/Server/src/main/webapp/auth/bioid/css/uui.css new file mode 100644 index 00000000..32a7b745 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/bioid/css/uui.css @@ -0,0 +1,764 @@ +/* Extra small devices (portrait phones, less than 740px) */ +@media (max-width: 575.98px ) and (max-height: 739.98px ) { + html { + font-size: 11px; + } + + .navigation { + top: 60px; + left: 0; + width: 60px; + } + + .instruction { + margin-top: 30px; + font-size: 1.4rem; + height: 50px; + } + + .promt { + font-size: 1.3rem; + } + + .uploadstatus-single { + display: none; + } + + .col-right-border { + border: none; + } + + .col-8 { + flex: 0 0 85%; + max-width: 85%; + } + + .intro-title { + margin-top: 30px; + } + + .demo-video { + max-width: 250px; + } + + .abortbutton { + left: 20px; + } + + .logo { + width: 11rem; + } + + .header { + height: 4rem; + box-shadow: 0 1rem 0 0 rgb(111,118,127); + } +} + +/* Extra small devices (portrait phones, less than 576px) */ +@media (max-width: 575.98px) and (min-height: 740px) { + html { + font-size: 11px; + } + + .navigation { + top: 10px; + left: 0; + width: 60px; + } + + .instruction { + margin-top: 30px; + font-size: 1.6rem; + height: 50px; + } + + .promt { + font-size: 1.3rem; + } + + .uploadstatus-single { + display: none; + } + + .col-right-border { + border: none; + } + + .col-8 { + flex: 0 0 85%; + max-width: 85%; + } + + .intro-title { + margin-top: 30px; + } + + .float-right { + margin: 0; + float: none; + } + + .float-left { + margin: 0; + float: none; + } + + .demo-video { + max-width: 350px; + } + + .abortbutton { + left: 20px; + } + + .logo { + width: 11rem; + } + + .header { + height: 4rem; + box-shadow: 0 1rem 0 0 rgb(111,118,127); + } +} + +/* Small devices (landscape phones, 576px and up) */ +@media (min-width: 576px) { + html { + font-size: 12px; + } + + .navigation { + top: 10px; + left: 10px; + width: 60px; + } + + .uploadstatus-single { + display: none; + } + + .col-right-border { + border: none; + } + + .col-8 { + flex: 0 0 85%; + max-width: 85%; + } + + .demo-video { + max-width: 400px; + } + + .intro-title { + margin-top: 30px; + } + + .instruction { + font-size: 1.6rem; + margin-top: 30px; + height: 80px; + } + + .abortbutton { + top: 30px; + left: 30px; + } + + .logo { + width: 15rem; + } + + .header { + height: 5rem; + box-shadow: 0 1.5rem 0 0 rgb(111,118,127); + } +} + +/* Medium devices (tablets, 768px and up) */ +@media (min-width: 745px) { + html { + font-size: 15px; + } + + .navigation { + top: 40px; + left: 35px; + width: 120px; + } + + .uploadstatus-single { + display: none; + } + + .col-right-border { + border: none; + } + + .col-8 { + flex: 0 0 50%; + max-width: 50%; + } +} + +@media (max-width: 896px) { + + .float-left, + .float-right { + margin: 0 auto; + } +} + +@media (min-width: 897px) { + .col-right-border { + border-right: 1px solid black; + } +} + +/* Large devices (desktops, 992px and up) */ +@media (min-width: 992px) { + html { + font-size: 16px; + } + + .uploadstatus-single { + display: block; + } + + .float-left { + float: left !important; + margin-left: 80px; + } + + .float-right { + float: right !important; + margin-right: 80px; + } +} + +body { + font-family: "Open Sans", "Segoe UI", Helvetica, Arial, Sans-Serif; + background-color: white; + min-height: 100%; +} + +.logo { + position: absolute; + top: 0; + left: 0; +} + +.header { + position: fixed; + top: 0; + right: 0; + left: 0; + padding: 0; + background-color: rgb(48,55,66); + z-index: 3; +} + +.main { + position: absolute; + box-sizing: border-box; + top: 6rem; + right: 0; + left: 0; + width: 100%; +} + +.abortbutton { + position: absolute; +} + +.red { + color: red; + font-weight: 600; +} + +.blue { + color: #3A61A0; + font-weight: 600; +} + +.uppercase { + text-transform: uppercase; +} + +.button { + border: none; + border-radius: 12px; + padding: 10px 32px; + color: white; + background-color: #93D50B; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 18px; + font-weight: 500; + line-height: 30px; + cursor: pointer; +} + +.button:hover { + background-color: #82BA0B; +} + +.button:focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(127, 186, 6, 0.5); +} + +.button:hover { + background-color: #82BA0B; +} + +.button:disabled { + background-color: gray; + color: lightgray; + cursor: default; +} + +.button-accept { + width: 300px; +} + +.button-ok { + border: 1px solid #BE0000; + background-color: white; + color: #BE0000; + padding: 10px 70px; + margin: 2px 30px; +} + +.button-ok:hover { + background-color: #E0E0E0; +} + +.button-ok:focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(190, 0, 0, 0.5); +} + +.button-info { + display: none; + background: rgb(52,152,219); + margin: 10px 10px 10px 0; + float: left; +} + +.button-info:hover { + background: rgb(60,176,253); +} + +.button-info:focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(60, 176, 253, 0.5); +} + +a .button-back { + width: 25px; + height: auto; + vertical-align: middle; +} + +.navigation { + position: absolute; + top: 200px; + left: 300px; + z-index: 2; +} + +.hidden { + display: none; +} + +.title { + margin-top: 1.5rem; + text-align: center; + color: white; + font-size: 1.4rem; +} + +.prompt { + font-size: 1rem; +} + +.webapp { + display: none; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.instruction { + z-index: 1; + font-weight: 600; + color: #3A61A0; +} + +.liveview { + display: block; + z-index: -2; +} + +.live { + position: absolute; + top: 50%; + left: 50%; + width: 10px; + height: auto; + z-index: -2; +} + +.canvasview { + position: relative; + max-width: 800px; + max-height: 600px; + opacity: 1.0; + z-index: -1; + -webkit-transform: scaleX(-1); +} + +.modal { + display: none; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 1.3rem; + background-color: #FFFFFF; + color: #BE0000; + padding: 50px; + opacity: 0.8; +} + +.modal-footer { + margin-top: 2rem; +} + +.head { + display: block; + position: relative; + text-align: center; + opacity: 0.6; + z-index: 1; +} + +.stats { + display: block; + position: fixed; + bottom: 20px; + left: 10px; + color: black; + font-size: 1.2rem; + text-align: center; + z-index: 1; +} + +.uploadstatus-single { + position: fixed; + bottom: 0; + right: 0; + color: black; + text-align: right; +} + +.uploadstatus-compact { + position: fixed; + bottom: 5px; + left: 5px; + right: 5px; +} + +.image { + display: none; + box-sizing: border-box; + border: 1px solid rgb(44,100,162); + float: right; + margin: 4px; + width: 90px; + height: 120px; + margin-bottom: 25px; + opacity: 0.4; +} + +.image-uploaded { + text-align: center; + width: 90px; + height: 120px; +} + +.progress-single { + display: none; + margin-top: 5px; + width: 90px; + background-color: #e9ecef; + border-radius: .25rem; +} + +.progress-compact { + display: none; + width: 100%; + background-color: #e9ecef; + border-radius: .25rem; +} + +.progressbar { + display: block; + height: 10px; + width: 0; + background-color: #007bff; + border-radius: .25rem; +} + +.transparent-background { + background-color: rgb(255,255,255); + background-color: rgba(255, 255, 255, .9); +} + +.alert-danger { + color: rgb(255,80,80); +} + +.intro-title { + font-size: 2.4rem; + font-weight: 700; + color: #3A61A0; + text-transform: uppercase; + text-align: center; +} + +.intro-subtitle { + font-size: 2.4rem; + font-weight: 700; + color: #3A61A0; + text-transform: uppercase; + text-align: center; +} + +.intro-description { + font-size: 1.6rem; + font-weight: 500; + color: #3A61A0; + text-align: center; +} + +.intro-attention { + font-size: 1rem; + text-align: center; +} + +.image-wrong { + font-size: 1rem; + text-align: center; + color: red; + margin-top: 20px; + margin-bottom: 20px; +} + +.image-right { + font-size: 1rem; + font-weight: 600; + text-align: center; + color: #3A61A0; + margin-top: 20px; + margin-bottom: 20px; +} + +.intro-skip { + font-size: 1.6rem; + text-align: center; + line-height: 1.6rem; +} + +input.intro-checkbox { + width: 20px; + height: 20px; +} + +.row { + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + margin-right: 0; + margin-left: 0; +} + +.col { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; +} + +.col-fix-size { + max-width: 590px; +} + +.col-8 { + position: relative; + width: 100%; + padding-right: 15px; + padding-left: 15px; +} + +.justify-content-center { + -ms-flex-pack: center !important; + justify-content: center !important; +} + +.checkbox { + display: block; + position: relative; + cursor: pointer; + font-size: 22px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.center { + text-align: center; +} + +.mt-3 { + margin-top: 1rem !important; +} + +.mt-9 { + margin-top: 3rem !important; +} + +.mb-3 { + margin-bottom: 1rem !important; +} + +.ml-3 { + margin-left: 1rem !important; +} + +.ml-5 { + margin-left: 2rem !important; +} + +.m-3 { + margin: 1rem !important; +} + +.mb-5 { + margin-bottom: 2rem !important; +} + +.checkbox input { + vertical-align: middle; +} + +.checkbox span { + vertical-align: middle; + font-size: 0.9rem; + padding-left: 10px; +} + +.action-title { + color: #3A61A0; + font-weight: 500; +} + +.spinner { + position: relative; + width: 100%; + height: 100%; +} + +.spinner:before, .spinner:after { + content: ""; + position: absolute; + border-width: 4px; + border-style: solid; + border-radius: 50%; +} + +.spinner-wait:before { + top: 40px; + left: 20px; + width: 40px; + height: 40px; + border-color: rgb(33,33,33); + animation: scale 1.5s linear 0s infinite alternate; +} + +.spinner-wait:after { + top: 40px; + left: 20px; + width: 40px; + height: 40px; + border-color: rgb(33,33,33); + animation: scale 1.5s linear 0s infinite alternate-reverse; +} + +.spinner-upload:before { + top: 40px; + left: 20px; + width: 40px; + height: 40px; + border-bottom-color: rgb(33,33,33); + border-right-color: rgb(33,33,33); + border-top-color: rgb(33,33,33); + border-top-color: rgba(33,33,33,0); + border-left-color: rgb(33,33,33); + border-left-color: rgba(33,33,33,0); + animation: rotate-animation 1s linear 0s infinite; +} + +.spinner-upload:after { + top: 50px; + left: 30px; + width: 20px; + height: 20px; + border-bottom-color: rgb(33,33,33); + border-right-color: rgb(33,33,33); + border-top-color: rgb(33,33,33); + border-top-color: rgba(33,33,33, 0); + border-left-color: rgb(33,33,33); + border-left-color: rgba(33,33,33, 0); + animation: anti-rotate-animation 0.85s linear 0s infinite; +} + +@keyframes scale { + 0% { + transform: scale(1); + border-style: solid; + } + + 100% { + transform: scale(0); + border-style: dashed; + } +} + +@keyframes rotate-animation { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +@keyframes anti-rotate-animation { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(-360deg); + } +} diff --git a/oxAuth/Server/src/main/webapp/auth/bioid/images/back.svg b/oxAuth/Server/src/main/webapp/auth/bioid/images/back.svg new file mode 100644 index 00000000..d2db5764 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/bioid/images/back.svg @@ -0,0 +1,3 @@ + + + diff --git a/oxAuth/Server/src/main/webapp/auth/bioid/images/logo.svg b/oxAuth/Server/src/main/webapp/auth/bioid/images/logo.svg new file mode 100644 index 00000000..ba35d511 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/bioid/images/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/auth/bioid/images/perfect.png b/oxAuth/Server/src/main/webapp/auth/bioid/images/perfect.png new file mode 100644 index 0000000000000000000000000000000000000000..66a0e6bddd47456518517e8ab06fd0566cce6d81 GIT binary patch literal 48739 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 BOI!c| literal 0 HcmV?d00001 diff --git a/oxAuth/Server/src/main/webapp/auth/bioid/images/toofaraway.png b/oxAuth/Server/src/main/webapp/auth/bioid/images/toofaraway.png new file mode 100644 index 0000000000000000000000000000000000000000..d6cae3c1d408e3426a488e15a802ccc1aa031855 GIT binary patch literal 47230 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 settings.maxheight ? settings.maxheight : video.videoHeight; + copycanvas.width = copycanvas.height * aspectratio; + motioncanvas.height = settings.motionareaheight; + motioncanvas.width = motioncanvas.height * aspectratio; + + // if mirroring required + if (settings.mirror) { mirror(); } + + // set an interval-timer to grab about 20 frames per second + processInterval = setInterval(processFrame, 50); + } + + function initRecording() { + if (statusCallback) { statusCallback('UserInstruction-Start'); } + recording(true); + startActivityTimer(); + } + + // start or stop recording + function recording(capture) { + clearInterval(noMotionTimer); + clearInterval(noActivityTimer); + uploaded = 0; + uploading = 0; + captured = 0; + template = null; + capturing = capture; + } + + // private worker method for each frame + function processFrame() { + let w = copycanvas.width, h = copycanvas.height, aspectratio = w / h; + let cutoff = video.videoWidth - (video.videoHeight * aspectratio); + let draw = canvas.getContext('2d'); + let copy = copycanvas.getContext('2d'); + + // we draw the frames manually using the private video element and + // the copy interim canvas + copy.drawImage(video, cutoff / 2, 0, video.videoWidth - cutoff, video.videoHeight, 0, 0, copycanvas.width, copycanvas.height); + + // at first we need aspectration of the video - portrait or + // landscape size + let aspectrationvideo = video.videoWidth / video.videoHeight; + let offset = 0; + + if (aspectrationvideo > 1) { // e.g 640x480 + if (window.innerWidth / window.innerHeight > 1) { + canvas.height = window.innerHeight/3*1.7; + canvas.width = canvas.height / aspectratio; + } + else { + canvas.width = window.innerWidth - 20; + canvas.height = canvas.width * aspectratio; + } + } + else { // 0.75 e.g. 480/640 + offset = 10; // for circle + canvas.width = window.innerWidth - 20; + canvas.height = canvas.width / aspectrationvideo; + } + + draw.drawImage(video, 0, 0, canvas.width, canvas.height); + + // Drawing default white background e.g. Safari does not support + // canvas filter 'blur'! + w = canvas.height * aspectratio - offset; + let gradient = draw.createRadialGradient(canvas.width / 2, canvas.height / 2, 0, canvas.width / 2, canvas.height / 2, w * 0.5); + gradient.addColorStop(0.98, 'transparent'); + gradient.addColorStop(0.99, 'rgba(255, 255, 255, 0.8)'); + draw.fillStyle = gradient; + draw.setTransform(1, 0, 0, 1, 0, 0); + draw.fillRect(0, 0, canvas.width, canvas.height); + + // Drawing white circle into liveview + draw.filter = 'none'; + + draw.beginPath(); + draw.arc(canvas.width / 2, canvas.height / 2, w * 0.5, 0, 2 * Math.PI); + draw.lineWidth = 5; + draw.strokeStyle = '#FFFFFF'; + draw.stroke(); + draw.closePath(); + draw.clip(); + + draw.drawImage(video, 0, 0, canvas.width, canvas.height); + draw.restore(); + + // fire event for uuicanvas size change + var event = new Event('uuiresize'); + document.dispatchEvent(event); + + if (capturing && uploaded < settings.recordings) { + // we may need to switch on the tags again ?????? + // if (settings.challengeResponse && tag === 'any') { setTag(); + // } + + if (captured > settings.maxupload) { + stop(); + doneCallback('The maximum number of uploads has been reached!'); + } + + // scale current image into the motion canvas + let motionctx = motioncanvas.getContext('2d'); + motionctx.drawImage(copycanvas, copycanvas.width / 8, copycanvas.height / 8, copycanvas.width - copycanvas.width / 4, copycanvas.height - copycanvas.height / 4, 0, 0, motioncanvas.width, motioncanvas.height); + let currentImageData = motionctx.getImageData(0, 0, motioncanvas.width, motioncanvas.height); + + let movement = 100; + if (template) { + // calculate motion + movement = motionDetection(currentImageData, template); + } + + // trigger if movement is above threshold (default: when 20% of + // maximum movement is exceeded) + if (movement > settings.threshold) { + if (uploaded + uploading < settings.recordings) { + // in case we are not already bussy with some uploads + // start upload procedure + upload(); + // current image is the new reference frame - create + // template + template = createTemplate(currentImageData); + } + } + } + } + + /* + * ------------------------ Timer functions + * ----------------------------------- + */ + + // we give a NoMovement response every 5 seconds + function startMotionTimer() { + clearInterval(noMotionTimer); + noMotionTimer = setInterval(function () { + if (uploading + uploaded < settings.recordings) { + if (statusCallback) { statusCallback('UserInstruction-NoMovement'); } + } + }, 5000); + } + + // after a given time without activity from the user we abort the + // process + function startActivityTimer() { + clearInterval(noActivityTimer); + noActivityTimer = setInterval(function () { + if (uploading === 0) { + stop(); + doneCallback('Activity time is over!'); + } + else { + startActivityTimer(); + } + }, 30000); + } + + /* + * ------------------------ BWS Web Api calls + * --------------------------------- + */ + + // uploads an image to the BWS + function upload() { + startMotionTimer(); + + // start upload procedure, but only if we still have to + if (capturing && uploaded + uploading < settings.recordings) { + captured++; + uploading++; + let dataURL = copycanvas.toDataURL(); + console.log('sizeof dataURL', dataURL.length); + + if (statusCallback) { + statusCallback('Uploading'); + } + + if (!$.support.cors) { + // the call below typically requires Cross-Origin Resource + // Sharing! + console.log('this browser does not support cors, e.g. IE8 or 9'); + } + let jqxhr = $.ajax({ + type: 'POST', + url: settings.apiurl + 'upload?tag=' + tag + '&index=' + captured + '&trait=' + settings.trait, + data: dataURL, + // don't forget the authentication header + headers: { 'Authorization': 'Bearer ' + token }, + // upload progress + xhr: function () { + var xhr = new window.XMLHttpRequest(); + xhr.upload.id = captured; + xhr.upload.addEventListener('progress', function (event) { + let percent = 0; + if (event.lengthComputable) { + percent = Math.ceil(event.loaded / event.total * 100); + let progressData = { id: this.id, progress: percent }; + if (statusCallback) { statusCallback('UploadProgress', progressData); } + } + }, false); + return xhr; + } + }).done(function (data) { + uploading--; + if (data.Accepted) { + uploaded++; + console.log('upload succeeded', data.Warnings); + if (statusCallback) { statusCallback('Uploaded', data.Warnings.toString(), dataURL); } + if (uploaded >= settings.recordings && uploading === 0) { + // go for biometric task + performTask(); + } + } else { + console.log('upload error', data.Error); + if (statusCallback) { statusCallback(data.Error); } + + if (uploaded < 1) { + // restart process (retry) + doneCallback('NoFaceFound', true); + } + else { + // use performTask to cleanup already uploaded image + // TODO: this is a dummy call! + capturing = false; + performTask(); + } + } + }).fail(function (jqXHR, textStatus, errorThrown) { + // ups, call failed, typically due to + // Unauthorized (invalid token) or + // BadRequest (Invalid or unsupported sample format) or + // InternalServerError (An exception occured) + console.log('upload failed'+textStatus+errorThrown+jqXHR.responseText+textStatus+errorThrown+ jqXHR.responseText); + stop(); + // redirect to caller with error response.. + doneCallback(errorThrown); + }); + // show a new tag if neccessary + if (uploaded + uploading < settings.recordings) { + setTag(); + } + } + } + + // perform biometric task enrollment, verification, identification or + // liveness detection with already uploaded images + function performTask() { + // we already have all images the motion timer is no longer required + clearInterval(noMotionTimer); + + stop(); + doneCallback(); + $(":input:submit[id='bioIDForm:enrollButton']").click(); + } + + /* + * ------------------------ Set challenge response tag + * ------------------------- + */ + + // generate a new challenge response tag or resets it to 'any' + function setTag() { + if (settings.challengeResponse) { + let currentRecording = uploaded + uploading; + if (currentRecording > 0 && currentRecording < settings.recordings) { + if (tags.length >= currentRecording) { + // use the preset (typically via the BWS access token) + // tags! + tag = tags[currentRecording - 1]; + } + else { + let newtag = tag; + if (currentRecording % 2 === 1) { + // create a random tag + let r = Math.random(); + if (currentRecording === 1) { + if (r < 0.25) { newtag = 'up'; } + else if (r < 0.5) { newtag = 'down'; } + else if (r < 0.75) { newtag = 'left'; } + else { newtag = 'right'; } + } + else { + // create a tag in a direction different to the + // last movement axis + if (tag === 'up' || tag === 'down') { + if (r < 0.5) { newtag = 'left'; } + else { newtag = 'right'; } + } + else { + if (r < 0.5) { newtag = 'up'; } + else { newtag = 'down'; } + } + } + } + else { + // create a tag in the opposite direction of the + // last tag + switch (tag) { + case 'left': + newtag = 'right'; + break; + case 'right': + newtag = 'left'; + break; + case 'up': + newtag = 'down'; + break; + case 'down': + newtag = 'up'; + break; + default: + break; + } + } + console.log('Switched tag for recording #' + currentRecording + ' from ' + tag + ' to ' + newtag); + tag = newtag; + } + } + else { tag = 'any'; } + } + + if (statusCallback) { statusCallback('DisplayTag', tag); } + + if (capturing) { + // give user some time to react! + capturing = false; + setTimeout(function () { if (template !== null) capturing = true; }, 1000); + } + } + + /* + * ------------------------ Motion Detection functions + * ------------------------ + */ + + // template for cross-correlation + function createTemplate(imageData) { + // cut out the template + // we use a small width, quarter-size image around the center as + // template + var template = { + centerX: imageData.width / 2, + centerY: imageData.height / 2, + width: imageData.width / 4, + height: imageData.height / 4 + imageData.height / 8 + }; + + template.xPos = template.centerX - template.width / 2; + template.yPos = template.centerY - template.height / 2; + template.buffer = new Uint8ClampedArray(template.width * template.height); + + let counter = 0; + let p = imageData.data; + for (let y = template.yPos; y < template.yPos + template.height; y++) { + // we use only the green plane here + let bufferIndex = (y * imageData.width * 4) + template.xPos * 4 + 1; + for (let x = template.xPos; x < template.xPos + template.width; x++) { + let templatepixel = p[bufferIndex]; + template.buffer[counter++] = templatepixel; + // we use only the green plane here + bufferIndex += 4; + } + } + console.log('Created new cross-correlation template', template); + return template; + } + + // motion detection by a normalized cross-correlation + function motionDetection(imageData, template) { + // this is the major computing step: Perform a normalized + // cross-correlation between the template of the first image and + // each incoming image + // this algorithm is basically called "Template Matching" - we use + // the normalized cross correlation to be independent of lighting + // changes + // we calculate the correlation of template and image over the whole + // image area + let bestHitX = 0, + bestHitY = 0, + maxCorr = 0, + searchWidth = imageData.width / 4, + searchHeight = imageData.height / 4, + p = imageData.data; + + for (let y = template.centerY - searchHeight; y <= template.centerY + searchHeight - template.height; y++) { + for (let x = template.centerX - searchWidth; x <= template.centerX + searchWidth - template.width; x++) { + let nominator = 0, denominator = 0, templateIndex = 0; + + // Calculate the normalized cross-correlation coefficient + // for this position + for (let ty = 0; ty < template.height; ty++) { + // we use only the green plane here + let bufferIndex = x * 4 + 1 + (y + ty) * imageData.width * 4; + for (let tx = 0; tx < template.width; tx++) { + let imagepixel = p[bufferIndex]; + nominator += template.buffer[templateIndex++] * imagepixel; + denominator += imagepixel * imagepixel; + // we use only the green plane here + bufferIndex += 4; + } + } + + // The NCC coefficient is then (watch out for + // division-by-zero errors for pure black images) + let ncc = 0.0; + if (denominator > 0) { + ncc = nominator * nominator / denominator; + } + // Is it higher than what we had before? + if (ncc > maxCorr) { + maxCorr = ncc; + bestHitX = x; + bestHitY = y; + } + } + } + // now the most similar position of the template is (bestHitX, + // bestHitY). Calculate the difference from the origin + let distX = bestHitX - template.xPos, + distY = bestHitY - template.yPos, + movementDiff = Math.sqrt(distX * distX + distY * distY); + // the maximum movement possible is a complete shift into one of the + // corners, i.e + let maxDistX = searchWidth - template.width / 2, + maxDistY = searchHeight - template.height / 2, + maximumMovement = Math.sqrt(maxDistX * maxDistX + maxDistY * maxDistY); + + // the percentage of the detected movement is therefore + var movementPercentage = movementDiff / maximumMovement * 100; + if (movementPercentage > 100) { + movementPercentage = 100; + } + // console.log('Calculated movement: ', movementPercentage); + return movementPercentage; + } + + return { + start: start, + stop: stop, + startRecording: startRecording, + stopRecording: function () { recording(false); }, + upload: upload, + mirror: mirror, + getUploading: function () { return uploading; }, + getUploaded: function () { return uploaded; } + }; + }; +}(window.bws = window.bws || {}, jQuery)); \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/auth/bioid/js/getUserMedia.js b/oxAuth/Server/src/main/webapp/auth/bioid/js/getUserMedia.js new file mode 100644 index 00000000..b4db51da --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/bioid/js/getUserMedia.js @@ -0,0 +1,29 @@ +// mediaDevices - getUserMedia - polyfill +// usage: navigator.mediaDevices.getUserMedia({ video: true }).then(function (mediaStream) { ... }).catch(function (err) { ... }); +'use strict'; + +(function () { + var promisifiedOldGUM = function promisifiedOldGUM(constraints, successCallback, errorCallback) { + // First get ahold of getUserMedia, if present + var getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; + // Some browsers just don't implement it - return a rejected promise with an error to keep a consistent interface + if (!getUserMedia) { + return Promise.reject(new Error('getUserMedia is not implemented in this browser')); + } + // Otherwise, wrap the call to the old navigator.getUserMedia with a Promise + return new Promise(function (successCallback, errorCallback) { + getUserMedia.call(navigator, constraints, successCallback, errorCallback); + }); + }; + + // Older browsers might not implement mediaDevices at all, so we set an empty object first + if (navigator.mediaDevices === undefined) { + navigator.mediaDevices = {}; + } + // Some browsers partially implement mediaDevices. We can't just assign an object + // with getUserMedia as it would overwrite existing properties. + // Here, we will just add the getUserMedia property if it's missing. + if (navigator.mediaDevices.getUserMedia === undefined) { + navigator.mediaDevices.getUserMedia = promisifiedOldGUM; + } +})(); diff --git a/oxAuth/Server/src/main/webapp/auth/bioid/js/getUserMedia.min.js b/oxAuth/Server/src/main/webapp/auth/bioid/js/getUserMedia.min.js new file mode 100644 index 00000000..f00ba86d --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/bioid/js/getUserMedia.min.js @@ -0,0 +1 @@ +"use strict";(function(){var n=function(n){var t=navigator.getUserMedia||navigator.webkitGetUserMedia||navigator.mozGetUserMedia||navigator.msGetUserMedia;return t?new Promise(function(i,r){t.call(navigator,n,i,r)}):Promise.reject(new Error("getUserMedia is not implemented in this browser"))};navigator.mediaDevices===undefined&&(navigator.mediaDevices={});navigator.mediaDevices.getUserMedia===undefined&&(navigator.mediaDevices.getUserMedia=n)})(); \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/auth/bioid/js/jquery-3.5.1.min.js b/oxAuth/Server/src/main/webapp/auth/bioid/js/jquery-3.5.1.min.js new file mode 100644 index 00000000..b0614034 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/bioid/js/jquery-3.5.1.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.5.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.5.1",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function D(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||j,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,j=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function qe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function Le(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function He(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Oe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||S.expando+"_"+Ct.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(Et.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Ut=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Ut.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):("number"==typeof f.top&&(f.top+="px"),"number"==typeof f.left&&(f.left+="px"),c.css(f))}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=$e(y.pixelPosition,function(e,t){if(t)return t=Be(e,n),Me.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0 0 ? libraries[ libraries.length - 1 ] : '' ), + smooth: ( previous !== undefined ? previous.smooth : this.smooth ), + groupStart: ( previous !== undefined ? previous.groupEnd : 0 ), + groupEnd: - 1, + groupCount: - 1, + inherited: false, + + clone: function ( index ) { + + var cloned = { + index: ( typeof index === 'number' ? index : this.index ), + name: this.name, + mtllib: this.mtllib, + smooth: this.smooth, + groupStart: 0, + groupEnd: - 1, + groupCount: - 1, + inherited: false + }; + cloned.clone = this.clone.bind( cloned ); + return cloned; + + } + }; + + this.materials.push( material ); + + return material; + + }, + + currentMaterial: function () { + + if ( this.materials.length > 0 ) { + + return this.materials[ this.materials.length - 1 ]; + + } + + return undefined; + + }, + + _finalize: function ( end ) { + + var lastMultiMaterial = this.currentMaterial(); + if ( lastMultiMaterial && lastMultiMaterial.groupEnd === - 1 ) { + + lastMultiMaterial.groupEnd = this.geometry.vertices.length / 3; + lastMultiMaterial.groupCount = lastMultiMaterial.groupEnd - lastMultiMaterial.groupStart; + lastMultiMaterial.inherited = false; + + } + + // Ignore objects tail materials if no face declarations followed them before a new o/g started. + if ( end && this.materials.length > 1 ) { + + for ( var mi = this.materials.length - 1; mi >= 0; mi -- ) { + + if ( this.materials[ mi ].groupCount <= 0 ) { + + this.materials.splice( mi, 1 ); + + } + + } + + } + + // Guarantee at least one empty material, this makes the creation later more straight forward. + if ( end && this.materials.length === 0 ) { + + this.materials.push( { + name: '', + smooth: this.smooth + } ); + + } + + return lastMultiMaterial; + + } + }; + + // Inherit previous objects material. + // Spec tells us that a declared material must be set to all objects until a new material is declared. + // If a usemtl declaration is encountered while this new object is being parsed, it will + // overwrite the inherited material. Exception being that there was already face declarations + // to the inherited material, then it will be preserved for proper MultiMaterial continuation. + + if ( previousMaterial && previousMaterial.name && typeof previousMaterial.clone === 'function' ) { + + var declared = previousMaterial.clone( 0 ); + declared.inherited = true; + this.object.materials.push( declared ); + + } + + this.objects.push( this.object ); + + }, + + finalize: function () { + + if ( this.object && typeof this.object._finalize === 'function' ) { + + this.object._finalize( true ); + + } + + }, + + parseVertexIndex: function ( value, len ) { + + var index = parseInt( value, 10 ); + return ( index >= 0 ? index - 1 : index + len / 3 ) * 3; + + }, + + parseNormalIndex: function ( value, len ) { + + var index = parseInt( value, 10 ); + return ( index >= 0 ? index - 1 : index + len / 3 ) * 3; + + }, + + parseUVIndex: function ( value, len ) { + + var index = parseInt( value, 10 ); + return ( index >= 0 ? index - 1 : index + len / 2 ) * 2; + + }, + + addVertex: function ( a, b, c ) { + + var src = this.vertices; + var dst = this.object.geometry.vertices; + + dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] ); + dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] ); + dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] ); + + }, + + addVertexPoint: function ( a ) { + + var src = this.vertices; + var dst = this.object.geometry.vertices; + + dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] ); + + }, + + addVertexLine: function ( a ) { + + var src = this.vertices; + var dst = this.object.geometry.vertices; + + dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] ); + + }, + + addNormal: function ( a, b, c ) { + + var src = this.normals; + var dst = this.object.geometry.normals; + + dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] ); + dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] ); + dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] ); + + }, + + addColor: function ( a, b, c ) { + + var src = this.colors; + var dst = this.object.geometry.colors; + + dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] ); + dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] ); + dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] ); + + }, + + addUV: function ( a, b, c ) { + + var src = this.uvs; + var dst = this.object.geometry.uvs; + + dst.push( src[ a + 0 ], src[ a + 1 ] ); + dst.push( src[ b + 0 ], src[ b + 1 ] ); + dst.push( src[ c + 0 ], src[ c + 1 ] ); + + }, + + addUVLine: function ( a ) { + + var src = this.uvs; + var dst = this.object.geometry.uvs; + + dst.push( src[ a + 0 ], src[ a + 1 ] ); + + }, + + addFace: function ( a, b, c, ua, ub, uc, na, nb, nc ) { + + var vLen = this.vertices.length; + + var ia = this.parseVertexIndex( a, vLen ); + var ib = this.parseVertexIndex( b, vLen ); + var ic = this.parseVertexIndex( c, vLen ); + + this.addVertex( ia, ib, ic ); + + if ( ua !== undefined && ua !== '' ) { + + var uvLen = this.uvs.length; + ia = this.parseUVIndex( ua, uvLen ); + ib = this.parseUVIndex( ub, uvLen ); + ic = this.parseUVIndex( uc, uvLen ); + this.addUV( ia, ib, ic ); + + } + + if ( na !== undefined && na !== '' ) { + + // Normals are many times the same. If so, skip function call and parseInt. + var nLen = this.normals.length; + ia = this.parseNormalIndex( na, nLen ); + + ib = na === nb ? ia : this.parseNormalIndex( nb, nLen ); + ic = na === nc ? ia : this.parseNormalIndex( nc, nLen ); + + this.addNormal( ia, ib, ic ); + + } + + if ( this.colors.length > 0 ) { + + this.addColor( ia, ib, ic ); + + } + + }, + + addPointGeometry: function ( vertices ) { + + this.object.geometry.type = 'Points'; + + var vLen = this.vertices.length; + + for ( var vi = 0, l = vertices.length; vi < l; vi ++ ) { + + this.addVertexPoint( this.parseVertexIndex( vertices[ vi ], vLen ) ); + + } + + }, + + addLineGeometry: function ( vertices, uvs ) { + + this.object.geometry.type = 'Line'; + + var vLen = this.vertices.length; + var uvLen = this.uvs.length; + + for ( var vi = 0, l = vertices.length; vi < l; vi ++ ) { + + this.addVertexLine( this.parseVertexIndex( vertices[ vi ], vLen ) ); + + } + + for ( var uvi = 0, l = uvs.length; uvi < l; uvi ++ ) { + + this.addUVLine( this.parseUVIndex( uvs[ uvi ], uvLen ) ); + + } + + } + + }; + + state.startObject( '', false ); + + return state; + + } + + // + + function OBJLoader( manager ) { + + this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager; + + this.materials = null; + + } + + OBJLoader.prototype = { + + constructor: OBJLoader, + + load: function ( url, onLoad, onProgress, onError ) { + + var scope = this; + + var loader = new THREE.FileLoader( scope.manager ); + loader.setPath( this.path ); + loader.load( url, function ( text ) { + + onLoad( scope.parse( text ) ); + + }, onProgress, onError ); + + }, + + setPath: function ( value ) { + + this.path = value; + + }, + + setMaterials: function ( materials ) { + + this.materials = materials; + + return this; + + }, + + parse: function ( text ) { + + console.time( 'OBJLoader' ); + + var state = new ParserState(); + + if ( text.indexOf( '\r\n' ) !== - 1 ) { + + // This is faster than String.split with regex that splits on both + text = text.replace( /\r\n/g, '\n' ); + + } + + if ( text.indexOf( '\\\n' ) !== - 1 ) { + + // join lines separated by a line continuation character (\) + text = text.replace( /\\\n/g, '' ); + + } + + var lines = text.split( '\n' ); + var line = '', lineFirstChar = ''; + var lineLength = 0; + var result = []; + + // Faster to just trim left side of the line. Use if available. + var trimLeft = ( typeof ''.trimLeft === 'function' ); + + for ( var i = 0, l = lines.length; i < l; i ++ ) { + + line = lines[ i ]; + + line = trimLeft ? line.trimLeft() : line.trim(); + + lineLength = line.length; + + if ( lineLength === 0 ) continue; + + lineFirstChar = line.charAt( 0 ); + + // @todo invoke passed in handler if any + if ( lineFirstChar === '#' ) continue; + + if ( lineFirstChar === 'v' ) { + + var data = line.split( /\s+/ ); + + switch ( data[ 0 ] ) { + + case 'v': + state.vertices.push( + parseFloat( data[ 1 ] ), + parseFloat( data[ 2 ] ), + parseFloat( data[ 3 ] ) + ); + if ( data.length === 8 ) { + + state.colors.push( + parseFloat( data[ 4 ] ), + parseFloat( data[ 5 ] ), + parseFloat( data[ 6 ] ) + + ); + + } + break; + case 'vn': + state.normals.push( + parseFloat( data[ 1 ] ), + parseFloat( data[ 2 ] ), + parseFloat( data[ 3 ] ) + ); + break; + case 'vt': + state.uvs.push( + parseFloat( data[ 1 ] ), + parseFloat( data[ 2 ] ) + ); + break; + + } + + } else if ( lineFirstChar === 'f' ) { + + var lineData = line.substr( 1 ).trim(); + var vertexData = lineData.split( /\s+/ ); + var faceVertices = []; + + // Parse the face vertex data into an easy to work with format + + for ( var j = 0, jl = vertexData.length; j < jl; j ++ ) { + + var vertex = vertexData[ j ]; + + if ( vertex.length > 0 ) { + + var vertexParts = vertex.split( '/' ); + faceVertices.push( vertexParts ); + + } + + } + + // Draw an edge between the first vertex and all subsequent vertices to form an n-gon + + var v1 = faceVertices[ 0 ]; + + for ( var j = 1, jl = faceVertices.length - 1; j < jl; j ++ ) { + + var v2 = faceVertices[ j ]; + var v3 = faceVertices[ j + 1 ]; + + state.addFace( + v1[ 0 ], v2[ 0 ], v3[ 0 ], + v1[ 1 ], v2[ 1 ], v3[ 1 ], + v1[ 2 ], v2[ 2 ], v3[ 2 ] + ); + + } + + } else if ( lineFirstChar === 'l' ) { + + var lineParts = line.substring( 1 ).trim().split( " " ); + var lineVertices = [], lineUVs = []; + + if ( line.indexOf( "/" ) === - 1 ) { + + lineVertices = lineParts; + + } else { + + for ( var li = 0, llen = lineParts.length; li < llen; li ++ ) { + + var parts = lineParts[ li ].split( "/" ); + + if ( parts[ 0 ] !== "" ) lineVertices.push( parts[ 0 ] ); + if ( parts[ 1 ] !== "" ) lineUVs.push( parts[ 1 ] ); + + } + + } + state.addLineGeometry( lineVertices, lineUVs ); + + } else if ( lineFirstChar === 'p' ) { + + var lineData = line.substr( 1 ).trim(); + var pointData = lineData.split( " " ); + + state.addPointGeometry( pointData ); + + } else if ( ( result = object_pattern.exec( line ) ) !== null ) { + + // o object_name + // or + // g group_name + + // WORKAROUND: https://bugs.chromium.org/p/v8/issues/detail?id=2869 + // var name = result[ 0 ].substr( 1 ).trim(); + var name = ( " " + result[ 0 ].substr( 1 ).trim() ).substr( 1 ); + + state.startObject( name ); + + } else if ( material_use_pattern.test( line ) ) { + + // material + + state.object.startMaterial( line.substring( 7 ).trim(), state.materialLibraries ); + + } else if ( material_library_pattern.test( line ) ) { + + // mtl file + + state.materialLibraries.push( line.substring( 7 ).trim() ); + + } else if ( lineFirstChar === 's' ) { + + result = line.split( ' ' ); + + // smooth shading + + // @todo Handle files that have varying smooth values for a set of faces inside one geometry, + // but does not define a usemtl for each face set. + // This should be detected and a dummy material created (later MultiMaterial and geometry groups). + // This requires some care to not create extra material on each smooth value for "normal" obj files. + // where explicit usemtl defines geometry groups. + // Example asset: examples/models/obj/cerberus/Cerberus.obj + + /* + * http://paulbourke.net/dataformats/obj/ + * or + * http://www.cs.utah.edu/~boulos/cs3505/obj_spec.pdf + * + * From chapter "Grouping" Syntax explanation "s group_number": + * "group_number is the smoothing group number. To turn off smoothing groups, use a value of 0 or off. + * Polygonal elements use group numbers to put elements in different smoothing groups. For free-form + * surfaces, smoothing groups are either turned on or off; there is no difference between values greater + * than 0." + */ + if ( result.length > 1 ) { + + var value = result[ 1 ].trim().toLowerCase(); + state.object.smooth = ( value !== '0' && value !== 'off' ); + + } else { + + // ZBrush can produce "s" lines #11707 + state.object.smooth = true; + + } + var material = state.object.currentMaterial(); + if ( material ) material.smooth = state.object.smooth; + + } else { + + // Handle null terminated files without exception + if ( line === '\0' ) continue; + + throw new Error( 'THREE.OBJLoader: Unexpected line: "' + line + '"' ); + + } + + } + + state.finalize(); + + var container = new THREE.Group(); + container.materialLibraries = [].concat( state.materialLibraries ); + + for ( var i = 0, l = state.objects.length; i < l; i ++ ) { + + var object = state.objects[ i ]; + var geometry = object.geometry; + var materials = object.materials; + var isLine = ( geometry.type === 'Line' ); + var isPoints = ( geometry.type === 'Points' ); + var hasVertexColors = false; + + // Skip o/g line declarations that did not follow with any faces + if ( geometry.vertices.length === 0 ) continue; + + var buffergeometry = new THREE.BufferGeometry(); + + buffergeometry.addAttribute( 'position', new THREE.Float32BufferAttribute( geometry.vertices, 3 ) ); + + if ( geometry.normals.length > 0 ) { + + buffergeometry.addAttribute( 'normal', new THREE.Float32BufferAttribute( geometry.normals, 3 ) ); + + } else { + + buffergeometry.computeVertexNormals(); + + } + + if ( geometry.colors.length > 0 ) { + + hasVertexColors = true; + buffergeometry.addAttribute( 'color', new THREE.Float32BufferAttribute( geometry.colors, 3 ) ); + + } + + if ( geometry.uvs.length > 0 ) { + + buffergeometry.addAttribute( 'uv', new THREE.Float32BufferAttribute( geometry.uvs, 2 ) ); + + } + + // Create materials + + var createdMaterials = []; + + for ( var mi = 0, miLen = materials.length; mi < miLen; mi ++ ) { + + var sourceMaterial = materials[ mi ]; + var material = undefined; + + if ( this.materials !== null ) { + + material = this.materials.create( sourceMaterial.name ); + + // mtl etc. loaders probably can't create line materials correctly, copy properties to a line material. + if ( isLine && material && ! ( material instanceof THREE.LineBasicMaterial ) ) { + + var materialLine = new THREE.LineBasicMaterial(); + materialLine.copy( material ); + materialLine.lights = false; // TOFIX + material = materialLine; + + } else if ( isPoints && material && ! ( material instanceof THREE.PointsMaterial ) ) { + + var materialPoints = new THREE.PointsMaterial( { size: 10, sizeAttenuation: false } ); + materialLine.copy( material ); + material = materialPoints; + + } + + } + + if ( ! material ) { + + if ( isLine ) { + + material = new THREE.LineBasicMaterial(); + + } else if ( isPoints ) { + + material = new THREE.PointsMaterial( { size: 1, sizeAttenuation: false } ); + + } else { + + material = new THREE.MeshPhongMaterial(); + + } + + material.name = sourceMaterial.name; + + } + + material.flatShading = sourceMaterial.smooth ? false : true; + material.vertexColors = hasVertexColors ? THREE.VertexColors : THREE.NoColors; + + createdMaterials.push( material ); + + } + + // Create mesh + + var mesh; + + if ( createdMaterials.length > 1 ) { + + for ( var mi = 0, miLen = materials.length; mi < miLen; mi ++ ) { + + var sourceMaterial = materials[ mi ]; + buffergeometry.addGroup( sourceMaterial.groupStart, sourceMaterial.groupCount, mi ); + + } + + if ( isLine ) { + + mesh = new THREE.LineSegments( buffergeometry, createdMaterials ); + + } else if ( isPoints ) { + + mesh = new THREE.Points( buffergeometry, createdMaterials ); + + } else { + + mesh = new THREE.Mesh( buffergeometry, createdMaterials ); + + } + + } else { + + if ( isLine ) { + + mesh = new THREE.LineSegments( buffergeometry, createdMaterials[ 0 ] ); + + } else if ( isPoints ) { + + mesh = new THREE.Points( buffergeometry, createdMaterials[ 0 ] ); + + } else { + + mesh = new THREE.Mesh( buffergeometry, createdMaterials[ 0 ] ); + + } + + } + + mesh.name = object.name; + + container.add( mesh ); + + } + + console.timeEnd( 'OBJLoader' ); + + return container; + + } + + }; + + return OBJLoader; + +} )(); \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/auth/bioid/js/objLoader.min.js b/oxAuth/Server/src/main/webapp/auth/bioid/js/objLoader.min.js new file mode 100644 index 00000000..11db37d1 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/bioid/js/objLoader.min.js @@ -0,0 +1 @@ +THREE.OBJLoader=function(){var t=/^[og]\s*(.+)?/,e=/^mtllib /,r=/^usemtl /;function i(t){this.manager=void 0!==t?t:THREE.DefaultLoadingManager,this.materials=null}return i.prototype={constructor:i,load:function(t,e,r,i){var s=this,a=new THREE.FileLoader(s.manager);a.setPath(this.path),a.load(t,function(t){e(s.parse(t))},r,i)},setPath:function(t){this.path=t},setMaterials:function(t){return this.materials=t,this},parse:function(i){console.time("OBJLoader");var s=new function(){var t={objects:[],object:{},vertices:[],normals:[],colors:[],uvs:[],materialLibraries:[],startObject:function(t,e){if(this.object&&!1===this.object.fromDeclaration)return this.object.name=t,void(this.object.fromDeclaration=!1!==e);var r=this.object&&"function"==typeof this.object.currentMaterial?this.object.currentMaterial():void 0;if(this.object&&"function"==typeof this.object._finalize&&this.object._finalize(!0),this.object={name:t||"",fromDeclaration:!1!==e,geometry:{vertices:[],normals:[],colors:[],uvs:[]},materials:[],smooth:!0,startMaterial:function(t,e){var r=this._finalize(!1);r&&(r.inherited||r.groupCount<=0)&&this.materials.splice(r.index,1);var i={index:this.materials.length,name:t||"",mtllib:Array.isArray(e)&&e.length>0?e[e.length-1]:"",smooth:void 0!==r?r.smooth:this.smooth,groupStart:void 0!==r?r.groupEnd:0,groupEnd:-1,groupCount:-1,inherited:!1,clone:function(t){var e={index:"number"==typeof t?t:this.index,name:this.name,mtllib:this.mtllib,smooth:this.smooth,groupStart:0,groupEnd:-1,groupCount:-1,inherited:!1};return e.clone=this.clone.bind(e),e}};return this.materials.push(i),i},currentMaterial:function(){if(this.materials.length>0)return this.materials[this.materials.length-1]},_finalize:function(t){var e=this.currentMaterial();if(e&&-1===e.groupEnd&&(e.groupEnd=this.geometry.vertices.length/3,e.groupCount=e.groupEnd-e.groupStart,e.inherited=!1),t&&this.materials.length>1)for(var r=this.materials.length-1;r>=0;r--)this.materials[r].groupCount<=0&&this.materials.splice(r,1);return t&&0===this.materials.length&&this.materials.push({name:"",smooth:this.smooth}),e}},r&&r.name&&"function"==typeof r.clone){var i=r.clone(0);i.inherited=!0,this.object.materials.push(i)}this.objects.push(this.object)},finalize:function(){this.object&&"function"==typeof this.object._finalize&&this.object._finalize(!0)},parseVertexIndex:function(t,e){var r=parseInt(t,10);return 3*(r>=0?r-1:r+e/3)},parseNormalIndex:function(t,e){var r=parseInt(t,10);return 3*(r>=0?r-1:r+e/3)},parseUVIndex:function(t,e){var r=parseInt(t,10);return 2*(r>=0?r-1:r+e/2)},addVertex:function(t,e,r){var i=this.vertices,s=this.object.geometry.vertices;s.push(i[t+0],i[t+1],i[t+2]),s.push(i[e+0],i[e+1],i[e+2]),s.push(i[r+0],i[r+1],i[r+2])},addVertexPoint:function(t){var e=this.vertices;this.object.geometry.vertices.push(e[t+0],e[t+1],e[t+2])},addVertexLine:function(t){var e=this.vertices;this.object.geometry.vertices.push(e[t+0],e[t+1],e[t+2])},addNormal:function(t,e,r){var i=this.normals,s=this.object.geometry.normals;s.push(i[t+0],i[t+1],i[t+2]),s.push(i[e+0],i[e+1],i[e+2]),s.push(i[r+0],i[r+1],i[r+2])},addColor:function(t,e,r){var i=this.colors,s=this.object.geometry.colors;s.push(i[t+0],i[t+1],i[t+2]),s.push(i[e+0],i[e+1],i[e+2]),s.push(i[r+0],i[r+1],i[r+2])},addUV:function(t,e,r){var i=this.uvs,s=this.object.geometry.uvs;s.push(i[t+0],i[t+1]),s.push(i[e+0],i[e+1]),s.push(i[r+0],i[r+1])},addUVLine:function(t){var e=this.uvs;this.object.geometry.uvs.push(e[t+0],e[t+1])},addFace:function(t,e,r,i,s,a,n,o,h){var l=this.vertices.length,u=this.parseVertexIndex(t,l),c=this.parseVertexIndex(e,l),p=this.parseVertexIndex(r,l);if(this.addVertex(u,c,p),void 0!==i&&""!==i){var m=this.uvs.length;u=this.parseUVIndex(i,m),c=this.parseUVIndex(s,m),p=this.parseUVIndex(a,m),this.addUV(u,c,p)}if(void 0!==n&&""!==n){var f=this.normals.length;u=this.parseNormalIndex(n,f),c=n===o?u:this.parseNormalIndex(o,f),p=n===h?u:this.parseNormalIndex(h,f),this.addNormal(u,c,p)}this.colors.length>0&&this.addColor(u,c,p)},addPointGeometry:function(t){this.object.geometry.type="Points";for(var e=this.vertices.length,r=0,i=t.length;r0){var b=g.split("/");f.push(b)}}var E=f[0];for(d=1,v=f.length-1;d1){var F=h[1].trim().toLowerCase();s.object.smooth="0"!==F&&"off"!==F}else s.object.smooth=!0;(J=s.object.currentMaterial())&&(J.smooth=s.object.smooth)}s.finalize();var I=new THREE.Group;I.materialLibraries=[].concat(s.materialLibraries);for(u=0,c=s.objects.length;u0?U.addAttribute("normal",new THREE.Float32BufferAttribute(A.normals,3)):U.computeVertexNormals(),A.colors.length>0&&(O=!0,U.addAttribute("color",new THREE.Float32BufferAttribute(A.colors,3))),A.uvs.length>0&&U.addAttribute("uv",new THREE.Float32BufferAttribute(A.uvs,2));for(var N,G=[],S=0,_=z.length;S<_;S++){var D=z[S],J=void 0;if(null!==this.materials)if(J=this.materials.create(D.name),!B||!J||J instanceof THREE.LineBasicMaterial){if(C&&J&&!(J instanceof THREE.PointsMaterial)){var k=new THREE.PointsMaterial({size:10,sizeAttenuation:!1});q.copy(J),J=k}}else{var q=new THREE.LineBasicMaterial;q.copy(J),q.lights=!1,J=q}J||((J=B?new THREE.LineBasicMaterial:C?new THREE.PointsMaterial({size:1,sizeAttenuation:!1}):new THREE.MeshPhongMaterial).name=D.name),J.flatShading=!D.smooth,J.vertexColors=O?THREE.VertexColors:THREE.NoColors,G.push(J)}if(G.length>1){for(S=0,_=z.length;S<_;S++){D=z[S];U.addGroup(D.groupStart,D.groupCount,S)}N=B?new THREE.LineSegments(U,G):C?new THREE.Points(U,G):new THREE.Mesh(U,G)}else N=B?new THREE.LineSegments(U,G[0]):C?new THREE.Points(U,G[0]):new THREE.Mesh(U,G[0]);N.name=P.name,I.add(N)}}return console.timeEnd("OBJLoader"),I}},i}(); \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/auth/bioid/js/three.min.js b/oxAuth/Server/src/main/webapp/auth/bioid/js/three.min.js new file mode 100644 index 00000000..96fc18a6 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/bioid/js/three.min.js @@ -0,0 +1,927 @@ +// threejs.org/license +(function(k,xa){"object"===typeof exports&&"undefined"!==typeof module?xa(exports):"function"===typeof define&&define.amd?define(["exports"],xa):xa(k.THREE={})})(this,function(k){function xa(){}function C(a,b){this.x=a||0;this.y=b||0}function M(){this.elements=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1];0b&&(b=a[c]);return b}function F(){Object.defineProperty(this,"id",{value:Bf+=2});this.uuid=S.generateUUID();this.name="";this.type="BufferGeometry";this.index=null;this.attributes={};this.morphAttributes={};this.groups=[];this.boundingSphere=this.boundingBox=null;this.drawRange= +{start:0,count:Infinity}}function Kb(a,b,c,d,e,f){N.call(this);this.type="BoxGeometry";this.parameters={width:a,height:b,depth:c,widthSegments:d,heightSegments:e,depthSegments:f};this.fromBufferGeometry(new mb(a,b,c,d,e,f));this.mergeVertices()}function mb(a,b,c,d,e,f){function g(a,b,c,d,e,f,g,k,P,H,ta){var q=f/P,v=g/H,x=f/2,K=g/2,w=k/2;g=P+1;var y=H+1,B=f=0,G,C,z=new p;for(C=0;Cm;m++){if(n=d[m])if(h=n[0],l=n[1]){u&&e.addAttribute("morphTarget"+m,u[h]);f&&e.addAttribute("morphNormal"+m,f[h]);c[m]=l;continue}c[m]=0}g.getUniforms().setValue(a,"morphTargetInfluences",c)}}}function Nf(a,b){var c={};return{update:function(d){var e=b.render.frame,f=d.geometry,g=a.get(d,f);c[g.id]!==e&&(f.isGeometry&&g.updateFromObject(d), +a.update(g),c[g.id]=e);return g},dispose:function(){c={}}}}function ab(a,b,c,d,e,f,g,h,l,m){a=void 0!==a?a:[];Y.call(this,a,void 0!==b?b:301,c,d,e,f,g,h,l,m);this.flipY=!1}function Mb(a,b,c){var d=a[0];if(0>=d||0/gm,function(a,c){a=V[c];if(void 0===a)throw Error("Can not resolve #include <"+c+ +">");return Wd(a)})}function Te(a){return a.replace(/#pragma unroll_loop[\s]+?for \( int i = (\d+); i < (\d+); i \+\+ \) \{([\s\S]+?)(?=\})\}/g,function(a,c,d,e){a="";for(c=parseInt(c);c 0 ) {\n\t\tfloat fogFactor = 0.0;\n\t\tif ( fogType == 1 ) {\n\t\t\tfogFactor = smoothstep( fogNear, fogFar, fogDepth );\n\t\t} else {\n\t\t\tconst float LOG2 = 1.442695;\n\t\t\tfogFactor = exp2( - fogDensity * fogDensity * fogDepth * fogDepth * LOG2 );\n\t\t\tfogFactor = 1.0 - clamp( fogFactor, 0.0, 1.0 );\n\t\t}\n\t\tgl_FragColor.rgb = mix( gl_FragColor.rgb, fogColor, fogFactor );\n\t}\n}"].join("\n")); +b.compileShader(ya);b.compileShader(Q);b.attachShader(ta,ya);b.attachShader(ta,Q);b.linkProgram(ta);Z=ta;K=b.getAttribLocation(Z,"position");P=b.getAttribLocation(Z,"uv");f=b.getUniformLocation(Z,"uvOffset");g=b.getUniformLocation(Z,"uvScale");h=b.getUniformLocation(Z,"rotation");l=b.getUniformLocation(Z,"center");m=b.getUniformLocation(Z,"scale");u=b.getUniformLocation(Z,"color");n=b.getUniformLocation(Z,"map");t=b.getUniformLocation(Z,"opacity");r=b.getUniformLocation(Z,"modelViewMatrix");k=b.getUniformLocation(Z, +"projectionMatrix");v=b.getUniformLocation(Z,"fogType");x=b.getUniformLocation(Z,"fogDensity");y=b.getUniformLocation(Z,"fogNear");w=b.getUniformLocation(Z,"fogFar");B=b.getUniformLocation(Z,"fogColor");b.getUniformLocation(Z,"fogDepth");G=b.getUniformLocation(Z,"alphaTest");ta=document.createElementNS("http://www.w3.org/1999/xhtml","canvas");ta.width=8;ta.height=8;ya=ta.getContext("2d");ya.fillStyle="white";ya.fillRect(0,0,8,8);z=new zc(ta)}c.useProgram(Z);c.initAttributes();c.enableAttribute(K); +c.enableAttribute(P);c.disableUnusedAttributes();c.disable(b.CULL_FACE);c.enable(b.BLEND);b.bindBuffer(b.ARRAY_BUFFER,C);b.vertexAttribPointer(K,2,b.FLOAT,!1,16,0);b.vertexAttribPointer(P,2,b.FLOAT,!1,16,8);b.bindBuffer(b.ELEMENT_ARRAY_BUFFER,nb);b.uniformMatrix4fv(k,!1,md.projectionMatrix.elements);c.activeTexture(b.TEXTURE0);b.uniform1i(n,0);ya=ta=0;(Q=p.fog)?(b.uniform3f(B,Q.color.r,Q.color.g,Q.color.b),Q.isFog?(b.uniform1f(y,Q.near),b.uniform1f(w,Q.far),b.uniform1i(v,1),ya=ta=1):Q.isFogExp2&& +(b.uniform1f(x,Q.density),b.uniform1i(v,2),ya=ta=2)):(b.uniform1i(v,0),ya=ta=0);Q=0;for(var L=q.length;Qb||a.height>b){if("data"in a){console.warn("THREE.WebGLRenderer: image in DataTexture is too big ("+a.width+"x"+a.height+").");return}b/=Math.max(a.width,a.height);var c=document.createElementNS("http://www.w3.org/1999/xhtml","canvas");c.width=Math.floor(a.width*b);c.height=Math.floor(a.height* +b);c.getContext("2d").drawImage(a,0,0,a.width,a.height,0,0,c.width,c.height);console.warn("THREE.WebGLRenderer: image is too big ("+a.width+"x"+a.height+"). Resized to "+c.width+"x"+c.height,a);return c}return a}function l(a){return S.isPowerOfTwo(a.width)&&S.isPowerOfTwo(a.height)}function m(a,b){return a.generateMipmaps&&b&&1003!==a.minFilter&&1006!==a.minFilter}function u(b,c,e,f){a.generateMipmap(b);d.get(c).__maxMipLevel=Math.log2(Math.max(e,f))}function n(b){return 1003===b||1004===b||1005=== +b?a.NEAREST:a.LINEAR}function t(b){b=b.target;b.removeEventListener("dispose",t);a:{var c=d.get(b);if(b.image&&c.__image__webglTextureCube)a.deleteTexture(c.__image__webglTextureCube);else{if(void 0===c.__webglInit)break a;a.deleteTexture(c.__webglTexture)}d.remove(b)}b.isVideoTexture&&delete B[b.id];g.memory.textures--}function k(b){b=b.target;b.removeEventListener("dispose",k);var c=d.get(b),e=d.get(b.texture);if(b){void 0!==e.__webglTexture&&a.deleteTexture(e.__webglTexture);b.depthTexture&&b.depthTexture.dispose(); +if(b.isWebGLRenderTargetCube)for(e=0;6>e;e++)a.deleteFramebuffer(c.__webglFramebuffer[e]),c.__webglDepthbuffer&&a.deleteRenderbuffer(c.__webglDepthbuffer[e]);else a.deleteFramebuffer(c.__webglFramebuffer),c.__webglDepthbuffer&&a.deleteRenderbuffer(c.__webglDepthbuffer);d.remove(b.texture);d.remove(b)}g.memory.textures--}function q(b,n){var k=d.get(b);if(b.isVideoTexture){var r=b.id,q=g.render.frame;B[r]!==q&&(B[r]=q,b.update())}if(0p;p++)q[p]=n||r?r?b.image[p].image:b.image[p]:h(b.image[p],e.maxCubemapSize);var x=q[0],w=l(x),K=f.convert(b.format),y=f.convert(b.type);v(a.TEXTURE_CUBE_MAP,b,w);for(p=0;6>p;p++)if(n)for(var B,G=q[p].mipmaps,P=0,C=G.length;Pt;t++)e.__webglFramebuffer[t]=a.createFramebuffer()}else e.__webglFramebuffer=a.createFramebuffer();if(h){c.bindTexture(a.TEXTURE_CUBE_MAP,f.__webglTexture);v(a.TEXTURE_CUBE_MAP,b.texture,n);for(t=0;6>t;t++)p(e.__webglFramebuffer[t],b,a.COLOR_ATTACHMENT0,a.TEXTURE_CUBE_MAP_POSITIVE_X+t);m(b.texture,n)&&u(a.TEXTURE_CUBE_MAP,b.texture,b.width,b.height);c.bindTexture(a.TEXTURE_CUBE_MAP,null)}else c.bindTexture(a.TEXTURE_2D,f.__webglTexture),v(a.TEXTURE_2D,b.texture,n),p(e.__webglFramebuffer, +b,a.COLOR_ATTACHMENT0,a.TEXTURE_2D),m(b.texture,n)&&u(a.TEXTURE_2D,b.texture,b.width,b.height),c.bindTexture(a.TEXTURE_2D,null);if(b.depthBuffer){e=d.get(b);f=!0===b.isWebGLRenderTargetCube;if(b.depthTexture){if(f)throw Error("target.depthTexture not supported in Cube render targets");if(b&&b.isWebGLRenderTargetCube)throw Error("Depth Texture with cube render targets is not supported");a.bindFramebuffer(a.FRAMEBUFFER,e.__webglFramebuffer);if(!b.depthTexture||!b.depthTexture.isDepthTexture)throw Error("renderTarget.depthTexture must be an instance of THREE.DepthTexture"); +d.get(b.depthTexture).__webglTexture&&b.depthTexture.image.width===b.width&&b.depthTexture.image.height===b.height||(b.depthTexture.image.width=b.width,b.depthTexture.image.height=b.height,b.depthTexture.needsUpdate=!0);q(b.depthTexture,0);e=d.get(b.depthTexture).__webglTexture;if(1026===b.depthTexture.format)a.framebufferTexture2D(a.FRAMEBUFFER,a.DEPTH_ATTACHMENT,a.TEXTURE_2D,e,0);else if(1027===b.depthTexture.format)a.framebufferTexture2D(a.FRAMEBUFFER,a.DEPTH_STENCIL_ATTACHMENT,a.TEXTURE_2D,e, +0);else throw Error("Unknown depthTexture format");}else if(f)for(e.__webglDepthbuffer=[],f=0;6>f;f++)a.bindFramebuffer(a.FRAMEBUFFER,e.__webglFramebuffer[f]),e.__webglDepthbuffer[f]=a.createRenderbuffer(),y(e.__webglDepthbuffer[f],b);else a.bindFramebuffer(a.FRAMEBUFFER,e.__webglFramebuffer),e.__webglDepthbuffer=a.createRenderbuffer(),y(e.__webglDepthbuffer,b);a.bindFramebuffer(a.FRAMEBUFFER,null)}};this.updateRenderTargetMipmap=function(b){var e=b.texture,f=l(b);if(m(e,f)){f=b.isWebGLRenderTargetCube? +a.TEXTURE_CUBE_MAP:a.TEXTURE_2D;var g=d.get(e).__webglTexture;c.bindTexture(f,g);u(f,e,b.width,b.height);c.bindTexture(f,null)}}}function Ve(a,b){return{convert:function(c){if(1E3===c)return a.REPEAT;if(1001===c)return a.CLAMP_TO_EDGE;if(1002===c)return a.MIRRORED_REPEAT;if(1003===c)return a.NEAREST;if(1004===c)return a.NEAREST_MIPMAP_NEAREST;if(1005===c)return a.NEAREST_MIPMAP_LINEAR;if(1006===c)return a.LINEAR;if(1007===c)return a.LINEAR_MIPMAP_NEAREST;if(1008===c)return a.LINEAR_MIPMAP_LINEAR; +if(1009===c)return a.UNSIGNED_BYTE;if(1017===c)return a.UNSIGNED_SHORT_4_4_4_4;if(1018===c)return a.UNSIGNED_SHORT_5_5_5_1;if(1019===c)return a.UNSIGNED_SHORT_5_6_5;if(1010===c)return a.BYTE;if(1011===c)return a.SHORT;if(1012===c)return a.UNSIGNED_SHORT;if(1013===c)return a.INT;if(1014===c)return a.UNSIGNED_INT;if(1015===c)return a.FLOAT;if(1016===c){var d=b.get("OES_texture_half_float");if(null!==d)return d.HALF_FLOAT_OES}if(1021===c)return a.ALPHA;if(1022===c)return a.RGB;if(1023===c)return a.RGBA; +if(1024===c)return a.LUMINANCE;if(1025===c)return a.LUMINANCE_ALPHA;if(1026===c)return a.DEPTH_COMPONENT;if(1027===c)return a.DEPTH_STENCIL;if(100===c)return a.FUNC_ADD;if(101===c)return a.FUNC_SUBTRACT;if(102===c)return a.FUNC_REVERSE_SUBTRACT;if(200===c)return a.ZERO;if(201===c)return a.ONE;if(202===c)return a.SRC_COLOR;if(203===c)return a.ONE_MINUS_SRC_COLOR;if(204===c)return a.SRC_ALPHA;if(205===c)return a.ONE_MINUS_SRC_ALPHA;if(206===c)return a.DST_ALPHA;if(207===c)return a.ONE_MINUS_DST_ALPHA; +if(208===c)return a.DST_COLOR;if(209===c)return a.ONE_MINUS_DST_COLOR;if(210===c)return a.SRC_ALPHA_SATURATE;if(33776===c||33777===c||33778===c||33779===c)if(d=b.get("WEBGL_compressed_texture_s3tc"),null!==d){if(33776===c)return d.COMPRESSED_RGB_S3TC_DXT1_EXT;if(33777===c)return d.COMPRESSED_RGBA_S3TC_DXT1_EXT;if(33778===c)return d.COMPRESSED_RGBA_S3TC_DXT3_EXT;if(33779===c)return d.COMPRESSED_RGBA_S3TC_DXT5_EXT}if(35840===c||35841===c||35842===c||35843===c)if(d=b.get("WEBGL_compressed_texture_pvrtc"), +null!==d){if(35840===c)return d.COMPRESSED_RGB_PVRTC_4BPPV1_IMG;if(35841===c)return d.COMPRESSED_RGB_PVRTC_2BPPV1_IMG;if(35842===c)return d.COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;if(35843===c)return d.COMPRESSED_RGBA_PVRTC_2BPPV1_IMG}if(36196===c&&(d=b.get("WEBGL_compressed_texture_etc1"),null!==d))return d.COMPRESSED_RGB_ETC1_WEBGL;if(37808===c||37809===c||37810===c||37811===c||37812===c||37813===c||37814===c||37815===c||37816===c||37817===c||37818===c||37819===c||37820===c||37821===c)if(d=b.get("WEBGL_compressed_texture_astc"), +null!==d)return c;if(103===c||104===c)if(d=b.get("EXT_blend_minmax"),null!==d){if(103===c)return d.MIN_EXT;if(104===c)return d.MAX_EXT}return 1020===c&&(d=b.get("WEBGL_depth_texture"),null!==d)?d.UNSIGNED_INT_24_8_WEBGL:0}}}function la(a,b,c,d){Qa.call(this);this.type="PerspectiveCamera";this.fov=void 0!==a?a:50;this.zoom=1;this.near=void 0!==c?c:.1;this.far=void 0!==d?d:2E3;this.focus=10;this.aspect=void 0!==b?b:1;this.view=null;this.filmGauge=35;this.filmOffset=0;this.updateProjectionMatrix()}function qd(a){la.call(this); +this.cameras=a||[]}function We(a){function b(){if(null!==d&&d.isPresenting){var b=d.getEyeParameters("left"),e=b.renderWidth;b=b.renderHeight;v=a.getPixelRatio();q=a.getSize();a.setDrawingBufferSize(2*e,b,1)}else c.enabled&&a.setDrawingBufferSize(q.width,q.height,v)}var c=this,d=null,e=null,f=null,g=new M,h=new M;"undefined"!==typeof window&&"VRFrameData"in window&&(e=new window.VRFrameData);var l=new M,m=new ja,u=new p,n=new la;n.bounds=new ea(0,0,.5,1);n.layers.enable(1);var t=new la;t.bounds=new ea(.5, +0,.5,1);t.layers.enable(2);var k=new qd([n,t]);k.layers.enable(1);k.layers.enable(2);var q,v;"undefined"!==typeof window&&window.addEventListener("vrdisplaypresentchange",b,!1);this.enabled=!1;this.userHeight=1.6;this.getDevice=function(){return d};this.setDevice=function(a){void 0!==a&&(d=a)};this.setPoseTarget=function(a){void 0!==a&&(f=a)};this.getCamera=function(a){if(null===d)return a;d.depthNear=a.near;d.depthFar=a.far;d.getFrameData(e);var b=d.stageParameters;b?g.fromArray(b.sittingToStandingTransform): +g.makeTranslation(0,c.userHeight,0);b=e.pose;var r=null!==f?f:a;r.matrix.copy(g);r.matrix.decompose(r.position,r.quaternion,r.scale);null!==b.orientation&&(m.fromArray(b.orientation),r.quaternion.multiply(m));null!==b.position&&(m.setFromRotationMatrix(g),u.fromArray(b.position),u.applyQuaternion(m),r.position.add(u));r.updateMatrixWorld();if(!1===d.isPresenting)return a;n.near=a.near;t.near=a.near;n.far=a.far;t.far=a.far;k.matrixWorld.copy(a.matrixWorld);k.matrixWorldInverse.copy(a.matrixWorldInverse); +n.matrixWorldInverse.fromArray(e.leftViewMatrix);t.matrixWorldInverse.fromArray(e.rightViewMatrix);h.getInverse(g);n.matrixWorldInverse.multiply(h);t.matrixWorldInverse.multiply(h);a=r.parent;null!==a&&(l.getInverse(a.matrixWorld),n.matrixWorldInverse.multiply(l),t.matrixWorldInverse.multiply(l));n.matrixWorld.getInverse(n.matrixWorldInverse);t.matrixWorld.getInverse(t.matrixWorldInverse);n.projectionMatrix.fromArray(e.leftProjectionMatrix);t.projectionMatrix.fromArray(e.rightProjectionMatrix);k.projectionMatrix.copy(n.projectionMatrix); +a=d.getLayers();a.length&&(a=a[0],null!==a.leftBounds&&4===a.leftBounds.length&&n.bounds.fromArray(a.leftBounds),null!==a.rightBounds&&4===a.rightBounds.length&&t.bounds.fromArray(a.rightBounds));return k};this.getStandingMatrix=function(){return g};this.submitFrame=function(){d&&d.isPresenting&&d.submitFrame()};this.dispose=function(){"undefined"!==typeof window&&window.removeEventListener("vrdisplaypresentchange",b)}}function Xd(a){function b(){ka=new Hf(D);ka.get("WEBGL_depth_texture");ka.get("OES_texture_float"); +ka.get("OES_texture_float_linear");ka.get("OES_texture_half_float");ka.get("OES_texture_half_float_linear");ka.get("OES_standard_derivatives");ka.get("OES_element_index_uint");ka.get("ANGLE_instanced_arrays");ia=new Ve(D,ka);Ra=new Ff(D,ka,a);ba=new Eg(D,ka,ia);ba.scissor(V.copy(ca).multiplyScalar(R));ba.viewport(ob.copy(Y).multiplyScalar(R));da=new Kf(D);X=new tg;fa=new Fg(D,ka,ba,X,Ra,ia,da);qa=new yf(D);ra=new If(D,qa,da);sa=new Nf(ra,da);wa=new Mf(D);oa=new sg(A,ka,Ra);ua=new xg;pa=new Cg;ma= +new Df(A,ba,ra,P);xa=new Ef(D,ka,da);za=new Jf(D,ka,da);Aa=new Dg(A,D,ba,fa,Ra);da.programs=oa.programs;A.context=D;A.capabilities=Ra;A.extensions=ka;A.properties=X;A.renderLists=ua;A.state=ba;A.info=da}function c(a){a.preventDefault();console.log("THREE.WebGLRenderer: Context Lost.");E=!0}function d(){console.log("THREE.WebGLRenderer: Context Restored.");E=!1;b()}function e(a){a=a.target;a.removeEventListener("dispose",e);f(a);X.remove(a)}function f(a){var b=X.get(a).program;a.program=void 0;void 0!== +b&&oa.releaseProgram(b)}function g(a,b,c){a.render(function(a){A.renderBufferImmediate(a,b,c)})}function h(){var a=na.getDevice();a&&a.isPresenting?a.requestAnimationFrame(l):window.requestAnimationFrame(l)}function l(a){!1!==va&&(Ba(a),h())}function m(a,b,c){if(!1!==a.visible){if(a.layers.test(b.layers))if(a.isLight)Z.pushLight(a),a.castShadow&&Z.pushShadow(a);else if(a.isSprite)a.frustumCulled&&!ha.intersectsSprite(a)||Z.pushSprite(a);else if(a.isImmediateRenderObject)c&&Nb.setFromMatrixPosition(a.matrixWorld).applyMatrix4(pd), +z.push(a,null,a.material,Nb.z,null);else if(a.isMesh||a.isLine||a.isPoints)if(a.isSkinnedMesh&&a.skeleton.update(),!a.frustumCulled||ha.intersectsObject(a)){c&&Nb.setFromMatrixPosition(a.matrixWorld).applyMatrix4(pd);var d=sa.update(a),e=a.material;if(Array.isArray(e))for(var f=d.groups,g=0,h=f.length;ga.matrixWorld.determinant();ba.setMaterial(e,h);h=k(c,b.fog,e,a);N="";g(a,h,e)}else A.renderBufferDirect(c,b.fog,d,e,a,f);a.onAfterRender(A,b,c,d,e,f);Z=pa.get(b,T||c)}function t(a,b,c){var d=X.get(a),g=Z.state.lights;c=oa.getParameters(a,g.state,Z.state.shadowsArray,b,Ga.numPlanes,Ga.numIntersection,c);var h=oa.getProgramCode(a,c),l=d.program,m=!0;if(void 0===l)a.addEventListener("dispose",e);else if(l.code!==h)f(a);else{if(d.lightsHash!==g.state.hash)X.update(a, +"lightsHash",g.state.hash);else if(void 0!==c.shaderID)return;m=!1}m&&(c.shaderID?(l=rb[c.shaderID],d.shader={name:a.type,uniforms:Da.clone(l.uniforms),vertexShader:l.vertexShader,fragmentShader:l.fragmentShader}):d.shader={name:a.type,uniforms:a.uniforms,vertexShader:a.vertexShader,fragmentShader:a.fragmentShader},a.onBeforeCompile(d.shader,A),l=oa.acquireProgram(a,d.shader,c,h),d.program=l,a.program=l);c=l.getAttributes();if(a.morphTargets)for(h=a.numSupportedMorphTargets=0;he.matrixWorld.determinant();ba.setMaterial(d,g);var h=k(a,b,d,e);a=c.id+"_"+h.id+"_"+(!0===d.wireframe);var l=!1;a!==N&&(N=a,l=!0);e.morphTargetInfluences&&(wa.update(e,c,d,h),l=!0);g=c.index;var m=c.attributes.position;b=1;!0===d.wireframe&&(g=ra.getWireframeAttribute(c),b=2);a=xa;if(null!==g){var n=qa.get(g);a=za;a.setIndex(n)}if(l){l=void 0;if(c&&c.isInstancedBufferGeometry&&null===ka.get("ANGLE_instanced_arrays"))console.error("THREE.WebGLRenderer.setupVertexAttributes: using THREE.InstancedBufferGeometry but hardware does not support extension ANGLE_instanced_arrays."); +else{void 0===l&&(l=0);ba.initAttributes();var u=c.attributes;h=h.getAttributes();var t=d.defaultAttributeValues;for(K in h){var r=h[K];if(0<=r){var q=u[K];if(void 0!==q){var p=q.normalized,v=q.itemSize,x=qa.get(q);if(void 0!==x){var w=x.buffer,B=x.type;x=x.bytesPerElement;if(q.isInterleavedBufferAttribute){var y=q.data,G=y.stride;q=q.offset;y&&y.isInstancedInterleavedBuffer?(ba.enableAttributeAndDivisor(r,y.meshPerAttribute),void 0===c.maxInstancedCount&&(c.maxInstancedCount=y.meshPerAttribute*y.count)): +ba.enableAttribute(r);D.bindBuffer(D.ARRAY_BUFFER,w);D.vertexAttribPointer(r,v,B,p,G*x,(l*G+q)*x)}else q.isInstancedBufferAttribute?(ba.enableAttributeAndDivisor(r,q.meshPerAttribute),void 0===c.maxInstancedCount&&(c.maxInstancedCount=q.meshPerAttribute*q.count)):ba.enableAttribute(r),D.bindBuffer(D.ARRAY_BUFFER,w),D.vertexAttribPointer(r,v,B,p,0,l*v*x)}}else if(void 0!==t&&(p=t[K],void 0!==p))switch(p.length){case 2:D.vertexAttrib2fv(r,p);break;case 3:D.vertexAttrib3fv(r,p);break;case 4:D.vertexAttrib4fv(r, +p);break;default:D.vertexAttrib1fv(r,p)}}}ba.disableUnusedAttributes()}null!==g&&D.bindBuffer(D.ELEMENT_ARRAY_BUFFER,n.buffer)}n=Infinity;null!==g?n=g.count:void 0!==m&&(n=m.count);g=c.drawRange.start*b;m=null!==f?f.start*b:0;var K=Math.max(g,m);f=Math.max(0,Math.min(n,g+c.drawRange.count*b,m+(null!==f?f.count*b:Infinity))-1-K+1);if(0!==f){if(e.isMesh)if(!0===d.wireframe)ba.setLineWidth(d.wireframeLinewidth*(null===F?R:1)),a.setMode(D.LINES);else switch(e.drawMode){case 0:a.setMode(D.TRIANGLES);break; +case 1:a.setMode(D.TRIANGLE_STRIP);break;case 2:a.setMode(D.TRIANGLE_FAN)}else e.isLine?(d=d.linewidth,void 0===d&&(d=1),ba.setLineWidth(d*(null===F?R:1)),e.isLineSegments?a.setMode(D.LINES):e.isLineLoop?a.setMode(D.LINE_LOOP):a.setMode(D.LINE_STRIP)):e.isPoints&&a.setMode(D.POINTS);c&&c.isInstancedBufferGeometry?0=Ra.maxTextures&&console.warn("THREE.WebGLRenderer: Trying to use "+ +a+" texture units while this GPU supports only "+Ra.maxTextures);aa+=1;return a};this.setTexture2D=function(){var a=!1;return function(b,c){b&&b.isWebGLRenderTarget&&(a||(console.warn("THREE.WebGLRenderer.setTexture2D: don't use render targets as textures. Use their .texture property instead."),a=!0),b=b.texture);fa.setTexture2D(b,c)}}();this.setTexture=function(){var a=!1;return function(b,c){a||(console.warn("THREE.WebGLRenderer: .setTexture is deprecated, use setTexture2D instead."),a=!0);fa.setTexture2D(b, +c)}}();this.setTextureCube=function(){var a=!1;return function(b,c){b&&b.isWebGLRenderTargetCube&&(a||(console.warn("THREE.WebGLRenderer.setTextureCube: don't use cube render targets as textures. Use their .texture property instead."),a=!0),b=b.texture);b&&b.isCubeTexture||Array.isArray(b.image)&&6===b.image.length?fa.setTextureCube(b,c):fa.setTextureCubeDynamic(b,c)}}();this.getRenderTarget=function(){return F};this.setRenderTarget=function(a){(F=a)&&void 0===X.get(a).__webglFramebuffer&&fa.setupRenderTarget(a); +var b=null,c=!1;a?(b=X.get(a).__webglFramebuffer,a.isWebGLRenderTargetCube&&(b=b[a.activeCubeFace],c=!0),ob.copy(a.viewport),V.copy(a.scissor),U=a.scissorTest):(ob.copy(Y).multiplyScalar(R),V.copy(ca).multiplyScalar(R),U=ja);I!==b&&(D.bindFramebuffer(D.FRAMEBUFFER,b),I=b);ba.viewport(ob);ba.scissor(V);ba.setScissorTest(U);c&&(c=X.get(a.texture),D.framebufferTexture2D(D.FRAMEBUFFER,D.COLOR_ATTACHMENT0,D.TEXTURE_CUBE_MAP_POSITIVE_X+a.activeCubeFace,c.__webglTexture,a.activeMipMapLevel))};this.readRenderTargetPixels= +function(a,b,c,d,e,f){if(a&&a.isWebGLRenderTarget){var g=X.get(a).__webglFramebuffer;if(g){var h=!1;g!==I&&(D.bindFramebuffer(D.FRAMEBUFFER,g),h=!0);try{var l=a.texture,m=l.format,n=l.type;1023!==m&&ia.convert(m)!==D.getParameter(D.IMPLEMENTATION_COLOR_READ_FORMAT)?console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in RGBA or implementation defined format."):1009===n||ia.convert(n)===D.getParameter(D.IMPLEMENTATION_COLOR_READ_TYPE)||1015===n&&(ka.get("OES_texture_float")|| +ka.get("WEBGL_color_buffer_float"))||1016===n&&ka.get("EXT_color_buffer_half_float")?D.checkFramebufferStatus(D.FRAMEBUFFER)===D.FRAMEBUFFER_COMPLETE?0<=b&&b<=a.width-d&&0<=c&&c<=a.height-e&&D.readPixels(b,c,d,e,ia.convert(m),ia.convert(n),f):console.error("THREE.WebGLRenderer.readRenderTargetPixels: readPixels from renderTarget failed. Framebuffer not complete."):console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in UnsignedByteType or implementation defined type.")}finally{h&& +D.bindFramebuffer(D.FRAMEBUFFER,I)}}}else console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.")};this.copyFramebufferToTexture=function(a,b,c){var d=b.image.width,e=b.image.height,f=ia.convert(b.format);this.setTexture2D(b,0);D.copyTexImage2D(D.TEXTURE_2D,c||0,f,a.x,a.y,d,e,0)};this.copyTextureToTexture=function(a,b,c,d){var e=b.image.width,f=b.image.height,g=ia.convert(c.format),h=ia.convert(c.type);b=b.isDataTexture?b.image.data:b.image;this.setTexture2D(c, +0);D.texSubImage2D(D.TEXTURE_2D,d||0,a.x,a.y,e,f,g,h,b)}}function Ob(a,b){this.name="";this.color=new I(a);this.density=void 0!==b?b:2.5E-4}function Pb(a,b,c){this.name="";this.color=new I(a);this.near=void 0!==b?b:1;this.far=void 0!==c?c:1E3}function rd(){A.call(this);this.type="Scene";this.overrideMaterial=this.fog=this.background=null;this.autoUpdate=!0}function eb(a){O.call(this);this.type="SpriteMaterial";this.color=new I(16777215);this.map=null;this.rotation=0;this.lights=this.fog=!1;this.setValues(a)} +function Ac(a){A.call(this);this.type="Sprite";this.material=void 0!==a?a:new eb;this.center=new C(.5,.5)}function Bc(){A.call(this);this.type="LOD";Object.defineProperties(this,{levels:{enumerable:!0,value:[]}})}function Cc(a,b){a=a||[];this.bones=a.slice(0);this.boneMatrices=new Float32Array(16*this.bones.length);if(void 0===b)this.calculateInverses();else if(this.bones.length===b.length)this.boneInverses=b.slice(0);else for(console.warn("THREE.Skeleton boneInverses is the wrong length."),this.boneInverses= +[],a=0,b=this.bones.length;ac;c++){var n=u[h[c]];var t=u[h[(c+1)%3]];f[0]=Math.min(n,t);f[1]=Math.max(n,t);n=f[0]+","+f[1];void 0===g[n]&&(g[n]={index1:f[0],index2:f[1]})}}for(n in g)m=g[n],h=a.vertices[m.index1],b.push(h.x,h.y,h.z),h=a.vertices[m.index2],b.push(h.x,h.y,h.z)}else if(a&&a.isBufferGeometry)if(h=new p,null!==a.index){l=a.attributes.position;u=a.index;var k=a.groups;0===k.length&&(k=[{start:0,count:u.count,materialIndex:0}]);a=0;for(e=k.length;ac;c++)n=u.getX(m+c),t=u.getX(m+(c+1)%3),f[0]=Math.min(n,t),f[1]=Math.max(n,t),n=f[0]+","+f[1],void 0===g[n]&&(g[n]={index1:f[0],index2:f[1]});for(n in g)m=g[n],h.fromBufferAttribute(l,m.index1),b.push(h.x,h.y,h.z),h.fromBufferAttribute(l,m.index2),b.push(h.x,h.y,h.z)}else for(l=a.attributes.position,m=0,d=l.count/3;mc;c++)g=3*m+c,h.fromBufferAttribute(l,g),b.push(h.x,h.y,h.z),g=3*m+(c+1)%3,h.fromBufferAttribute(l,g),b.push(h.x, +h.y,h.z);this.addAttribute("position",new z(b,3))}function Ec(a,b,c){N.call(this);this.type="ParametricGeometry";this.parameters={func:a,slices:b,stacks:c};this.fromBufferGeometry(new Tb(a,b,c));this.mergeVertices()}function Tb(a,b,c){F.call(this);this.type="ParametricBufferGeometry";this.parameters={func:a,slices:b,stacks:c};var d=[],e=[],f=[],g=[],h=new p,l=new p,m=new p,u=new p,n=new p,t,k,q=b+1;for(t=0;t<=c;t++){var v=t/c;for(k=0;k<=b;k++){var x=k/b;a(x,v,l);e.push(l.x,l.y,l.z);0<=x-1E-5?(a(x- +1E-5,v,m),u.subVectors(l,m)):(a(x+1E-5,v,m),u.subVectors(m,l));0<=v-1E-5?(a(x,v-1E-5,m),n.subVectors(l,m)):(a(x,v+1E-5,m),n.subVectors(m,l));h.crossVectors(u,n).normalize();f.push(h.x,h.y,h.z);g.push(x,v)}}for(t=0;td&&1===a.x&&(l[b]=a.x-1);0===c.x&&0===c.z&&(l[b]=d/2/Math.PI+.5)}F.call(this);this.type="PolyhedronBufferGeometry";this.parameters={vertices:a,indices:b,radius:c,detail:d};c=c||1;d=d||0;var h=[],l=[];(function(a){for(var c=new p,d=new p,g=new p,h=0;h< +b.length;h+=3){f(b[h+0],c);f(b[h+1],d);f(b[h+2],g);var l,m,k=c,y=d,w=g,B=Math.pow(2,a),G=[];for(m=0;m<=B;m++){G[m]=[];var K=k.clone().lerp(w,m/B),P=y.clone().lerp(w,m/B),H=B-m;for(l=0;l<=H;l++)G[m][l]=0===l&&m===B?K:K.clone().lerp(P,l/H)}for(m=0;me&&(.2>b&&(l[a+0]+=1),.2>c&&(l[a+2]+=1),.2>d&&(l[a+4]+=1))})();this.addAttribute("position",new z(h,3));this.addAttribute("normal",new z(h.slice(),3));this.addAttribute("uv",new z(l,2));0===d?this.computeVertexNormals():this.normalizeNormals()}function Gc(a,b){N.call(this);this.type="TetrahedronGeometry";this.parameters={radius:a,detail:b};this.fromBufferGeometry(new Ub(a, +b));this.mergeVertices()}function Ub(a,b){pa.call(this,[1,1,1,-1,-1,1,-1,1,-1,1,-1,-1],[2,1,0,0,3,2,1,3,0,2,3,1],a,b);this.type="TetrahedronBufferGeometry";this.parameters={radius:a,detail:b}}function Hc(a,b){N.call(this);this.type="OctahedronGeometry";this.parameters={radius:a,detail:b};this.fromBufferGeometry(new sb(a,b));this.mergeVertices()}function sb(a,b){pa.call(this,[1,0,0,-1,0,0,0,1,0,0,-1,0,0,0,1,0,0,-1],[0,2,4,0,4,3,0,3,5,0,5,2,1,2,5,1,5,3,1,3,4,1,4,2],a,b);this.type="OctahedronBufferGeometry"; +this.parameters={radius:a,detail:b}}function Ic(a,b){N.call(this);this.type="IcosahedronGeometry";this.parameters={radius:a,detail:b};this.fromBufferGeometry(new Vb(a,b));this.mergeVertices()}function Vb(a,b){var c=(1+Math.sqrt(5))/2;pa.call(this,[-1,c,0,1,c,0,-1,-c,0,1,-c,0,0,-1,c,0,1,c,0,-1,-c,0,1,-c,c,0,-1,c,0,1,-c,0,-1,-c,0,1],[0,11,5,0,5,1,0,1,7,0,7,10,0,10,11,1,5,9,5,11,4,11,10,2,10,7,6,7,1,8,3,9,4,3,4,2,3,2,6,3,6,8,3,8,9,4,9,5,2,4,11,6,2,10,8,6,7,9,8,1],a,b);this.type="IcosahedronBufferGeometry"; +this.parameters={radius:a,detail:b}}function Jc(a,b){N.call(this);this.type="DodecahedronGeometry";this.parameters={radius:a,detail:b};this.fromBufferGeometry(new Wb(a,b));this.mergeVertices()}function Wb(a,b){var c=(1+Math.sqrt(5))/2,d=1/c;pa.call(this,[-1,-1,-1,-1,-1,1,-1,1,-1,-1,1,1,1,-1,-1,1,-1,1,1,1,-1,1,1,1,0,-d,-c,0,-d,c,0,d,-c,0,d,c,-d,-c,0,-d,c,0,d,-c,0,d,c,0,-c,0,-d,c,0,-d,-c,0,d,c,0,d],[3,11,7,3,7,15,3,15,13,7,19,17,7,17,6,7,6,15,17,4,8,17,8,10,17,10,6,8,0,16,8,16,2,8,2,10,0,12,1,0,1,18, +0,18,16,6,10,2,6,2,13,6,13,15,2,16,18,2,18,3,2,3,13,18,1,9,18,9,11,18,11,3,4,14,12,4,12,0,4,0,8,11,9,5,11,5,19,11,19,7,19,5,14,19,14,4,19,4,17,1,12,14,1,14,5,1,5,9],a,b);this.type="DodecahedronBufferGeometry";this.parameters={radius:a,detail:b}}function Kc(a,b,c,d,e,f){N.call(this);this.type="TubeGeometry";this.parameters={path:a,tubularSegments:b,radius:c,radialSegments:d,closed:e};void 0!==f&&console.warn("THREE.TubeGeometry: taper has been removed.");a=new Xb(a,b,c,d,e);this.tangents=a.tangents; +this.normals=a.normals;this.binormals=a.binormals;this.fromBufferGeometry(a);this.mergeVertices()}function Xb(a,b,c,d,e){function f(e){u=a.getPointAt(e/b,u);var f=g.normals[e];e=g.binormals[e];for(k=0;k<=d;k++){var m=k/d*Math.PI*2,n=Math.sin(m);m=-Math.cos(m);l.x=m*f.x+n*e.x;l.y=m*f.y+n*e.y;l.z=m*f.z+n*e.z;l.normalize();q.push(l.x,l.y,l.z);h.x=u.x+c*l.x;h.y=u.y+c*l.y;h.z=u.z+c*l.z;r.push(h.x,h.y,h.z)}}F.call(this);this.type="TubeBufferGeometry";this.parameters={path:a,tubularSegments:b,radius:c,radialSegments:d, +closed:e};b=b||64;c=c||1;d=d||8;e=e||!1;var g=a.computeFrenetFrames(b,e);this.tangents=g.tangents;this.normals=g.normals;this.binormals=g.binormals;var h=new p,l=new p,m=new C,u=new p,n,k,r=[],q=[],v=[],x=[];for(n=0;n=b;e-=d)f=Ye(e,a[e],a[e+1],f);f&&tb(f,f.next)&&(Nc(f),f=f.next);return f}function Oc(a,b){if(!a)return a;b||(b=a);do{var c=!1;if(a.steiner||!tb(a,a.next)&&0!==ra(a.prev,a,a.next))a=a.next;else{Nc(a);a=b=a.prev;if(a===a.next)break;c=!0}}while(c||a!==b);return b}function Pc(a,b,c,d,e,f,g){if(a){if(!g&&f){var h=a,l=h;do null===l.z&&(l.z=Zd(l.x,l.y,d,e,f)),l.prevZ=l.prev,l=l.nextZ= +l.next;while(l!==h);l.prevZ.nextZ=null;l.prevZ=null;h=l;var m,u,n,k,r=1;do{l=h;var q=h=null;for(u=0;l;){u++;var p=l;for(m=n=0;mn.x?u.x>r.x?u.x:r.x:n.x>r.x?n.x:r.x,B=u.y>n.y?u.y>r.y?u.y:r.y:n.y> +r.y?n.y:r.y;m=Zd(u.x=m;){if(x!==q.prev&&x!==q.next&&wd(u.x,u.y,n.x,n.y,r.x,r.y,x.x,x.y)&&0<=ra(x.prev,x,x.next)){q=!1;break a}x=x.prevZ}q=!0}}else a:if(q=a,u=q.prev,n=q,r=q.next,0<=ra(u,n,r))q=!1;else{for(m=q.next.next;m!==q.prev;){if(wd(u.x,u.y, +n.x,n.y,r.x,r.y,m.x,m.y)&&0<=ra(m.prev,m,m.next)){q=!1;break a}m=m.next}q=!0}if(q)b.push(l.i/c),b.push(a.i/c),b.push(p.i/c),Nc(a),h=a=p.next;else if(a=p,a===h){if(!g)Pc(Oc(a),b,c,d,e,f,1);else if(1===g){g=b;h=c;l=a;do p=l.prev,q=l.next.next,!tb(p,q)&&Ze(p,l,l.next,q)&&Qc(p,q)&&Qc(q,p)&&(g.push(p.i/h),g.push(l.i/h),g.push(q.i/h),Nc(l),Nc(l.next),l=a=q),l=l.next;while(l!==a);a=l;Pc(a,b,c,d,e,f,2)}else if(2===g)a:{g=a;do{for(h=g.next.next;h!==g.prev;){if(l=g.i!==h.i){l=g;p=h;if(q=l.next.i!==p.i&&l.prev.i!== +p.i){b:{q=l;do{if(q.i!==l.i&&q.next.i!==l.i&&q.i!==p.i&&q.next.i!==p.i&&Ze(q,q.next,l,p)){q=!0;break b}q=q.next}while(q!==l);q=!1}q=!q}if(q=q&&Qc(l,p)&&Qc(p,l)){q=l;u=!1;n=(l.x+p.x)/2;p=(l.y+p.y)/2;do q.y>p!==q.next.y>p&&q.next.y!==q.y&&n<(q.next.x-q.x)*(p-q.y)/(q.next.y-q.y)+q.x&&(u=!u),q=q.next;while(q!==l);q=u}l=q}if(l){a=$e(g,h);g=Oc(g,g.next);a=Oc(a,a.next);Pc(g,b,c,d,e,f);Pc(a,b,c,d,e,f);break a}h=h.next}g=g.next}while(g!==a)}break}}}}function Hg(a,b){return a.x-b.x}function Ig(a,b){var c=b, +d=a.x,e=a.y,f=-Infinity;do{if(e<=c.y&&e>=c.next.y&&c.next.y!==c.y){var g=c.x+(e-c.y)*(c.next.x-c.x)/(c.next.y-c.y);if(g<=d&&g>f){f=g;if(g===d){if(e===c.y)return c;if(e===c.next.y)return c.next}var h=c.x=c.x&&c.x>=g&&d!==c.x&&wd(eh.x)&&Qc(c,a)&&(h=c,m=u)}c=c.next}return h}function Zd(a, +b,c,d,e){a=32767*(a-c)*e;b=32767*(b-d)*e;a=(a|a<<8)&16711935;a=(a|a<<4)&252645135;a=(a|a<<2)&858993459;b=(b|b<<8)&16711935;b=(b|b<<4)&252645135;b=(b|b<<2)&858993459;return(a|a<<1)&1431655765|((b|b<<1)&1431655765)<<1}function Jg(a){var b=a,c=a;do b.xra(a.prev,a,a.next)?0<=ra(a,b,a.next)&&0<=ra(a,a.prev,b):0>ra(a,b,a.prev)||0>ra(a,a.next,b)}function $e(a,b){var c=new $d(a.i,a.x,a.y),d=new $d(b.i,b.x,b.y),e=a.next,f=b.prev;a.next=b;b.prev=a;c.next=e;e.prev=c;d.next=c;c.prev=d;f.next=d;d.prev=f;return d}function Ye(a,b,c,d){a=new $d(a,b,c);d?(a.next=d.next,a.prev=d,d.next.prev=a,d.next=a): +(a.prev=a,a.next=a);return a}function Nc(a){a.next.prev=a.prev;a.prev.next=a.next;a.prevZ&&(a.prevZ.nextZ=a.nextZ);a.nextZ&&(a.nextZ.prevZ=a.prevZ)}function $d(a,b,c){this.i=a;this.x=b;this.y=c;this.nextZ=this.prevZ=this.z=this.next=this.prev=null;this.steiner=!1}function af(a){var b=a.length;2k;k++){var n=m[f[k]];var t=m[f[(k+1)%3]];d[0]=Math.min(n,t);d[1]=Math.max(n,t);n=d[0]+","+d[1];void 0===e[n]?e[n]={index1:d[0],index2:d[1],face1:h,face2:void 0}:e[n].face2=h}for(n in e)if(d=e[n],void 0===d.face2||g[d.face1].normal.dot(g[d.face2].normal)<=b)f=a[d.index1],c.push(f.x,f.y,f.z),f=a[d.index2],c.push(f.x,f.y,f.z);this.addAttribute("position",new z(c,3))}function xb(a,b,c,d,e,f,g,h){N.call(this);this.type="CylinderGeometry";this.parameters={radiusTop:a, +radiusBottom:b,height:c,radialSegments:d,heightSegments:e,openEnded:f,thetaStart:g,thetaLength:h};this.fromBufferGeometry(new Ya(a,b,c,d,e,f,g,h));this.mergeVertices()}function Ya(a,b,c,d,e,f,g,h){function l(c){var e,f=new C,l=new p,u=0,v=!0===c?a:b,w=!0===c?1:-1;var A=q;for(e=1;e<=d;e++)n.push(0,x*w,0),t.push(0,w,0),r.push(.5,.5),q++;var z=q;for(e=0;e<=d;e++){var E=e/d*h+g,F=Math.cos(E);E=Math.sin(E);l.x=v*E;l.y=x*w;l.z=v*F;n.push(l.x,l.y,l.z);t.push(0,w,0);f.x=.5*F+.5;f.y=.5*E*w+.5;r.push(f.x,f.y); +q++}for(e=0;ethis.duration&&this.resetDuration();this.optimize()}function Md(a){this.manager=void 0!==a?a:ma;this.textures={}}function ee(a){this.manager=void 0!==a?a:ma}function ic(){}function fe(a){"boolean"===typeof a&&(console.warn("THREE.JSONLoader: showStatus parameter has been removed from constructor."),a=void 0);this.manager=void 0!==a?a:ma;this.withCredentials=!1}function ff(a){this.manager=void 0!==a?a:ma;this.texturePath=""} +function ge(a){"undefined"===typeof createImageBitmap&&console.warn("THREE.ImageBitmapLoader: createImageBitmap() not supported.");"undefined"===typeof fetch&&console.warn("THREE.ImageBitmapLoader: fetch() not supported.");this.manager=void 0!==a?a:ma;this.options=void 0}function he(){this.type="ShapePath";this.subPaths=[];this.currentPath=null}function ie(a){this.type="Font";this.data=a}function gf(a){this.manager=void 0!==a?a:ma}function je(a){this.manager=void 0!==a?a:ma}function hf(){this.type= +"StereoCamera";this.aspect=1;this.eyeSep=.064;this.cameraL=new la;this.cameraL.layers.enable(1);this.cameraL.matrixAutoUpdate=!1;this.cameraR=new la;this.cameraR.layers.enable(2);this.cameraR.matrixAutoUpdate=!1}function cd(a,b,c){A.call(this);this.type="CubeCamera";var d=new la(90,1,a,b);d.up.set(0,-1,0);d.lookAt(new p(1,0,0));this.add(d);var e=new la(90,1,a,b);e.up.set(0,-1,0);e.lookAt(new p(-1,0,0));this.add(e);var f=new la(90,1,a,b);f.up.set(0,0,1);f.lookAt(new p(0,1,0));this.add(f);var g=new la(90, +1,a,b);g.up.set(0,0,-1);g.lookAt(new p(0,-1,0));this.add(g);var h=new la(90,1,a,b);h.up.set(0,-1,0);h.lookAt(new p(0,0,1));this.add(h);var l=new la(90,1,a,b);l.up.set(0,-1,0);l.lookAt(new p(0,0,-1));this.add(l);this.renderTarget=new Ib(c,c,{format:1022,magFilter:1006,minFilter:1006});this.renderTarget.texture.name="CubeCamera";this.update=function(a,b){null===this.parent&&this.updateMatrixWorld();var c=this.renderTarget,m=c.texture.generateMipmaps;c.texture.generateMipmaps=!1;c.activeCubeFace=0;a.render(b, +d,c);c.activeCubeFace=1;a.render(b,e,c);c.activeCubeFace=2;a.render(b,f,c);c.activeCubeFace=3;a.render(b,g,c);c.activeCubeFace=4;a.render(b,h,c);c.texture.generateMipmaps=m;c.activeCubeFace=5;a.render(b,l,c);a.setRenderTarget(null)};this.clear=function(a,b,c,d){for(var e=this.renderTarget,f=0;6>f;f++)e.activeCubeFace=f,a.setRenderTarget(e),a.clear(b,c,d);a.setRenderTarget(null)}}function ke(){A.call(this);this.type="AudioListener";this.context=le.getContext();this.gain=this.context.createGain();this.gain.connect(this.context.destination); +this.filter=null}function jc(a){A.call(this);this.type="Audio";this.context=a.context;this.gain=this.context.createGain();this.gain.connect(a.getInput());this.autoplay=!1;this.buffer=null;this.loop=!1;this.offset=this.startTime=0;this.playbackRate=1;this.isPlaying=!1;this.hasPlaybackControl=!0;this.sourceType="empty";this.filters=[]}function me(a){jc.call(this,a);this.panner=this.context.createPanner();this.panner.connect(this.gain)}function ne(a,b){this.analyser=a.context.createAnalyser();this.analyser.fftSize= +void 0!==b?b:2048;this.data=new Uint8Array(this.analyser.frequencyBinCount);a.getOutput().connect(this.analyser)}function oe(a,b,c){this.binding=a;this.valueSize=c;a=Float64Array;switch(b){case "quaternion":b=this._slerp;break;case "string":case "bool":a=Array;b=this._select;break;default:b=this._lerp}this.buffer=new a(4*c);this._mixBufferRegion=b;this.referenceCount=this.useCount=this.cumulativeWeight=0}function jf(a,b,c){c=c||qa.parseTrackName(b);this._targetGroup=a;this._bindings=a.subscribe_(b, +c)}function qa(a,b,c){this.path=b;this.parsedPath=c||qa.parseTrackName(b);this.node=qa.findNode(a,this.parsedPath.nodeName)||a;this.rootNode=a}function kf(){this.uuid=S.generateUUID();this._objects=Array.prototype.slice.call(arguments);this.nCachedObjects_=0;var a={};this._indicesByUUID=a;for(var b=0,c=arguments.length;b!==c;++b)a[arguments[b].uuid]=b;this._paths=[];this._parsedPaths=[];this._bindings=[];this._bindingsIndicesByPath={};var d=this;this.stats={objects:{get total(){return d._objects.length}, +get inUse(){return this.total-d.nCachedObjects_}},get bindingsPerObject(){return d._bindings.length}}}function lf(a,b,c){this._mixer=a;this._clip=b;this._localRoot=c||null;a=b.tracks;b=a.length;c=Array(b);for(var d={endingStart:2400,endingEnd:2400},e=0;e!==b;++e){var f=a[e].createInterpolant(null);c[e]=f;f.settings=d}this._interpolantSettings=d;this._interpolants=c;this._propertyBindings=Array(b);this._weightInterpolant=this._timeScaleInterpolant=this._byClipCacheIndex=this._cacheIndex=null;this.loop= +2201;this._loopCount=-1;this._startTime=null;this.time=0;this._effectiveWeight=this.weight=this._effectiveTimeScale=this.timeScale=1;this.repetitions=Infinity;this.paused=!1;this.enabled=!0;this.clampWhenFinished=!1;this.zeroSlopeAtEnd=this.zeroSlopeAtStart=!0}function pe(a){this._root=a;this._initMemoryManager();this.time=this._accuIndex=0;this.timeScale=1}function Nd(a,b){"string"===typeof a&&(console.warn("THREE.Uniform: Type parameter is no longer needed."),a=b);this.value=a}function qe(){F.call(this); +this.type="InstancedBufferGeometry";this.maxInstancedCount=void 0}function re(a,b,c,d){this.data=a;this.itemSize=b;this.offset=c;this.normalized=!0===d}function kc(a,b){this.array=a;this.stride=b;this.count=void 0!==a?a.length/b:0;this.dynamic=!1;this.updateRange={offset:0,count:-1};this.version=0}function se(a,b,c){kc.call(this,a,b);this.meshPerAttribute=c||1}function te(a,b,c){T.call(this,a,b);this.meshPerAttribute=c||1}function mf(a,b,c,d){this.ray=new qb(a,b);this.near=c||0;this.far=d||Infinity; +this.params={Mesh:{},Line:{},LOD:{},Points:{threshold:1},Sprite:{}};Object.defineProperties(this.params,{PointCloud:{get:function(){console.warn("THREE.Raycaster: params.PointCloud has been renamed to params.Points.");return this.Points}}})}function nf(a,b){return a.distance-b.distance}function ue(a,b,c,d){if(!1!==a.visible&&(a.raycast(b,c),!0===d)){a=a.children;d=0;for(var e=a.length;dc;c++,d++){var e= +c/32*Math.PI*2,f=d/32*Math.PI*2;b.push(Math.cos(e),Math.sin(e),1,Math.cos(f),Math.sin(f),1)}a.addAttribute("position",new z(b,3));b=new U({fog:!1});this.cone=new aa(a,b);this.add(this.cone);this.update()}function rf(a){var b=[];a&&a.isBone&&b.push(a);for(var c=0;ca?-1:0b;b++)a[b]=(16>b?"0":"")+b.toString(16).toUpperCase();return function(){var b=4294967295*Math.random()|0,d=4294967295*Math.random()|0,e=4294967295*Math.random()|0,f=4294967295*Math.random()|0;return a[b&255]+a[b>>8&255]+a[b>>16&255]+a[b>>24&255]+"-"+a[d&255]+a[d>>8&255]+"-"+a[d>>16&15|64]+a[d>>24&255]+"-"+a[e&63|128]+a[e>>8&255]+"-"+a[e>>16&255]+a[e>>24&255]+a[f&255]+a[f>>8&255]+a[f>>16&255]+a[f>>24&255]}}(),clamp:function(a,b,c){return Math.max(b,Math.min(c,a))},euclideanModulo:function(a, +b){return(a%b+b)%b},mapLinear:function(a,b,c,d,e){return d+(a-b)*(e-d)/(c-b)},lerp:function(a,b,c){return(1-c)*a+c*b},smoothstep:function(a,b,c){if(a<=b)return 0;if(a>=c)return 1;a=(a-b)/(c-b);return a*a*(3-2*a)},smootherstep:function(a,b,c){if(a<=b)return 0;if(a>=c)return 1;a=(a-b)/(c-b);return a*a*a*(a*(6*a-15)+10)},randInt:function(a,b){return a+Math.floor(Math.random()*(b-a+1))},randFloat:function(a,b){return a+Math.random()*(b-a)},randFloatSpread:function(a){return a*(.5-Math.random())},degToRad:function(a){return a* +S.DEG2RAD},radToDeg:function(a){return a*S.RAD2DEG},isPowerOfTwo:function(a){return 0===(a&a-1)&&0!==a},ceilPowerOfTwo:function(a){return Math.pow(2,Math.ceil(Math.log(a)/Math.LN2))},floorPowerOfTwo:function(a){return Math.pow(2,Math.floor(Math.log(a)/Math.LN2))}};Object.defineProperties(C.prototype,{width:{get:function(){return this.x},set:function(a){this.x=a}},height:{get:function(){return this.y},set:function(a){this.y=a}}});Object.assign(C.prototype,{isVector2:!0,set:function(a,b){this.x=a;this.y= +b;return this},setScalar:function(a){this.y=this.x=a;return this},setX:function(a){this.x=a;return this},setY:function(a){this.y=a;return this},setComponent:function(a,b){switch(a){case 0:this.x=b;break;case 1:this.y=b;break;default:throw Error("index is out of range: "+a);}return this},getComponent:function(a){switch(a){case 0:return this.x;case 1:return this.y;default:throw Error("index is out of range: "+a);}},clone:function(){return new this.constructor(this.x,this.y)},copy:function(a){this.x= +a.x;this.y=a.y;return this},add:function(a,b){if(void 0!==b)return console.warn("THREE.Vector2: .add() now only accepts one argument. Use .addVectors( a, b ) instead."),this.addVectors(a,b);this.x+=a.x;this.y+=a.y;return this},addScalar:function(a){this.x+=a;this.y+=a;return this},addVectors:function(a,b){this.x=a.x+b.x;this.y=a.y+b.y;return this},addScaledVector:function(a,b){this.x+=a.x*b;this.y+=a.y*b;return this},sub:function(a,b){if(void 0!==b)return console.warn("THREE.Vector2: .sub() now only accepts one argument. Use .subVectors( a, b ) instead."), +this.subVectors(a,b);this.x-=a.x;this.y-=a.y;return this},subScalar:function(a){this.x-=a;this.y-=a;return this},subVectors:function(a,b){this.x=a.x-b.x;this.y=a.y-b.y;return this},multiply:function(a){this.x*=a.x;this.y*=a.y;return this},multiplyScalar:function(a){this.x*=a;this.y*=a;return this},divide:function(a){this.x/=a.x;this.y/=a.y;return this},divideScalar:function(a){return this.multiplyScalar(1/a)},applyMatrix3:function(a){var b=this.x,c=this.y;a=a.elements;this.x=a[0]*b+a[3]*c+a[6];this.y= +a[1]*b+a[4]*c+a[7];return this},min:function(a){this.x=Math.min(this.x,a.x);this.y=Math.min(this.y,a.y);return this},max:function(a){this.x=Math.max(this.x,a.x);this.y=Math.max(this.y,a.y);return this},clamp:function(a,b){this.x=Math.max(a.x,Math.min(b.x,this.x));this.y=Math.max(a.y,Math.min(b.y,this.y));return this},clampScalar:function(){var a=new C,b=new C;return function(c,d){a.set(c,c);b.set(d,d);return this.clamp(a,b)}}(),clampLength:function(a,b){var c=this.length();return this.divideScalar(c|| +1).multiplyScalar(Math.max(a,Math.min(b,c)))},floor:function(){this.x=Math.floor(this.x);this.y=Math.floor(this.y);return this},ceil:function(){this.x=Math.ceil(this.x);this.y=Math.ceil(this.y);return this},round:function(){this.x=Math.round(this.x);this.y=Math.round(this.y);return this},roundToZero:function(){this.x=0>this.x?Math.ceil(this.x):Math.floor(this.x);this.y=0>this.y?Math.ceil(this.y):Math.floor(this.y);return this},negate:function(){this.x=-this.x;this.y=-this.y;return this},dot:function(a){return this.x* +a.x+this.y*a.y},lengthSq:function(){return this.x*this.x+this.y*this.y},length:function(){return Math.sqrt(this.x*this.x+this.y*this.y)},manhattanLength:function(){return Math.abs(this.x)+Math.abs(this.y)},normalize:function(){return this.divideScalar(this.length()||1)},angle:function(){var a=Math.atan2(this.y,this.x);0>a&&(a+=2*Math.PI);return a},distanceTo:function(a){return Math.sqrt(this.distanceToSquared(a))},distanceToSquared:function(a){var b=this.x-a.x;a=this.y-a.y;return b*b+a*a},manhattanDistanceTo:function(a){return Math.abs(this.x- +a.x)+Math.abs(this.y-a.y)},setLength:function(a){return this.normalize().multiplyScalar(a)},lerp:function(a,b){this.x+=(a.x-this.x)*b;this.y+=(a.y-this.y)*b;return this},lerpVectors:function(a,b,c){return this.subVectors(b,a).multiplyScalar(c).add(a)},equals:function(a){return a.x===this.x&&a.y===this.y},fromArray:function(a,b){void 0===b&&(b=0);this.x=a[b];this.y=a[b+1];return this},toArray:function(a,b){void 0===a&&(a=[]);void 0===b&&(b=0);a[b]=this.x;a[b+1]=this.y;return a},fromBufferAttribute:function(a, +b,c){void 0!==c&&console.warn("THREE.Vector2: offset has been removed from .fromBufferAttribute().");this.x=a.getX(b);this.y=a.getY(b);return this},rotateAround:function(a,b){var c=Math.cos(b);b=Math.sin(b);var d=this.x-a.x,e=this.y-a.y;this.x=d*c-e*b+a.x;this.y=d*b+e*c+a.y;return this}});Object.assign(M.prototype,{isMatrix4:!0,set:function(a,b,c,d,e,f,g,h,l,m,k,n,t,r,q,p){var u=this.elements;u[0]=a;u[4]=b;u[8]=c;u[12]=d;u[1]=e;u[5]=f;u[9]=g;u[13]=h;u[2]=l;u[6]=m;u[10]=k;u[14]=n;u[3]=t;u[7]=r;u[11]= +q;u[15]=p;return this},identity:function(){this.set(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1);return this},clone:function(){return(new M).fromArray(this.elements)},copy:function(a){var b=this.elements;a=a.elements;b[0]=a[0];b[1]=a[1];b[2]=a[2];b[3]=a[3];b[4]=a[4];b[5]=a[5];b[6]=a[6];b[7]=a[7];b[8]=a[8];b[9]=a[9];b[10]=a[10];b[11]=a[11];b[12]=a[12];b[13]=a[13];b[14]=a[14];b[15]=a[15];return this},copyPosition:function(a){var b=this.elements;a=a.elements;b[12]=a[12];b[13]=a[13];b[14]=a[14];return this},extractBasis:function(a, +b,c){a.setFromMatrixColumn(this,0);b.setFromMatrixColumn(this,1);c.setFromMatrixColumn(this,2);return this},makeBasis:function(a,b,c){this.set(a.x,b.x,c.x,0,a.y,b.y,c.y,0,a.z,b.z,c.z,0,0,0,0,1);return this},extractRotation:function(){var a=new p;return function(b){var c=this.elements,d=b.elements,e=1/a.setFromMatrixColumn(b,0).length(),f=1/a.setFromMatrixColumn(b,1).length();b=1/a.setFromMatrixColumn(b,2).length();c[0]=d[0]*e;c[1]=d[1]*e;c[2]=d[2]*e;c[4]=d[4]*f;c[5]=d[5]*f;c[6]=d[6]*f;c[8]=d[8]*b; +c[9]=d[9]*b;c[10]=d[10]*b;return this}}(),makeRotationFromEuler:function(a){a&&a.isEuler||console.error("THREE.Matrix4: .makeRotationFromEuler() now expects a Euler rotation rather than a Vector3 and order.");var b=this.elements,c=a.x,d=a.y,e=a.z,f=Math.cos(c);c=Math.sin(c);var g=Math.cos(d);d=Math.sin(d);var h=Math.cos(e);e=Math.sin(e);if("XYZ"===a.order){a=f*h;var l=f*e,m=c*h,k=c*e;b[0]=g*h;b[4]=-g*e;b[8]=d;b[1]=l+m*d;b[5]=a-k*d;b[9]=-c*g;b[2]=k-a*d;b[6]=m+l*d;b[10]=f*g}else"YXZ"===a.order?(a=g* +h,l=g*e,m=d*h,k=d*e,b[0]=a+k*c,b[4]=m*c-l,b[8]=f*d,b[1]=f*e,b[5]=f*h,b[9]=-c,b[2]=l*c-m,b[6]=k+a*c,b[10]=f*g):"ZXY"===a.order?(a=g*h,l=g*e,m=d*h,k=d*e,b[0]=a-k*c,b[4]=-f*e,b[8]=m+l*c,b[1]=l+m*c,b[5]=f*h,b[9]=k-a*c,b[2]=-f*d,b[6]=c,b[10]=f*g):"ZYX"===a.order?(a=f*h,l=f*e,m=c*h,k=c*e,b[0]=g*h,b[4]=m*d-l,b[8]=a*d+k,b[1]=g*e,b[5]=k*d+a,b[9]=l*d-m,b[2]=-d,b[6]=c*g,b[10]=f*g):"YZX"===a.order?(a=f*g,l=f*d,m=c*g,k=c*d,b[0]=g*h,b[4]=k-a*e,b[8]=m*e+l,b[1]=e,b[5]=f*h,b[9]=-c*h,b[2]=-d*h,b[6]=l*e+m,b[10]=a-k* +e):"XZY"===a.order&&(a=f*g,l=f*d,m=c*g,k=c*d,b[0]=g*h,b[4]=-e,b[8]=d*h,b[1]=a*e+k,b[5]=f*h,b[9]=l*e-m,b[2]=m*e-l,b[6]=c*h,b[10]=k*e+a);b[3]=0;b[7]=0;b[11]=0;b[12]=0;b[13]=0;b[14]=0;b[15]=1;return this},makeRotationFromQuaternion:function(a){var b=this.elements,c=a._x,d=a._y,e=a._z,f=a._w,g=c+c,h=d+d,l=e+e;a=c*g;var m=c*h;c*=l;var k=d*h;d*=l;e*=l;g*=f;h*=f;f*=l;b[0]=1-(k+e);b[4]=m-f;b[8]=c+h;b[1]=m+f;b[5]=1-(a+e);b[9]=d-g;b[2]=c-h;b[6]=d+g;b[10]=1-(a+k);b[3]=0;b[7]=0;b[11]=0;b[12]=0;b[13]=0;b[14]= +0;b[15]=1;return this},lookAt:function(){var a=new p,b=new p,c=new p;return function(d,e,f){var g=this.elements;c.subVectors(d,e);0===c.lengthSq()&&(c.z=1);c.normalize();a.crossVectors(f,c);0===a.lengthSq()&&(1===Math.abs(f.z)?c.x+=1E-4:c.z+=1E-4,c.normalize(),a.crossVectors(f,c));a.normalize();b.crossVectors(c,a);g[0]=a.x;g[4]=b.x;g[8]=c.x;g[1]=a.y;g[5]=b.y;g[9]=c.y;g[2]=a.z;g[6]=b.z;g[10]=c.z;return this}}(),multiply:function(a,b){return void 0!==b?(console.warn("THREE.Matrix4: .multiply() now only accepts one argument. Use .multiplyMatrices( a, b ) instead."), +this.multiplyMatrices(a,b)):this.multiplyMatrices(this,a)},premultiply:function(a){return this.multiplyMatrices(a,this)},multiplyMatrices:function(a,b){var c=a.elements,d=b.elements;b=this.elements;a=c[0];var e=c[4],f=c[8],g=c[12],h=c[1],l=c[5],m=c[9],k=c[13],n=c[2],t=c[6],r=c[10],q=c[14],p=c[3],x=c[7],y=c[11];c=c[15];var w=d[0],B=d[4],G=d[8],K=d[12],P=d[1],H=d[5],C=d[9],A=d[13],z=d[2],E=d[6],F=d[10],I=d[14],L=d[3],Q=d[7],N=d[11];d=d[15];b[0]=a*w+e*P+f*z+g*L;b[4]=a*B+e*H+f*E+g*Q;b[8]=a*G+e*C+f*F+ +g*N;b[12]=a*K+e*A+f*I+g*d;b[1]=h*w+l*P+m*z+k*L;b[5]=h*B+l*H+m*E+k*Q;b[9]=h*G+l*C+m*F+k*N;b[13]=h*K+l*A+m*I+k*d;b[2]=n*w+t*P+r*z+q*L;b[6]=n*B+t*H+r*E+q*Q;b[10]=n*G+t*C+r*F+q*N;b[14]=n*K+t*A+r*I+q*d;b[3]=p*w+x*P+y*z+c*L;b[7]=p*B+x*H+y*E+c*Q;b[11]=p*G+x*C+y*F+c*N;b[15]=p*K+x*A+y*I+c*d;return this},multiplyScalar:function(a){var b=this.elements;b[0]*=a;b[4]*=a;b[8]*=a;b[12]*=a;b[1]*=a;b[5]*=a;b[9]*=a;b[13]*=a;b[2]*=a;b[6]*=a;b[10]*=a;b[14]*=a;b[3]*=a;b[7]*=a;b[11]*=a;b[15]*=a;return this},applyToBufferAttribute:function(){var a= +new p;return function(b){for(var c=0,d=b.count;cthis.determinant()&&(g=-g);c.x=f[12];c.y=f[13];c.z=f[14];b.copy(this);c=1/g;f=1/h;var m=1/l;b.elements[0]*=c;b.elements[1]*=c;b.elements[2]*=c;b.elements[4]*=f;b.elements[5]*= +f;b.elements[6]*=f;b.elements[8]*=m;b.elements[9]*=m;b.elements[10]*=m;d.setFromRotationMatrix(b);e.x=g;e.y=h;e.z=l;return this}}(),makePerspective:function(a,b,c,d,e,f){void 0===f&&console.warn("THREE.Matrix4: .makePerspective() has been redefined and has a new signature. Please check the docs.");var g=this.elements;g[0]=2*e/(b-a);g[4]=0;g[8]=(b+a)/(b-a);g[12]=0;g[1]=0;g[5]=2*e/(c-d);g[9]=(c+d)/(c-d);g[13]=0;g[2]=0;g[6]=0;g[10]=-(f+e)/(f-e);g[14]=-2*f*e/(f-e);g[3]=0;g[7]=0;g[11]=-1;g[15]=0;return this}, +makeOrthographic:function(a,b,c,d,e,f){var g=this.elements,h=1/(b-a),l=1/(c-d),m=1/(f-e);g[0]=2*h;g[4]=0;g[8]=0;g[12]=-((b+a)*h);g[1]=0;g[5]=2*l;g[9]=0;g[13]=-((c+d)*l);g[2]=0;g[6]=0;g[10]=-2*m;g[14]=-((f+e)*m);g[3]=0;g[7]=0;g[11]=0;g[15]=1;return this},equals:function(a){var b=this.elements;a=a.elements;for(var c=0;16>c;c++)if(b[c]!==a[c])return!1;return!0},fromArray:function(a,b){void 0===b&&(b=0);for(var c=0;16>c;c++)this.elements[c]=a[c+b];return this},toArray:function(a,b){void 0===a&&(a=[]); +void 0===b&&(b=0);var c=this.elements;a[b]=c[0];a[b+1]=c[1];a[b+2]=c[2];a[b+3]=c[3];a[b+4]=c[4];a[b+5]=c[5];a[b+6]=c[6];a[b+7]=c[7];a[b+8]=c[8];a[b+9]=c[9];a[b+10]=c[10];a[b+11]=c[11];a[b+12]=c[12];a[b+13]=c[13];a[b+14]=c[14];a[b+15]=c[15];return a}});Object.assign(ja,{slerp:function(a,b,c,d){return c.copy(a).slerp(b,d)},slerpFlat:function(a,b,c,d,e,f,g){var h=c[d+0],l=c[d+1],m=c[d+2];c=c[d+3];d=e[f+0];var k=e[f+1],n=e[f+2];e=e[f+3];if(c!==e||h!==d||l!==k||m!==n){f=1-g;var p=h*d+l*k+m*n+c*e,r=0<= +p?1:-1,q=1-p*p;q>Number.EPSILON&&(q=Math.sqrt(q),p=Math.atan2(q,p*r),f=Math.sin(f*p)/q,g=Math.sin(g*p)/q);r*=g;h=h*f+d*r;l=l*f+k*r;m=m*f+n*r;c=c*f+e*r;f===1-g&&(g=1/Math.sqrt(h*h+l*l+m*m+c*c),h*=g,l*=g,m*=g,c*=g)}a[b]=h;a[b+1]=l;a[b+2]=m;a[b+3]=c}});Object.defineProperties(ja.prototype,{x:{get:function(){return this._x},set:function(a){this._x=a;this.onChangeCallback()}},y:{get:function(){return this._y},set:function(a){this._y=a;this.onChangeCallback()}},z:{get:function(){return this._z},set:function(a){this._z= +a;this.onChangeCallback()}},w:{get:function(){return this._w},set:function(a){this._w=a;this.onChangeCallback()}}});Object.assign(ja.prototype,{set:function(a,b,c,d){this._x=a;this._y=b;this._z=c;this._w=d;this.onChangeCallback();return this},clone:function(){return new this.constructor(this._x,this._y,this._z,this._w)},copy:function(a){this._x=a.x;this._y=a.y;this._z=a.z;this._w=a.w;this.onChangeCallback();return this},setFromEuler:function(a,b){if(!a||!a.isEuler)throw Error("THREE.Quaternion: .setFromEuler() now expects an Euler rotation rather than a Vector3 and order."); +var c=a._x,d=a._y,e=a._z;a=a.order;var f=Math.cos,g=Math.sin,h=f(c/2),l=f(d/2);f=f(e/2);c=g(c/2);d=g(d/2);e=g(e/2);"XYZ"===a?(this._x=c*l*f+h*d*e,this._y=h*d*f-c*l*e,this._z=h*l*e+c*d*f,this._w=h*l*f-c*d*e):"YXZ"===a?(this._x=c*l*f+h*d*e,this._y=h*d*f-c*l*e,this._z=h*l*e-c*d*f,this._w=h*l*f+c*d*e):"ZXY"===a?(this._x=c*l*f-h*d*e,this._y=h*d*f+c*l*e,this._z=h*l*e+c*d*f,this._w=h*l*f-c*d*e):"ZYX"===a?(this._x=c*l*f-h*d*e,this._y=h*d*f+c*l*e,this._z=h*l*e-c*d*f,this._w=h*l*f+c*d*e):"YZX"===a?(this._x= +c*l*f+h*d*e,this._y=h*d*f+c*l*e,this._z=h*l*e-c*d*f,this._w=h*l*f-c*d*e):"XZY"===a&&(this._x=c*l*f-h*d*e,this._y=h*d*f-c*l*e,this._z=h*l*e+c*d*f,this._w=h*l*f+c*d*e);if(!1!==b)this.onChangeCallback();return this},setFromAxisAngle:function(a,b){b/=2;var c=Math.sin(b);this._x=a.x*c;this._y=a.y*c;this._z=a.z*c;this._w=Math.cos(b);this.onChangeCallback();return this},setFromRotationMatrix:function(a){var b=a.elements,c=b[0];a=b[4];var d=b[8],e=b[1],f=b[5],g=b[9],h=b[2],l=b[6];b=b[10];var m=c+f+b;0f&&c>b?(c=2*Math.sqrt(1+c-f-b),this._w=(l-g)/c,this._x=.25*c,this._y=(a+e)/c,this._z=(d+h)/c):f>b?(c=2*Math.sqrt(1+f-c-b),this._w=(d-h)/c,this._x=(a+e)/c,this._y=.25*c,this._z=(g+l)/c):(c=2*Math.sqrt(1+b-c-f),this._w=(e-a)/c,this._x=(d+h)/c,this._y=(g+l)/c,this._z=.25*c);this.onChangeCallback();return this},setFromUnitVectors:function(){var a=new p,b;return function(c,d){void 0===a&&(a=new p);b=c.dot(d)+1;1E-6>b? +(b=0,Math.abs(c.x)>Math.abs(c.z)?a.set(-c.y,c.x,0):a.set(0,-c.z,c.y)):a.crossVectors(c,d);this._x=a.x;this._y=a.y;this._z=a.z;this._w=b;return this.normalize()}}(),inverse:function(){return this.conjugate()},conjugate:function(){this._x*=-1;this._y*=-1;this._z*=-1;this.onChangeCallback();return this},dot:function(a){return this._x*a._x+this._y*a._y+this._z*a._z+this._w*a._w},lengthSq:function(){return this._x*this._x+this._y*this._y+this._z*this._z+this._w*this._w},length:function(){return Math.sqrt(this._x* +this._x+this._y*this._y+this._z*this._z+this._w*this._w)},normalize:function(){var a=this.length();0===a?(this._z=this._y=this._x=0,this._w=1):(a=1/a,this._x*=a,this._y*=a,this._z*=a,this._w*=a);this.onChangeCallback();return this},multiply:function(a,b){return void 0!==b?(console.warn("THREE.Quaternion: .multiply() now only accepts one argument. Use .multiplyQuaternions( a, b ) instead."),this.multiplyQuaternions(a,b)):this.multiplyQuaternions(this,a)},premultiply:function(a){return this.multiplyQuaternions(a, +this)},multiplyQuaternions:function(a,b){var c=a._x,d=a._y,e=a._z;a=a._w;var f=b._x,g=b._y,h=b._z;b=b._w;this._x=c*b+a*f+d*h-e*g;this._y=d*b+a*g+e*f-c*h;this._z=e*b+a*h+c*g-d*f;this._w=a*b-c*f-d*g-e*h;this.onChangeCallback();return this},slerp:function(a,b){if(0===b)return this;if(1===b)return this.copy(a);var c=this._x,d=this._y,e=this._z,f=this._w,g=f*a._w+c*a._x+d*a._y+e*a._z;0>g?(this._w=-a._w,this._x=-a._x,this._y=-a._y,this._z=-a._z,g=-g):this.copy(a);if(1<=g)return this._w=f,this._x=c,this._y= +d,this._z=e,this;a=Math.sqrt(1-g*g);if(.001>Math.abs(a))return this._w=.5*(f+this._w),this._x=.5*(c+this._x),this._y=.5*(d+this._y),this._z=.5*(e+this._z),this;var h=Math.atan2(a,g);g=Math.sin((1-b)*h)/a;b=Math.sin(b*h)/a;this._w=f*g+this._w*b;this._x=c*g+this._x*b;this._y=d*g+this._y*b;this._z=e*g+this._z*b;this.onChangeCallback();return this},equals:function(a){return a._x===this._x&&a._y===this._y&&a._z===this._z&&a._w===this._w},fromArray:function(a,b){void 0===b&&(b=0);this._x=a[b];this._y=a[b+ +1];this._z=a[b+2];this._w=a[b+3];this.onChangeCallback();return this},toArray:function(a,b){void 0===a&&(a=[]);void 0===b&&(b=0);a[b]=this._x;a[b+1]=this._y;a[b+2]=this._z;a[b+3]=this._w;return a},onChange:function(a){this.onChangeCallback=a;return this},onChangeCallback:function(){}});Object.assign(p.prototype,{isVector3:!0,set:function(a,b,c){this.x=a;this.y=b;this.z=c;return this},setScalar:function(a){this.z=this.y=this.x=a;return this},setX:function(a){this.x=a;return this},setY:function(a){this.y= +a;return this},setZ:function(a){this.z=a;return this},setComponent:function(a,b){switch(a){case 0:this.x=b;break;case 1:this.y=b;break;case 2:this.z=b;break;default:throw Error("index is out of range: "+a);}return this},getComponent:function(a){switch(a){case 0:return this.x;case 1:return this.y;case 2:return this.z;default:throw Error("index is out of range: "+a);}},clone:function(){return new this.constructor(this.x,this.y,this.z)},copy:function(a){this.x=a.x;this.y=a.y;this.z=a.z;return this}, +add:function(a,b){if(void 0!==b)return console.warn("THREE.Vector3: .add() now only accepts one argument. Use .addVectors( a, b ) instead."),this.addVectors(a,b);this.x+=a.x;this.y+=a.y;this.z+=a.z;return this},addScalar:function(a){this.x+=a;this.y+=a;this.z+=a;return this},addVectors:function(a,b){this.x=a.x+b.x;this.y=a.y+b.y;this.z=a.z+b.z;return this},addScaledVector:function(a,b){this.x+=a.x*b;this.y+=a.y*b;this.z+=a.z*b;return this},sub:function(a,b){if(void 0!==b)return console.warn("THREE.Vector3: .sub() now only accepts one argument. Use .subVectors( a, b ) instead."), +this.subVectors(a,b);this.x-=a.x;this.y-=a.y;this.z-=a.z;return this},subScalar:function(a){this.x-=a;this.y-=a;this.z-=a;return this},subVectors:function(a,b){this.x=a.x-b.x;this.y=a.y-b.y;this.z=a.z-b.z;return this},multiply:function(a,b){if(void 0!==b)return console.warn("THREE.Vector3: .multiply() now only accepts one argument. Use .multiplyVectors( a, b ) instead."),this.multiplyVectors(a,b);this.x*=a.x;this.y*=a.y;this.z*=a.z;return this},multiplyScalar:function(a){this.x*=a;this.y*=a;this.z*= +a;return this},multiplyVectors:function(a,b){this.x=a.x*b.x;this.y=a.y*b.y;this.z=a.z*b.z;return this},applyEuler:function(){var a=new ja;return function(b){b&&b.isEuler||console.error("THREE.Vector3: .applyEuler() now expects an Euler rotation rather than a Vector3 and order.");return this.applyQuaternion(a.setFromEuler(b))}}(),applyAxisAngle:function(){var a=new ja;return function(b,c){return this.applyQuaternion(a.setFromAxisAngle(b,c))}}(),applyMatrix3:function(a){var b=this.x,c=this.y,d=this.z; +a=a.elements;this.x=a[0]*b+a[3]*c+a[6]*d;this.y=a[1]*b+a[4]*c+a[7]*d;this.z=a[2]*b+a[5]*c+a[8]*d;return this},applyMatrix4:function(a){var b=this.x,c=this.y,d=this.z;a=a.elements;var e=1/(a[3]*b+a[7]*c+a[11]*d+a[15]);this.x=(a[0]*b+a[4]*c+a[8]*d+a[12])*e;this.y=(a[1]*b+a[5]*c+a[9]*d+a[13])*e;this.z=(a[2]*b+a[6]*c+a[10]*d+a[14])*e;return this},applyQuaternion:function(a){var b=this.x,c=this.y,d=this.z,e=a.x,f=a.y,g=a.z;a=a.w;var h=a*b+f*d-g*c,l=a*c+g*b-e*d,m=a*d+e*c-f*b;b=-e*b-f*c-g*d;this.x=h*a+b* +-e+l*-g-m*-f;this.y=l*a+b*-f+m*-e-h*-g;this.z=m*a+b*-g+h*-f-l*-e;return this},project:function(){var a=new M;return function(b){a.multiplyMatrices(b.projectionMatrix,a.getInverse(b.matrixWorld));return this.applyMatrix4(a)}}(),unproject:function(){var a=new M;return function(b){a.multiplyMatrices(b.matrixWorld,a.getInverse(b.projectionMatrix));return this.applyMatrix4(a)}}(),transformDirection:function(a){var b=this.x,c=this.y,d=this.z;a=a.elements;this.x=a[0]*b+a[4]*c+a[8]*d;this.y=a[1]*b+a[5]*c+ +a[9]*d;this.z=a[2]*b+a[6]*c+a[10]*d;return this.normalize()},divide:function(a){this.x/=a.x;this.y/=a.y;this.z/=a.z;return this},divideScalar:function(a){return this.multiplyScalar(1/a)},min:function(a){this.x=Math.min(this.x,a.x);this.y=Math.min(this.y,a.y);this.z=Math.min(this.z,a.z);return this},max:function(a){this.x=Math.max(this.x,a.x);this.y=Math.max(this.y,a.y);this.z=Math.max(this.z,a.z);return this},clamp:function(a,b){this.x=Math.max(a.x,Math.min(b.x,this.x));this.y=Math.max(a.y,Math.min(b.y, +this.y));this.z=Math.max(a.z,Math.min(b.z,this.z));return this},clampScalar:function(){var a=new p,b=new p;return function(c,d){a.set(c,c,c);b.set(d,d,d);return this.clamp(a,b)}}(),clampLength:function(a,b){var c=this.length();return this.divideScalar(c||1).multiplyScalar(Math.max(a,Math.min(b,c)))},floor:function(){this.x=Math.floor(this.x);this.y=Math.floor(this.y);this.z=Math.floor(this.z);return this},ceil:function(){this.x=Math.ceil(this.x);this.y=Math.ceil(this.y);this.z=Math.ceil(this.z);return this}, +round:function(){this.x=Math.round(this.x);this.y=Math.round(this.y);this.z=Math.round(this.z);return this},roundToZero:function(){this.x=0>this.x?Math.ceil(this.x):Math.floor(this.x);this.y=0>this.y?Math.ceil(this.y):Math.floor(this.y);this.z=0>this.z?Math.ceil(this.z):Math.floor(this.z);return this},negate:function(){this.x=-this.x;this.y=-this.y;this.z=-this.z;return this},dot:function(a){return this.x*a.x+this.y*a.y+this.z*a.z},lengthSq:function(){return this.x*this.x+this.y*this.y+this.z*this.z}, +length:function(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z)},manhattanLength:function(){return Math.abs(this.x)+Math.abs(this.y)+Math.abs(this.z)},normalize:function(){return this.divideScalar(this.length()||1)},setLength:function(a){return this.normalize().multiplyScalar(a)},lerp:function(a,b){this.x+=(a.x-this.x)*b;this.y+=(a.y-this.y)*b;this.z+=(a.z-this.z)*b;return this},lerpVectors:function(a,b,c){return this.subVectors(b,a).multiplyScalar(c).add(a)},cross:function(a,b){return void 0!== +b?(console.warn("THREE.Vector3: .cross() now only accepts one argument. Use .crossVectors( a, b ) instead."),this.crossVectors(a,b)):this.crossVectors(this,a)},crossVectors:function(a,b){var c=a.x,d=a.y;a=a.z;var e=b.x,f=b.y;b=b.z;this.x=d*b-a*f;this.y=a*e-c*b;this.z=c*f-d*e;return this},projectOnVector:function(a){var b=a.dot(this)/a.lengthSq();return this.copy(a).multiplyScalar(b)},projectOnPlane:function(){var a=new p;return function(b){a.copy(this).projectOnVector(b);return this.sub(a)}}(),reflect:function(){var a= +new p;return function(b){return this.sub(a.copy(b).multiplyScalar(2*this.dot(b)))}}(),angleTo:function(a){a=this.dot(a)/Math.sqrt(this.lengthSq()*a.lengthSq());return Math.acos(S.clamp(a,-1,1))},distanceTo:function(a){return Math.sqrt(this.distanceToSquared(a))},distanceToSquared:function(a){var b=this.x-a.x,c=this.y-a.y;a=this.z-a.z;return b*b+c*c+a*a},manhattanDistanceTo:function(a){return Math.abs(this.x-a.x)+Math.abs(this.y-a.y)+Math.abs(this.z-a.z)},setFromSpherical:function(a){var b=Math.sin(a.phi)* +a.radius;this.x=b*Math.sin(a.theta);this.y=Math.cos(a.phi)*a.radius;this.z=b*Math.cos(a.theta);return this},setFromCylindrical:function(a){this.x=a.radius*Math.sin(a.theta);this.y=a.y;this.z=a.radius*Math.cos(a.theta);return this},setFromMatrixPosition:function(a){a=a.elements;this.x=a[12];this.y=a[13];this.z=a[14];return this},setFromMatrixScale:function(a){var b=this.setFromMatrixColumn(a,0).length(),c=this.setFromMatrixColumn(a,1).length();a=this.setFromMatrixColumn(a,2).length();this.x=b;this.y= +c;this.z=a;return this},setFromMatrixColumn:function(a,b){return this.fromArray(a.elements,4*b)},equals:function(a){return a.x===this.x&&a.y===this.y&&a.z===this.z},fromArray:function(a,b){void 0===b&&(b=0);this.x=a[b];this.y=a[b+1];this.z=a[b+2];return this},toArray:function(a,b){void 0===a&&(a=[]);void 0===b&&(b=0);a[b]=this.x;a[b+1]=this.y;a[b+2]=this.z;return a},fromBufferAttribute:function(a,b,c){void 0!==c&&console.warn("THREE.Vector3: offset has been removed from .fromBufferAttribute()."); +this.x=a.getX(b);this.y=a.getY(b);this.z=a.getZ(b);return this}});Object.assign(sa.prototype,{isMatrix3:!0,set:function(a,b,c,d,e,f,g,h,l){var m=this.elements;m[0]=a;m[1]=d;m[2]=g;m[3]=b;m[4]=e;m[5]=h;m[6]=c;m[7]=f;m[8]=l;return this},identity:function(){this.set(1,0,0,0,1,0,0,0,1);return this},clone:function(){return(new this.constructor).fromArray(this.elements)},copy:function(a){var b=this.elements;a=a.elements;b[0]=a[0];b[1]=a[1];b[2]=a[2];b[3]=a[3];b[4]=a[4];b[5]=a[5];b[6]=a[6];b[7]=a[7];b[8]= +a[8];return this},setFromMatrix4:function(a){a=a.elements;this.set(a[0],a[4],a[8],a[1],a[5],a[9],a[2],a[6],a[10]);return this},applyToBufferAttribute:function(){var a=new p;return function(b){for(var c=0,d=b.count;cc;c++)if(b[c]!==a[c])return!1;return!0},fromArray:function(a,b){void 0===b&&(b=0);for(var c=0;9>c;c++)this.elements[c]=a[c+b];return this},toArray:function(a,b){void 0===a&&(a=[]);void 0===b&&(b=0);var c=this.elements;a[b]=c[0];a[b+1]=c[1];a[b+2]=c[2];a[b+3]=c[3];a[b+4]=c[4];a[b+5]=c[5];a[b+6]=c[6];a[b+7]=c[7];a[b+8]=c[8]; +return a}});var xf=0;Y.DEFAULT_IMAGE=void 0;Y.DEFAULT_MAPPING=300;Y.prototype=Object.assign(Object.create(xa.prototype),{constructor:Y,isTexture:!0,clone:function(){return(new this.constructor).copy(this)},copy:function(a){this.name=a.name;this.image=a.image;this.mipmaps=a.mipmaps.slice(0);this.mapping=a.mapping;this.wrapS=a.wrapS;this.wrapT=a.wrapT;this.magFilter=a.magFilter;this.minFilter=a.minFilter;this.anisotropy=a.anisotropy;this.format=a.format;this.type=a.type;this.offset.copy(a.offset);this.repeat.copy(a.repeat); +this.center.copy(a.center);this.rotation=a.rotation;this.matrixAutoUpdate=a.matrixAutoUpdate;this.matrix.copy(a.matrix);this.generateMipmaps=a.generateMipmaps;this.premultiplyAlpha=a.premultiplyAlpha;this.flipY=a.flipY;this.unpackAlignment=a.unpackAlignment;this.encoding=a.encoding;return this},toJSON:function(a){var b=void 0===a||"string"===typeof a;if(!b&&void 0!==a.textures[this.uuid])return a.textures[this.uuid];var c={metadata:{version:4.5,type:"Texture",generator:"Texture.toJSON"},uuid:this.uuid, +name:this.name,mapping:this.mapping,repeat:[this.repeat.x,this.repeat.y],offset:[this.offset.x,this.offset.y],center:[this.center.x,this.center.y],rotation:this.rotation,wrap:[this.wrapS,this.wrapT],format:this.format,minFilter:this.minFilter,magFilter:this.magFilter,anisotropy:this.anisotropy,flipY:this.flipY};if(void 0!==this.image){var d=this.image;void 0===d.uuid&&(d.uuid=S.generateUUID());if(!b&&void 0===a.images[d.uuid]){var e=a.images,f=d.uuid,g=d.uuid;if(d instanceof HTMLCanvasElement)var h= +d;else{h=document.createElementNS("http://www.w3.org/1999/xhtml","canvas");h.width=d.width;h.height=d.height;var l=h.getContext("2d");d instanceof ImageData?l.putImageData(d,0,0):l.drawImage(d,0,0,d.width,d.height)}h=2048a.x||1a.x?0:1;break;case 1002:a.x=1===Math.abs(Math.floor(a.x)%2)?Math.ceil(a.x)-a.x:a.x-Math.floor(a.x)}if(0>a.y||1a.y?0:1;break;case 1002:a.y=1===Math.abs(Math.floor(a.y)%2)?Math.ceil(a.y)-a.y:a.y-Math.floor(a.y)}this.flipY&&(a.y=1-a.y)}}});Object.defineProperty(Y.prototype,"needsUpdate",{set:function(a){!0===a&&this.version++}});Object.assign(ea.prototype, +{isVector4:!0,set:function(a,b,c,d){this.x=a;this.y=b;this.z=c;this.w=d;return this},setScalar:function(a){this.w=this.z=this.y=this.x=a;return this},setX:function(a){this.x=a;return this},setY:function(a){this.y=a;return this},setZ:function(a){this.z=a;return this},setW:function(a){this.w=a;return this},setComponent:function(a,b){switch(a){case 0:this.x=b;break;case 1:this.y=b;break;case 2:this.z=b;break;case 3:this.w=b;break;default:throw Error("index is out of range: "+a);}return this},getComponent:function(a){switch(a){case 0:return this.x; +case 1:return this.y;case 2:return this.z;case 3:return this.w;default:throw Error("index is out of range: "+a);}},clone:function(){return new this.constructor(this.x,this.y,this.z,this.w)},copy:function(a){this.x=a.x;this.y=a.y;this.z=a.z;this.w=void 0!==a.w?a.w:1;return this},add:function(a,b){if(void 0!==b)return console.warn("THREE.Vector4: .add() now only accepts one argument. Use .addVectors( a, b ) instead."),this.addVectors(a,b);this.x+=a.x;this.y+=a.y;this.z+=a.z;this.w+=a.w;return this}, +addScalar:function(a){this.x+=a;this.y+=a;this.z+=a;this.w+=a;return this},addVectors:function(a,b){this.x=a.x+b.x;this.y=a.y+b.y;this.z=a.z+b.z;this.w=a.w+b.w;return this},addScaledVector:function(a,b){this.x+=a.x*b;this.y+=a.y*b;this.z+=a.z*b;this.w+=a.w*b;return this},sub:function(a,b){if(void 0!==b)return console.warn("THREE.Vector4: .sub() now only accepts one argument. Use .subVectors( a, b ) instead."),this.subVectors(a,b);this.x-=a.x;this.y-=a.y;this.z-=a.z;this.w-=a.w;return this},subScalar:function(a){this.x-= +a;this.y-=a;this.z-=a;this.w-=a;return this},subVectors:function(a,b){this.x=a.x-b.x;this.y=a.y-b.y;this.z=a.z-b.z;this.w=a.w-b.w;return this},multiplyScalar:function(a){this.x*=a;this.y*=a;this.z*=a;this.w*=a;return this},applyMatrix4:function(a){var b=this.x,c=this.y,d=this.z,e=this.w;a=a.elements;this.x=a[0]*b+a[4]*c+a[8]*d+a[12]*e;this.y=a[1]*b+a[5]*c+a[9]*d+a[13]*e;this.z=a[2]*b+a[6]*c+a[10]*d+a[14]*e;this.w=a[3]*b+a[7]*c+a[11]*d+a[15]*e;return this},divideScalar:function(a){return this.multiplyScalar(1/ +a)},setAxisAngleFromQuaternion:function(a){this.w=2*Math.acos(a.w);var b=Math.sqrt(1-a.w*a.w);1E-4>b?(this.x=1,this.z=this.y=0):(this.x=a.x/b,this.y=a.y/b,this.z=a.z/b);return this},setAxisAngleFromRotationMatrix:function(a){a=a.elements;var b=a[0];var c=a[4];var d=a[8],e=a[1],f=a[5],g=a[9];var h=a[2];var l=a[6];var m=a[10];if(.01>Math.abs(c-e)&&.01>Math.abs(d-h)&&.01>Math.abs(g-l)){if(.1>Math.abs(c+e)&&.1>Math.abs(d+h)&&.1>Math.abs(g+l)&&.1>Math.abs(b+f+m-3))return this.set(1,0,0,0),this;a=Math.PI; +b=(b+1)/2;f=(f+1)/2;m=(m+1)/2;c=(c+e)/4;d=(d+h)/4;g=(g+l)/4;b>f&&b>m?.01>b?(l=0,c=h=.707106781):(l=Math.sqrt(b),h=c/l,c=d/l):f>m?.01>f?(l=.707106781,h=0,c=.707106781):(h=Math.sqrt(f),l=c/h,c=g/h):.01>m?(h=l=.707106781,c=0):(c=Math.sqrt(m),l=d/c,h=g/c);this.set(l,h,c,a);return this}a=Math.sqrt((l-g)*(l-g)+(d-h)*(d-h)+(e-c)*(e-c));.001>Math.abs(a)&&(a=1);this.x=(l-g)/a;this.y=(d-h)/a;this.z=(e-c)/a;this.w=Math.acos((b+f+m-1)/2);return this},min:function(a){this.x=Math.min(this.x,a.x);this.y=Math.min(this.y, +a.y);this.z=Math.min(this.z,a.z);this.w=Math.min(this.w,a.w);return this},max:function(a){this.x=Math.max(this.x,a.x);this.y=Math.max(this.y,a.y);this.z=Math.max(this.z,a.z);this.w=Math.max(this.w,a.w);return this},clamp:function(a,b){this.x=Math.max(a.x,Math.min(b.x,this.x));this.y=Math.max(a.y,Math.min(b.y,this.y));this.z=Math.max(a.z,Math.min(b.z,this.z));this.w=Math.max(a.w,Math.min(b.w,this.w));return this},clampScalar:function(){var a,b;return function(c,d){void 0===a&&(a=new ea,b=new ea);a.set(c, +c,c,c);b.set(d,d,d,d);return this.clamp(a,b)}}(),clampLength:function(a,b){var c=this.length();return this.divideScalar(c||1).multiplyScalar(Math.max(a,Math.min(b,c)))},floor:function(){this.x=Math.floor(this.x);this.y=Math.floor(this.y);this.z=Math.floor(this.z);this.w=Math.floor(this.w);return this},ceil:function(){this.x=Math.ceil(this.x);this.y=Math.ceil(this.y);this.z=Math.ceil(this.z);this.w=Math.ceil(this.w);return this},round:function(){this.x=Math.round(this.x);this.y=Math.round(this.y); +this.z=Math.round(this.z);this.w=Math.round(this.w);return this},roundToZero:function(){this.x=0>this.x?Math.ceil(this.x):Math.floor(this.x);this.y=0>this.y?Math.ceil(this.y):Math.floor(this.y);this.z=0>this.z?Math.ceil(this.z):Math.floor(this.z);this.w=0>this.w?Math.ceil(this.w):Math.floor(this.w);return this},negate:function(){this.x=-this.x;this.y=-this.y;this.z=-this.z;this.w=-this.w;return this},dot:function(a){return this.x*a.x+this.y*a.y+this.z*a.z+this.w*a.w},lengthSq:function(){return this.x* +this.x+this.y*this.y+this.z*this.z+this.w*this.w},length:function(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z+this.w*this.w)},manhattanLength:function(){return Math.abs(this.x)+Math.abs(this.y)+Math.abs(this.z)+Math.abs(this.w)},normalize:function(){return this.divideScalar(this.length()||1)},setLength:function(a){return this.normalize().multiplyScalar(a)},lerp:function(a,b){this.x+=(a.x-this.x)*b;this.y+=(a.y-this.y)*b;this.z+=(a.z-this.z)*b;this.w+=(a.w-this.w)*b;return this},lerpVectors:function(a, +b,c){return this.subVectors(b,a).multiplyScalar(c).add(a)},equals:function(a){return a.x===this.x&&a.y===this.y&&a.z===this.z&&a.w===this.w},fromArray:function(a,b){void 0===b&&(b=0);this.x=a[b];this.y=a[b+1];this.z=a[b+2];this.w=a[b+3];return this},toArray:function(a,b){void 0===a&&(a=[]);void 0===b&&(b=0);a[b]=this.x;a[b+1]=this.y;a[b+2]=this.z;a[b+3]=this.w;return a},fromBufferAttribute:function(a,b,c){void 0!==c&&console.warn("THREE.Vector4: offset has been removed from .fromBufferAttribute()."); +this.x=a.getX(b);this.y=a.getY(b);this.z=a.getZ(b);this.w=a.getW(b);return this}});hb.prototype=Object.assign(Object.create(xa.prototype),{constructor:hb,isWebGLRenderTarget:!0,setSize:function(a,b){if(this.width!==a||this.height!==b)this.width=a,this.height=b,this.dispose();this.viewport.set(0,0,a,b);this.scissor.set(0,0,a,b)},clone:function(){return(new this.constructor).copy(this)},copy:function(a){this.width=a.width;this.height=a.height;this.viewport.copy(a.viewport);this.texture=a.texture.clone(); +this.depthBuffer=a.depthBuffer;this.stencilBuffer=a.stencilBuffer;this.depthTexture=a.depthTexture;return this},dispose:function(){this.dispatchEvent({type:"dispose"})}});Ib.prototype=Object.create(hb.prototype);Ib.prototype.constructor=Ib;Ib.prototype.isWebGLRenderTargetCube=!0;ib.prototype=Object.create(Y.prototype);ib.prototype.constructor=ib;ib.prototype.isDataTexture=!0;Object.assign(Va.prototype,{isBox3:!0,set:function(a,b){this.min.copy(a);this.max.copy(b);return this},setFromArray:function(a){for(var b= +Infinity,c=Infinity,d=Infinity,e=-Infinity,f=-Infinity,g=-Infinity,h=0,l=a.length;he&&(e=m);k>f&&(f=k);n>g&&(g=n)}this.min.set(b,c,d);this.max.set(e,f,g);return this},setFromBufferAttribute:function(a){for(var b=Infinity,c=Infinity,d=Infinity,e=-Infinity,f=-Infinity,g=-Infinity,h=0,l=a.count;he&&(e=m);k>f&&(f=k);n>g&&(g=n)}this.min.set(b,c,d); +this.max.set(e,f,g);return this},setFromPoints:function(a){this.makeEmpty();for(var b=0,c=a.length;bthis.max.x||a.ythis.max.y||a.zthis.max.z?!1:!0},containsBox:function(a){return this.min.x<=a.min.x&&a.max.x<=this.max.x&&this.min.y<=a.min.y&&a.max.y<=this.max.y&&this.min.z<=a.min.z&& +a.max.z<=this.max.z},getParameter:function(a,b){void 0===b&&(console.warn("THREE.Box3: .getParameter() target is now required"),b=new p);return b.set((a.x-this.min.x)/(this.max.x-this.min.x),(a.y-this.min.y)/(this.max.y-this.min.y),(a.z-this.min.z)/(this.max.z-this.min.z))},intersectsBox:function(a){return a.max.xthis.max.x||a.max.ythis.max.y||a.max.zthis.max.z?!1:!0},intersectsSphere:function(){var a=new p;return function(b){this.clampPoint(b.center, +a);return a.distanceToSquared(b.center)<=b.radius*b.radius}}(),intersectsPlane:function(a){if(0=a.constant},intersectsTriangle:function(){function a(a){var e; +var f=0;for(e=a.length-3;f<=e;f+=3){h.fromArray(a,f);var g=m.x*Math.abs(h.x)+m.y*Math.abs(h.y)+m.z*Math.abs(h.z),l=b.dot(h),k=c.dot(h),n=d.dot(h);if(Math.max(-Math.max(l,k,n),Math.min(l,k,n))>g)return!1}return!0}var b=new p,c=new p,d=new p,e=new p,f=new p,g=new p,h=new p,l=new p,m=new p,k=new p;return function(h){if(this.isEmpty())return!1;this.getCenter(l);m.subVectors(this.max,l);b.subVectors(h.a,l);c.subVectors(h.b,l);d.subVectors(h.c,l);e.subVectors(c,b);f.subVectors(d,c);g.subVectors(b,d);h= +[0,-e.z,e.y,0,-f.z,f.y,0,-g.z,g.y,e.z,0,-e.x,f.z,0,-f.x,g.z,0,-g.x,-e.y,e.x,0,-f.y,f.x,0,-g.y,g.x,0];if(!a(h))return!1;h=[1,0,0,0,1,0,0,0,1];if(!a(h))return!1;k.crossVectors(e,f);h=[k.x,k.y,k.z];return a(h)}}(),clampPoint:function(a,b){void 0===b&&(console.warn("THREE.Box3: .clampPoint() target is now required"),b=new p);return b.copy(a).clamp(this.min,this.max)},distanceToPoint:function(){var a=new p;return function(b){return a.copy(b).clamp(this.min,this.max).sub(b).length()}}(),getBoundingSphere:function(){var a= +new p;return function(b){void 0===b&&(console.warn("THREE.Box3: .getBoundingSphere() target is now required"),b=new Ea);this.getCenter(b.center);b.radius=.5*this.getSize(a).length();return b}}(),intersect:function(a){this.min.max(a.min);this.max.min(a.max);this.isEmpty()&&this.makeEmpty();return this},union:function(a){this.min.min(a.min);this.max.max(a.max);return this},applyMatrix4:function(){var a=[new p,new p,new p,new p,new p,new p,new p,new p];return function(b){if(this.isEmpty())return this; +a[0].set(this.min.x,this.min.y,this.min.z).applyMatrix4(b);a[1].set(this.min.x,this.min.y,this.max.z).applyMatrix4(b);a[2].set(this.min.x,this.max.y,this.min.z).applyMatrix4(b);a[3].set(this.min.x,this.max.y,this.max.z).applyMatrix4(b);a[4].set(this.max.x,this.min.y,this.min.z).applyMatrix4(b);a[5].set(this.max.x,this.min.y,this.max.z).applyMatrix4(b);a[6].set(this.max.x,this.max.y,this.min.z).applyMatrix4(b);a[7].set(this.max.x,this.max.y,this.max.z).applyMatrix4(b);this.setFromPoints(a);return this}}(), +translate:function(a){this.min.add(a);this.max.add(a);return this},equals:function(a){return a.min.equals(this.min)&&a.max.equals(this.max)}});Object.assign(Ea.prototype,{set:function(a,b){this.center.copy(a);this.radius=b;return this},setFromPoints:function(){var a=new Va;return function(b,c){var d=this.center;void 0!==c?d.copy(c):a.setFromPoints(b).getCenter(d);for(var e=c=0,f=b.length;e=this.radius},containsPoint:function(a){return a.distanceToSquared(this.center)<=this.radius*this.radius},distanceToPoint:function(a){return a.distanceTo(this.center)-this.radius},intersectsSphere:function(a){var b=this.radius+a.radius;return a.center.distanceToSquared(this.center)<=b*b},intersectsBox:function(a){return a.intersectsSphere(this)},intersectsPlane:function(a){return Math.abs(a.distanceToPoint(this.center))<= +this.radius},clampPoint:function(a,b){var c=this.center.distanceToSquared(a);void 0===b&&(console.warn("THREE.Sphere: .clampPoint() target is now required"),b=new p);b.copy(a);c>this.radius*this.radius&&(b.sub(this.center).normalize(),b.multiplyScalar(this.radius).add(this.center));return b},getBoundingBox:function(a){void 0===a&&(console.warn("THREE.Sphere: .getBoundingBox() target is now required"),a=new Va);a.set(this.center,this.center);a.expandByScalar(this.radius);return a},applyMatrix4:function(a){this.center.applyMatrix4(a); +this.radius*=a.getMaxScaleOnAxis();return this},translate:function(a){this.center.add(a);return this},equals:function(a){return a.center.equals(this.center)&&a.radius===this.radius}});Object.assign(Fa.prototype,{set:function(a,b){this.normal.copy(a);this.constant=b;return this},setComponents:function(a,b,c,d){this.normal.set(a,b,c);this.constant=d;return this},setFromNormalAndCoplanarPoint:function(a,b){this.normal.copy(a);this.constant=-b.dot(this.normal);return this},setFromCoplanarPoints:function(){var a= +new p,b=new p;return function(c,d,e){d=a.subVectors(e,d).cross(b.subVectors(c,d)).normalize();this.setFromNormalAndCoplanarPoint(d,c);return this}}(),clone:function(){return(new this.constructor).copy(this)},copy:function(a){this.normal.copy(a.normal);this.constant=a.constant;return this},normalize:function(){var a=1/this.normal.length();this.normal.multiplyScalar(a);this.constant*=a;return this},negate:function(){this.constant*=-1;this.normal.negate();return this},distanceToPoint:function(a){return this.normal.dot(a)+ +this.constant},distanceToSphere:function(a){return this.distanceToPoint(a.center)-a.radius},projectPoint:function(a,b){void 0===b&&(console.warn("THREE.Plane: .projectPoint() target is now required"),b=new p);return b.copy(this.normal).multiplyScalar(-this.distanceToPoint(a)).add(a)},intersectLine:function(){var a=new p;return function(b,c){void 0===c&&(console.warn("THREE.Plane: .intersectLine() target is now required"),c=new p);var d=b.delta(a),e=this.normal.dot(d);if(0===e){if(0===this.distanceToPoint(b.start))return c.copy(b.start)}else if(e= +-(b.start.dot(this.normal)+this.constant)/e,!(0>e||1b&&0a&&0c;c++)b[c].copy(a.planes[c]);return this},setFromMatrix:function(a){var b=this.planes,c=a.elements;a=c[0];var d=c[1],e=c[2],f=c[3],g=c[4],h=c[5],l=c[6],m=c[7],k=c[8],n=c[9],p=c[10],r=c[11],q=c[12],v=c[13],x=c[14];c=c[15];b[0].setComponents(f-a,m-g,r-k,c-q).normalize();b[1].setComponents(f+a,m+g,r+k,c+q).normalize();b[2].setComponents(f+d,m+h,r+n,c+v).normalize();b[3].setComponents(f- +d,m-h,r-n,c-v).normalize();b[4].setComponents(f-e,m-l,r-p,c-x).normalize();b[5].setComponents(f+e,m+l,r+p,c+x).normalize();return this},intersectsObject:function(){var a=new Ea;return function(b){var c=b.geometry;null===c.boundingSphere&&c.computeBoundingSphere();a.copy(c.boundingSphere).applyMatrix4(b.matrixWorld);return this.intersectsSphere(a)}}(),intersectsSprite:function(){var a=new Ea;return function(b){a.center.set(0,0,0);a.radius=.7071067811865476;a.applyMatrix4(b.matrixWorld);return this.intersectsSphere(a)}}(), +intersectsSphere:function(a){var b=this.planes,c=a.center;a=-a.radius;for(var d=0;6>d;d++)if(b[d].distanceToPoint(c)e;e++){var f=d[e];a.x=0 +g&&0>f)return!1}return!0}}(),containsPoint:function(a){for(var b=this.planes,c=0;6>c;c++)if(0>b[c].distanceToPoint(a))return!1;return!0}});var V={alphamap_fragment:"#ifdef USE_ALPHAMAP\n\tdiffuseColor.a *= texture2D( alphaMap, vUv ).g;\n#endif\n",alphamap_pars_fragment:"#ifdef USE_ALPHAMAP\n\tuniform sampler2D alphaMap;\n#endif\n",alphatest_fragment:"#ifdef ALPHATEST\n\tif ( diffuseColor.a < ALPHATEST ) discard;\n#endif\n",aomap_fragment:"#ifdef USE_AOMAP\n\tfloat ambientOcclusion = ( texture2D( aoMap, vUv2 ).r - 1.0 ) * aoMapIntensity + 1.0;\n\treflectedLight.indirectDiffuse *= ambientOcclusion;\n\t#if defined( USE_ENVMAP ) && defined( PHYSICAL )\n\t\tfloat dotNV = saturate( dot( geometry.normal, geometry.viewDir ) );\n\t\treflectedLight.indirectSpecular *= computeSpecularOcclusion( dotNV, ambientOcclusion, material.specularRoughness );\n\t#endif\n#endif\n", +aomap_pars_fragment:"#ifdef USE_AOMAP\n\tuniform sampler2D aoMap;\n\tuniform float aoMapIntensity;\n#endif",begin_vertex:"\nvec3 transformed = vec3( position );\n",beginnormal_vertex:"\nvec3 objectNormal = vec3( normal );\n",bsdfs:"float punctualLightIntensityToIrradianceFactor( const in float lightDistance, const in float cutoffDistance, const in float decayExponent ) {\n\tif( decayExponent > 0.0 ) {\n#if defined ( PHYSICALLY_CORRECT_LIGHTS )\n\t\tfloat distanceFalloff = 1.0 / max( pow( lightDistance, decayExponent ), 0.01 );\n\t\tfloat maxDistanceCutoffFactor = pow2( saturate( 1.0 - pow4( lightDistance / cutoffDistance ) ) );\n\t\treturn distanceFalloff * maxDistanceCutoffFactor;\n#else\n\t\treturn pow( saturate( -lightDistance / cutoffDistance + 1.0 ), decayExponent );\n#endif\n\t}\n\treturn 1.0;\n}\nvec3 BRDF_Diffuse_Lambert( const in vec3 diffuseColor ) {\n\treturn RECIPROCAL_PI * diffuseColor;\n}\nvec3 F_Schlick( const in vec3 specularColor, const in float dotLH ) {\n\tfloat fresnel = exp2( ( -5.55473 * dotLH - 6.98316 ) * dotLH );\n\treturn ( 1.0 - specularColor ) * fresnel + specularColor;\n}\nfloat G_GGX_Smith( const in float alpha, const in float dotNL, const in float dotNV ) {\n\tfloat a2 = pow2( alpha );\n\tfloat gl = dotNL + sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNL ) );\n\tfloat gv = dotNV + sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNV ) );\n\treturn 1.0 / ( gl * gv );\n}\nfloat G_GGX_SmithCorrelated( const in float alpha, const in float dotNL, const in float dotNV ) {\n\tfloat a2 = pow2( alpha );\n\tfloat gv = dotNL * sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNV ) );\n\tfloat gl = dotNV * sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNL ) );\n\treturn 0.5 / max( gv + gl, EPSILON );\n}\nfloat D_GGX( const in float alpha, const in float dotNH ) {\n\tfloat a2 = pow2( alpha );\n\tfloat denom = pow2( dotNH ) * ( a2 - 1.0 ) + 1.0;\n\treturn RECIPROCAL_PI * a2 / pow2( denom );\n}\nvec3 BRDF_Specular_GGX( const in IncidentLight incidentLight, const in GeometricContext geometry, const in vec3 specularColor, const in float roughness ) {\n\tfloat alpha = pow2( roughness );\n\tvec3 halfDir = normalize( incidentLight.direction + geometry.viewDir );\n\tfloat dotNL = saturate( dot( geometry.normal, incidentLight.direction ) );\n\tfloat dotNV = saturate( dot( geometry.normal, geometry.viewDir ) );\n\tfloat dotNH = saturate( dot( geometry.normal, halfDir ) );\n\tfloat dotLH = saturate( dot( incidentLight.direction, halfDir ) );\n\tvec3 F = F_Schlick( specularColor, dotLH );\n\tfloat G = G_GGX_SmithCorrelated( alpha, dotNL, dotNV );\n\tfloat D = D_GGX( alpha, dotNH );\n\treturn F * ( G * D );\n}\nvec2 LTC_Uv( const in vec3 N, const in vec3 V, const in float roughness ) {\n\tconst float LUT_SIZE = 64.0;\n\tconst float LUT_SCALE = ( LUT_SIZE - 1.0 ) / LUT_SIZE;\n\tconst float LUT_BIAS = 0.5 / LUT_SIZE;\n\tfloat dotNV = saturate( dot( N, V ) );\n\tvec2 uv = vec2( roughness, sqrt( 1.0 - dotNV ) );\n\tuv = uv * LUT_SCALE + LUT_BIAS;\n\treturn uv;\n}\nfloat LTC_ClippedSphereFormFactor( const in vec3 f ) {\n\tfloat l = length( f );\n\treturn max( ( l * l + f.z ) / ( l + 1.0 ), 0.0 );\n}\nvec3 LTC_EdgeVectorFormFactor( const in vec3 v1, const in vec3 v2 ) {\n\tfloat x = dot( v1, v2 );\n\tfloat y = abs( x );\n\tfloat a = 0.8543985 + ( 0.4965155 + 0.0145206 * y ) * y;\n\tfloat b = 3.4175940 + ( 4.1616724 + y ) * y;\n\tfloat v = a / b;\n\tfloat theta_sintheta = ( x > 0.0 ) ? v : 0.5 * inversesqrt( max( 1.0 - x * x, 1e-7 ) ) - v;\n\treturn cross( v1, v2 ) * theta_sintheta;\n}\nvec3 LTC_Evaluate( const in vec3 N, const in vec3 V, const in vec3 P, const in mat3 mInv, const in vec3 rectCoords[ 4 ] ) {\n\tvec3 v1 = rectCoords[ 1 ] - rectCoords[ 0 ];\n\tvec3 v2 = rectCoords[ 3 ] - rectCoords[ 0 ];\n\tvec3 lightNormal = cross( v1, v2 );\n\tif( dot( lightNormal, P - rectCoords[ 0 ] ) < 0.0 ) return vec3( 0.0 );\n\tvec3 T1, T2;\n\tT1 = normalize( V - N * dot( V, N ) );\n\tT2 = - cross( N, T1 );\n\tmat3 mat = mInv * transposeMat3( mat3( T1, T2, N ) );\n\tvec3 coords[ 4 ];\n\tcoords[ 0 ] = mat * ( rectCoords[ 0 ] - P );\n\tcoords[ 1 ] = mat * ( rectCoords[ 1 ] - P );\n\tcoords[ 2 ] = mat * ( rectCoords[ 2 ] - P );\n\tcoords[ 3 ] = mat * ( rectCoords[ 3 ] - P );\n\tcoords[ 0 ] = normalize( coords[ 0 ] );\n\tcoords[ 1 ] = normalize( coords[ 1 ] );\n\tcoords[ 2 ] = normalize( coords[ 2 ] );\n\tcoords[ 3 ] = normalize( coords[ 3 ] );\n\tvec3 vectorFormFactor = vec3( 0.0 );\n\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 0 ], coords[ 1 ] );\n\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 1 ], coords[ 2 ] );\n\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 2 ], coords[ 3 ] );\n\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 3 ], coords[ 0 ] );\n\tfloat result = LTC_ClippedSphereFormFactor( vectorFormFactor );\n\treturn vec3( result );\n}\nvec3 BRDF_Specular_GGX_Environment( const in GeometricContext geometry, const in vec3 specularColor, const in float roughness ) {\n\tfloat dotNV = saturate( dot( geometry.normal, geometry.viewDir ) );\n\tconst vec4 c0 = vec4( - 1, - 0.0275, - 0.572, 0.022 );\n\tconst vec4 c1 = vec4( 1, 0.0425, 1.04, - 0.04 );\n\tvec4 r = roughness * c0 + c1;\n\tfloat a004 = min( r.x * r.x, exp2( - 9.28 * dotNV ) ) * r.x + r.y;\n\tvec2 AB = vec2( -1.04, 1.04 ) * a004 + r.zw;\n\treturn specularColor * AB.x + AB.y;\n}\nfloat G_BlinnPhong_Implicit( ) {\n\treturn 0.25;\n}\nfloat D_BlinnPhong( const in float shininess, const in float dotNH ) {\n\treturn RECIPROCAL_PI * ( shininess * 0.5 + 1.0 ) * pow( dotNH, shininess );\n}\nvec3 BRDF_Specular_BlinnPhong( const in IncidentLight incidentLight, const in GeometricContext geometry, const in vec3 specularColor, const in float shininess ) {\n\tvec3 halfDir = normalize( incidentLight.direction + geometry.viewDir );\n\tfloat dotNH = saturate( dot( geometry.normal, halfDir ) );\n\tfloat dotLH = saturate( dot( incidentLight.direction, halfDir ) );\n\tvec3 F = F_Schlick( specularColor, dotLH );\n\tfloat G = G_BlinnPhong_Implicit( );\n\tfloat D = D_BlinnPhong( shininess, dotNH );\n\treturn F * ( G * D );\n}\nfloat GGXRoughnessToBlinnExponent( const in float ggxRoughness ) {\n\treturn ( 2.0 / pow2( ggxRoughness + 0.0001 ) - 2.0 );\n}\nfloat BlinnExponentToGGXRoughness( const in float blinnExponent ) {\n\treturn sqrt( 2.0 / ( blinnExponent + 2.0 ) );\n}\n", +bumpmap_pars_fragment:"#ifdef USE_BUMPMAP\n\tuniform sampler2D bumpMap;\n\tuniform float bumpScale;\n\tvec2 dHdxy_fwd() {\n\t\tvec2 dSTdx = dFdx( vUv );\n\t\tvec2 dSTdy = dFdy( vUv );\n\t\tfloat Hll = bumpScale * texture2D( bumpMap, vUv ).x;\n\t\tfloat dBx = bumpScale * texture2D( bumpMap, vUv + dSTdx ).x - Hll;\n\t\tfloat dBy = bumpScale * texture2D( bumpMap, vUv + dSTdy ).x - Hll;\n\t\treturn vec2( dBx, dBy );\n\t}\n\tvec3 perturbNormalArb( vec3 surf_pos, vec3 surf_norm, vec2 dHdxy ) {\n\t\tvec3 vSigmaX = vec3( dFdx( surf_pos.x ), dFdx( surf_pos.y ), dFdx( surf_pos.z ) );\n\t\tvec3 vSigmaY = vec3( dFdy( surf_pos.x ), dFdy( surf_pos.y ), dFdy( surf_pos.z ) );\n\t\tvec3 vN = surf_norm;\n\t\tvec3 R1 = cross( vSigmaY, vN );\n\t\tvec3 R2 = cross( vN, vSigmaX );\n\t\tfloat fDet = dot( vSigmaX, R1 );\n\t\tvec3 vGrad = sign( fDet ) * ( dHdxy.x * R1 + dHdxy.y * R2 );\n\t\treturn normalize( abs( fDet ) * surf_norm - vGrad );\n\t}\n#endif\n", +clipping_planes_fragment:"#if NUM_CLIPPING_PLANES > 0\n\tvec4 plane;\n\t#pragma unroll_loop\n\tfor ( int i = 0; i < UNION_CLIPPING_PLANES; i ++ ) {\n\t\tplane = clippingPlanes[ i ];\n\t\tif ( dot( vViewPosition, plane.xyz ) > plane.w ) discard;\n\t}\n\t#if UNION_CLIPPING_PLANES < NUM_CLIPPING_PLANES\n\t\tbool clipped = true;\n\t\t#pragma unroll_loop\n\t\tfor ( int i = UNION_CLIPPING_PLANES; i < NUM_CLIPPING_PLANES; i ++ ) {\n\t\t\tplane = clippingPlanes[ i ];\n\t\t\tclipped = ( dot( vViewPosition, plane.xyz ) > plane.w ) && clipped;\n\t\t}\n\t\tif ( clipped ) discard;\n\t#endif\n#endif\n", +clipping_planes_pars_fragment:"#if NUM_CLIPPING_PLANES > 0\n\t#if ! defined( PHYSICAL ) && ! defined( PHONG )\n\t\tvarying vec3 vViewPosition;\n\t#endif\n\tuniform vec4 clippingPlanes[ NUM_CLIPPING_PLANES ];\n#endif\n",clipping_planes_pars_vertex:"#if NUM_CLIPPING_PLANES > 0 && ! defined( PHYSICAL ) && ! defined( PHONG )\n\tvarying vec3 vViewPosition;\n#endif\n",clipping_planes_vertex:"#if NUM_CLIPPING_PLANES > 0 && ! defined( PHYSICAL ) && ! defined( PHONG )\n\tvViewPosition = - mvPosition.xyz;\n#endif\n", +color_fragment:"#ifdef USE_COLOR\n\tdiffuseColor.rgb *= vColor;\n#endif",color_pars_fragment:"#ifdef USE_COLOR\n\tvarying vec3 vColor;\n#endif\n",color_pars_vertex:"#ifdef USE_COLOR\n\tvarying vec3 vColor;\n#endif",color_vertex:"#ifdef USE_COLOR\n\tvColor.xyz = color.xyz;\n#endif",common:"#define PI 3.14159265359\n#define PI2 6.28318530718\n#define PI_HALF 1.5707963267949\n#define RECIPROCAL_PI 0.31830988618\n#define RECIPROCAL_PI2 0.15915494\n#define LOG2 1.442695\n#define EPSILON 1e-6\n#define saturate(a) clamp( a, 0.0, 1.0 )\n#define whiteCompliment(a) ( 1.0 - saturate( a ) )\nfloat pow2( const in float x ) { return x*x; }\nfloat pow3( const in float x ) { return x*x*x; }\nfloat pow4( const in float x ) { float x2 = x*x; return x2*x2; }\nfloat average( const in vec3 color ) { return dot( color, vec3( 0.3333 ) ); }\nhighp float rand( const in vec2 uv ) {\n\tconst highp float a = 12.9898, b = 78.233, c = 43758.5453;\n\thighp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI );\n\treturn fract(sin(sn) * c);\n}\nstruct IncidentLight {\n\tvec3 color;\n\tvec3 direction;\n\tbool visible;\n};\nstruct ReflectedLight {\n\tvec3 directDiffuse;\n\tvec3 directSpecular;\n\tvec3 indirectDiffuse;\n\tvec3 indirectSpecular;\n};\nstruct GeometricContext {\n\tvec3 position;\n\tvec3 normal;\n\tvec3 viewDir;\n};\nvec3 transformDirection( in vec3 dir, in mat4 matrix ) {\n\treturn normalize( ( matrix * vec4( dir, 0.0 ) ).xyz );\n}\nvec3 inverseTransformDirection( in vec3 dir, in mat4 matrix ) {\n\treturn normalize( ( vec4( dir, 0.0 ) * matrix ).xyz );\n}\nvec3 projectOnPlane(in vec3 point, in vec3 pointOnPlane, in vec3 planeNormal ) {\n\tfloat distance = dot( planeNormal, point - pointOnPlane );\n\treturn - distance * planeNormal + point;\n}\nfloat sideOfPlane( in vec3 point, in vec3 pointOnPlane, in vec3 planeNormal ) {\n\treturn sign( dot( point - pointOnPlane, planeNormal ) );\n}\nvec3 linePlaneIntersect( in vec3 pointOnLine, in vec3 lineDirection, in vec3 pointOnPlane, in vec3 planeNormal ) {\n\treturn lineDirection * ( dot( planeNormal, pointOnPlane - pointOnLine ) / dot( planeNormal, lineDirection ) ) + pointOnLine;\n}\nmat3 transposeMat3( const in mat3 m ) {\n\tmat3 tmp;\n\ttmp[ 0 ] = vec3( m[ 0 ].x, m[ 1 ].x, m[ 2 ].x );\n\ttmp[ 1 ] = vec3( m[ 0 ].y, m[ 1 ].y, m[ 2 ].y );\n\ttmp[ 2 ] = vec3( m[ 0 ].z, m[ 1 ].z, m[ 2 ].z );\n\treturn tmp;\n}\nfloat linearToRelativeLuminance( const in vec3 color ) {\n\tvec3 weights = vec3( 0.2126, 0.7152, 0.0722 );\n\treturn dot( weights, color.rgb );\n}\n", +cube_uv_reflection_fragment:"#ifdef ENVMAP_TYPE_CUBE_UV\n#define cubeUV_textureSize (1024.0)\nint getFaceFromDirection(vec3 direction) {\n\tvec3 absDirection = abs(direction);\n\tint face = -1;\n\tif( absDirection.x > absDirection.z ) {\n\t\tif(absDirection.x > absDirection.y )\n\t\t\tface = direction.x > 0.0 ? 0 : 3;\n\t\telse\n\t\t\tface = direction.y > 0.0 ? 1 : 4;\n\t}\n\telse {\n\t\tif(absDirection.z > absDirection.y )\n\t\t\tface = direction.z > 0.0 ? 2 : 5;\n\t\telse\n\t\t\tface = direction.y > 0.0 ? 1 : 4;\n\t}\n\treturn face;\n}\n#define cubeUV_maxLods1 (log2(cubeUV_textureSize*0.25) - 1.0)\n#define cubeUV_rangeClamp (exp2((6.0 - 1.0) * 2.0))\nvec2 MipLevelInfo( vec3 vec, float roughnessLevel, float roughness ) {\n\tfloat scale = exp2(cubeUV_maxLods1 - roughnessLevel);\n\tfloat dxRoughness = dFdx(roughness);\n\tfloat dyRoughness = dFdy(roughness);\n\tvec3 dx = dFdx( vec * scale * dxRoughness );\n\tvec3 dy = dFdy( vec * scale * dyRoughness );\n\tfloat d = max( dot( dx, dx ), dot( dy, dy ) );\n\td = clamp(d, 1.0, cubeUV_rangeClamp);\n\tfloat mipLevel = 0.5 * log2(d);\n\treturn vec2(floor(mipLevel), fract(mipLevel));\n}\n#define cubeUV_maxLods2 (log2(cubeUV_textureSize*0.25) - 2.0)\n#define cubeUV_rcpTextureSize (1.0 / cubeUV_textureSize)\nvec2 getCubeUV(vec3 direction, float roughnessLevel, float mipLevel) {\n\tmipLevel = roughnessLevel > cubeUV_maxLods2 - 3.0 ? 0.0 : mipLevel;\n\tfloat a = 16.0 * cubeUV_rcpTextureSize;\n\tvec2 exp2_packed = exp2( vec2( roughnessLevel, mipLevel ) );\n\tvec2 rcp_exp2_packed = vec2( 1.0 ) / exp2_packed;\n\tfloat powScale = exp2_packed.x * exp2_packed.y;\n\tfloat scale = rcp_exp2_packed.x * rcp_exp2_packed.y * 0.25;\n\tfloat mipOffset = 0.75*(1.0 - rcp_exp2_packed.y) * rcp_exp2_packed.x;\n\tbool bRes = mipLevel == 0.0;\n\tscale = bRes && (scale < a) ? a : scale;\n\tvec3 r;\n\tvec2 offset;\n\tint face = getFaceFromDirection(direction);\n\tfloat rcpPowScale = 1.0 / powScale;\n\tif( face == 0) {\n\t\tr = vec3(direction.x, -direction.z, direction.y);\n\t\toffset = vec2(0.0+mipOffset,0.75 * rcpPowScale);\n\t\toffset.y = bRes && (offset.y < 2.0*a) ? a : offset.y;\n\t}\n\telse if( face == 1) {\n\t\tr = vec3(direction.y, direction.x, direction.z);\n\t\toffset = vec2(scale+mipOffset, 0.75 * rcpPowScale);\n\t\toffset.y = bRes && (offset.y < 2.0*a) ? a : offset.y;\n\t}\n\telse if( face == 2) {\n\t\tr = vec3(direction.z, direction.x, direction.y);\n\t\toffset = vec2(2.0*scale+mipOffset, 0.75 * rcpPowScale);\n\t\toffset.y = bRes && (offset.y < 2.0*a) ? a : offset.y;\n\t}\n\telse if( face == 3) {\n\t\tr = vec3(direction.x, direction.z, direction.y);\n\t\toffset = vec2(0.0+mipOffset,0.5 * rcpPowScale);\n\t\toffset.y = bRes && (offset.y < 2.0*a) ? 0.0 : offset.y;\n\t}\n\telse if( face == 4) {\n\t\tr = vec3(direction.y, direction.x, -direction.z);\n\t\toffset = vec2(scale+mipOffset, 0.5 * rcpPowScale);\n\t\toffset.y = bRes && (offset.y < 2.0*a) ? 0.0 : offset.y;\n\t}\n\telse {\n\t\tr = vec3(direction.z, -direction.x, direction.y);\n\t\toffset = vec2(2.0*scale+mipOffset, 0.5 * rcpPowScale);\n\t\toffset.y = bRes && (offset.y < 2.0*a) ? 0.0 : offset.y;\n\t}\n\tr = normalize(r);\n\tfloat texelOffset = 0.5 * cubeUV_rcpTextureSize;\n\tvec2 s = ( r.yz / abs( r.x ) + vec2( 1.0 ) ) * 0.5;\n\tvec2 base = offset + vec2( texelOffset );\n\treturn base + s * ( scale - 2.0 * texelOffset );\n}\n#define cubeUV_maxLods3 (log2(cubeUV_textureSize*0.25) - 3.0)\nvec4 textureCubeUV(vec3 reflectedDirection, float roughness ) {\n\tfloat roughnessVal = roughness* cubeUV_maxLods3;\n\tfloat r1 = floor(roughnessVal);\n\tfloat r2 = r1 + 1.0;\n\tfloat t = fract(roughnessVal);\n\tvec2 mipInfo = MipLevelInfo(reflectedDirection, r1, roughness);\n\tfloat s = mipInfo.y;\n\tfloat level0 = mipInfo.x;\n\tfloat level1 = level0 + 1.0;\n\tlevel1 = level1 > 5.0 ? 5.0 : level1;\n\tlevel0 += min( floor( s + 0.5 ), 5.0 );\n\tvec2 uv_10 = getCubeUV(reflectedDirection, r1, level0);\n\tvec4 color10 = envMapTexelToLinear(texture2D(envMap, uv_10));\n\tvec2 uv_20 = getCubeUV(reflectedDirection, r2, level0);\n\tvec4 color20 = envMapTexelToLinear(texture2D(envMap, uv_20));\n\tvec4 result = mix(color10, color20, t);\n\treturn vec4(result.rgb, 1.0);\n}\n#endif\n", +defaultnormal_vertex:"vec3 transformedNormal = normalMatrix * objectNormal;\n#ifdef FLIP_SIDED\n\ttransformedNormal = - transformedNormal;\n#endif\n",displacementmap_pars_vertex:"#ifdef USE_DISPLACEMENTMAP\n\tuniform sampler2D displacementMap;\n\tuniform float displacementScale;\n\tuniform float displacementBias;\n#endif\n",displacementmap_vertex:"#ifdef USE_DISPLACEMENTMAP\n\ttransformed += normalize( objectNormal ) * ( texture2D( displacementMap, uv ).x * displacementScale + displacementBias );\n#endif\n", +emissivemap_fragment:"#ifdef USE_EMISSIVEMAP\n\tvec4 emissiveColor = texture2D( emissiveMap, vUv );\n\temissiveColor.rgb = emissiveMapTexelToLinear( emissiveColor ).rgb;\n\ttotalEmissiveRadiance *= emissiveColor.rgb;\n#endif\n",emissivemap_pars_fragment:"#ifdef USE_EMISSIVEMAP\n\tuniform sampler2D emissiveMap;\n#endif\n",encodings_fragment:" gl_FragColor = linearToOutputTexel( gl_FragColor );\n",encodings_pars_fragment:"\nvec4 LinearToLinear( in vec4 value ) {\n\treturn value;\n}\nvec4 GammaToLinear( in vec4 value, in float gammaFactor ) {\n\treturn vec4( pow( value.xyz, vec3( gammaFactor ) ), value.w );\n}\nvec4 LinearToGamma( in vec4 value, in float gammaFactor ) {\n\treturn vec4( pow( value.xyz, vec3( 1.0 / gammaFactor ) ), value.w );\n}\nvec4 sRGBToLinear( in vec4 value ) {\n\treturn vec4( mix( pow( value.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), value.rgb * 0.0773993808, vec3( lessThanEqual( value.rgb, vec3( 0.04045 ) ) ) ), value.w );\n}\nvec4 LinearTosRGB( in vec4 value ) {\n\treturn vec4( mix( pow( value.rgb, vec3( 0.41666 ) ) * 1.055 - vec3( 0.055 ), value.rgb * 12.92, vec3( lessThanEqual( value.rgb, vec3( 0.0031308 ) ) ) ), value.w );\n}\nvec4 RGBEToLinear( in vec4 value ) {\n\treturn vec4( value.rgb * exp2( value.a * 255.0 - 128.0 ), 1.0 );\n}\nvec4 LinearToRGBE( in vec4 value ) {\n\tfloat maxComponent = max( max( value.r, value.g ), value.b );\n\tfloat fExp = clamp( ceil( log2( maxComponent ) ), -128.0, 127.0 );\n\treturn vec4( value.rgb / exp2( fExp ), ( fExp + 128.0 ) / 255.0 );\n}\nvec4 RGBMToLinear( in vec4 value, in float maxRange ) {\n\treturn vec4( value.xyz * value.w * maxRange, 1.0 );\n}\nvec4 LinearToRGBM( in vec4 value, in float maxRange ) {\n\tfloat maxRGB = max( value.x, max( value.g, value.b ) );\n\tfloat M = clamp( maxRGB / maxRange, 0.0, 1.0 );\n\tM = ceil( M * 255.0 ) / 255.0;\n\treturn vec4( value.rgb / ( M * maxRange ), M );\n}\nvec4 RGBDToLinear( in vec4 value, in float maxRange ) {\n\treturn vec4( value.rgb * ( ( maxRange / 255.0 ) / value.a ), 1.0 );\n}\nvec4 LinearToRGBD( in vec4 value, in float maxRange ) {\n\tfloat maxRGB = max( value.x, max( value.g, value.b ) );\n\tfloat D = max( maxRange / maxRGB, 1.0 );\n\tD = min( floor( D ) / 255.0, 1.0 );\n\treturn vec4( value.rgb * ( D * ( 255.0 / maxRange ) ), D );\n}\nconst mat3 cLogLuvM = mat3( 0.2209, 0.3390, 0.4184, 0.1138, 0.6780, 0.7319, 0.0102, 0.1130, 0.2969 );\nvec4 LinearToLogLuv( in vec4 value ) {\n\tvec3 Xp_Y_XYZp = value.rgb * cLogLuvM;\n\tXp_Y_XYZp = max(Xp_Y_XYZp, vec3(1e-6, 1e-6, 1e-6));\n\tvec4 vResult;\n\tvResult.xy = Xp_Y_XYZp.xy / Xp_Y_XYZp.z;\n\tfloat Le = 2.0 * log2(Xp_Y_XYZp.y) + 127.0;\n\tvResult.w = fract(Le);\n\tvResult.z = (Le - (floor(vResult.w*255.0))/255.0)/255.0;\n\treturn vResult;\n}\nconst mat3 cLogLuvInverseM = mat3( 6.0014, -2.7008, -1.7996, -1.3320, 3.1029, -5.7721, 0.3008, -1.0882, 5.6268 );\nvec4 LogLuvToLinear( in vec4 value ) {\n\tfloat Le = value.z * 255.0 + value.w;\n\tvec3 Xp_Y_XYZp;\n\tXp_Y_XYZp.y = exp2((Le - 127.0) / 2.0);\n\tXp_Y_XYZp.z = Xp_Y_XYZp.y / value.y;\n\tXp_Y_XYZp.x = value.x * Xp_Y_XYZp.z;\n\tvec3 vRGB = Xp_Y_XYZp.rgb * cLogLuvInverseM;\n\treturn vec4( max(vRGB, 0.0), 1.0 );\n}\n", +envmap_fragment:"#ifdef USE_ENVMAP\n\t#if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG )\n\t\tvec3 cameraToVertex = normalize( vWorldPosition - cameraPosition );\n\t\tvec3 worldNormal = inverseTransformDirection( normal, viewMatrix );\n\t\t#ifdef ENVMAP_MODE_REFLECTION\n\t\t\tvec3 reflectVec = reflect( cameraToVertex, worldNormal );\n\t\t#else\n\t\t\tvec3 reflectVec = refract( cameraToVertex, worldNormal, refractionRatio );\n\t\t#endif\n\t#else\n\t\tvec3 reflectVec = vReflect;\n\t#endif\n\t#ifdef ENVMAP_TYPE_CUBE\n\t\tvec4 envColor = textureCube( envMap, vec3( flipEnvMap * reflectVec.x, reflectVec.yz ) );\n\t#elif defined( ENVMAP_TYPE_EQUIREC )\n\t\tvec2 sampleUV;\n\t\treflectVec = normalize( reflectVec );\n\t\tsampleUV.y = asin( clamp( reflectVec.y, - 1.0, 1.0 ) ) * RECIPROCAL_PI + 0.5;\n\t\tsampleUV.x = atan( reflectVec.z, reflectVec.x ) * RECIPROCAL_PI2 + 0.5;\n\t\tvec4 envColor = texture2D( envMap, sampleUV );\n\t#elif defined( ENVMAP_TYPE_SPHERE )\n\t\treflectVec = normalize( reflectVec );\n\t\tvec3 reflectView = normalize( ( viewMatrix * vec4( reflectVec, 0.0 ) ).xyz + vec3( 0.0, 0.0, 1.0 ) );\n\t\tvec4 envColor = texture2D( envMap, reflectView.xy * 0.5 + 0.5 );\n\t#else\n\t\tvec4 envColor = vec4( 0.0 );\n\t#endif\n\tenvColor = envMapTexelToLinear( envColor );\n\t#ifdef ENVMAP_BLENDING_MULTIPLY\n\t\toutgoingLight = mix( outgoingLight, outgoingLight * envColor.xyz, specularStrength * reflectivity );\n\t#elif defined( ENVMAP_BLENDING_MIX )\n\t\toutgoingLight = mix( outgoingLight, envColor.xyz, specularStrength * reflectivity );\n\t#elif defined( ENVMAP_BLENDING_ADD )\n\t\toutgoingLight += envColor.xyz * specularStrength * reflectivity;\n\t#endif\n#endif\n", +envmap_pars_fragment:"#if defined( USE_ENVMAP ) || defined( PHYSICAL )\n\tuniform float reflectivity;\n\tuniform float envMapIntensity;\n#endif\n#ifdef USE_ENVMAP\n\t#if ! defined( PHYSICAL ) && ( defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG ) )\n\t\tvarying vec3 vWorldPosition;\n\t#endif\n\t#ifdef ENVMAP_TYPE_CUBE\n\t\tuniform samplerCube envMap;\n\t#else\n\t\tuniform sampler2D envMap;\n\t#endif\n\tuniform float flipEnvMap;\n\tuniform int maxMipLevel;\n\t#if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG ) || defined( PHYSICAL )\n\t\tuniform float refractionRatio;\n\t#else\n\t\tvarying vec3 vReflect;\n\t#endif\n#endif\n", +envmap_pars_vertex:"#ifdef USE_ENVMAP\n\t#if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG )\n\t\tvarying vec3 vWorldPosition;\n\t#else\n\t\tvarying vec3 vReflect;\n\t\tuniform float refractionRatio;\n\t#endif\n#endif\n",envmap_vertex:"#ifdef USE_ENVMAP\n\t#if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG )\n\t\tvWorldPosition = worldPosition.xyz;\n\t#else\n\t\tvec3 cameraToVertex = normalize( worldPosition.xyz - cameraPosition );\n\t\tvec3 worldNormal = inverseTransformDirection( transformedNormal, viewMatrix );\n\t\t#ifdef ENVMAP_MODE_REFLECTION\n\t\t\tvReflect = reflect( cameraToVertex, worldNormal );\n\t\t#else\n\t\t\tvReflect = refract( cameraToVertex, worldNormal, refractionRatio );\n\t\t#endif\n\t#endif\n#endif\n", +fog_vertex:"\n#ifdef USE_FOG\nfogDepth = -mvPosition.z;\n#endif",fog_pars_vertex:"#ifdef USE_FOG\n varying float fogDepth;\n#endif\n",fog_fragment:"#ifdef USE_FOG\n\t#ifdef FOG_EXP2\n\t\tfloat fogFactor = whiteCompliment( exp2( - fogDensity * fogDensity * fogDepth * fogDepth * LOG2 ) );\n\t#else\n\t\tfloat fogFactor = smoothstep( fogNear, fogFar, fogDepth );\n\t#endif\n\tgl_FragColor.rgb = mix( gl_FragColor.rgb, fogColor, fogFactor );\n#endif\n",fog_pars_fragment:"#ifdef USE_FOG\n\tuniform vec3 fogColor;\n\tvarying float fogDepth;\n\t#ifdef FOG_EXP2\n\t\tuniform float fogDensity;\n\t#else\n\t\tuniform float fogNear;\n\t\tuniform float fogFar;\n\t#endif\n#endif\n", +gradientmap_pars_fragment:"#ifdef TOON\n\tuniform sampler2D gradientMap;\n\tvec3 getGradientIrradiance( vec3 normal, vec3 lightDirection ) {\n\t\tfloat dotNL = dot( normal, lightDirection );\n\t\tvec2 coord = vec2( dotNL * 0.5 + 0.5, 0.0 );\n\t\t#ifdef USE_GRADIENTMAP\n\t\t\treturn texture2D( gradientMap, coord ).rgb;\n\t\t#else\n\t\t\treturn ( coord.x < 0.7 ) ? vec3( 0.7 ) : vec3( 1.0 );\n\t\t#endif\n\t}\n#endif\n",lightmap_fragment:"#ifdef USE_LIGHTMAP\n\treflectedLight.indirectDiffuse += PI * texture2D( lightMap, vUv2 ).xyz * lightMapIntensity;\n#endif\n", +lightmap_pars_fragment:"#ifdef USE_LIGHTMAP\n\tuniform sampler2D lightMap;\n\tuniform float lightMapIntensity;\n#endif",lights_lambert_vertex:"vec3 diffuse = vec3( 1.0 );\nGeometricContext geometry;\ngeometry.position = mvPosition.xyz;\ngeometry.normal = normalize( transformedNormal );\ngeometry.viewDir = normalize( -mvPosition.xyz );\nGeometricContext backGeometry;\nbackGeometry.position = geometry.position;\nbackGeometry.normal = -geometry.normal;\nbackGeometry.viewDir = geometry.viewDir;\nvLightFront = vec3( 0.0 );\n#ifdef DOUBLE_SIDED\n\tvLightBack = vec3( 0.0 );\n#endif\nIncidentLight directLight;\nfloat dotNL;\nvec3 directLightColor_Diffuse;\n#if NUM_POINT_LIGHTS > 0\n\t#pragma unroll_loop\n\tfor ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {\n\t\tgetPointDirectLightIrradiance( pointLights[ i ], geometry, directLight );\n\t\tdotNL = dot( geometry.normal, directLight.direction );\n\t\tdirectLightColor_Diffuse = PI * directLight.color;\n\t\tvLightFront += saturate( dotNL ) * directLightColor_Diffuse;\n\t\t#ifdef DOUBLE_SIDED\n\t\t\tvLightBack += saturate( -dotNL ) * directLightColor_Diffuse;\n\t\t#endif\n\t}\n#endif\n#if NUM_SPOT_LIGHTS > 0\n\t#pragma unroll_loop\n\tfor ( int i = 0; i < NUM_SPOT_LIGHTS; i ++ ) {\n\t\tgetSpotDirectLightIrradiance( spotLights[ i ], geometry, directLight );\n\t\tdotNL = dot( geometry.normal, directLight.direction );\n\t\tdirectLightColor_Diffuse = PI * directLight.color;\n\t\tvLightFront += saturate( dotNL ) * directLightColor_Diffuse;\n\t\t#ifdef DOUBLE_SIDED\n\t\t\tvLightBack += saturate( -dotNL ) * directLightColor_Diffuse;\n\t\t#endif\n\t}\n#endif\n#if NUM_DIR_LIGHTS > 0\n\t#pragma unroll_loop\n\tfor ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) {\n\t\tgetDirectionalDirectLightIrradiance( directionalLights[ i ], geometry, directLight );\n\t\tdotNL = dot( geometry.normal, directLight.direction );\n\t\tdirectLightColor_Diffuse = PI * directLight.color;\n\t\tvLightFront += saturate( dotNL ) * directLightColor_Diffuse;\n\t\t#ifdef DOUBLE_SIDED\n\t\t\tvLightBack += saturate( -dotNL ) * directLightColor_Diffuse;\n\t\t#endif\n\t}\n#endif\n#if NUM_HEMI_LIGHTS > 0\n\t#pragma unroll_loop\n\tfor ( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) {\n\t\tvLightFront += getHemisphereLightIrradiance( hemisphereLights[ i ], geometry );\n\t\t#ifdef DOUBLE_SIDED\n\t\t\tvLightBack += getHemisphereLightIrradiance( hemisphereLights[ i ], backGeometry );\n\t\t#endif\n\t}\n#endif\n", +lights_pars_begin:"uniform vec3 ambientLightColor;\nvec3 getAmbientLightIrradiance( const in vec3 ambientLightColor ) {\n\tvec3 irradiance = ambientLightColor;\n\t#ifndef PHYSICALLY_CORRECT_LIGHTS\n\t\tirradiance *= PI;\n\t#endif\n\treturn irradiance;\n}\n#if NUM_DIR_LIGHTS > 0\n\tstruct DirectionalLight {\n\t\tvec3 direction;\n\t\tvec3 color;\n\t\tint shadow;\n\t\tfloat shadowBias;\n\t\tfloat shadowRadius;\n\t\tvec2 shadowMapSize;\n\t};\n\tuniform DirectionalLight directionalLights[ NUM_DIR_LIGHTS ];\n\tvoid getDirectionalDirectLightIrradiance( const in DirectionalLight directionalLight, const in GeometricContext geometry, out IncidentLight directLight ) {\n\t\tdirectLight.color = directionalLight.color;\n\t\tdirectLight.direction = directionalLight.direction;\n\t\tdirectLight.visible = true;\n\t}\n#endif\n#if NUM_POINT_LIGHTS > 0\n\tstruct PointLight {\n\t\tvec3 position;\n\t\tvec3 color;\n\t\tfloat distance;\n\t\tfloat decay;\n\t\tint shadow;\n\t\tfloat shadowBias;\n\t\tfloat shadowRadius;\n\t\tvec2 shadowMapSize;\n\t\tfloat shadowCameraNear;\n\t\tfloat shadowCameraFar;\n\t};\n\tuniform PointLight pointLights[ NUM_POINT_LIGHTS ];\n\tvoid getPointDirectLightIrradiance( const in PointLight pointLight, const in GeometricContext geometry, out IncidentLight directLight ) {\n\t\tvec3 lVector = pointLight.position - geometry.position;\n\t\tdirectLight.direction = normalize( lVector );\n\t\tfloat lightDistance = length( lVector );\n\t\tdirectLight.color = pointLight.color;\n\t\tdirectLight.color *= punctualLightIntensityToIrradianceFactor( lightDistance, pointLight.distance, pointLight.decay );\n\t\tdirectLight.visible = ( directLight.color != vec3( 0.0 ) );\n\t}\n#endif\n#if NUM_SPOT_LIGHTS > 0\n\tstruct SpotLight {\n\t\tvec3 position;\n\t\tvec3 direction;\n\t\tvec3 color;\n\t\tfloat distance;\n\t\tfloat decay;\n\t\tfloat coneCos;\n\t\tfloat penumbraCos;\n\t\tint shadow;\n\t\tfloat shadowBias;\n\t\tfloat shadowRadius;\n\t\tvec2 shadowMapSize;\n\t};\n\tuniform SpotLight spotLights[ NUM_SPOT_LIGHTS ];\n\tvoid getSpotDirectLightIrradiance( const in SpotLight spotLight, const in GeometricContext geometry, out IncidentLight directLight ) {\n\t\tvec3 lVector = spotLight.position - geometry.position;\n\t\tdirectLight.direction = normalize( lVector );\n\t\tfloat lightDistance = length( lVector );\n\t\tfloat angleCos = dot( directLight.direction, spotLight.direction );\n\t\tif ( angleCos > spotLight.coneCos ) {\n\t\t\tfloat spotEffect = smoothstep( spotLight.coneCos, spotLight.penumbraCos, angleCos );\n\t\t\tdirectLight.color = spotLight.color;\n\t\t\tdirectLight.color *= spotEffect * punctualLightIntensityToIrradianceFactor( lightDistance, spotLight.distance, spotLight.decay );\n\t\t\tdirectLight.visible = true;\n\t\t} else {\n\t\t\tdirectLight.color = vec3( 0.0 );\n\t\t\tdirectLight.visible = false;\n\t\t}\n\t}\n#endif\n#if NUM_RECT_AREA_LIGHTS > 0\n\tstruct RectAreaLight {\n\t\tvec3 color;\n\t\tvec3 position;\n\t\tvec3 halfWidth;\n\t\tvec3 halfHeight;\n\t};\n\tuniform sampler2D ltc_1;\tuniform sampler2D ltc_2;\n\tuniform RectAreaLight rectAreaLights[ NUM_RECT_AREA_LIGHTS ];\n#endif\n#if NUM_HEMI_LIGHTS > 0\n\tstruct HemisphereLight {\n\t\tvec3 direction;\n\t\tvec3 skyColor;\n\t\tvec3 groundColor;\n\t};\n\tuniform HemisphereLight hemisphereLights[ NUM_HEMI_LIGHTS ];\n\tvec3 getHemisphereLightIrradiance( const in HemisphereLight hemiLight, const in GeometricContext geometry ) {\n\t\tfloat dotNL = dot( geometry.normal, hemiLight.direction );\n\t\tfloat hemiDiffuseWeight = 0.5 * dotNL + 0.5;\n\t\tvec3 irradiance = mix( hemiLight.groundColor, hemiLight.skyColor, hemiDiffuseWeight );\n\t\t#ifndef PHYSICALLY_CORRECT_LIGHTS\n\t\t\tirradiance *= PI;\n\t\t#endif\n\t\treturn irradiance;\n\t}\n#endif\n", +lights_pars_maps:"#if defined( USE_ENVMAP ) && defined( PHYSICAL )\n\tvec3 getLightProbeIndirectIrradiance( const in GeometricContext geometry, const in int maxMIPLevel ) {\n\t\tvec3 worldNormal = inverseTransformDirection( geometry.normal, viewMatrix );\n\t\t#ifdef ENVMAP_TYPE_CUBE\n\t\t\tvec3 queryVec = vec3( flipEnvMap * worldNormal.x, worldNormal.yz );\n\t\t\t#ifdef TEXTURE_LOD_EXT\n\t\t\t\tvec4 envMapColor = textureCubeLodEXT( envMap, queryVec, float( maxMIPLevel ) );\n\t\t\t#else\n\t\t\t\tvec4 envMapColor = textureCube( envMap, queryVec, float( maxMIPLevel ) );\n\t\t\t#endif\n\t\t\tenvMapColor.rgb = envMapTexelToLinear( envMapColor ).rgb;\n\t\t#elif defined( ENVMAP_TYPE_CUBE_UV )\n\t\t\tvec3 queryVec = vec3( flipEnvMap * worldNormal.x, worldNormal.yz );\n\t\t\tvec4 envMapColor = textureCubeUV( queryVec, 1.0 );\n\t\t#else\n\t\t\tvec4 envMapColor = vec4( 0.0 );\n\t\t#endif\n\t\treturn PI * envMapColor.rgb * envMapIntensity;\n\t}\n\tfloat getSpecularMIPLevel( const in float blinnShininessExponent, const in int maxMIPLevel ) {\n\t\tfloat maxMIPLevelScalar = float( maxMIPLevel );\n\t\tfloat desiredMIPLevel = maxMIPLevelScalar + 0.79248 - 0.5 * log2( pow2( blinnShininessExponent ) + 1.0 );\n\t\treturn clamp( desiredMIPLevel, 0.0, maxMIPLevelScalar );\n\t}\n\tvec3 getLightProbeIndirectRadiance( const in GeometricContext geometry, const in float blinnShininessExponent, const in int maxMIPLevel ) {\n\t\t#ifdef ENVMAP_MODE_REFLECTION\n\t\t\tvec3 reflectVec = reflect( -geometry.viewDir, geometry.normal );\n\t\t#else\n\t\t\tvec3 reflectVec = refract( -geometry.viewDir, geometry.normal, refractionRatio );\n\t\t#endif\n\t\treflectVec = inverseTransformDirection( reflectVec, viewMatrix );\n\t\tfloat specularMIPLevel = getSpecularMIPLevel( blinnShininessExponent, maxMIPLevel );\n\t\t#ifdef ENVMAP_TYPE_CUBE\n\t\t\tvec3 queryReflectVec = vec3( flipEnvMap * reflectVec.x, reflectVec.yz );\n\t\t\t#ifdef TEXTURE_LOD_EXT\n\t\t\t\tvec4 envMapColor = textureCubeLodEXT( envMap, queryReflectVec, specularMIPLevel );\n\t\t\t#else\n\t\t\t\tvec4 envMapColor = textureCube( envMap, queryReflectVec, specularMIPLevel );\n\t\t\t#endif\n\t\t\tenvMapColor.rgb = envMapTexelToLinear( envMapColor ).rgb;\n\t\t#elif defined( ENVMAP_TYPE_CUBE_UV )\n\t\t\tvec3 queryReflectVec = vec3( flipEnvMap * reflectVec.x, reflectVec.yz );\n\t\t\tvec4 envMapColor = textureCubeUV(queryReflectVec, BlinnExponentToGGXRoughness(blinnShininessExponent));\n\t\t#elif defined( ENVMAP_TYPE_EQUIREC )\n\t\t\tvec2 sampleUV;\n\t\t\tsampleUV.y = asin( clamp( reflectVec.y, - 1.0, 1.0 ) ) * RECIPROCAL_PI + 0.5;\n\t\t\tsampleUV.x = atan( reflectVec.z, reflectVec.x ) * RECIPROCAL_PI2 + 0.5;\n\t\t\t#ifdef TEXTURE_LOD_EXT\n\t\t\t\tvec4 envMapColor = texture2DLodEXT( envMap, sampleUV, specularMIPLevel );\n\t\t\t#else\n\t\t\t\tvec4 envMapColor = texture2D( envMap, sampleUV, specularMIPLevel );\n\t\t\t#endif\n\t\t\tenvMapColor.rgb = envMapTexelToLinear( envMapColor ).rgb;\n\t\t#elif defined( ENVMAP_TYPE_SPHERE )\n\t\t\tvec3 reflectView = normalize( ( viewMatrix * vec4( reflectVec, 0.0 ) ).xyz + vec3( 0.0,0.0,1.0 ) );\n\t\t\t#ifdef TEXTURE_LOD_EXT\n\t\t\t\tvec4 envMapColor = texture2DLodEXT( envMap, reflectView.xy * 0.5 + 0.5, specularMIPLevel );\n\t\t\t#else\n\t\t\t\tvec4 envMapColor = texture2D( envMap, reflectView.xy * 0.5 + 0.5, specularMIPLevel );\n\t\t\t#endif\n\t\t\tenvMapColor.rgb = envMapTexelToLinear( envMapColor ).rgb;\n\t\t#endif\n\t\treturn envMapColor.rgb * envMapIntensity;\n\t}\n#endif\n", +lights_phong_fragment:"BlinnPhongMaterial material;\nmaterial.diffuseColor = diffuseColor.rgb;\nmaterial.specularColor = specular;\nmaterial.specularShininess = shininess;\nmaterial.specularStrength = specularStrength;\n",lights_phong_pars_fragment:"varying vec3 vViewPosition;\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n#endif\nstruct BlinnPhongMaterial {\n\tvec3\tdiffuseColor;\n\tvec3\tspecularColor;\n\tfloat\tspecularShininess;\n\tfloat\tspecularStrength;\n};\nvoid RE_Direct_BlinnPhong( const in IncidentLight directLight, const in GeometricContext geometry, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) {\n\t#ifdef TOON\n\t\tvec3 irradiance = getGradientIrradiance( geometry.normal, directLight.direction ) * directLight.color;\n\t#else\n\t\tfloat dotNL = saturate( dot( geometry.normal, directLight.direction ) );\n\t\tvec3 irradiance = dotNL * directLight.color;\n\t#endif\n\t#ifndef PHYSICALLY_CORRECT_LIGHTS\n\t\tirradiance *= PI;\n\t#endif\n\treflectedLight.directDiffuse += irradiance * BRDF_Diffuse_Lambert( material.diffuseColor );\n\treflectedLight.directSpecular += irradiance * BRDF_Specular_BlinnPhong( directLight, geometry, material.specularColor, material.specularShininess ) * material.specularStrength;\n}\nvoid RE_IndirectDiffuse_BlinnPhong( const in vec3 irradiance, const in GeometricContext geometry, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) {\n\treflectedLight.indirectDiffuse += irradiance * BRDF_Diffuse_Lambert( material.diffuseColor );\n}\n#define RE_Direct\t\t\t\tRE_Direct_BlinnPhong\n#define RE_IndirectDiffuse\t\tRE_IndirectDiffuse_BlinnPhong\n#define Material_LightProbeLOD( material )\t(0)\n", +lights_physical_fragment:"PhysicalMaterial material;\nmaterial.diffuseColor = diffuseColor.rgb * ( 1.0 - metalnessFactor );\nmaterial.specularRoughness = clamp( roughnessFactor, 0.04, 1.0 );\n#ifdef STANDARD\n\tmaterial.specularColor = mix( vec3( DEFAULT_SPECULAR_COEFFICIENT ), diffuseColor.rgb, metalnessFactor );\n#else\n\tmaterial.specularColor = mix( vec3( MAXIMUM_SPECULAR_COEFFICIENT * pow2( reflectivity ) ), diffuseColor.rgb, metalnessFactor );\n\tmaterial.clearCoat = saturate( clearCoat );\tmaterial.clearCoatRoughness = clamp( clearCoatRoughness, 0.04, 1.0 );\n#endif\n", +lights_physical_pars_fragment:"struct PhysicalMaterial {\n\tvec3\tdiffuseColor;\n\tfloat\tspecularRoughness;\n\tvec3\tspecularColor;\n\t#ifndef STANDARD\n\t\tfloat clearCoat;\n\t\tfloat clearCoatRoughness;\n\t#endif\n};\n#define MAXIMUM_SPECULAR_COEFFICIENT 0.16\n#define DEFAULT_SPECULAR_COEFFICIENT 0.04\nfloat clearCoatDHRApprox( const in float roughness, const in float dotNL ) {\n\treturn DEFAULT_SPECULAR_COEFFICIENT + ( 1.0 - DEFAULT_SPECULAR_COEFFICIENT ) * ( pow( 1.0 - dotNL, 5.0 ) * pow( 1.0 - roughness, 2.0 ) );\n}\n#if NUM_RECT_AREA_LIGHTS > 0\n\tvoid RE_Direct_RectArea_Physical( const in RectAreaLight rectAreaLight, const in GeometricContext geometry, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) {\n\t\tvec3 normal = geometry.normal;\n\t\tvec3 viewDir = geometry.viewDir;\n\t\tvec3 position = geometry.position;\n\t\tvec3 lightPos = rectAreaLight.position;\n\t\tvec3 halfWidth = rectAreaLight.halfWidth;\n\t\tvec3 halfHeight = rectAreaLight.halfHeight;\n\t\tvec3 lightColor = rectAreaLight.color;\n\t\tfloat roughness = material.specularRoughness;\n\t\tvec3 rectCoords[ 4 ];\n\t\trectCoords[ 0 ] = lightPos - halfWidth - halfHeight;\t\trectCoords[ 1 ] = lightPos + halfWidth - halfHeight;\n\t\trectCoords[ 2 ] = lightPos + halfWidth + halfHeight;\n\t\trectCoords[ 3 ] = lightPos - halfWidth + halfHeight;\n\t\tvec2 uv = LTC_Uv( normal, viewDir, roughness );\n\t\tvec4 t1 = texture2D( ltc_1, uv );\n\t\tvec4 t2 = texture2D( ltc_2, uv );\n\t\tmat3 mInv = mat3(\n\t\t\tvec3( t1.x, 0, t1.y ),\n\t\t\tvec3( 0, 1, 0 ),\n\t\t\tvec3( t1.z, 0, t1.w )\n\t\t);\n\t\tvec3 fresnel = ( material.specularColor * t2.x + ( vec3( 1.0 ) - material.specularColor ) * t2.y );\n\t\treflectedLight.directSpecular += lightColor * fresnel * LTC_Evaluate( normal, viewDir, position, mInv, rectCoords );\n\t\treflectedLight.directDiffuse += lightColor * material.diffuseColor * LTC_Evaluate( normal, viewDir, position, mat3( 1.0 ), rectCoords );\n\t}\n#endif\nvoid RE_Direct_Physical( const in IncidentLight directLight, const in GeometricContext geometry, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) {\n\tfloat dotNL = saturate( dot( geometry.normal, directLight.direction ) );\n\tvec3 irradiance = dotNL * directLight.color;\n\t#ifndef PHYSICALLY_CORRECT_LIGHTS\n\t\tirradiance *= PI;\n\t#endif\n\t#ifndef STANDARD\n\t\tfloat clearCoatDHR = material.clearCoat * clearCoatDHRApprox( material.clearCoatRoughness, dotNL );\n\t#else\n\t\tfloat clearCoatDHR = 0.0;\n\t#endif\n\treflectedLight.directSpecular += ( 1.0 - clearCoatDHR ) * irradiance * BRDF_Specular_GGX( directLight, geometry, material.specularColor, material.specularRoughness );\n\treflectedLight.directDiffuse += ( 1.0 - clearCoatDHR ) * irradiance * BRDF_Diffuse_Lambert( material.diffuseColor );\n\t#ifndef STANDARD\n\t\treflectedLight.directSpecular += irradiance * material.clearCoat * BRDF_Specular_GGX( directLight, geometry, vec3( DEFAULT_SPECULAR_COEFFICIENT ), material.clearCoatRoughness );\n\t#endif\n}\nvoid RE_IndirectDiffuse_Physical( const in vec3 irradiance, const in GeometricContext geometry, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) {\n\treflectedLight.indirectDiffuse += irradiance * BRDF_Diffuse_Lambert( material.diffuseColor );\n}\nvoid RE_IndirectSpecular_Physical( const in vec3 radiance, const in vec3 clearCoatRadiance, const in GeometricContext geometry, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) {\n\t#ifndef STANDARD\n\t\tfloat dotNV = saturate( dot( geometry.normal, geometry.viewDir ) );\n\t\tfloat dotNL = dotNV;\n\t\tfloat clearCoatDHR = material.clearCoat * clearCoatDHRApprox( material.clearCoatRoughness, dotNL );\n\t#else\n\t\tfloat clearCoatDHR = 0.0;\n\t#endif\n\treflectedLight.indirectSpecular += ( 1.0 - clearCoatDHR ) * radiance * BRDF_Specular_GGX_Environment( geometry, material.specularColor, material.specularRoughness );\n\t#ifndef STANDARD\n\t\treflectedLight.indirectSpecular += clearCoatRadiance * material.clearCoat * BRDF_Specular_GGX_Environment( geometry, vec3( DEFAULT_SPECULAR_COEFFICIENT ), material.clearCoatRoughness );\n\t#endif\n}\n#define RE_Direct\t\t\t\tRE_Direct_Physical\n#define RE_Direct_RectArea\t\tRE_Direct_RectArea_Physical\n#define RE_IndirectDiffuse\t\tRE_IndirectDiffuse_Physical\n#define RE_IndirectSpecular\t\tRE_IndirectSpecular_Physical\n#define Material_BlinnShininessExponent( material ) GGXRoughnessToBlinnExponent( material.specularRoughness )\n#define Material_ClearCoat_BlinnShininessExponent( material ) GGXRoughnessToBlinnExponent( material.clearCoatRoughness )\nfloat computeSpecularOcclusion( const in float dotNV, const in float ambientOcclusion, const in float roughness ) {\n\treturn saturate( pow( dotNV + ambientOcclusion, exp2( - 16.0 * roughness - 1.0 ) ) - 1.0 + ambientOcclusion );\n}\n", +lights_fragment_begin:"\nGeometricContext geometry;\ngeometry.position = - vViewPosition;\ngeometry.normal = normal;\ngeometry.viewDir = normalize( vViewPosition );\nIncidentLight directLight;\n#if ( NUM_POINT_LIGHTS > 0 ) && defined( RE_Direct )\n\tPointLight pointLight;\n\t#pragma unroll_loop\n\tfor ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {\n\t\tpointLight = pointLights[ i ];\n\t\tgetPointDirectLightIrradiance( pointLight, geometry, directLight );\n\t\t#ifdef USE_SHADOWMAP\n\t\tdirectLight.color *= all( bvec2( pointLight.shadow, directLight.visible ) ) ? getPointShadow( pointShadowMap[ i ], pointLight.shadowMapSize, pointLight.shadowBias, pointLight.shadowRadius, vPointShadowCoord[ i ], pointLight.shadowCameraNear, pointLight.shadowCameraFar ) : 1.0;\n\t\t#endif\n\t\tRE_Direct( directLight, geometry, material, reflectedLight );\n\t}\n#endif\n#if ( NUM_SPOT_LIGHTS > 0 ) && defined( RE_Direct )\n\tSpotLight spotLight;\n\t#pragma unroll_loop\n\tfor ( int i = 0; i < NUM_SPOT_LIGHTS; i ++ ) {\n\t\tspotLight = spotLights[ i ];\n\t\tgetSpotDirectLightIrradiance( spotLight, geometry, directLight );\n\t\t#ifdef USE_SHADOWMAP\n\t\tdirectLight.color *= all( bvec2( spotLight.shadow, directLight.visible ) ) ? getShadow( spotShadowMap[ i ], spotLight.shadowMapSize, spotLight.shadowBias, spotLight.shadowRadius, vSpotShadowCoord[ i ] ) : 1.0;\n\t\t#endif\n\t\tRE_Direct( directLight, geometry, material, reflectedLight );\n\t}\n#endif\n#if ( NUM_DIR_LIGHTS > 0 ) && defined( RE_Direct )\n\tDirectionalLight directionalLight;\n\t#pragma unroll_loop\n\tfor ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) {\n\t\tdirectionalLight = directionalLights[ i ];\n\t\tgetDirectionalDirectLightIrradiance( directionalLight, geometry, directLight );\n\t\t#ifdef USE_SHADOWMAP\n\t\tdirectLight.color *= all( bvec2( directionalLight.shadow, directLight.visible ) ) ? getShadow( directionalShadowMap[ i ], directionalLight.shadowMapSize, directionalLight.shadowBias, directionalLight.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0;\n\t\t#endif\n\t\tRE_Direct( directLight, geometry, material, reflectedLight );\n\t}\n#endif\n#if ( NUM_RECT_AREA_LIGHTS > 0 ) && defined( RE_Direct_RectArea )\n\tRectAreaLight rectAreaLight;\n\t#pragma unroll_loop\n\tfor ( int i = 0; i < NUM_RECT_AREA_LIGHTS; i ++ ) {\n\t\trectAreaLight = rectAreaLights[ i ];\n\t\tRE_Direct_RectArea( rectAreaLight, geometry, material, reflectedLight );\n\t}\n#endif\n#if defined( RE_IndirectDiffuse )\n\tvec3 irradiance = getAmbientLightIrradiance( ambientLightColor );\n\t#if ( NUM_HEMI_LIGHTS > 0 )\n\t\t#pragma unroll_loop\n\t\tfor ( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) {\n\t\t\tirradiance += getHemisphereLightIrradiance( hemisphereLights[ i ], geometry );\n\t\t}\n\t#endif\n#endif\n#if defined( RE_IndirectSpecular )\n\tvec3 radiance = vec3( 0.0 );\n\tvec3 clearCoatRadiance = vec3( 0.0 );\n#endif\n", +lights_fragment_maps:"#if defined( RE_IndirectDiffuse )\n\t#ifdef USE_LIGHTMAP\n\t\tvec3 lightMapIrradiance = texture2D( lightMap, vUv2 ).xyz * lightMapIntensity;\n\t\t#ifndef PHYSICALLY_CORRECT_LIGHTS\n\t\t\tlightMapIrradiance *= PI;\n\t\t#endif\n\t\tirradiance += lightMapIrradiance;\n\t#endif\n\t#if defined( USE_ENVMAP ) && defined( PHYSICAL ) && defined( ENVMAP_TYPE_CUBE_UV )\n\t\tirradiance += getLightProbeIndirectIrradiance( geometry, maxMipLevel );\n\t#endif\n#endif\n#if defined( USE_ENVMAP ) && defined( RE_IndirectSpecular )\n\tradiance += getLightProbeIndirectRadiance( geometry, Material_BlinnShininessExponent( material ), maxMipLevel );\n\t#ifndef STANDARD\n\t\tclearCoatRadiance += getLightProbeIndirectRadiance( geometry, Material_ClearCoat_BlinnShininessExponent( material ), maxMipLevel );\n\t#endif\n#endif\n", +lights_fragment_end:"#if defined( RE_IndirectDiffuse )\n\tRE_IndirectDiffuse( irradiance, geometry, material, reflectedLight );\n#endif\n#if defined( RE_IndirectSpecular )\n\tRE_IndirectSpecular( radiance, clearCoatRadiance, geometry, material, reflectedLight );\n#endif\n",logdepthbuf_fragment:"#if defined( USE_LOGDEPTHBUF ) && defined( USE_LOGDEPTHBUF_EXT )\n\tgl_FragDepthEXT = log2( vFragDepth ) * logDepthBufFC * 0.5;\n#endif",logdepthbuf_pars_fragment:"#ifdef USE_LOGDEPTHBUF\n\tuniform float logDepthBufFC;\n\t#ifdef USE_LOGDEPTHBUF_EXT\n\t\tvarying float vFragDepth;\n\t#endif\n#endif\n", +logdepthbuf_pars_vertex:"#ifdef USE_LOGDEPTHBUF\n\t#ifdef USE_LOGDEPTHBUF_EXT\n\t\tvarying float vFragDepth;\n\t#endif\n\tuniform float logDepthBufFC;\n#endif",logdepthbuf_vertex:"#ifdef USE_LOGDEPTHBUF\n\t#ifdef USE_LOGDEPTHBUF_EXT\n\t\tvFragDepth = 1.0 + gl_Position.w;\n\t#else\n\t\tgl_Position.z = log2( max( EPSILON, gl_Position.w + 1.0 ) ) * logDepthBufFC - 1.0;\n\t\tgl_Position.z *= gl_Position.w;\n\t#endif\n#endif\n",map_fragment:"#ifdef USE_MAP\n\tvec4 texelColor = texture2D( map, vUv );\n\ttexelColor = mapTexelToLinear( texelColor );\n\tdiffuseColor *= texelColor;\n#endif\n", +map_pars_fragment:"#ifdef USE_MAP\n\tuniform sampler2D map;\n#endif\n",map_particle_fragment:"#ifdef USE_MAP\n\tvec2 uv = ( uvTransform * vec3( gl_PointCoord.x, 1.0 - gl_PointCoord.y, 1 ) ).xy;\n\tvec4 mapTexel = texture2D( map, uv );\n\tdiffuseColor *= mapTexelToLinear( mapTexel );\n#endif\n",map_particle_pars_fragment:"#ifdef USE_MAP\n\tuniform mat3 uvTransform;\n\tuniform sampler2D map;\n#endif\n",metalnessmap_fragment:"float metalnessFactor = metalness;\n#ifdef USE_METALNESSMAP\n\tvec4 texelMetalness = texture2D( metalnessMap, vUv );\n\tmetalnessFactor *= texelMetalness.b;\n#endif\n", +metalnessmap_pars_fragment:"#ifdef USE_METALNESSMAP\n\tuniform sampler2D metalnessMap;\n#endif",morphnormal_vertex:"#ifdef USE_MORPHNORMALS\n\tobjectNormal += ( morphNormal0 - normal ) * morphTargetInfluences[ 0 ];\n\tobjectNormal += ( morphNormal1 - normal ) * morphTargetInfluences[ 1 ];\n\tobjectNormal += ( morphNormal2 - normal ) * morphTargetInfluences[ 2 ];\n\tobjectNormal += ( morphNormal3 - normal ) * morphTargetInfluences[ 3 ];\n#endif\n",morphtarget_pars_vertex:"#ifdef USE_MORPHTARGETS\n\t#ifndef USE_MORPHNORMALS\n\tuniform float morphTargetInfluences[ 8 ];\n\t#else\n\tuniform float morphTargetInfluences[ 4 ];\n\t#endif\n#endif", +morphtarget_vertex:"#ifdef USE_MORPHTARGETS\n\ttransformed += ( morphTarget0 - position ) * morphTargetInfluences[ 0 ];\n\ttransformed += ( morphTarget1 - position ) * morphTargetInfluences[ 1 ];\n\ttransformed += ( morphTarget2 - position ) * morphTargetInfluences[ 2 ];\n\ttransformed += ( morphTarget3 - position ) * morphTargetInfluences[ 3 ];\n\t#ifndef USE_MORPHNORMALS\n\ttransformed += ( morphTarget4 - position ) * morphTargetInfluences[ 4 ];\n\ttransformed += ( morphTarget5 - position ) * morphTargetInfluences[ 5 ];\n\ttransformed += ( morphTarget6 - position ) * morphTargetInfluences[ 6 ];\n\ttransformed += ( morphTarget7 - position ) * morphTargetInfluences[ 7 ];\n\t#endif\n#endif\n", +normal_fragment_begin:"#ifdef FLAT_SHADED\n\tvec3 fdx = vec3( dFdx( vViewPosition.x ), dFdx( vViewPosition.y ), dFdx( vViewPosition.z ) );\n\tvec3 fdy = vec3( dFdy( vViewPosition.x ), dFdy( vViewPosition.y ), dFdy( vViewPosition.z ) );\n\tvec3 normal = normalize( cross( fdx, fdy ) );\n#else\n\tvec3 normal = normalize( vNormal );\n\t#ifdef DOUBLE_SIDED\n\t\tnormal = normal * ( float( gl_FrontFacing ) * 2.0 - 1.0 );\n\t#endif\n#endif\n",normal_fragment_maps:"#ifdef USE_NORMALMAP\n\tnormal = perturbNormal2Arb( -vViewPosition, normal );\n#elif defined( USE_BUMPMAP )\n\tnormal = perturbNormalArb( -vViewPosition, normal, dHdxy_fwd() );\n#endif\n", +normalmap_pars_fragment:"#ifdef USE_NORMALMAP\n\tuniform sampler2D normalMap;\n\tuniform vec2 normalScale;\n\tvec3 perturbNormal2Arb( vec3 eye_pos, vec3 surf_norm ) {\n\t\tvec3 q0 = vec3( dFdx( eye_pos.x ), dFdx( eye_pos.y ), dFdx( eye_pos.z ) );\n\t\tvec3 q1 = vec3( dFdy( eye_pos.x ), dFdy( eye_pos.y ), dFdy( eye_pos.z ) );\n\t\tvec2 st0 = dFdx( vUv.st );\n\t\tvec2 st1 = dFdy( vUv.st );\n\t\tvec3 S = normalize( q0 * st1.t - q1 * st0.t );\n\t\tvec3 T = normalize( -q0 * st1.s + q1 * st0.s );\n\t\tvec3 N = normalize( surf_norm );\n\t\tvec3 mapN = texture2D( normalMap, vUv ).xyz * 2.0 - 1.0;\n\t\tmapN.xy = normalScale * mapN.xy;\n\t\tmat3 tsn = mat3( S, T, N );\n\t\treturn normalize( tsn * mapN );\n\t}\n#endif\n", +packing:"vec3 packNormalToRGB( const in vec3 normal ) {\n\treturn normalize( normal ) * 0.5 + 0.5;\n}\nvec3 unpackRGBToNormal( const in vec3 rgb ) {\n\treturn 2.0 * rgb.xyz - 1.0;\n}\nconst float PackUpscale = 256. / 255.;const float UnpackDownscale = 255. / 256.;\nconst vec3 PackFactors = vec3( 256. * 256. * 256., 256. * 256., 256. );\nconst vec4 UnpackFactors = UnpackDownscale / vec4( PackFactors, 1. );\nconst float ShiftRight8 = 1. / 256.;\nvec4 packDepthToRGBA( const in float v ) {\n\tvec4 r = vec4( fract( v * PackFactors ), v );\n\tr.yzw -= r.xyz * ShiftRight8;\treturn r * PackUpscale;\n}\nfloat unpackRGBAToDepth( const in vec4 v ) {\n\treturn dot( v, UnpackFactors );\n}\nfloat viewZToOrthographicDepth( const in float viewZ, const in float near, const in float far ) {\n\treturn ( viewZ + near ) / ( near - far );\n}\nfloat orthographicDepthToViewZ( const in float linearClipZ, const in float near, const in float far ) {\n\treturn linearClipZ * ( near - far ) - near;\n}\nfloat viewZToPerspectiveDepth( const in float viewZ, const in float near, const in float far ) {\n\treturn (( near + viewZ ) * far ) / (( far - near ) * viewZ );\n}\nfloat perspectiveDepthToViewZ( const in float invClipZ, const in float near, const in float far ) {\n\treturn ( near * far ) / ( ( far - near ) * invClipZ - far );\n}\n", +premultiplied_alpha_fragment:"#ifdef PREMULTIPLIED_ALPHA\n\tgl_FragColor.rgb *= gl_FragColor.a;\n#endif\n",project_vertex:"vec4 mvPosition = modelViewMatrix * vec4( transformed, 1.0 );\ngl_Position = projectionMatrix * mvPosition;\n",dithering_fragment:"#if defined( DITHERING )\n gl_FragColor.rgb = dithering( gl_FragColor.rgb );\n#endif\n",dithering_pars_fragment:"#if defined( DITHERING )\n\tvec3 dithering( vec3 color ) {\n\t\tfloat grid_position = rand( gl_FragCoord.xy );\n\t\tvec3 dither_shift_RGB = vec3( 0.25 / 255.0, -0.25 / 255.0, 0.25 / 255.0 );\n\t\tdither_shift_RGB = mix( 2.0 * dither_shift_RGB, -2.0 * dither_shift_RGB, grid_position );\n\t\treturn color + dither_shift_RGB;\n\t}\n#endif\n", +roughnessmap_fragment:"float roughnessFactor = roughness;\n#ifdef USE_ROUGHNESSMAP\n\tvec4 texelRoughness = texture2D( roughnessMap, vUv );\n\troughnessFactor *= texelRoughness.g;\n#endif\n",roughnessmap_pars_fragment:"#ifdef USE_ROUGHNESSMAP\n\tuniform sampler2D roughnessMap;\n#endif",shadowmap_pars_fragment:"#ifdef USE_SHADOWMAP\n\t#if NUM_DIR_LIGHTS > 0\n\t\tuniform sampler2D directionalShadowMap[ NUM_DIR_LIGHTS ];\n\t\tvarying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHTS ];\n\t#endif\n\t#if NUM_SPOT_LIGHTS > 0\n\t\tuniform sampler2D spotShadowMap[ NUM_SPOT_LIGHTS ];\n\t\tvarying vec4 vSpotShadowCoord[ NUM_SPOT_LIGHTS ];\n\t#endif\n\t#if NUM_POINT_LIGHTS > 0\n\t\tuniform sampler2D pointShadowMap[ NUM_POINT_LIGHTS ];\n\t\tvarying vec4 vPointShadowCoord[ NUM_POINT_LIGHTS ];\n\t#endif\n\tfloat texture2DCompare( sampler2D depths, vec2 uv, float compare ) {\n\t\treturn step( compare, unpackRGBAToDepth( texture2D( depths, uv ) ) );\n\t}\n\tfloat texture2DShadowLerp( sampler2D depths, vec2 size, vec2 uv, float compare ) {\n\t\tconst vec2 offset = vec2( 0.0, 1.0 );\n\t\tvec2 texelSize = vec2( 1.0 ) / size;\n\t\tvec2 centroidUV = floor( uv * size + 0.5 ) / size;\n\t\tfloat lb = texture2DCompare( depths, centroidUV + texelSize * offset.xx, compare );\n\t\tfloat lt = texture2DCompare( depths, centroidUV + texelSize * offset.xy, compare );\n\t\tfloat rb = texture2DCompare( depths, centroidUV + texelSize * offset.yx, compare );\n\t\tfloat rt = texture2DCompare( depths, centroidUV + texelSize * offset.yy, compare );\n\t\tvec2 f = fract( uv * size + 0.5 );\n\t\tfloat a = mix( lb, lt, f.y );\n\t\tfloat b = mix( rb, rt, f.y );\n\t\tfloat c = mix( a, b, f.x );\n\t\treturn c;\n\t}\n\tfloat getShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowBias, float shadowRadius, vec4 shadowCoord ) {\n\t\tfloat shadow = 1.0;\n\t\tshadowCoord.xyz /= shadowCoord.w;\n\t\tshadowCoord.z += shadowBias;\n\t\tbvec4 inFrustumVec = bvec4 ( shadowCoord.x >= 0.0, shadowCoord.x <= 1.0, shadowCoord.y >= 0.0, shadowCoord.y <= 1.0 );\n\t\tbool inFrustum = all( inFrustumVec );\n\t\tbvec2 frustumTestVec = bvec2( inFrustum, shadowCoord.z <= 1.0 );\n\t\tbool frustumTest = all( frustumTestVec );\n\t\tif ( frustumTest ) {\n\t\t#if defined( SHADOWMAP_TYPE_PCF )\n\t\t\tvec2 texelSize = vec2( 1.0 ) / shadowMapSize;\n\t\t\tfloat dx0 = - texelSize.x * shadowRadius;\n\t\t\tfloat dy0 = - texelSize.y * shadowRadius;\n\t\t\tfloat dx1 = + texelSize.x * shadowRadius;\n\t\t\tfloat dy1 = + texelSize.y * shadowRadius;\n\t\t\tshadow = (\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, dy0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, dy0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy, shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, dy1 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy1 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, dy1 ), shadowCoord.z )\n\t\t\t) * ( 1.0 / 9.0 );\n\t\t#elif defined( SHADOWMAP_TYPE_PCF_SOFT )\n\t\t\tvec2 texelSize = vec2( 1.0 ) / shadowMapSize;\n\t\t\tfloat dx0 = - texelSize.x * shadowRadius;\n\t\t\tfloat dy0 = - texelSize.y * shadowRadius;\n\t\t\tfloat dx1 = + texelSize.x * shadowRadius;\n\t\t\tfloat dy1 = + texelSize.y * shadowRadius;\n\t\t\tshadow = (\n\t\t\t\ttexture2DShadowLerp( shadowMap, shadowMapSize, shadowCoord.xy + vec2( dx0, dy0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DShadowLerp( shadowMap, shadowMapSize, shadowCoord.xy + vec2( 0.0, dy0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DShadowLerp( shadowMap, shadowMapSize, shadowCoord.xy + vec2( dx1, dy0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DShadowLerp( shadowMap, shadowMapSize, shadowCoord.xy + vec2( dx0, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DShadowLerp( shadowMap, shadowMapSize, shadowCoord.xy, shadowCoord.z ) +\n\t\t\t\ttexture2DShadowLerp( shadowMap, shadowMapSize, shadowCoord.xy + vec2( dx1, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DShadowLerp( shadowMap, shadowMapSize, shadowCoord.xy + vec2( dx0, dy1 ), shadowCoord.z ) +\n\t\t\t\ttexture2DShadowLerp( shadowMap, shadowMapSize, shadowCoord.xy + vec2( 0.0, dy1 ), shadowCoord.z ) +\n\t\t\t\ttexture2DShadowLerp( shadowMap, shadowMapSize, shadowCoord.xy + vec2( dx1, dy1 ), shadowCoord.z )\n\t\t\t) * ( 1.0 / 9.0 );\n\t\t#else\n\t\t\tshadow = texture2DCompare( shadowMap, shadowCoord.xy, shadowCoord.z );\n\t\t#endif\n\t\t}\n\t\treturn shadow;\n\t}\n\tvec2 cubeToUV( vec3 v, float texelSizeY ) {\n\t\tvec3 absV = abs( v );\n\t\tfloat scaleToCube = 1.0 / max( absV.x, max( absV.y, absV.z ) );\n\t\tabsV *= scaleToCube;\n\t\tv *= scaleToCube * ( 1.0 - 2.0 * texelSizeY );\n\t\tvec2 planar = v.xy;\n\t\tfloat almostATexel = 1.5 * texelSizeY;\n\t\tfloat almostOne = 1.0 - almostATexel;\n\t\tif ( absV.z >= almostOne ) {\n\t\t\tif ( v.z > 0.0 )\n\t\t\t\tplanar.x = 4.0 - v.x;\n\t\t} else if ( absV.x >= almostOne ) {\n\t\t\tfloat signX = sign( v.x );\n\t\t\tplanar.x = v.z * signX + 2.0 * signX;\n\t\t} else if ( absV.y >= almostOne ) {\n\t\t\tfloat signY = sign( v.y );\n\t\t\tplanar.x = v.x + 2.0 * signY + 2.0;\n\t\t\tplanar.y = v.z * signY - 2.0;\n\t\t}\n\t\treturn vec2( 0.125, 0.25 ) * planar + vec2( 0.375, 0.75 );\n\t}\n\tfloat getPointShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowBias, float shadowRadius, vec4 shadowCoord, float shadowCameraNear, float shadowCameraFar ) {\n\t\tvec2 texelSize = vec2( 1.0 ) / ( shadowMapSize * vec2( 4.0, 2.0 ) );\n\t\tvec3 lightToPosition = shadowCoord.xyz;\n\t\tfloat dp = ( length( lightToPosition ) - shadowCameraNear ) / ( shadowCameraFar - shadowCameraNear );\t\tdp += shadowBias;\n\t\tvec3 bd3D = normalize( lightToPosition );\n\t\t#if defined( SHADOWMAP_TYPE_PCF ) || defined( SHADOWMAP_TYPE_PCF_SOFT )\n\t\t\tvec2 offset = vec2( - 1, 1 ) * shadowRadius * texelSize.y;\n\t\t\treturn (\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.xyy, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.yyy, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.xyx, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.yyx, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.xxy, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.yxy, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.xxx, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.yxx, texelSize.y ), dp )\n\t\t\t) * ( 1.0 / 9.0 );\n\t\t#else\n\t\t\treturn texture2DCompare( shadowMap, cubeToUV( bd3D, texelSize.y ), dp );\n\t\t#endif\n\t}\n#endif\n", +shadowmap_pars_vertex:"#ifdef USE_SHADOWMAP\n\t#if NUM_DIR_LIGHTS > 0\n\t\tuniform mat4 directionalShadowMatrix[ NUM_DIR_LIGHTS ];\n\t\tvarying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHTS ];\n\t#endif\n\t#if NUM_SPOT_LIGHTS > 0\n\t\tuniform mat4 spotShadowMatrix[ NUM_SPOT_LIGHTS ];\n\t\tvarying vec4 vSpotShadowCoord[ NUM_SPOT_LIGHTS ];\n\t#endif\n\t#if NUM_POINT_LIGHTS > 0\n\t\tuniform mat4 pointShadowMatrix[ NUM_POINT_LIGHTS ];\n\t\tvarying vec4 vPointShadowCoord[ NUM_POINT_LIGHTS ];\n\t#endif\n#endif\n", +shadowmap_vertex:"#ifdef USE_SHADOWMAP\n\t#if NUM_DIR_LIGHTS > 0\n\t#pragma unroll_loop\n\tfor ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) {\n\t\tvDirectionalShadowCoord[ i ] = directionalShadowMatrix[ i ] * worldPosition;\n\t}\n\t#endif\n\t#if NUM_SPOT_LIGHTS > 0\n\t#pragma unroll_loop\n\tfor ( int i = 0; i < NUM_SPOT_LIGHTS; i ++ ) {\n\t\tvSpotShadowCoord[ i ] = spotShadowMatrix[ i ] * worldPosition;\n\t}\n\t#endif\n\t#if NUM_POINT_LIGHTS > 0\n\t#pragma unroll_loop\n\tfor ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {\n\t\tvPointShadowCoord[ i ] = pointShadowMatrix[ i ] * worldPosition;\n\t}\n\t#endif\n#endif\n", +shadowmask_pars_fragment:"float getShadowMask() {\n\tfloat shadow = 1.0;\n\t#ifdef USE_SHADOWMAP\n\t#if NUM_DIR_LIGHTS > 0\n\tDirectionalLight directionalLight;\n\t#pragma unroll_loop\n\tfor ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) {\n\t\tdirectionalLight = directionalLights[ i ];\n\t\tshadow *= bool( directionalLight.shadow ) ? getShadow( directionalShadowMap[ i ], directionalLight.shadowMapSize, directionalLight.shadowBias, directionalLight.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0;\n\t}\n\t#endif\n\t#if NUM_SPOT_LIGHTS > 0\n\tSpotLight spotLight;\n\t#pragma unroll_loop\n\tfor ( int i = 0; i < NUM_SPOT_LIGHTS; i ++ ) {\n\t\tspotLight = spotLights[ i ];\n\t\tshadow *= bool( spotLight.shadow ) ? getShadow( spotShadowMap[ i ], spotLight.shadowMapSize, spotLight.shadowBias, spotLight.shadowRadius, vSpotShadowCoord[ i ] ) : 1.0;\n\t}\n\t#endif\n\t#if NUM_POINT_LIGHTS > 0\n\tPointLight pointLight;\n\t#pragma unroll_loop\n\tfor ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {\n\t\tpointLight = pointLights[ i ];\n\t\tshadow *= bool( pointLight.shadow ) ? getPointShadow( pointShadowMap[ i ], pointLight.shadowMapSize, pointLight.shadowBias, pointLight.shadowRadius, vPointShadowCoord[ i ], pointLight.shadowCameraNear, pointLight.shadowCameraFar ) : 1.0;\n\t}\n\t#endif\n\t#endif\n\treturn shadow;\n}\n", +skinbase_vertex:"#ifdef USE_SKINNING\n\tmat4 boneMatX = getBoneMatrix( skinIndex.x );\n\tmat4 boneMatY = getBoneMatrix( skinIndex.y );\n\tmat4 boneMatZ = getBoneMatrix( skinIndex.z );\n\tmat4 boneMatW = getBoneMatrix( skinIndex.w );\n#endif",skinning_pars_vertex:"#ifdef USE_SKINNING\n\tuniform mat4 bindMatrix;\n\tuniform mat4 bindMatrixInverse;\n\t#ifdef BONE_TEXTURE\n\t\tuniform sampler2D boneTexture;\n\t\tuniform int boneTextureSize;\n\t\tmat4 getBoneMatrix( const in float i ) {\n\t\t\tfloat j = i * 4.0;\n\t\t\tfloat x = mod( j, float( boneTextureSize ) );\n\t\t\tfloat y = floor( j / float( boneTextureSize ) );\n\t\t\tfloat dx = 1.0 / float( boneTextureSize );\n\t\t\tfloat dy = 1.0 / float( boneTextureSize );\n\t\t\ty = dy * ( y + 0.5 );\n\t\t\tvec4 v1 = texture2D( boneTexture, vec2( dx * ( x + 0.5 ), y ) );\n\t\t\tvec4 v2 = texture2D( boneTexture, vec2( dx * ( x + 1.5 ), y ) );\n\t\t\tvec4 v3 = texture2D( boneTexture, vec2( dx * ( x + 2.5 ), y ) );\n\t\t\tvec4 v4 = texture2D( boneTexture, vec2( dx * ( x + 3.5 ), y ) );\n\t\t\tmat4 bone = mat4( v1, v2, v3, v4 );\n\t\t\treturn bone;\n\t\t}\n\t#else\n\t\tuniform mat4 boneMatrices[ MAX_BONES ];\n\t\tmat4 getBoneMatrix( const in float i ) {\n\t\t\tmat4 bone = boneMatrices[ int(i) ];\n\t\t\treturn bone;\n\t\t}\n\t#endif\n#endif\n", +skinning_vertex:"#ifdef USE_SKINNING\n\tvec4 skinVertex = bindMatrix * vec4( transformed, 1.0 );\n\tvec4 skinned = vec4( 0.0 );\n\tskinned += boneMatX * skinVertex * skinWeight.x;\n\tskinned += boneMatY * skinVertex * skinWeight.y;\n\tskinned += boneMatZ * skinVertex * skinWeight.z;\n\tskinned += boneMatW * skinVertex * skinWeight.w;\n\ttransformed = ( bindMatrixInverse * skinned ).xyz;\n#endif\n",skinnormal_vertex:"#ifdef USE_SKINNING\n\tmat4 skinMatrix = mat4( 0.0 );\n\tskinMatrix += skinWeight.x * boneMatX;\n\tskinMatrix += skinWeight.y * boneMatY;\n\tskinMatrix += skinWeight.z * boneMatZ;\n\tskinMatrix += skinWeight.w * boneMatW;\n\tskinMatrix = bindMatrixInverse * skinMatrix * bindMatrix;\n\tobjectNormal = vec4( skinMatrix * vec4( objectNormal, 0.0 ) ).xyz;\n#endif\n", +specularmap_fragment:"float specularStrength;\n#ifdef USE_SPECULARMAP\n\tvec4 texelSpecular = texture2D( specularMap, vUv );\n\tspecularStrength = texelSpecular.r;\n#else\n\tspecularStrength = 1.0;\n#endif",specularmap_pars_fragment:"#ifdef USE_SPECULARMAP\n\tuniform sampler2D specularMap;\n#endif",tonemapping_fragment:"#if defined( TONE_MAPPING )\n gl_FragColor.rgb = toneMapping( gl_FragColor.rgb );\n#endif\n",tonemapping_pars_fragment:"#ifndef saturate\n\t#define saturate(a) clamp( a, 0.0, 1.0 )\n#endif\nuniform float toneMappingExposure;\nuniform float toneMappingWhitePoint;\nvec3 LinearToneMapping( vec3 color ) {\n\treturn toneMappingExposure * color;\n}\nvec3 ReinhardToneMapping( vec3 color ) {\n\tcolor *= toneMappingExposure;\n\treturn saturate( color / ( vec3( 1.0 ) + color ) );\n}\n#define Uncharted2Helper( x ) max( ( ( x * ( 0.15 * x + 0.10 * 0.50 ) + 0.20 * 0.02 ) / ( x * ( 0.15 * x + 0.50 ) + 0.20 * 0.30 ) ) - 0.02 / 0.30, vec3( 0.0 ) )\nvec3 Uncharted2ToneMapping( vec3 color ) {\n\tcolor *= toneMappingExposure;\n\treturn saturate( Uncharted2Helper( color ) / Uncharted2Helper( vec3( toneMappingWhitePoint ) ) );\n}\nvec3 OptimizedCineonToneMapping( vec3 color ) {\n\tcolor *= toneMappingExposure;\n\tcolor = max( vec3( 0.0 ), color - 0.004 );\n\treturn pow( ( color * ( 6.2 * color + 0.5 ) ) / ( color * ( 6.2 * color + 1.7 ) + 0.06 ), vec3( 2.2 ) );\n}\n", +uv_pars_fragment:"#if defined( USE_MAP ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( USE_SPECULARMAP ) || defined( USE_ALPHAMAP ) || defined( USE_EMISSIVEMAP ) || defined( USE_ROUGHNESSMAP ) || defined( USE_METALNESSMAP )\n\tvarying vec2 vUv;\n#endif",uv_pars_vertex:"#if defined( USE_MAP ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( USE_SPECULARMAP ) || defined( USE_ALPHAMAP ) || defined( USE_EMISSIVEMAP ) || defined( USE_ROUGHNESSMAP ) || defined( USE_METALNESSMAP )\n\tvarying vec2 vUv;\n\tuniform mat3 uvTransform;\n#endif\n", +uv_vertex:"#if defined( USE_MAP ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( USE_SPECULARMAP ) || defined( USE_ALPHAMAP ) || defined( USE_EMISSIVEMAP ) || defined( USE_ROUGHNESSMAP ) || defined( USE_METALNESSMAP )\n\tvUv = ( uvTransform * vec3( uv, 1 ) ).xy;\n#endif",uv2_pars_fragment:"#if defined( USE_LIGHTMAP ) || defined( USE_AOMAP )\n\tvarying vec2 vUv2;\n#endif",uv2_pars_vertex:"#if defined( USE_LIGHTMAP ) || defined( USE_AOMAP )\n\tattribute vec2 uv2;\n\tvarying vec2 vUv2;\n#endif", +uv2_vertex:"#if defined( USE_LIGHTMAP ) || defined( USE_AOMAP )\n\tvUv2 = uv2;\n#endif",worldpos_vertex:"#if defined( USE_ENVMAP ) || defined( DISTANCE ) || defined ( USE_SHADOWMAP )\n\tvec4 worldPosition = modelMatrix * vec4( transformed, 1.0 );\n#endif\n",cube_frag:"uniform samplerCube tCube;\nuniform float tFlip;\nuniform float opacity;\nvarying vec3 vWorldPosition;\nvoid main() {\n\tgl_FragColor = textureCube( tCube, vec3( tFlip * vWorldPosition.x, vWorldPosition.yz ) );\n\tgl_FragColor.a *= opacity;\n}\n", +cube_vert:"varying vec3 vWorldPosition;\n#include \nvoid main() {\n\tvWorldPosition = transformDirection( position, modelMatrix );\n\t#include \n\t#include \n\tgl_Position.z = gl_Position.w;\n}\n",depth_frag:"#if DEPTH_PACKING == 3200\n\tuniform float opacity;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tvec4 diffuseColor = vec4( 1.0 );\n\t#if DEPTH_PACKING == 3200\n\t\tdiffuseColor.a = opacity;\n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#if DEPTH_PACKING == 3200\n\t\tgl_FragColor = vec4( vec3( 1.0 - gl_FragCoord.z ), opacity );\n\t#elif DEPTH_PACKING == 3201\n\t\tgl_FragColor = packDepthToRGBA( gl_FragCoord.z );\n\t#endif\n}\n", +depth_vert:"#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#ifdef USE_DISPLACEMENTMAP\n\t\t#include \n\t\t#include \n\t\t#include \n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}\n", +distanceRGBA_frag:"#define DISTANCE\nuniform vec3 referencePosition;\nuniform float nearDistance;\nuniform float farDistance;\nvarying vec3 vWorldPosition;\n#include \n#include \n#include \n#include \n#include \n#include \nvoid main () {\n\t#include \n\tvec4 diffuseColor = vec4( 1.0 );\n\t#include \n\t#include \n\t#include \n\tfloat dist = length( vWorldPosition - referencePosition );\n\tdist = ( dist - nearDistance ) / ( farDistance - nearDistance );\n\tdist = saturate( dist );\n\tgl_FragColor = packDepthToRGBA( dist );\n}\n", +distanceRGBA_vert:"#define DISTANCE\nvarying vec3 vWorldPosition;\n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#ifdef USE_DISPLACEMENTMAP\n\t\t#include \n\t\t#include \n\t\t#include \n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvWorldPosition = worldPosition.xyz;\n}\n", +equirect_frag:"uniform sampler2D tEquirect;\nvarying vec3 vWorldPosition;\n#include \nvoid main() {\n\tvec3 direction = normalize( vWorldPosition );\n\tvec2 sampleUV;\n\tsampleUV.y = asin( clamp( direction.y, - 1.0, 1.0 ) ) * RECIPROCAL_PI + 0.5;\n\tsampleUV.x = atan( direction.z, direction.x ) * RECIPROCAL_PI2 + 0.5;\n\tgl_FragColor = texture2D( tEquirect, sampleUV );\n}\n",equirect_vert:"varying vec3 vWorldPosition;\n#include \nvoid main() {\n\tvWorldPosition = transformDirection( position, modelMatrix );\n\t#include \n\t#include \n}\n", +linedashed_frag:"uniform vec3 diffuse;\nuniform float opacity;\nuniform float dashSize;\nuniform float totalSize;\nvarying float vLineDistance;\n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tif ( mod( vLineDistance, totalSize ) > dashSize ) {\n\t\tdiscard;\n\t}\n\tvec3 outgoingLight = vec3( 0.0 );\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\t#include \n\toutgoingLight = diffuseColor.rgb;\n\tgl_FragColor = vec4( outgoingLight, diffuseColor.a );\n\t#include \n\t#include \n\t#include \n\t#include \n}\n", +linedashed_vert:"uniform float scale;\nattribute float lineDistance;\nvarying float vLineDistance;\n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tvLineDistance = scale * lineDistance;\n\tvec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );\n\tgl_Position = projectionMatrix * mvPosition;\n\t#include \n\t#include \n\t#include \n}\n", +meshbasic_frag:"uniform vec3 diffuse;\nuniform float opacity;\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\t#ifdef USE_LIGHTMAP\n\t\treflectedLight.indirectDiffuse += texture2D( lightMap, vUv2 ).xyz * lightMapIntensity;\n\t#else\n\t\treflectedLight.indirectDiffuse += vec3( 1.0 );\n\t#endif\n\t#include \n\treflectedLight.indirectDiffuse *= diffuseColor.rgb;\n\tvec3 outgoingLight = reflectedLight.indirectDiffuse;\n\t#include \n\tgl_FragColor = vec4( outgoingLight, diffuseColor.a );\n\t#include \n\t#include \n\t#include \n\t#include \n}\n", +meshbasic_vert:"#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#ifdef USE_ENVMAP\n\t#include \n\t#include \n\t#include \n\t#include \n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}\n", +meshlambert_frag:"uniform vec3 diffuse;\nuniform vec3 emissive;\nuniform float opacity;\nvarying vec3 vLightFront;\n#ifdef DOUBLE_SIDED\n\tvarying vec3 vLightBack;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\treflectedLight.indirectDiffuse = getAmbientLightIrradiance( ambientLightColor );\n\t#include \n\treflectedLight.indirectDiffuse *= BRDF_Diffuse_Lambert( diffuseColor.rgb );\n\t#ifdef DOUBLE_SIDED\n\t\treflectedLight.directDiffuse = ( gl_FrontFacing ) ? vLightFront : vLightBack;\n\t#else\n\t\treflectedLight.directDiffuse = vLightFront;\n\t#endif\n\treflectedLight.directDiffuse *= BRDF_Diffuse_Lambert( diffuseColor.rgb ) * getShadowMask();\n\t#include \n\tvec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance;\n\t#include \n\tgl_FragColor = vec4( outgoingLight, diffuseColor.a );\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}\n", +meshlambert_vert:"#define LAMBERT\nvarying vec3 vLightFront;\n#ifdef DOUBLE_SIDED\n\tvarying vec3 vLightBack;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}\n", +meshphong_frag:"#define PHONG\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform vec3 specular;\nuniform float shininess;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance;\n\t#include \n\tgl_FragColor = vec4( outgoingLight, diffuseColor.a );\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}\n", +meshphong_vert:"#define PHONG\nvarying vec3 vViewPosition;\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n#ifndef FLAT_SHADED\n\tvNormal = normalize( transformedNormal );\n#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvViewPosition = - mvPosition.xyz;\n\t#include \n\t#include \n\t#include \n\t#include \n}\n", +meshphysical_frag:"#define PHYSICAL\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform float roughness;\nuniform float metalness;\nuniform float opacity;\n#ifndef STANDARD\n\tuniform float clearCoat;\n\tuniform float clearCoatRoughness;\n#endif\nvarying vec3 vViewPosition;\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance;\n\tgl_FragColor = vec4( outgoingLight, diffuseColor.a );\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}\n", +meshphysical_vert:"#define PHYSICAL\nvarying vec3 vViewPosition;\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n#ifndef FLAT_SHADED\n\tvNormal = normalize( transformedNormal );\n#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvViewPosition = - mvPosition.xyz;\n\t#include \n\t#include \n\t#include \n}\n", +normal_frag:"#define NORMAL\nuniform float opacity;\n#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP )\n\tvarying vec3 vViewPosition;\n#endif\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n#endif\n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\tgl_FragColor = vec4( packNormalToRGB( normal ), opacity );\n}\n", +normal_vert:"#define NORMAL\n#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP )\n\tvarying vec3 vViewPosition;\n#endif\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n#endif\n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n#ifndef FLAT_SHADED\n\tvNormal = normalize( transformedNormal );\n#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP )\n\tvViewPosition = - mvPosition.xyz;\n#endif\n}\n", +points_frag:"uniform vec3 diffuse;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tvec3 outgoingLight = vec3( 0.0 );\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\t#include \n\t#include \n\t#include \n\toutgoingLight = diffuseColor.rgb;\n\tgl_FragColor = vec4( outgoingLight, diffuseColor.a );\n\t#include \n\t#include \n\t#include \n\t#include \n}\n", +points_vert:"uniform float size;\nuniform float scale;\n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#ifdef USE_SIZEATTENUATION\n\t\tgl_PointSize = size * ( scale / - mvPosition.z );\n\t#else\n\t\tgl_PointSize = size;\n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}\n", +shadow_frag:"uniform vec3 color;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tgl_FragColor = vec4( color, opacity * ( 1.0 - getShadowMask() ) );\n\t#include \n}\n",shadow_vert:"#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}\n"}, +Da={merge:function(a){for(var b={},c=0;c>16&255)/255;this.g=(a>>8&255)/255;this.b=(a&255)/255;return this},setRGB:function(a,b,c){this.r=a;this.g=b;this.b=c;return this},setHSL:function(){function a(a,c,d){0>d&&(d+=1);1d?c:d<2/3?a+6*(c-a)*(2/3-d):a}return function(b, +c,d){b=S.euclideanModulo(b,1);c=S.clamp(c,0,1);d=S.clamp(d,0,1);0===c?this.r=this.g=this.b=d:(c=.5>=d?d*(1+c):d+c-d*c,d=2*d-c,this.r=a(d,c,b+1/3),this.g=a(d,c,b),this.b=a(d,c,b-1/3));return this}}(),setStyle:function(a){function b(b){void 0!==b&&1>parseFloat(b)&&console.warn("THREE.Color: Alpha component of "+a+" will be ignored.")}var c;if(c=/^((?:rgb|hsl)a?)\(\s*([^\)]*)\)/.exec(a)){var d=c[2];switch(c[1]){case "rgb":case "rgba":if(c=/^(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(,\s*([0-9]*\.?[0-9]+)\s*)?$/.exec(d))return this.r= +Math.min(255,parseInt(c[1],10))/255,this.g=Math.min(255,parseInt(c[2],10))/255,this.b=Math.min(255,parseInt(c[3],10))/255,b(c[5]),this;if(c=/^(\d+)%\s*,\s*(\d+)%\s*,\s*(\d+)%\s*(,\s*([0-9]*\.?[0-9]+)\s*)?$/.exec(d))return this.r=Math.min(100,parseInt(c[1],10))/100,this.g=Math.min(100,parseInt(c[2],10))/100,this.b=Math.min(100,parseInt(c[3],10))/100,b(c[5]),this;break;case "hsl":case "hsla":if(c=/^([0-9]*\.?[0-9]+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*(,\s*([0-9]*\.?[0-9]+)\s*)?$/.exec(d)){d=parseFloat(c[1])/ +360;var e=parseInt(c[2],10)/100,f=parseInt(c[3],10)/100;b(c[5]);return this.setHSL(d,e,f)}}}else if(c=/^#([A-Fa-f0-9]+)$/.exec(a)){c=c[1];d=c.length;if(3===d)return this.r=parseInt(c.charAt(0)+c.charAt(0),16)/255,this.g=parseInt(c.charAt(1)+c.charAt(1),16)/255,this.b=parseInt(c.charAt(2)+c.charAt(2),16)/255,this;if(6===d)return this.r=parseInt(c.charAt(0)+c.charAt(1),16)/255,this.g=parseInt(c.charAt(2)+c.charAt(3),16)/255,this.b=parseInt(c.charAt(4)+c.charAt(5),16)/255,this}a&&0=h?l/(e+f):l/(2-e-f);switch(e){case b:g=(c-d)/l+(cMath.abs(g)?(this._x=Math.atan2(-m,e),this._z=Math.atan2(-f,a)):(this._x=Math.atan2(n,l),this._z=0)):"YXZ"===b?(this._x=Math.asin(-d(m,-1,1)),.99999>Math.abs(m)? +(this._y=Math.atan2(g,e),this._z=Math.atan2(h,l)):(this._y=Math.atan2(-k,a),this._z=0)):"ZXY"===b?(this._x=Math.asin(d(n,-1,1)),.99999>Math.abs(n)?(this._y=Math.atan2(-k,e),this._z=Math.atan2(-f,l)):(this._y=0,this._z=Math.atan2(h,a))):"ZYX"===b?(this._y=Math.asin(-d(k,-1,1)),.99999>Math.abs(k)?(this._x=Math.atan2(n,e),this._z=Math.atan2(h,a)):(this._x=0,this._z=Math.atan2(-f,l))):"YZX"===b?(this._z=Math.asin(d(h,-1,1)),.99999>Math.abs(h)?(this._x=Math.atan2(-m,l),this._y=Math.atan2(-k,a)):(this._x= +0,this._y=Math.atan2(g,e))):"XZY"===b?(this._z=Math.asin(-d(f,-1,1)),.99999>Math.abs(f)?(this._x=Math.atan2(n,l),this._y=Math.atan2(g,a)):(this._x=Math.atan2(-m,e),this._y=0)):console.warn("THREE.Euler: .setFromRotationMatrix() given unsupported order: "+b);this._order=b;if(!1!==c)this.onChangeCallback();return this},setFromQuaternion:function(){var a=new M;return function(b,c,d){a.makeRotationFromQuaternion(b);return this.setFromRotationMatrix(a,c,d)}}(),setFromVector3:function(a,b){return this.set(a.x, +a.y,a.z,b||this._order)},reorder:function(){var a=new ja;return function(b){a.setFromEuler(this);return this.setFromQuaternion(a,b)}}(),equals:function(a){return a._x===this._x&&a._y===this._y&&a._z===this._z&&a._order===this._order},fromArray:function(a){this._x=a[0];this._y=a[1];this._z=a[2];void 0!==a[3]&&(this._order=a[3]);this.onChangeCallback();return this},toArray:function(a,b){void 0===a&&(a=[]);void 0===b&&(b=0);a[b]=this._x;a[b+1]=this._y;a[b+2]=this._z;a[b+3]=this._order;return a},toVector3:function(a){return a? +a.set(this._x,this._y,this._z):new p(this._x,this._y,this._z)},onChange:function(a){this.onChangeCallback=a;return this},onChangeCallback:function(){}});Object.assign(Sd.prototype,{set:function(a){this.mask=1<g;g++)if(d[g]===d[(g+1)%3]){a.push(f);break}for(f=a.length-1;0<=f;f--)for(d=a[f],this.faces.splice(d,1),c=0,e=this.faceVertexUvs.length;cthis.opacity&&(d.opacity=this.opacity); +!0===this.transparent&&(d.transparent=this.transparent);d.depthFunc=this.depthFunc;d.depthTest=this.depthTest;d.depthWrite=this.depthWrite;0!==this.rotation&&(d.rotation=this.rotation);1!==this.linewidth&&(d.linewidth=this.linewidth);void 0!==this.dashSize&&(d.dashSize=this.dashSize);void 0!==this.gapSize&&(d.gapSize=this.gapSize);void 0!==this.scale&&(d.scale=this.scale);!0===this.dithering&&(d.dithering=!0);0 +a?b.copy(this.origin):b.copy(this.direction).multiplyScalar(a).add(this.origin)},distanceToPoint:function(a){return Math.sqrt(this.distanceSqToPoint(a))},distanceSqToPoint:function(){var a=new p;return function(b){var c=a.subVectors(b,this.origin).dot(this.direction);if(0>c)return this.origin.distanceToSquared(b);a.copy(this.direction).multiplyScalar(c).add(this.origin);return a.distanceToSquared(b)}}(),distanceSqToSegment:function(){var a=new p,b=new p,c=new p;return function(d,e,f,g){a.copy(d).add(e).multiplyScalar(.5); +b.copy(e).sub(d).normalize();c.copy(this.origin).sub(a);var h=.5*d.distanceTo(e),l=-this.direction.dot(b),m=c.dot(this.direction),k=-c.dot(b),n=c.lengthSq(),p=Math.abs(1-l*l);if(0=-r?e<=r?(h=1/p,d*=h,e*=h,l=d*(d+l*e+2*m)+e*(l*d+e+2*k)+n):(e=h,d=Math.max(0,-(l*e+m)),l=-d*d+e*(e+2*k)+n):(e=-h,d=Math.max(0,-(l*e+m)),l=-d*d+e*(e+2*k)+n):e<=-r?(d=Math.max(0,-(-l*h+m)),e=0b)return null;b=Math.sqrt(b-e);e=d-b;d+=b;return 0>e&&0>d?null:0>e?this.at(d, +c):this.at(e,c)}}(),intersectsSphere:function(a){return this.distanceToPoint(a.center)<=a.radius},distanceToPlane:function(a){var b=a.normal.dot(this.direction);if(0===b)return 0===a.distanceToPoint(this.origin)?0:null;a=-(this.origin.dot(a.normal)+a.constant)/b;return 0<=a?a:null},intersectPlane:function(a,b){a=this.distanceToPlane(a);return null===a?null:this.at(a,b)},intersectsPlane:function(a){var b=a.distanceToPoint(this.origin);return 0===b||0>a.normal.dot(this.direction)*b?!0:!1},intersectBox:function(a, +b){var c=1/this.direction.x;var d=1/this.direction.y;var e=1/this.direction.z,f=this.origin;if(0<=c){var g=(a.min.x-f.x)*c;c*=a.max.x-f.x}else g=(a.max.x-f.x)*c,c*=a.min.x-f.x;if(0<=d){var h=(a.min.y-f.y)*d;d*=a.max.y-f.y}else h=(a.max.y-f.y)*d,d*=a.min.y-f.y;if(g>d||h>c)return null;if(h>g||g!==g)g=h;if(da||h>c)return null;if(h>g||g!==g)g=h;if(ac?null:this.at(0<=g?g:c,b)},intersectsBox:function(){var a= +new p;return function(b){return null!==this.intersectBox(b,a)}}(),intersectTriangle:function(){var a=new p,b=new p,c=new p,d=new p;return function(e,f,g,h,l){b.subVectors(f,e);c.subVectors(g,e);d.crossVectors(b,c);f=this.direction.dot(d);if(0f)h=-1,f=-f;else return null;a.subVectors(this.origin,e);e=h*this.direction.dot(c.crossVectors(a,c));if(0>e)return null;g=h*this.direction.dot(b.cross(a));if(0>g||e+g>f)return null;e=-h*a.dot(d);return 0>e?null:this.at(e/f,l)}}(), +applyMatrix4:function(a){this.origin.applyMatrix4(a);this.direction.transformDirection(a);return this},equals:function(a){return a.origin.equals(this.origin)&&a.direction.equals(this.direction)}});Object.assign(Lb.prototype,{set:function(a,b){this.start.copy(a);this.end.copy(b);return this},clone:function(){return(new this.constructor).copy(this)},copy:function(a){this.start.copy(a.start);this.end.copy(a.end);return this},getCenter:function(a){void 0===a&&(console.warn("THREE.Line3: .getCenter() target is now required"), +a=new p);return a.addVectors(this.start,this.end).multiplyScalar(.5)},delta:function(a){void 0===a&&(console.warn("THREE.Line3: .delta() target is now required"),a=new p);return a.subVectors(this.end,this.start)},distanceSq:function(){return this.start.distanceToSquared(this.end)},distance:function(){return this.start.distanceTo(this.end)},at:function(a,b){void 0===b&&(console.warn("THREE.Line3: .at() target is now required"),b=new p);return this.delta(b).multiplyScalar(a).add(this.start)},closestPointToPointParameter:function(){var a= +new p,b=new p;return function(c,d){a.subVectors(c,this.start);b.subVectors(this.end,this.start);c=b.dot(b);c=b.dot(a)/c;d&&(c=S.clamp(c,0,1));return c}}(),closestPointToPoint:function(a,b,c){a=this.closestPointToPointParameter(a,b);void 0===c&&(console.warn("THREE.Line3: .closestPointToPoint() target is now required"),c=new p);return this.delta(c).multiplyScalar(a).add(this.start)},applyMatrix4:function(a){this.start.applyMatrix4(a);this.end.applyMatrix4(a);return this},equals:function(a){return a.start.equals(this.start)&& +a.end.equals(this.end)}});Object.assign(Aa,{getNormal:function(){var a=new p;return function(b,c,d,e){void 0===e&&(console.warn("THREE.Triangle: .getNormal() target is now required"),e=new p);e.subVectors(d,c);a.subVectors(b,c);e.cross(a);b=e.lengthSq();return 0=a.x+a.y}}()});Object.assign(Aa.prototype,{set:function(a,b,c){this.a.copy(a);this.b.copy(b);this.c.copy(c);return this},setFromPointsAndIndices:function(a,b,c,d){this.a.copy(a[b]);this.b.copy(a[c]); +this.c.copy(a[d]);return this},clone:function(){return(new this.constructor).copy(this)},copy:function(a){this.a.copy(a.a);this.b.copy(a.b);this.c.copy(a.c);return this},getArea:function(){var a=new p,b=new p;return function(){a.subVectors(this.c,this.b);b.subVectors(this.a,this.b);return.5*a.cross(b).length()}}(),getMidpoint:function(a){void 0===a&&(console.warn("THREE.Triangle: .getMidpoint() target is now required"),a=new p);return a.addVectors(this.a,this.b).add(this.c).multiplyScalar(1/3)},getNormal:function(a){return Aa.getNormal(this.a, +this.b,this.c,a)},getPlane:function(a){void 0===a&&(console.warn("THREE.Triangle: .getPlane() target is now required"),a=new p);return a.setFromCoplanarPoints(this.a,this.b,this.c)},getBarycoord:function(a,b){return Aa.getBarycoord(a,this.a,this.b,this.c,b)},containsPoint:function(a){return Aa.containsPoint(a,this.a,this.b,this.c)},intersectsBox:function(a){return a.intersectsTriangle(this)},closestPointToPoint:function(){var a=new Fa,b=[new Lb,new Lb,new Lb],c=new p,d=new p;return function(e,f){void 0=== +f&&(console.warn("THREE.Triangle: .closestPointToPoint() target is now required"),f=new p);var g=Infinity;a.setFromCoplanarPoints(this.a,this.b,this.c);a.projectPoint(e,c);if(!0===this.containsPoint(c))f.copy(c);else for(b[0].set(this.a,this.b),b[1].set(this.b,this.c),b[2].set(this.c,this.a),e=0;ec.far?null:{distance:b,point:y.clone(),object:a}}function c(c,d,e,f,m,k,n,p){g.fromBufferAttribute(f,k);h.fromBufferAttribute(f,n);l.fromBufferAttribute(f,p);if(c=b(c,c.material, +d,e,g,h,l,x))m&&(t.fromBufferAttribute(m,k),r.fromBufferAttribute(m,n),q.fromBufferAttribute(m,p),c.uv=a(x,g,h,l,t,r,q)),m=new Wa(k,n,p),Aa.getNormal(g,h,l,m.normal),c.face=m,c.faceIndex=k;return c}var d=new M,e=new qb,f=new Ea,g=new p,h=new p,l=new p,m=new p,k=new p,n=new p,t=new C,r=new C,q=new C,v=new p,x=new p,y=new p;return function(p,u){var v=this.geometry,w=this.material,y=this.matrixWorld;if(void 0!==w&&(null===v.boundingSphere&&v.computeBoundingSphere(),f.copy(v.boundingSphere),f.applyMatrix4(y), +!1!==p.ray.intersectsSphere(f)&&(d.getInverse(y),e.copy(p.ray).applyMatrix4(d),null===v.boundingBox||!1!==e.intersectsBox(v.boundingBox)))){var B;if(v.isBufferGeometry){w=v.index;var C=v.attributes.position;y=v.attributes.uv;var A;if(null!==w){var z=0;for(A=w.count;zf||(f=d.ray.origin.distanceTo(a),fd.far||e.push({distance:f,point:a.clone(),face:null,object:this}))}}(),clone:function(){return(new this.constructor(this.material)).copy(this)},copy:function(a){A.prototype.copy.call(this,a);void 0!==a.center&& +this.center.copy(a.center);return this}});Bc.prototype=Object.assign(Object.create(A.prototype),{constructor:Bc,copy:function(a){A.prototype.copy.call(this,a,!1);a=a.levels;for(var b=0,c=a.length;b=d[e].distance)d[e-1].object.visible= +!1,d[e].object.visible=!0;else break;for(;ef||(k.applyMatrix4(this.matrixWorld),v=d.ray.origin.distanceTo(k),vd.far||e.push({distance:v,point:h.clone().applyMatrix4(this.matrixWorld),index:g,face:null,faceIndex:null,object:this}))}}else for(g=0,q=r.length/3-1;gf||(k.applyMatrix4(this.matrixWorld),v=d.ray.origin.distanceTo(k),vd.far||e.push({distance:v,point:h.clone().applyMatrix4(this.matrixWorld), +index:g,face:null,faceIndex:null,object:this}))}else if(g.isGeometry)for(l=g.vertices,m=l.length,g=0;gf||(k.applyMatrix4(this.matrixWorld),v=d.ray.origin.distanceTo(k),vd.far||e.push({distance:v,point:h.clone().applyMatrix4(this.matrixWorld),index:g,face:null,faceIndex:null,object:this}))}}}(),clone:function(){return(new this.constructor(this.geometry,this.material)).copy(this)}});aa.prototype=Object.assign(Object.create(ua.prototype), +{constructor:aa,isLineSegments:!0,computeLineDistances:function(){var a=new p,b=new p;return function(){var c=this.geometry;if(c.isBufferGeometry)if(null===c.index){for(var d=c.attributes.position,e=[],f=0,g=d.count;fd.far||e.push({distance:a,distanceToRay:Math.sqrt(f),point:n.clone(),index:c,face:null,object:g}))}var g=this,h=this.geometry,l=this.matrixWorld,m=d.params.Points.threshold;null===h.boundingSphere&&h.computeBoundingSphere(); +c.copy(h.boundingSphere);c.applyMatrix4(l);c.radius+=m;if(!1!==d.ray.intersectsSphere(c)){a.getInverse(l);b.copy(d.ray).applyMatrix4(a);m/=(this.scale.x+this.scale.y+this.scale.z)/3;var k=m*m;m=new p;var n=new p;if(h.isBufferGeometry){var t=h.index;h=h.attributes.position.array;if(null!==t){var r=t.array;t=0;for(var q=r.length;t=a.HAVE_CURRENT_DATA&&(this.needsUpdate=!0)}});Rb.prototype=Object.create(Y.prototype);Rb.prototype.constructor=Rb;Rb.prototype.isCompressedTexture=!0;Dc.prototype=Object.create(Y.prototype);Dc.prototype.constructor=Dc;Dc.prototype.isDepthTexture=!0;Sb.prototype= +Object.create(F.prototype);Sb.prototype.constructor=Sb;Ec.prototype=Object.create(N.prototype);Ec.prototype.constructor=Ec;Tb.prototype=Object.create(F.prototype);Tb.prototype.constructor=Tb;Fc.prototype=Object.create(N.prototype);Fc.prototype.constructor=Fc;pa.prototype=Object.create(F.prototype);pa.prototype.constructor=pa;Gc.prototype=Object.create(N.prototype);Gc.prototype.constructor=Gc;Ub.prototype=Object.create(pa.prototype);Ub.prototype.constructor=Ub;Hc.prototype=Object.create(N.prototype); +Hc.prototype.constructor=Hc;sb.prototype=Object.create(pa.prototype);sb.prototype.constructor=sb;Ic.prototype=Object.create(N.prototype);Ic.prototype.constructor=Ic;Vb.prototype=Object.create(pa.prototype);Vb.prototype.constructor=Vb;Jc.prototype=Object.create(N.prototype);Jc.prototype.constructor=Jc;Wb.prototype=Object.create(pa.prototype);Wb.prototype.constructor=Wb;Kc.prototype=Object.create(N.prototype);Kc.prototype.constructor=Kc;Xb.prototype=Object.create(F.prototype);Xb.prototype.constructor= +Xb;Lc.prototype=Object.create(N.prototype);Lc.prototype.constructor=Lc;Yb.prototype=Object.create(F.prototype);Yb.prototype.constructor=Yb;Mc.prototype=Object.create(N.prototype);Mc.prototype.constructor=Mc;Zb.prototype=Object.create(F.prototype);Zb.prototype.constructor=Zb;var Lg={triangulate:function(a,b,c){c=c||2;var d=b&&b.length,e=d?b[0]*c:a.length,f=Xe(a,0,e,c,!0),g=[];if(!f)return g;var h;if(d){var l=c;d=[];var m;var k=0;for(m=b.length;k80*c){var r=h=a[0];var q=d=a[1];for(l=c;lh&&(h=k),b>d&&(d=b);h=Math.max(h-r,d-q);h=0!==h?1/h:0}Pc(f,g,c,r,q,h);return g}},Xa={area:function(a){for(var b=a.length,c=0,d=b-1,e=0;eXa.area(a)},triangulateShape:function(a, +b){var c=[],d=[],e=[];af(a);bf(c,a);var f=a.length;b.forEach(af);for(a=0;aNumber.EPSILON){var l=Math.sqrt(h),m=Math.sqrt(f*f+g*g);h=b.x-e/l;b=b.y+d/l;g=((c.x-g/m-h)*g-(c.y+f/m-b)*f)/(d*g-e*f);f=h+d*g-a.x;d=b+e*g-a.y;e=f*f+d*d;if(2>=e)return new C(f,d);e=Math.sqrt(e/2)}else a=!1,d>Number.EPSILON?f>Number.EPSILON&&(a=!0):d<-Number.EPSILON?f<-Number.EPSILON&&(a=!0):Math.sign(e)===Math.sign(g)&&(a=!0),a?(f=-e,e=Math.sqrt(h)):(f=d,d=e,e=Math.sqrt(h/2));return new C(f/e,d/ +e)}function e(a,b){for(J=a.length;0<=--J;){var c=J;var d=J-1;0>d&&(d=a.length-1);var e,f=B+2*x;for(e=0;eMath.abs(g-l)?[new C(a,1-c),new C(h,1-d),new C(m,1-e),new C(n,1-b)]:[new C(g,1-c),new C(l,1-d),new C(k,1-e),new C(p,1-b)]}};Rc.prototype=Object.create(N.prototype);Rc.prototype.constructor=Rc;$b.prototype=Object.create(Ia.prototype);$b.prototype.constructor=$b;Sc.prototype=Object.create(N.prototype);Sc.prototype.constructor=Sc;ub.prototype=Object.create(F.prototype);ub.prototype.constructor=ub;Tc.prototype=Object.create(N.prototype);Tc.prototype.constructor= +Tc;ac.prototype=Object.create(F.prototype);ac.prototype.constructor=ac;Uc.prototype=Object.create(N.prototype);Uc.prototype.constructor=Uc;bc.prototype=Object.create(F.prototype);bc.prototype.constructor=bc;vb.prototype=Object.create(N.prototype);vb.prototype.constructor=vb;vb.prototype.toJSON=function(){var a=N.prototype.toJSON.call(this);return cf(this.parameters.shapes,a)};wb.prototype=Object.create(F.prototype);wb.prototype.constructor=wb;wb.prototype.toJSON=function(){var a=F.prototype.toJSON.call(this); +return cf(this.parameters.shapes,a)};cc.prototype=Object.create(F.prototype);cc.prototype.constructor=cc;xb.prototype=Object.create(N.prototype);xb.prototype.constructor=xb;Ya.prototype=Object.create(F.prototype);Ya.prototype.constructor=Ya;Vc.prototype=Object.create(xb.prototype);Vc.prototype.constructor=Vc;Wc.prototype=Object.create(Ya.prototype);Wc.prototype.constructor=Wc;Xc.prototype=Object.create(N.prototype);Xc.prototype.constructor=Xc;dc.prototype=Object.create(F.prototype);dc.prototype.constructor= +dc;var Ca=Object.freeze({WireframeGeometry:Sb,ParametricGeometry:Ec,ParametricBufferGeometry:Tb,TetrahedronGeometry:Gc,TetrahedronBufferGeometry:Ub,OctahedronGeometry:Hc,OctahedronBufferGeometry:sb,IcosahedronGeometry:Ic,IcosahedronBufferGeometry:Vb,DodecahedronGeometry:Jc,DodecahedronBufferGeometry:Wb,PolyhedronGeometry:Fc,PolyhedronBufferGeometry:pa,TubeGeometry:Kc,TubeBufferGeometry:Xb,TorusKnotGeometry:Lc,TorusKnotBufferGeometry:Yb,TorusGeometry:Mc,TorusBufferGeometry:Zb,TextGeometry:Rc,TextBufferGeometry:$b, +SphereGeometry:Sc,SphereBufferGeometry:ub,RingGeometry:Tc,RingBufferGeometry:ac,PlaneGeometry:xc,PlaneBufferGeometry:pb,LatheGeometry:Uc,LatheBufferGeometry:bc,ShapeGeometry:vb,ShapeBufferGeometry:wb,ExtrudeGeometry:fb,ExtrudeBufferGeometry:Ia,EdgesGeometry:cc,ConeGeometry:Vc,ConeBufferGeometry:Wc,CylinderGeometry:xb,CylinderBufferGeometry:Ya,CircleGeometry:Xc,CircleBufferGeometry:dc,BoxGeometry:Kb,BoxBufferGeometry:mb});yb.prototype=Object.create(O.prototype);yb.prototype.constructor=yb;yb.prototype.isShadowMaterial= +!0;yb.prototype.copy=function(a){O.prototype.copy.call(this,a);this.color.copy(a.color);return this};ec.prototype=Object.create(va.prototype);ec.prototype.constructor=ec;ec.prototype.isRawShaderMaterial=!0;Sa.prototype=Object.create(O.prototype);Sa.prototype.constructor=Sa;Sa.prototype.isMeshStandardMaterial=!0;Sa.prototype.copy=function(a){O.prototype.copy.call(this,a);this.defines={STANDARD:""};this.color.copy(a.color);this.roughness=a.roughness;this.metalness=a.metalness;this.map=a.map;this.lightMap= +a.lightMap;this.lightMapIntensity=a.lightMapIntensity;this.aoMap=a.aoMap;this.aoMapIntensity=a.aoMapIntensity;this.emissive.copy(a.emissive);this.emissiveMap=a.emissiveMap;this.emissiveIntensity=a.emissiveIntensity;this.bumpMap=a.bumpMap;this.bumpScale=a.bumpScale;this.normalMap=a.normalMap;this.normalScale.copy(a.normalScale);this.displacementMap=a.displacementMap;this.displacementScale=a.displacementScale;this.displacementBias=a.displacementBias;this.roughnessMap=a.roughnessMap;this.metalnessMap= +a.metalnessMap;this.alphaMap=a.alphaMap;this.envMap=a.envMap;this.envMapIntensity=a.envMapIntensity;this.refractionRatio=a.refractionRatio;this.wireframe=a.wireframe;this.wireframeLinewidth=a.wireframeLinewidth;this.wireframeLinecap=a.wireframeLinecap;this.wireframeLinejoin=a.wireframeLinejoin;this.skinning=a.skinning;this.morphTargets=a.morphTargets;this.morphNormals=a.morphNormals;return this};zb.prototype=Object.create(Sa.prototype);zb.prototype.constructor=zb;zb.prototype.isMeshPhysicalMaterial= +!0;zb.prototype.copy=function(a){Sa.prototype.copy.call(this,a);this.defines={PHYSICAL:""};this.reflectivity=a.reflectivity;this.clearCoat=a.clearCoat;this.clearCoatRoughness=a.clearCoatRoughness;return this};Ja.prototype=Object.create(O.prototype);Ja.prototype.constructor=Ja;Ja.prototype.isMeshPhongMaterial=!0;Ja.prototype.copy=function(a){O.prototype.copy.call(this,a);this.color.copy(a.color);this.specular.copy(a.specular);this.shininess=a.shininess;this.map=a.map;this.lightMap=a.lightMap;this.lightMapIntensity= +a.lightMapIntensity;this.aoMap=a.aoMap;this.aoMapIntensity=a.aoMapIntensity;this.emissive.copy(a.emissive);this.emissiveMap=a.emissiveMap;this.emissiveIntensity=a.emissiveIntensity;this.bumpMap=a.bumpMap;this.bumpScale=a.bumpScale;this.normalMap=a.normalMap;this.normalScale.copy(a.normalScale);this.displacementMap=a.displacementMap;this.displacementScale=a.displacementScale;this.displacementBias=a.displacementBias;this.specularMap=a.specularMap;this.alphaMap=a.alphaMap;this.envMap=a.envMap;this.combine= +a.combine;this.reflectivity=a.reflectivity;this.refractionRatio=a.refractionRatio;this.wireframe=a.wireframe;this.wireframeLinewidth=a.wireframeLinewidth;this.wireframeLinecap=a.wireframeLinecap;this.wireframeLinejoin=a.wireframeLinejoin;this.skinning=a.skinning;this.morphTargets=a.morphTargets;this.morphNormals=a.morphNormals;return this};Ab.prototype=Object.create(Ja.prototype);Ab.prototype.constructor=Ab;Ab.prototype.isMeshToonMaterial=!0;Ab.prototype.copy=function(a){Ja.prototype.copy.call(this, +a);this.gradientMap=a.gradientMap;return this};Bb.prototype=Object.create(O.prototype);Bb.prototype.constructor=Bb;Bb.prototype.isMeshNormalMaterial=!0;Bb.prototype.copy=function(a){O.prototype.copy.call(this,a);this.bumpMap=a.bumpMap;this.bumpScale=a.bumpScale;this.normalMap=a.normalMap;this.normalScale.copy(a.normalScale);this.displacementMap=a.displacementMap;this.displacementScale=a.displacementScale;this.displacementBias=a.displacementBias;this.wireframe=a.wireframe;this.wireframeLinewidth=a.wireframeLinewidth; +this.skinning=a.skinning;this.morphTargets=a.morphTargets;this.morphNormals=a.morphNormals;return this};Cb.prototype=Object.create(O.prototype);Cb.prototype.constructor=Cb;Cb.prototype.isMeshLambertMaterial=!0;Cb.prototype.copy=function(a){O.prototype.copy.call(this,a);this.color.copy(a.color);this.map=a.map;this.lightMap=a.lightMap;this.lightMapIntensity=a.lightMapIntensity;this.aoMap=a.aoMap;this.aoMapIntensity=a.aoMapIntensity;this.emissive.copy(a.emissive);this.emissiveMap=a.emissiveMap;this.emissiveIntensity= +a.emissiveIntensity;this.specularMap=a.specularMap;this.alphaMap=a.alphaMap;this.envMap=a.envMap;this.combine=a.combine;this.reflectivity=a.reflectivity;this.refractionRatio=a.refractionRatio;this.wireframe=a.wireframe;this.wireframeLinewidth=a.wireframeLinewidth;this.wireframeLinecap=a.wireframeLinecap;this.wireframeLinejoin=a.wireframeLinejoin;this.skinning=a.skinning;this.morphTargets=a.morphTargets;this.morphNormals=a.morphNormals;return this};Db.prototype=Object.create(U.prototype);Db.prototype.constructor= +Db;Db.prototype.isLineDashedMaterial=!0;Db.prototype.copy=function(a){U.prototype.copy.call(this,a);this.scale=a.scale;this.dashSize=a.dashSize;this.gapSize=a.gapSize;return this};var Mg=Object.freeze({ShadowMaterial:yb,SpriteMaterial:eb,RawShaderMaterial:ec,ShaderMaterial:va,PointsMaterial:Ha,MeshPhysicalMaterial:zb,MeshStandardMaterial:Sa,MeshPhongMaterial:Ja,MeshToonMaterial:Ab,MeshNormalMaterial:Bb,MeshLambertMaterial:Cb,MeshDepthMaterial:cb,MeshDistanceMaterial:db,MeshBasicMaterial:za,LineDashedMaterial:Db, +LineBasicMaterial:U,Material:O}),Hb={enabled:!1,files:{},add:function(a,b){!1!==this.enabled&&(this.files[a]=b)},get:function(a){if(!1!==this.enabled)return this.files[a]},remove:function(a){delete this.files[a]},clear:function(){this.files={}}},ma=new ae,$a={};Object.assign(Ka.prototype,{load:function(a,b,c,d){void 0===a&&(a="");void 0!==this.path&&(a=this.path+a);a=this.manager.resolveURL(a);var e=this,f=Hb.get(a);if(void 0!==f)return e.manager.itemStart(a),setTimeout(function(){b&&b(f);e.manager.itemEnd(a)}, +0),f;if(void 0!==$a[a])$a[a].push({onLoad:b,onProgress:c,onError:d});else{var g=a.match(/^data:(.*?)(;base64)?,(.*)$/);if(g){c=g[1];var h=!!g[2];g=g[3];g=window.decodeURIComponent(g);h&&(g=window.atob(g));try{var l=(this.responseType||"").toLowerCase();switch(l){case "arraybuffer":case "blob":var m=new Uint8Array(g.length);for(h=0;hg)e=a+1;else if(0b&&(b=0);1Number.EPSILON&&(g.normalize(),c=Math.acos(S.clamp(d[l-1].dot(d[l]),-1,1)),e[l].applyMatrix4(h.makeRotationAxis(g,c))),f[l].crossVectors(d[l],e[l]);if(!0===b)for(c=Math.acos(S.clamp(e[0].dot(e[a]),-1,1)),c/=a,0d;)d+=c;for(;d>c;)d-=c;de&&(e=1);1E-4>d&&(d=e);1E-4>l&&(l=e);ye.initNonuniformCatmullRom(f.x,g.x,h.x,c.x,d,e,l);ze.initNonuniformCatmullRom(f.y,g.y,h.y,c.y,d,e,l);Ae.initNonuniformCatmullRom(f.z,g.z,h.z,c.z,d,e,l)}else"catmullrom"===this.curveType&&(ye.initCatmullRom(f.x,g.x,h.x,c.x,this.tension),ze.initCatmullRom(f.y,g.y,h.y,c.y,this.tension),Ae.initCatmullRom(f.z,g.z,h.z,c.z,this.tension));b.set(ye.calc(a), +ze.calc(a),Ae.calc(a));return b};X.prototype.copy=function(a){E.prototype.copy.call(this,a);this.points=[];for(var b=0,c=a.points.length;bc.length-2?c.length-1:a+1];c=c[a>c.length-3?c.length-1:a+2];b.set(ef(d,e.x,f.x,g.x,c.x),ef(d,e.y,f.y,g.y,c.y));return b};Oa.prototype.copy=function(a){E.prototype.copy.call(this,a);this.points=[];for(var b=0,c=a.points.length;b=b)return b=c[a]-b,a=this.curves[a],c=a.getLength(),a.getPointAt(0===c?0:1-b/c);a++}return null},getLength:function(){var a=this.getCurveLengths(); +return a[a.length-1]},updateArcLengths:function(){this.needsUpdate=!0;this.cacheLengths=null;this.getCurveLengths()},getCurveLengths:function(){if(this.cacheLengths&&this.cacheLengths.length===this.curves.length)return this.cacheLengths;for(var a=[],b=0,c=0,d=this.curves.length;c=e)break a;else{f=b[1];a=e)break b}d=c;c=0}}for(;c>>1,ab;)--f;++f;if(0!==e||f!==d)e>=f&&(f=Math.max(f,1),e=f-1),a=this.getValueSize(),this.times=fa.arraySlice(c,e,f),this.values=fa.arraySlice(this.values,e*a,f*a);return this},validate:function(){var a=!0,b=this.getValueSize();0!==b-Math.floor(b)&&(console.error("THREE.KeyframeTrack: Invalid value size in track.",this),a=!1);var c=this.times;b=this.values;var d=c.length;0===d&&(console.error("THREE.KeyframeTrack: Track is empty.", +this),a=!1);for(var e=null,f=0;f!==d;f++){var g=c[f];if("number"===typeof g&&isNaN(g)){console.error("THREE.KeyframeTrack: Time is not a valid number.",this,f,g);a=!1;break}if(null!==e&&e>g){console.error("THREE.KeyframeTrack: Out of order keys.",this,f,g,e);a=!1;break}e=g}if(void 0!==b&&fa.isTypedArray(b))for(f=0,c=b.length;f!==c;++f)if(d=b[f],isNaN(d)){console.error("THREE.KeyframeTrack: Value is not a valid number.",this,f,d);a=!1;break}return a},optimize:function(){for(var a=this.times,b=this.values, +c=this.getValueSize(),d=2302===this.getInterpolation(),e=1,f=a.length-1,g=1;gk.opacity&&(k.transparent=!0);d.setTextures(l);return d.parse(k)}}()});var Be={decodeText:function(a){if("undefined"!==typeof TextDecoder)return(new TextDecoder).decode(a);for(var b="",c=0,d=a.length;cf;f++){var z=h[r++];var A=B[2*z];z=B[2*z+1];A=new C(A,z);2!==f&&c.faceVertexUvs[e][v].push(A);0!==f&&c.faceVertexUvs[e][v+1].push(A)}}x&&(x=3*h[r++],q.normal.set(k[x++],k[x++],k[x]),w.normal.copy(q.normal));if(y)for(e=0;4>e;e++)x=3*h[r++],y=new p(k[x++],k[x++],k[x]),2!==e&&q.vertexNormals.push(y),0!==e&&w.vertexNormals.push(y);n&&(n= +h[r++],n=u[n],q.color.setHex(n),w.color.setHex(n));if(l)for(e=0;4>e;e++)n=h[r++],n=u[n],2!==e&&q.vertexColors.push(new I(n)),0!==e&&w.vertexColors.push(new I(n));c.faces.push(q);c.faces.push(w)}else{q=new Wa;q.a=h[r++];q.b=h[r++];q.c=h[r++];v&&(v=h[r++],q.materialIndex=v);v=c.faces.length;if(e)for(e=0;ef;f++)z=h[r++],A=B[2*z],z=B[2*z+1],A=new C(A,z),c.faceVertexUvs[e][v].push(A);x&&(x=3*h[r++],q.normal.set(k[x++],k[x++],k[x]));if(y)for(e=0;3>e;e++)x= +3*h[r++],y=new p(k[x++],k[x++],k[x]),q.vertexNormals.push(y);n&&(n=h[r++],q.color.setHex(u[n]));if(l)for(e=0;3>e;e++)n=h[r++],q.vertexColors.push(new I(u[n]));c.faces.push(q)}}d=a;r=void 0!==d.influencesPerVertex?d.influencesPerVertex:2;if(d.skinWeights)for(g=0,h=d.skinWeights.length;gNumber.EPSILON){if(0>k&&(g=b[f],l=-l,h=b[e],k=-k),!(a.yh.y))if(a.y===g.y){if(a.x===g.x)return!0}else{e=k*(a.x-g.x)-l*(a.y-g.y);if(0===e)return!0;0>e||(d=!d)}}else if(a.y===g.y&&(h.x<=a.x&&a.x<=g.x||g.x<=a.x&&a.x<=h.x))return!0}return d}var e=Xa.isClockWise,f=this.subPaths;if(0===f.length)return[];if(!0===b)return c(f);b=[];if(1===f.length){var g=f[0]; +var h=new gb;h.curves=g.curves;b.push(h);return b}var l=!e(f[0].getPoints());l=a?!l:l;h=[];var k=[],p=[],n=0;k[n]=void 0;p[n]=[];for(var t=0,r=f.length;td&&this._mixBufferRegion(c,a,3*b,1-d,b);d=b;for(var f=b+b;d!==f;++d)if(c[d]!==c[d+b]){e.setValue(c,a);break}}, +saveOriginalState:function(){var a=this.buffer,b=this.valueSize,c=3*b;this.binding.getValue(a,c);for(var d=b;d!==c;++d)a[d]=a[c+d%b];this.cumulativeWeight=0},restoreOriginalState:function(){this.binding.setValue(this.buffer,3*this.valueSize)},_select:function(a,b,c,d,e){if(.5<=d)for(d=0;d!==e;++d)a[b+d]=a[c+d]},_slerp:function(a,b,c,d){ja.slerpFlat(a,b,a,b,a,c,d)},_lerp:function(a,b,c,d,e){for(var f=1-d,g=0;g!==e;++g){var h=b+g;a[h]=a[h]*f+a[c+g]*d}}});Object.assign(jf.prototype,{getValue:function(a, +b){this.bind();var c=this._bindings[this._targetGroup.nCachedObjects_];void 0!==c&&c.getValue(a,b)},setValue:function(a,b){for(var c=this._bindings,d=this._targetGroup.nCachedObjects_,e=c.length;d!==e;++d)c[d].setValue(a,b)},bind:function(){for(var a=this._bindings,b=this._targetGroup.nCachedObjects_,c=a.length;b!==c;++b)a[b].bind()},unbind:function(){for(var a=this._bindings,b=this._targetGroup.nCachedObjects_,c=a.length;b!==c;++b)a[b].unbind()}});Object.assign(qa,{Composite:jf,create:function(a, +b,c){return a&&a.isAnimationObjectGroup?new qa.Composite(a,b,c):new qa(a,b,c)},sanitizeNodeName:function(){var a=/[\[\]\.:\/]/g;return function(b){return b.replace(/\s/g,"_").replace(a,"")}}(),parseTrackName:function(){var a="[^"+"\\[\\]\\.:\\/".replace("\\.","")+"]",b=/((?:WC+[\/:])*)/.source.replace("WC","[^\\[\\]\\.:\\/]");a=/(WCOD+)?/.source.replace("WCOD",a);var c=/(?:\.(WC+)(?:\[(.+)\])?)?/.source.replace("WC","[^\\[\\]\\.:\\/]"),d=/\.(WC+)(?:\[(.+)\])?/.source.replace("WC","[^\\[\\]\\.:\\/]"), +e=new RegExp("^"+b+a+c+d+"$"),f=["material","materials","bones"];return function(a){var b=e.exec(a);if(!b)throw Error("PropertyBinding: Cannot parse trackName: "+a);b={nodeName:b[2],objectName:b[3],objectIndex:b[4],propertyName:b[5],propertyIndex:b[6]};var c=b.nodeName&&b.nodeName.lastIndexOf(".");if(void 0!==c&&-1!==c){var d=b.nodeName.substring(c+1);-1!==f.indexOf(d)&&(b.nodeName=b.nodeName.substring(0,c),b.objectName=d)}if(null===b.propertyName||0===b.propertyName.length)throw Error("PropertyBinding: can not parse propertyName from trackName: "+ +a);return b}}(),findNode:function(a,b){if(!b||""===b||"root"===b||"."===b||-1===b||b===a.name||b===a.uuid)return a;if(a.skeleton){var c=a.skeleton.getBoneByName(b);if(void 0!==c)return c}if(a.children){var d=function(a){for(var c=0;c=b){var p=b++,n=a[p];c[n.uuid]=m;a[m]=n;c[k]=p;a[p]=h;h=0;for(k=e;h!==k;++h){n=d[h];var t=n[m];n[m]=n[p];n[p]=t}}}this.nCachedObjects_=b},uncache:function(){for(var a=this._objects,b=a.length,c=this.nCachedObjects_,d=this._indicesByUUID,e=this._bindings,f=e.length,g=0,h=arguments.length;g!==h;++g){var k= +arguments[g].uuid,m=d[k];if(void 0!==m)if(delete d[k],mb||0===c)return;this._startTime=null;b*=c}b*=this._updateTimeScale(a);c=this._updateTime(b);a=this._updateWeight(a);if(0c.parameterPositions[1]&&(this.stopFading(),0===d&&(this.enabled=!1))}}return this._effectiveWeight=b},_updateTimeScale:function(a){var b=0;if(!this.paused){b=this.timeScale;var c=this._timeScaleInterpolant;if(null!==c){var d=c.evaluate(a)[0];b*=d;a>c.parameterPositions[1]&&(this.stopWarping(),0===b?this.paused=!0:this.timeScale=b)}}return this._effectiveTimeScale=b},_updateTime:function(a){var b=this.time+a;if(0===a)return b;var c=this._clip.duration,d=this.loop,e=this._loopCount;if(2200=== +d)a:{if(-1===e&&(this._loopCount=0,this._setEndings(!0,!0,!1)),b>=c)b=c;else if(0>b)b=0;else break a;this.clampWhenFinished?this.paused=!0:this.enabled=!1;this._mixer.dispatchEvent({type:"finished",action:this,direction:0>a?-1:1})}else{d=2202===d;-1===e&&(0<=a?(e=0,this._setEndings(!0,0===this.repetitions,d)):this._setEndings(0===this.repetitions,!0,d));if(b>=c||0>b){var f=Math.floor(b/c);b-=c*f;e+=Math.abs(f);var g=this.repetitions-e;0>=g?(this.clampWhenFinished?this.paused=!0:this.enabled=!1,b= +0a,this._setEndings(a,!a,d)):this._setEndings(!1,!1,d),this._loopCount=e,this._mixer.dispatchEvent({type:"loop",action:this,loopDelta:f}))}if(d&&1===(e&1))return this.time=b,c-b}return this.time=b},_setEndings:function(a,b,c){var d=this._interpolantSettings;c?(d.endingStart=2401,d.endingEnd=2401):(d.endingStart=a?this.zeroSlopeAtStart?2401:2400:2402,d.endingEnd=b?this.zeroSlopeAtEnd?2401:2400:2402)},_scheduleFading:function(a, +b,c){var d=this._mixer,e=d.time,f=this._weightInterpolant;null===f&&(this._weightInterpolant=f=d._lendControlInterpolant());d=f.parameterPositions;f=f.sampleValues;d[0]=e;f[0]=b;d[1]=e+a;f[1]=c;return this}});pe.prototype=Object.assign(Object.create(xa.prototype),{constructor:pe,_bindAction:function(a,b){var c=a._localRoot||this._root,d=a._clip.tracks,e=d.length,f=a._propertyBindings;a=a._interpolants;var g=c.uuid,h=this._bindingsByRootAndName,k=h[g];void 0===k&&(k={},h[g]=k);for(h=0;h!==e;++h){var m= +d[h],p=m.name,n=k[p];if(void 0===n){n=f[h];if(void 0!==n){null===n._cacheIndex&&(++n.referenceCount,this._addInactiveBinding(n,g,p));continue}n=new oe(qa.create(c,p,b&&b._propertyBindings[h].binding.parsedPath),m.ValueTypeName,m.getValueSize());++n.referenceCount;this._addInactiveBinding(n,g,p)}f[h]=n;a[h].resultBuffer=n.buffer}},_activateAction:function(a){if(!this._isActiveAction(a)){if(null===a._cacheIndex){var b=(a._localRoot||this._root).uuid,c=a._clip.uuid,d=this._actionsByClip[c];this._bindAction(a, +d&&d.knownActions[0]);this._addInactiveAction(a,c,b)}b=a._propertyBindings;c=0;for(d=b.length;c!==d;++c){var e=b[c];0===e.useCount++&&(this._lendBinding(e),e.saveOriginalState())}this._lendAction(a)}},_deactivateAction:function(a){if(this._isActiveAction(a)){for(var b=a._propertyBindings,c=0,d=b.length;c!==d;++c){var e=b[c];0===--e.useCount&&(e.restoreOriginalState(),this._takeBackBinding(e))}this._takeBackAction(a)}},_initMemoryManager:function(){this._actions=[];this._nActiveActions=0;this._actionsByClip= +{};this._bindings=[];this._nActiveBindings=0;this._bindingsByRootAndName={};this._controlInterpolants=[];this._nActiveControlInterpolants=0;var a=this;this.stats={actions:{get total(){return a._actions.length},get inUse(){return a._nActiveActions}},bindings:{get total(){return a._bindings.length},get inUse(){return a._nActiveBindings}},controlInterpolants:{get total(){return a._controlInterpolants.length},get inUse(){return a._nActiveControlInterpolants}}}},_isActiveAction:function(a){a=a._cacheIndex; +return null!==a&&athis.max.x||a.ythis.max.y?!1:!0},containsBox:function(a){return this.min.x<=a.min.x&&a.max.x<=this.max.x&&this.min.y<=a.min.y&&a.max.y<=this.max.y},getParameter:function(a, +b){void 0===b&&(console.warn("THREE.Box2: .getParameter() target is now required"),b=new C);return b.set((a.x-this.min.x)/(this.max.x-this.min.x),(a.y-this.min.y)/(this.max.y-this.min.y))},intersectsBox:function(a){return a.max.xthis.max.x||a.max.ythis.max.y?!1:!0},clampPoint:function(a,b){void 0===b&&(console.warn("THREE.Box2: .clampPoint() target is now required"),b=new C);return b.copy(a).clamp(this.min,this.max)},distanceToPoint:function(){var a=new C; +return function(b){return a.copy(b).clamp(this.min,this.max).sub(b).length()}}(),intersect:function(a){this.min.max(a.min);this.max.min(a.max);return this},union:function(a){this.min.min(a.min);this.max.max(a.max);return this},translate:function(a){this.min.add(a);this.max.add(a);return this},equals:function(a){return a.min.equals(this.min)&&a.max.equals(this.max)}});dd.prototype=Object.create(A.prototype);dd.prototype.constructor=dd;dd.prototype.isImmediateRenderObject=!0;ed.prototype=Object.create(aa.prototype); +ed.prototype.constructor=ed;ed.prototype.update=function(){var a=new p,b=new p,c=new sa;return function(){var d=["a","b","c"];this.object.updateMatrixWorld(!0);c.getNormalMatrix(this.object.matrixWorld);var e=this.object.matrixWorld,f=this.geometry.attributes.position,g=this.object.geometry;if(g&&g.isGeometry)for(var h=g.vertices,k=g.faces,m=g=0,p=k.length;mMath.abs(b)&&(b=1E-8);this.scale.set(.5*this.size,.5*this.size,b);this.lookAt(this.plane.normal);A.prototype.updateMatrixWorld.call(this,a)};var Pd,we;Gb.prototype=Object.create(A.prototype);Gb.prototype.constructor=Gb;Gb.prototype.setDirection=function(){var a= +new p,b;return function(c){.99999c.y?this.quaternion.set(1,0,0,0):(a.set(c.z,0,-c.x).normalize(),b=Math.acos(c.y),this.quaternion.setFromAxisAngle(a,b))}}();Gb.prototype.setLength=function(a,b,c){void 0===b&&(b=.2*a);void 0===c&&(c=.2*b);this.line.scale.set(1,Math.max(0,a-b),1);this.line.updateMatrix();this.cone.scale.set(c,b,c);this.cone.position.y=a;this.cone.updateMatrix()};Gb.prototype.setColor=function(a){this.line.material.color.copy(a);this.cone.material.color.copy(a)}; +kd.prototype=Object.create(aa.prototype);kd.prototype.constructor=kd;E.create=function(a,b){console.log("THREE.Curve.create() has been deprecated");a.prototype=Object.create(E.prototype);a.prototype.constructor=a;a.prototype.getPoint=b;return a};Object.assign(Za.prototype,{createPointsGeometry:function(a){console.warn("THREE.CurvePath: .createPointsGeometry() has been removed. Use new THREE.Geometry().setFromPoints( points ) instead.");a=this.getPoints(a);return this.createGeometry(a)},createSpacedPointsGeometry:function(a){console.warn("THREE.CurvePath: .createSpacedPointsGeometry() has been removed. Use new THREE.Geometry().setFromPoints( points ) instead."); +a=this.getSpacedPoints(a);return this.createGeometry(a)},createGeometry:function(a){console.warn("THREE.CurvePath: .createGeometry() has been removed. Use new THREE.Geometry().setFromPoints( points ) instead.");for(var b=new N,c=0,d=a.length;c- Grant access to the camera.
- Use a different browser (e.g. the most recent versions of Opera, Firefox, Chrome or Edge).", + "mobileapp": "If you have installed the BioID App on your mobile device, you can use this app for enrollment or verification.", + + "uploadInfo": "Uploading...", + + "capture-error": "We could not capture an image.
Sorry, but without access to the camera, facial recognition and liveness detection aren't possible!", + "nogetUserMedia": "Your browser does not support the HTML5 Media Capture and Streams API. Please use a different browser or the BioID mobile app.", + "permissionDenied": "Permission Denied!", + "webgl-error": "WebGL is disabled or unavailable. If possible activate WebGL or use another browser.", + + "UserInstruction-CloseUp": "Come close before you start", + "UserInstruction-NodYourHead": "Please nod your head", + "UserInstruction-FollowMe": "Follow the blue head", + "UserInstruction-NoMovement": "Follow the head's movement", + "UserInstruction-PleaseWait": "Please wait", + + "Perform-enrollment": "Training...", + "Perform-verification": "Verifying...", + "Perform-identification": "Identifying...", + "Perform-livenessdetection": "Processing...", + + "NoMotionDetected": "We could not detect any motion.
For Liveness Detection please nod your head slightly.", + "NoFaceFound": "We could not find a suitable face.
Come close and look straight before you start.", + "MultipleFacesFound": "We found multiple faces or a strongly uneven background distracted us.
Your face should fill the circle completely.", + "LiveDetectionFailed": "Liveness Detection failed.
Look straight into the camera, then nod your head slightly.
Please ensure constant lighting.", + "ChallengeResponseFailed": "Challenge-Response failed!
Slowly follow the head's movement.", + "NotRecognized": "You have not been recognized!
Please ensure constant lighting. For improving recognition, please enroll again." + }; + + /* + * ----------------- Set button functionality + * ------------------------------------------ + */ + + + // jQuery - shortcut for $(document).ready() + // Document Object Model (DOM) is ready + + + function showIntroduction(show) { + if (show) { + $('#uuiintroduction').show(); + } + else { + $('#uuiintroduction').hide(); + $('#uuiwepapp').show(); + initCapture(); + + let checked = $('#introskip').prop("checked"); + if (checked) { + skipIntro = true; + // set cookie (1 year) to skip the introduction for the next + // time + // document.cookie = + // "BioIDSkipIntro=true;max-age=31536000;path=/"; + } + } + } + + function initCapture() { + // init BWS capture jQuery plugin (see bws.capture.js) + bwsCapture = bws.initcapture(document.getElementById('uuicanvas'), document.getElementById('livevideo'), token, { + apiurl: apiurl, + task: task, + trait: trait, + threshold: threshold, + challengeResponse: challengeResponse, + recordings: recordings, + maxheight: maxHeight + }); + let success = initHead(); + if (!success) { + $('#uuierror').html(formatText('webgl-error')); + $('#uuiskip').show(); + } + else { + // and start everything + onStart(); + } + } + + // called from Start button and onStart to initiate a new recording + function startRecording() { + $('#uuistart').attr('disabled', 'disabled'); + var tags = challengeResponse && challenges.length > currentExecution && challenges[currentExecution].length > 0 ? challenges[currentExecution] : []; + bwsCapture.startRecording(tags); + } + + // called from Mirror button to mirror the captured image + function mirror() { + bwsCapture.mirror(); + } + + + /* + * ---------------- Localization of strings + * ---------------------------------------------- + */ + + + // localization of displayed strings + function localize() { + // loops through all HTML elements that must be localized. + let resourceElements = $('[data-res]'); + for (let i = 0; i < resourceElements.length; i++) { + let element = resourceElements[i]; + let resourceKey = $(element).attr('data-res'); + if (resourceKey) { + // Get all the resources that start with the key. + for (let key in localizedData) { + if (key.indexOf(resourceKey) === 0) { + let value = localizedData[key]; + // Dot notation in resource key - assign the + // resource value to the elements property + if (key.indexOf('.') > -1) { + let attrKey = key.substring(key.indexOf('.') + 1); + $(element).attr(attrKey, value); + } + // No dot notation in resource key, assign the + // resource value to the element's innerHTML. + else if (key === resourceKey) { + $(element).html(value); + } + } + } + } + } + } + + // localization and string formatting (additional arguments replace {0}, + // {1}, etc. in localizedData[key]) + function formatText(key) { + var formatted = key; + if (localizedData[key] !== undefined) { + formatted = localizedData[key]; + } + for (let i = 1; i < arguments.length; i++) { + formatted = formatted.replace('{' + (i - 1) + '}', arguments[i]); + } + return formatted; + } + // jQuery - shortcut for $(document).ready() + // Document Object Model (DOM) is ready + $(function () { + initialize(); + + // set navigation for the buttons + $('#uuicancel').attr('href', returnURL + '?error=user_abort&access_token=' + token + '&state=' + state); + $('#uuiskip').attr('href', returnURL + '?error=user_skip&access_token=' + token + '&state=' + state); + + // set url for the BioID mobile app + if (task === 'verification') { + $('#uuimobileapp').attr('href', 'bioid-verify://?access_token=' + token + '&return_url=' + returnURL + '&state=' + state); + } + else if (task === 'enrollment') { + $('#uuimobileapp').attr('href', 'bioid-enroll://?access_token=' + token + '&return_url=' + returnURL + '&state=' + state); + } + + $('#uuiinstruction').attr('data-res', 'UserInstruction-CloseUp'); + + // hide button after first click + $('#uuimobileapp').click(function () { + $('#uuimobileapp').hide(); + }); + + // Check if cookie is set + // let skipCookie = false; + // var cookie = document.cookie; + // if (cookie != "") { + // skipCookie = cookie.includes('true'); + // } + + if (skipIntro /* || skipCookie */) { + showIntroduction(false); + } + else { + showIntroduction(true); + } + }); + + /* + * ----------------- Initialize BWS capture jQuery plugin + * -------------------------------- + */ + + + // initialize - load content in specific language and initialize bws + // capture + function initialize() { + // change title if task is enrollment + if (task === 'enrollment') { + $('#uuititle').attr('data-res', 'titleEnrollment'); + } + // change title if task is identification + else if (task === 'identification') { + $('#uuititle').attr('data-res', 'titleIdentification'); + } + else if (task === 'livenessdetection') { + $('#uuititle').attr('data-res', 'titleLiveDetection'); + } + + // try to get language info from the browser. + let userLangAttribute = navigator.language || navigator.userLanguage || navigator.browserLanguage || 'en'; + let userLang = userLangAttribute.slice(0, 2); + // let userLocation = userLangAttribute.slice(-2) || 'us'; + + $.getJSON('./language/' + userLang + '.json'). + done(function (data) { + console.log('Loaded the language-specific resource successfully'); + localizedData = data; + }).fail(function (textStatus, error) { + console.log('Loading of language-specific resource failed with: ' + textStatus + ', ' + error); + }).always(function () { + localize(); + }); + } + + /* + * ------------------ Start BWS capture jQuery plugin + * ----------------------------------- + */ + + // startup code + function onStart() { + bwsCapture.start(function () { + captureStarted(); + $('#uuicanvas').show(); + }, function (error) { + + // hide uuiwebapp + $('#uuiwebapp').hide(); + + // show default information about general issues + $('#uuisplash').show(); + + // show button for continue without biometrics (skip biometric + // task) + $('#uuiskip').show(); + // show button for BioID app (interapp communication) + if (task === 'verification' || task === 'enrollment') { + $('#uuimobileapp').show(); + } + if (error !== undefined) { + // different browsers use different errors + if (error.code === 1 || error.name === 'PermissionDeniedError') { + // in the spec we find code == 1 and name == + // PermissionDeniedError for the permission denied error + $('#uuierror').html(formatText('capture-error', formatText('PermissionDenied'))); + } else { + // otherwise try to print the error + $('#uuierror').html(formatText('capture-error', error)); + } + } else { + // no error info typically says that browser doesn't support + // getUserMedia + $('#uuierror').html(formatText('nogetUserMedia')); + } + }, function (error, retry) { + // done + stopRecording(); + currentExecution++; + + if (error !== undefined && retry && currentExecution < executions) { + console.log('Current Execution: ' + currentExecution); + } else { + // done: redirect to caller ... + let url = returnURL + '?access_token=' + token; + if (error !== undefined) { + url = url + '&error=' + error; + } + url = url + '&state=' + state + '&skipintro=' + skipIntro; + window.location.replace(url); + } + }, function (status, message, dataURL) { + let $msg; + if (status === 'UploadProgress') { + // for single upload status + let id = message.id; + let modId = ((id - 1) % 4) + 1; + // for compact upload status + let progresscompact = 0; + progressMap.set(id, message.progress); + progressMap.forEach(function (value) { return progresscompact += value }); + progresscompact = Math.ceil(progresscompact / recordings); + + if (progresscompact > 100) { + progresscompact = 100; + } + + // css media query decision + if ($('#uuisingleupload').is(':visible') == true) { + $('#uuiprogress' + modId).show(); + $('#uuiprogressbar' + modId).width(message.progress + '%'); + // if the window size changed + $('#uuiprogresscompact').hide(); + } + else { + $('#uuiprogresscompact').show(); + $('#uuiprogressbarcompact').width(progresscompact + '%'); + } + } + else if (status === 'DisplayTag') { + setCurrentTag(message); + $msg = $('#uuiinstruction'); + if (challengeResponse || task === 'enrollment') { + $msg.html(formatText('UserInstruction-FollowMe')); + } + else { + $msg.html(formatText('UserInstruction-NodYourHead')); + } + $msg.stop(true).fadeIn(); + } else { + // report a message on the screen + let msg = formatText(status); + + // user instructions + if (status.indexOf('UserInstruction') > -1) { + $msg = $('#uuiinstruction'); + if (status === 'UserInstruction-Start') { + let counter = recordings; + if (counter > 4) { + counter = 4; + } + for (let i = 1; i <= counter; i++) { + $('#uuiuploaded' + i).hide(); + $('#uuiupload' + i).hide(); + $('#uuiwait' + i).show(); + $('#uuiimage' + i).show(); + $('#uuiprogress' + i).hide(); + $('#uuiprogressbar' + i).width(0); + } + progressMap.clear(); + $('#uuiprogresscompact').hide(); + $('#uuiprogressbarcompact').width(0); + resetHeadDisplay(); + } + else { + $msg.html(msg); + $msg.stop(true).fadeIn(); + } + } + + // perform tasks + if (status.indexOf('Perform') > -1 || status.indexOf('Retry') > -1) { + // hide compact upload progress + $('#uuiprogresscompact').hide(); + } + + // results of uploading or perform task + if (status.indexOf('Failed') > -1 || + status.indexOf('NotRecognized') > -1 || + status.indexOf('NoFaceFound') > -1 || + status.indexOf('MultiFacesFound') > - 1) { + + changeLiveView(true); + + // show message + $('#uuiinstruction').text(''); + $('#uuistatus').show(); + $msg = $('#uuimessage'); + $msg.html(formatText(msg)); + $msg.stop(true).fadeIn(); + } + + // display some animations/images depending on the status + let uploaded = bwsCapture.getUploaded(); + let recording = uploaded + bwsCapture.getUploading(); + // use modulo calculation for images more than 4 + let modRecording = ((recording-1) % 4) + 1; + let modUploaded = ((uploaded-1) % 4) + 1; + + if (status === 'Uploading') { + // begin an upload - current image + $('#uuiwait' + modRecording).hide(); + $('#uuiupload' + modRecording).show(); + $('#uuiuploaded' + modRecording).hide(); + + // if uuiuploaded is not visible -> mobile view + if (recording >= recordings) { + $('#uuiinstruction').html(formatText('UserInstruction-PleaseWait')); + changeLiveView(true); + } + } else if (status === 'Uploaded') { + // successfull upload (we should have a dataURL) + if (dataURL) { + $('#uuiupload' + modUploaded).hide(); + $('#uuiprogress' + modUploaded).hide(); + let $image = $('#uuiuploaded' + modUploaded); + $image.attr('src', dataURL); + $image.show(); + } + } else if (status === 'NoFaceFound' || status === 'MultipleFacesFound') { + // upload failed + recording++; + modRecording = ((recording-1) % 4) + 1; + $('#uuiupload' + modRecording).hide(); + $('#uuiwait' + modRecording).show(); + } + } + }); + } + + // switch between liveview and displayed messages + function changeLiveView(blur) { + if (blur) { + // hide head and blur canvas + hideHead(); + $('#uuicanvas').css('filter', 'blur(10px)'); + $('#uuiprogresscompact').hide(); + } + else { + $('#uuicanvas').css('filter', 'none'); + showHead(); + } + } + + // called by onStart to update GUI + function captureStarted() { + $('#uuiwebapp').show(); + $('#uuimessage').show(); + $('#uuiinstruction').show(); + + // Currently not neccessary - therefore the button is not shown! + // $('#uuimirror').show().click(mirror); + + $('#uuistart').show().click(function () { startRecording(task === 'enrollment'); }); + + + $('#uuiok').show().click(function () { + $('#uuistatus').hide(); + $('#uuistart').prop('disabled', false); + $('#uuiinstruction').html(formatText('UserInstruction-CloseUp')); + changeLiveView(false); + }); + + setTimeout(function () { console.log('triggered showHead'); showHead(); }, 50); + + } + + // called from onStart when recording is done + function stopRecording() { + hideHead(); + + bwsCapture.stopRecording(); + + for (let i = 1; i <= 4; i++) { + $('#uuiimage' + i).hide(); + } + } + + /* + * -------------------- Displaying head + * --------------------------------------------------- + */ + + var camera, scene, renderer, id; + var startTime; + var resetHead = false; + const maxVertical = 0.20; + const maxHorizontal = 0.25; + + function initHead() { + // renderer + try { + renderer = new THREE.WebGLRenderer({ alpha: true }); + } + catch (e) { + return false; + } + + let container = document.getElementById('uuihead'); + document.body.appendChild(container); + + let width = $('#uuihead').width(); + let height = $('#uuihead').height(); + let uuihead = $('#uuihead'); + $('#uuiliveview').append(uuihead); + + // camera + camera = new THREE.PerspectiveCamera(20, width / height, 1, 1000); + camera.position.set(0, 0, 5.5); + + // scene + scene = new THREE.Scene(); + let ambientLight = new THREE.AmbientLight(0x4953FF, 0.4); + scene.add(ambientLight); + let pointLight = new THREE.PointLight(0x3067FF, 0.8); + camera.add(pointLight); + scene.add(camera); + + // texture + let manager = new THREE.LoadingManager(); + manager.onProgress = function (item, loaded, total) { + console.log(item, loaded, total); + }; + + // model + let onProgress = function (xhr) { + if (xhr.lengthComputable) { + let percentComplete = xhr.loaded / xhr.total * 100; + console.log(Math.round(percentComplete, 2) + '% downloaded'); + } + }; + let onError = function (xhr) {}; + let loader = new THREE.OBJLoader(manager); + let material = new THREE.MeshLambertMaterial({ transparent: false, opacity: 0.8 }); + + loader.load('./model/head.obj', function (head) { + head.traverse(function (child) { + if (child instanceof THREE.Mesh) { + // child.material = material; + } + }); + head.name = 'BioIDHead'; + head.position.y = 0; + scene.add(head); + }, onProgress, onError); + + renderer.setClearColor(0x000000, 0); // the default + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(width, height); + + container.appendChild(renderer.domElement); + document.addEventListener('uuiresize', onHeadResize, false); + + return true; + } + + function onHeadResize() { + + let canvasWidth = parseInt($('#uuicanvas').width()); + let canvasHeight = parseInt($('#uuicanvas').height()); + + $('#uuihead').css({ 'margin-top': -canvasHeight, 'margin-left': '0' }); + $('#uuihead').attr('width', canvasWidth); + $('#uuihead').attr('height', canvasHeight + 50); + + camera.aspect = canvasWidth / canvasHeight; + camera.updateProjectionMatrix(); + renderer.setSize(canvasWidth, canvasHeight); + renderer.render(scene, camera); + } + + function resetHeadDisplay() { + currentTag = ''; + parentTag = ''; + resetHead = true; + cancelAnimationFrame(id); + $('.head').css('opacity', '0.6'); + } + + function setCurrentTag(tag) { + if (currentTag !== '') { + parentTag = currentTag; + } + + currentTag = tag; + startTime = new Date().getTime(); + + if (currentTag === 'any' && task !== 'enrollment') { + constantAnimation(); + } + else { + animateHead(); + console.log('DisplayTag: ' + tag); + } + } + + function constantAnimation() { + // change css class 'head' + $('.head').css('opacity', '0.8'); + + // animation time + let delta = 0.005; + let head = scene.getObjectByName('BioIDHead'); + showHead(); + + let direction = 'down'; + var animate = function () { + + if (direction === 'up') { + if (head.rotation.x >= -maxVertical) { + head.rotation.x -= delta; + } + else { + direction = 'down'; + } + } + + if (direction === 'down') { + if (head.rotation.x <= maxVertical) { + head.rotation.x += delta; + } + else { + direction = 'up'; + } + } + + id = requestAnimationFrame(animate); + renderer.render(scene, camera); + }; + animate(); + } + + function animateHead() { + // animation time + let speed = 0.000005; + let endTime = new Date().getTime(); + let deltaTime = (endTime - startTime); + let delta = deltaTime * speed; + + let head = scene.getObjectByName('BioIDHead'); + let doAnimation = false; + + if (head) { + if (resetHead) { + // reset head rotation to center + head.rotation.x = 0; + head.rotation.y = 0; + resetHead = false; + doAnimation = true; + // change css class 'head' + $('.head').css('opacity', '0.8'); + showHead(); + } + else { + if (currentTag === 'any') { + if (task === 'enrollment') { + // get predefined direction for better enrollment + let recording = bwsCapture.getUploaded() + bwsCapture.getUploading() - 1; + currentTag = enrollmentTags[recording]; + } + else { + if (head.rotation.x >= -maxVertical && head.rotation.x <= 0) { + head.rotation.x -= delta; + doAnimation = true; + } + else { + head.rotation.x += delta; + doAnimation = true; + } + } + } + + if (currentTag === 'down') { + head.rotation.y = 0; + if (parentTag === 'up') { + if (head.rotation.x <= 0) { + head.rotation.x += delta; + doAnimation = true; + } + } + else { + if (head.rotation.x >= 0 && head.rotation.x < maxVertical) { + head.rotation.x += delta; + doAnimation = true; + } + } + } + else if (currentTag === 'up') { + head.rotation.y = 0; + if (parentTag === 'down') { + if (head.rotation.x >= 0) { + head.rotation.x -= delta; + doAnimation = true; + } + } + else { + if (head.rotation.x >= -maxVertical && head.rotation.x <= 0) { + head.rotation.x -= delta; + doAnimation = true; + } + } + } + else if (currentTag === 'left') { + head.rotation.x = 0; + if (parentTag === 'right') { + if (head.rotation.y >= 0) { + head.rotation.y -= delta; + doAnimation = true; + } + } + else { + if (head.rotation.y >= -maxHorizontal && head.rotation.y <= 0) { + head.rotation.y -= delta; + doAnimation = true; + } + } + } + else if (currentTag === 'right') { + head.rotation.x = 0; + if (parentTag === 'left') { + if (head.rotation.y <= 0) { + head.rotation.y += delta; + doAnimation = true; + } + } + else { + if (head.rotation.y >= 0 && head.rotation.y <= maxHorizontal) { + head.rotation.y += delta; + doAnimation = true; + } + } + } + } + + if (doAnimation) { + id = requestAnimationFrame(animateHead); + } + renderer.render(scene, camera); + } + } + + function showHead() { + + $('#uuihead').show(); + onHeadResize(); + console.log('showHead') + } + + function hideHead() { + $('#uuihead').hide(); + resetHeadDisplay(); + console.log('hideHead'); + } + diff --git a/oxAuth/Server/src/main/webapp/auth/bioid/language/de.json b/oxAuth/Server/src/main/webapp/auth/bioid/language/de.json new file mode 100644 index 00000000..0048a4de --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/bioid/language/de.json @@ -0,0 +1,53 @@ +{ + "titleEnrollment": "Einlernen", + "titleVerification": "Verifikation", + "titleIdentification": "Identifikation", + "titleLiveDetection": "Lebenderkennung", + + "introductionTitle": "So geht's", + "introductionStep1": "Schritt 1", + "introductionStep1Desc1": "Kommen Sie näher zur Kamera,", + "introductionStep1Desc2": "bevor Sie starten", + "introductionTooFarAway": "Zu weit entfernt!", + "introductionTooClose": "Zu nah!", + "introductionPerfect": "Perfekt!", + "introductionStep2": "Schritt 2", + "introductionStep2Desc1": "Für Lebenderkennung", + "introductionStep2Desc2": "bitte leicht nicken", + "introductionDontMoveDevice": "Bitte nicht das Gerät bewegen!", + "introductionDontShowAgain": "Verstanden! Diese Anleitung nicht wieder anzeigen", + + "buttonCancel": "zurück", + "buttonCancel.title": "Abbruch und zurück zum Aufrufer", + "buttonReadyToStart": "Ich bin bereit", + "buttonContinue": "Fortfahren ohne Biometrie", + "buttonMobileApp": "Starte BioID app", + + "prompt": "Es scheint, als hätten wir keinen Kamerazugriff. Um unsere biometrischen Anwendungen durchzuführen, versuchen Sie bitte Folgendes:
- Erlauben Sie Zugriff zur Kamera.
- Benutzen Sie einen anderen Browser (z.B. die aktuellste Version von Opera, Firefox, Chrome oder Edge).", + "mobileapp": "Wenn Sie die BioID app auf Ihrem Mobilgerät installiert haben, können Sie diese verwenden.", + + "uploadInfo": "Uploading...", + + "capture-error": "Die Bildaufnahme ist fehlgeschlagen.
Scheinbar ist der Kamerazugriff nicht erlaubt.
Leider ist dadurch keine Gesichts- und Lebenderkennung möglich.", + "nogetUserMedia": "Ihr Browser unterstützt das HTML5 Feature Media Capture und Streams API nicht. Bitte nutzen Sie einen anderen Browser oder die BioID App für Mobilgeräte.", + "permissionDenied": "Zugriff verweigert!", + "webgl-error": "WebGL ist deaktiviert oder nicht verfügbar. Wenn möglich aktivieren sie WebGL oder benutzen sie einen anderen Browser.", + + "UserInstruction-CloseUp": "Vor dem Start bitte näherkommen
und das Gesicht im blauen Kopf positionieren", + "UserInstruction-NodYourHead": "Nicken Sie mit dem Kopf", + "UserInstruction-FollowMe": "Folgen Sie dem blauen Kopf", + "UserInstruction-NoMovement": "Folgen Sie der Kopfbewegung", + "UserInstruction-PleaseWait": "Bitte warten", + + "Perform-enrollment": "Einlernen...", + "Perform-verification": "Verifikation...", + "Perform-identification": "Identifikation...", + "Perform-livenessdetection": "In Bearbeitung...", + + "NoMotionDetected": "Wir haben keine Bewegung erkannt.
Für Lebenderkennung bitte den Kopf leicht bewegen.", + "NoFaceFound": "Es wurde kein geeignetes Gesicht gefunden.
Bitte näherkommen und direkt in die Kamera blicken.", + "MultipleFacesFound": "Es wurde mehr als ein Gesicht gefunden oder der Hintergrund ist zu unruhig.
Ihr Gesicht soll den Kreis ganz ausfüllen.", + "LiveDetectionFailed": "Lebenderkennung fehlgeschlagen.
Gerade in die Kamera blicken, dann den Kopf langsam bewegen.
Achten Sie auf gleichmäßige Beleuchtung.", + "ChallengeResponseFailed": "Challenge-Response Erkennung fehlgeschlagen!
Folgen Sie langsam der Bewegung des Kopfes.", + "NotRecognized": "Erkennung fehlgeschlagen.
Sorgen Sie für gleichmäßige Beleuchtung. Für bessere Erkennung bitte nochmals einlernen (enrollment)." +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/auth/bioid/language/en.json b/oxAuth/Server/src/main/webapp/auth/bioid/language/en.json new file mode 100644 index 00000000..a7ed4cf5 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/bioid/language/en.json @@ -0,0 +1,53 @@ +{ + "titleEnrollment": "Enrollment", + "titleVerification": "Verification", + "titleIdentification": "Identification", + "titleLiveDetection": "Liveness Detection", + + "introductionTitle": "How it works", + "introductionStep1": "Step 1", + "introductionStep1Desc1": "Before you start", + "introductionStep1Desc2": "come closer to the camera", + "introductionTooFarAway": "Too far away!", + "introductionTooClose": "Too close!", + "introductionPerfect": "Perfect!", + "introductionStep2": "Step 2", + "introductionStep2Desc1": "For Liveness Detection", + "introductionStep2Desc2": "nod your head slightly", + "introductionDontMoveDevice": "Please don't move the device!", + "introductionDontShowAgain": "GOT IT! Don't show the instruction again", + + "buttonCancel": "back", + "buttonCancel.title": "Abort and navigate back to caller", + "buttonReadyToStart": "I'm ready to start", + "buttonContinue": "Skip biometrics", + "buttonMobileApp": "Start BioID app", + + "prompt": "It seems like we don't have access to the camera. For performing biometric operations, please try the following:
- Grant access to the camera.
- Use a different browser (e.g. the most recent versions of Opera, Firefox, Chrome or Edge).", + "mobileapp": "If you have installed the BioID App on your mobile device, you can use this app for enrollment or verification.", + + "uploadInfo": "Uploading...", + + "capture-error": "We could not capture an image.
Sorry, but without access to the camera, facial recognition and liveness detection aren't possible!", + "nogetUserMedia": "Your browser does not support the HTML5 Media Capture and Streams API. Please use a different browser or the BioID mobile app.", + "permissionDenied": "Permission Denied!", + "webgl-error": "WebGL is disabled or unavailable. If possible activate WebGL or use another browser.", + + "UserInstruction-CloseUp": "Come close and position your face
within the blue head before you start", + "UserInstruction-NodYourHead": "Please nod your head", + "UserInstruction-FollowMe": "Follow the blue head", + "UserInstruction-NoMovement": "Follow the head's movement", + "UserInstruction-PleaseWait": "Please wait", + + "Perform-enrollment": "Training...", + "Perform-verification": "Verifying...", + "Perform-identification": "Identifying...", + "Perform-livenessdetection": "Processing...", + + "NoMotionDetected": "We could not detect any motion.
For Liveness Detection please nod your head slightly.", + "NoFaceFound": "We could not find a suitable face.
Come close and look straight before you start.", + "MultipleFacesFound": "We found multiple faces or a strongly uneven background distracted us.
Your face should fill the circle completely.", + "LiveDetectionFailed": "Liveness Detection failed.
Look straight into the camera, then nod your head slightly.
Please ensure constant lighting.", + "ChallengeResponseFailed": "Challenge-Response failed!
Slowly follow the head's movement.", + "NotRecognized": "You have not been recognized!
Please ensure constant lighting. For improving recognition, please enroll again." +} \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/auth/bioid/model/head.obj b/oxAuth/Server/src/main/webapp/auth/bioid/model/head.obj new file mode 100644 index 00000000..dcbb7e44 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/bioid/model/head.obj @@ -0,0 +1,9857 @@ +# Blender v2.79 (sub 0) OBJ File: '' +# www.blender.org +mtllib head-0.2.mtl +o BioID-Head_Head +v -0.238778 -0.574189 -0.057439 +v -0.269509 -0.029791 0.070056 +v -0.161132 -0.585171 -0.136370 +v -0.124196 -0.584606 -0.251022 +v 0.126876 -0.584514 -0.253200 +v 0.040831 -0.584408 -0.064242 +v 0.368518 -0.557374 -0.023321 +v 0.344881 -0.565595 -0.066060 +v -0.013300 0.043919 0.508456 +v -0.025485 0.057428 0.518716 +v -0.000020 0.053811 0.529351 +v -0.000020 0.044110 0.512199 +v -0.064614 0.040374 0.467087 +v -0.040768 0.039002 0.479601 +v -0.052824 0.053517 0.492349 +v -0.067772 0.053374 0.475130 +v -0.083389 0.066586 0.453850 +v -0.087057 0.030386 0.454392 +v -0.067712 0.015048 0.462634 +v -0.044683 0.026180 0.470742 +v -0.038424 -0.185044 0.454327 +v -0.059393 -0.180661 0.447551 +v -0.043891 -0.201682 0.449983 +v -0.026252 -0.210006 0.450625 +v -0.135126 -0.260507 0.213517 +v -0.132204 -0.237107 0.241683 +v -0.154765 -0.217881 0.225959 +v -0.154207 -0.257129 0.190788 +v -0.113239 -0.272372 0.227279 +v -0.036271 -0.480126 0.189528 +v 0.007611 -0.491502 0.188892 +v -0.014212 -0.511218 0.181508 +v -0.266722 0.471345 0.207867 +v -0.325786 0.221899 0.059753 +v -0.312471 0.092007 -0.005832 +v -0.316313 0.103905 -0.018106 +v -0.361286 0.234614 -0.045583 +v -0.343348 0.124163 -0.044001 +v -0.346138 0.107439 -0.043493 +v -0.325220 0.113888 -0.029715 +v -0.355827 0.260678 -0.048804 +v -0.349485 0.262322 -0.063447 +v -0.364781 0.251618 -0.059154 +v -0.342604 0.270559 -0.041874 +v -0.357920 0.256988 -0.039294 +v -0.364047 0.243196 -0.052483 +v -0.338922 0.264925 -0.075894 +v -0.339969 -0.579252 -0.213601 +v -0.388418 -0.489811 -0.267966 +v -0.354293 -0.448976 -0.247272 +v -0.220451 -0.376333 -0.245096 +v -0.178638 -0.387940 -0.270320 +v -0.183052 -0.424439 -0.296285 +v -0.250211 -0.439819 -0.292907 +v -0.027912 -0.530848 -0.348232 +v -0.007572 -0.516692 -0.350556 +v -0.066074 0.059883 0.487609 +v -0.074619 0.067598 0.466671 +v -0.000295 -0.193899 0.457578 +v -0.008334 -0.175371 0.456849 +v -0.000020 -0.211973 0.452893 +v -0.010703 -0.292655 0.255346 +v 0.010001 -0.274465 0.277127 +v -0.015283 -0.270798 0.282514 +v -0.012027 -0.090150 -0.253173 +v -0.019492 -0.053692 -0.268976 +v -0.021836 -0.414495 -0.309315 +v -0.006282 -0.388653 -0.296859 +v -0.050539 -0.350899 -0.277154 +v -0.008733 -0.348413 -0.280051 +v -0.007444 -0.276997 -0.256889 +v -0.006634 -0.214646 -0.242655 +v -0.000013 -0.183691 -0.239443 +v -0.016306 -0.161361 -0.238363 +v -0.027688 -0.140200 -0.239353 +v -0.019614 -0.118820 -0.244102 +v -0.052615 -0.098553 -0.247771 +v -0.085659 -0.071208 -0.254493 +v -0.101897 -0.029025 -0.272699 +v -0.148729 0.058172 -0.308043 +v -0.174649 0.052933 -0.295150 +v -0.203982 0.099704 -0.301980 +v -0.227958 0.145500 -0.302541 +v -0.261130 0.170131 -0.275118 +v -0.263975 0.217036 -0.284036 +v -0.278364 0.202076 -0.259655 +v -0.292376 0.255284 -0.245255 +v -0.322285 0.305912 -0.166686 +v -0.328427 0.311248 -0.130919 +v -0.332234 0.318194 -0.097389 +v -0.335014 0.286268 -0.074862 +v -0.337329 0.276315 -0.060362 +v -0.362927 0.250962 -0.074044 +v -0.364564 0.238672 -0.084858 +v -0.369293 0.236479 -0.069839 +v -0.370301 0.217444 -0.085730 +v -0.369865 0.207067 -0.077110 +v -0.367475 0.206273 -0.065557 +v -0.362148 0.182393 -0.058797 +v -0.338132 0.170059 -0.026177 +v -0.337689 0.201670 -0.015129 +v -0.329099 0.170407 -0.008835 +v -0.310565 0.144079 -0.002028 +v -0.322251 0.128580 -0.027073 +v -0.306463 0.126404 -0.006450 +v -0.303954 0.104815 -0.001866 +v -0.328514 0.084015 -0.020212 +v -0.338228 0.089276 -0.032216 +v -0.319869 0.066033 0.000629 +v -0.307730 0.083132 0.011951 +v -0.300432 0.109884 0.008915 +v -0.302488 0.133766 0.010795 +v -0.307582 0.139338 0.049794 +v -0.311696 0.162073 0.059692 +v -0.324529 0.181816 0.013942 +v -0.329461 0.205587 0.028151 +v -0.331826 0.193359 0.001949 +v -0.341128 0.234753 0.007004 +v -0.338255 0.222493 -0.008103 +v -0.350389 0.248832 -0.018892 +v -0.349722 0.234059 -0.027022 +v -0.345859 0.253774 -0.008352 +v -0.340808 0.271632 -0.021184 +v -0.341354 0.264531 -0.009926 +v -0.336148 0.286524 -0.026986 +v -0.336063 0.285875 -0.044668 +v -0.330351 0.355419 -0.062441 +v -0.323003 0.360757 -0.137042 +v -0.310106 0.332223 -0.203223 +v -0.292322 0.312805 -0.246366 +v -0.271096 0.313278 -0.277727 +v -0.280921 0.260442 -0.265596 +v -0.252103 0.267871 -0.303014 +v -0.242710 0.210261 -0.306979 +v -0.225124 0.194980 -0.319427 +v -0.194298 0.184749 -0.338299 +v -0.191571 0.148441 -0.329259 +v -0.162803 0.107366 -0.327539 +v -0.115678 0.101780 -0.339140 +v -0.101144 0.049118 -0.315871 +v -0.088250 0.023252 -0.303968 +v -0.036861 0.025448 -0.310559 +v 0.000000 0.061788 -0.329979 +v -0.048320 0.095921 -0.344176 +v -0.021919 0.144392 -0.362840 +v -0.037736 0.278456 -0.383181 +v 0.001149 0.401888 -0.369721 +v -0.077138 0.426059 -0.362815 +v 0.000000 0.472437 -0.347332 +v 0.000000 0.568417 -0.284289 +v -0.058738 0.594600 -0.257755 +v 0.058738 0.594600 -0.257755 +v -0.044716 0.647034 -0.180103 +v -0.012165 0.667525 -0.137181 +v -0.011454 0.677489 -0.109856 +v 0.022194 0.684347 -0.082743 +v -0.049385 0.682986 -0.079583 +v -0.010653 0.689794 -0.054735 +v 0.022666 0.691478 -0.039694 +v -0.029669 0.692146 -0.012468 +v 0.020676 0.692090 0.012500 +v -0.030067 0.688079 0.043428 +v 0.000000 0.684158 0.073487 +v -0.029904 0.677520 0.100432 +v 0.000000 0.672327 0.123942 +v -0.019744 0.662010 0.156705 +v 0.024403 0.654589 0.177074 +v -0.018312 0.638636 0.217532 +v -0.006151 0.622123 0.251714 +v -0.010036 0.603978 0.279368 +v -0.009936 0.587744 0.299458 +v -0.010362 0.569254 0.318459 +v -0.010887 0.549038 0.335967 +v -0.013129 0.517236 0.358831 +v -0.018535 0.471869 0.383218 +v 0.013271 0.420084 0.403055 +v -0.037567 0.421469 0.401983 +v -0.020749 0.340771 0.423014 +v 0.019341 0.341696 0.422753 +v -0.000344 0.264274 0.435974 +v -0.020180 0.260659 0.435750 +v -0.012446 0.219520 0.452097 +v -0.016694 0.192423 0.468800 +v 0.014129 0.172623 0.485069 +v -0.000020 0.140911 0.511272 +v -0.018743 0.114171 0.528157 +v -0.000020 0.098773 0.541440 +v -0.022392 0.079998 0.533809 +v -0.000020 0.080638 0.546372 +v -0.000162 0.066557 0.540980 +v -0.021837 0.031528 0.480623 +v -0.000021 0.028137 0.476687 +v -0.023168 0.017274 0.470789 +v -0.000020 0.015476 0.468199 +v -0.016877 -0.004071 0.467560 +v -0.044307 -0.025355 0.470577 +v -0.000020 -0.029129 0.473190 +v -0.044479 -0.040431 0.468481 +v -0.000020 -0.043465 0.472256 +v -0.026936 -0.054529 0.463954 +v -0.020351 -0.069069 0.459581 +v -0.008952 -0.098405 0.461968 +v -0.016479 -0.117799 0.459101 +v -0.015454 -0.142627 0.456105 +v -0.061984 -0.137350 0.449619 +v -0.082476 -0.161150 0.436839 +v -0.052322 -0.213877 0.439866 +v -0.073312 -0.197362 0.428872 +v -0.035250 -0.221489 0.441771 +v -0.062256 -0.225295 0.413039 +v -0.038046 -0.231151 0.428887 +v -0.040120 -0.237614 0.413302 +v -0.042004 -0.241486 0.395405 +v -0.008284 -0.244573 0.406516 +v -0.019317 -0.251460 0.338479 +v 0.011977 -0.252061 0.338021 +v -0.016604 -0.258921 0.307451 +v -0.052063 -0.259413 0.293300 +v -0.043733 -0.275367 0.268868 +v -0.074838 -0.259163 0.276612 +v -0.066796 -0.280176 0.253296 +v -0.045829 -0.294824 0.247291 +v -0.095793 -0.263707 0.252487 +v -0.090835 -0.282773 0.236620 +v -0.069103 -0.303157 0.233241 +v -0.092625 -0.308469 0.220010 +v -0.114252 -0.317342 0.201947 +v -0.131845 -0.311124 0.189739 +v -0.150688 -0.328402 0.164235 +v -0.164653 -0.302034 0.153479 +v -0.176864 -0.321954 0.131655 +v -0.179800 -0.347212 0.128409 +v -0.154251 -0.368336 0.155928 +v -0.185005 -0.387504 0.127672 +v -0.202603 -0.360381 0.100667 +v -0.213386 -0.397138 0.101655 +v -0.194627 -0.426702 0.130030 +v -0.235519 -0.409584 0.085779 +v -0.233137 -0.441516 0.108296 +v -0.261995 -0.428857 0.075959 +v -0.276478 -0.471890 0.102240 +v -0.229738 -0.469168 0.126851 +v -0.286509 -0.449592 0.074469 +v -0.262696 -0.492416 0.125971 +v -0.313657 -0.476309 0.082433 +v -0.313639 -0.504259 0.110846 +v -0.361796 -0.505679 0.081714 +v -0.383902 -0.501333 0.059620 +v -0.400382 -0.495317 0.035224 +v -0.412893 -0.513516 0.044171 +v -0.433144 -0.506795 0.009781 +v -0.440150 -0.520238 0.007607 +v -0.453347 -0.501001 -0.037857 +v -0.459502 -0.516317 -0.034208 +v -0.467745 -0.525675 -0.054686 +v -0.470886 -0.513571 -0.071272 +v -0.457746 -0.538663 -0.048806 +v -0.474117 -0.537907 -0.114772 +v -0.466193 -0.548196 -0.131130 +v -0.461043 -0.551140 -0.182674 +v -0.443686 -0.560358 -0.171073 +v -0.397679 -0.571370 -0.166325 +v -0.390601 -0.572971 -0.222120 +v -0.337821 -0.575900 -0.276337 +v -0.338025 -0.578860 -0.161443 +v -0.430306 -0.561483 -0.124718 +v -0.411053 -0.561575 -0.094799 +v -0.447617 -0.551867 -0.092283 +v -0.436517 -0.549670 -0.053318 +v -0.439644 -0.539730 -0.010156 +v -0.441812 -0.530725 0.000669 +v -0.395223 -0.530633 0.073093 +v -0.415855 -0.524929 0.045562 +v -0.392336 -0.519134 0.069858 +v -0.355503 -0.523593 0.099319 +v -0.298011 -0.519120 0.128479 +v -0.241463 -0.510818 0.144599 +v -0.202829 -0.455008 0.134334 +v -0.188369 -0.465813 0.145036 +v -0.158882 -0.441183 0.155595 +v -0.162631 -0.405811 0.149110 +v -0.123618 -0.368009 0.180142 +v -0.093309 -0.351521 0.202687 +v -0.069476 -0.336206 0.216693 +v -0.038128 -0.324355 0.228921 +v -0.010916 -0.315836 0.236701 +v -0.009458 -0.349644 0.218894 +v -0.011336 -0.390104 0.204434 +v -0.010189 -0.429290 0.196430 +v -0.054615 -0.430064 0.192697 +v -0.107282 -0.504492 0.173637 +v -0.075241 -0.512387 0.176475 +v -0.118634 -0.531503 0.168961 +v -0.099213 -0.547068 0.162945 +v -0.160747 -0.546370 0.162446 +v -0.154097 -0.553459 0.157255 +v -0.171641 -0.533154 0.164328 +v -0.144012 -0.558488 0.149375 +v -0.134497 -0.561094 0.138386 +v -0.231179 -0.552837 0.144090 +v -0.225757 -0.557922 0.133250 +v -0.313153 -0.551534 0.055374 +v -0.265179 -0.557261 0.050448 +v -0.169789 -0.570781 0.027630 +v -0.197070 -0.579827 -0.081782 +v -0.376632 -0.572407 -0.135990 +v -0.243854 -0.583637 -0.166024 +v -0.408711 -0.552878 -0.032261 +v -0.410578 -0.542448 0.017991 +v -0.393102 -0.536673 0.061691 +v -0.336729 -0.544166 0.104675 +v -0.340994 -0.539962 0.110712 +v -0.296136 -0.543404 0.131717 +v -0.327324 -0.533636 0.117847 +v -0.277915 -0.531930 0.140633 +v -0.239452 -0.536850 0.152034 +v -0.229514 -0.526185 0.152984 +v -0.197170 -0.504522 0.154977 +v -0.163137 -0.483834 0.159326 +v -0.138944 -0.457765 0.165619 +v -0.124975 -0.422529 0.172314 +v -0.117076 -0.393538 0.179575 +v -0.090193 -0.412759 0.187913 +v -0.064376 -0.393245 0.199685 +v -0.047598 -0.346516 0.216992 +v -0.106320 -0.456042 0.177352 +v -0.139328 -0.500311 0.167656 +v -0.241345 -0.545939 0.148702 +v -0.276629 -0.551313 0.132177 +v -0.280693 -0.553661 0.120189 +v -0.327316 -0.548877 0.070319 +v -0.378701 -0.547177 0.025320 +v -0.368546 -0.557375 -0.023323 +v -0.344912 -0.565594 -0.066060 +v -0.205742 -0.559883 0.075974 +v -0.059269 -0.557864 0.150445 +v -0.056797 -0.531272 0.170727 +v -0.000017 -0.544690 0.161195 +v -0.012266 -0.556040 0.151277 +v -0.000020 -0.563241 0.138899 +v -0.000143 -0.564505 0.122341 +v 0.011273 -0.567438 0.090192 +v -0.024969 -0.569122 0.080798 +v -0.000038 -0.577132 0.052214 +v -0.000350 -0.581804 0.024023 +v -0.047596 -0.584314 -0.066265 +v -0.000017 -0.587631 -0.141409 +v -0.000020 -0.584051 -0.273223 +v -0.052941 -0.567216 -0.328012 +v 0.037227 -0.552937 -0.338939 +v -0.049032 -0.541233 -0.344565 +v -0.055568 -0.555445 -0.338026 +v -0.073443 -0.527766 -0.347634 +v -0.059958 -0.511595 -0.348638 +v -0.161818 -0.502486 -0.343170 +v -0.137191 -0.482289 -0.337543 +v -0.108784 -0.459953 -0.327771 +v -0.293185 -0.466104 -0.300121 +v -0.283817 -0.431658 -0.272963 +v -0.318711 -0.445924 -0.270504 +v -0.318123 -0.426686 -0.246920 +v -0.391078 -0.515362 -0.291269 +v -0.367889 -0.515143 -0.304555 +v -0.417754 -0.523582 -0.278450 +v -0.435596 -0.521354 -0.259001 +v -0.402644 -0.469153 -0.224097 +v -0.437589 -0.497915 -0.221945 +v -0.365024 -0.441168 -0.222251 +v -0.356567 -0.424947 -0.196234 +v -0.305391 -0.407574 -0.226658 +v -0.285240 -0.388092 -0.209751 +v -0.248711 -0.379277 -0.231027 +v -0.215008 -0.345405 -0.217857 +v -0.184715 -0.351085 -0.242315 +v -0.175851 -0.319178 -0.226925 +v -0.155676 -0.345481 -0.250955 +v -0.137975 -0.394155 -0.283597 +v -0.074381 -0.413774 -0.303918 +v -0.016367 -0.479542 -0.343747 +v -0.104110 -0.365415 -0.274415 +v -0.036150 -0.305102 -0.263013 +v -0.048531 -0.265491 -0.250343 +v -0.047695 -0.214354 -0.239528 +v -0.080095 -0.189091 -0.234211 +v -0.088431 -0.118052 -0.237505 +v -0.082793 -0.150329 -0.233041 +v -0.108939 -0.097931 -0.239393 +v -0.142332 -0.036940 -0.259230 +v -0.197757 0.044262 -0.278023 +v -0.222354 0.075436 -0.276668 +v -0.246049 0.101710 -0.265213 +v -0.266725 0.105299 -0.238427 +v -0.280629 0.133695 -0.229438 +v -0.293862 0.180541 -0.222002 +v -0.308905 0.279005 -0.210190 +v -0.317157 0.256980 -0.176318 +v -0.328337 0.230058 -0.121435 +v -0.334508 0.269494 -0.090185 +v -0.343367 0.249947 -0.089246 +v -0.332821 0.252801 -0.103182 +v -0.343746 0.230762 -0.099177 +v -0.355101 0.225586 -0.097820 +v -0.366191 0.216996 -0.096627 +v -0.357086 0.204434 -0.103791 +v -0.369793 0.187884 -0.091372 +v -0.354620 0.175887 -0.048556 +v -0.336726 0.155506 -0.031791 +v -0.349189 0.143968 -0.047867 +v -0.328279 0.061322 -0.012112 +v -0.308013 0.061856 0.026219 +v -0.304996 0.077595 0.031571 +v -0.303878 0.092778 0.041687 +v -0.319591 0.186081 0.042985 +v -0.333174 0.242911 0.031359 +v -0.335926 0.272370 0.006185 +v -0.332851 0.323761 0.024227 +v -0.325484 0.395842 -0.033341 +v -0.324129 0.394003 -0.082014 +v -0.314174 0.440595 -0.099223 +v -0.315703 0.398776 -0.142873 +v -0.308061 0.405926 -0.171197 +v -0.310580 0.364991 -0.189472 +v -0.291182 0.422350 -0.207381 +v -0.293002 0.363935 -0.233570 +v -0.271974 0.364740 -0.266003 +v -0.249257 0.335840 -0.299270 +v -0.217510 0.271087 -0.333150 +v -0.213770 0.235026 -0.334269 +v -0.185328 0.270568 -0.353153 +v -0.193817 0.221720 -0.345860 +v -0.162590 0.197302 -0.356686 +v -0.149049 0.152110 -0.350229 +v -0.116214 0.152369 -0.358851 +v -0.086931 0.132548 -0.355821 +v -0.104755 0.198417 -0.371585 +v -0.134134 0.197974 -0.365784 +v -0.124929 0.237370 -0.373007 +v -0.161600 0.250590 -0.363547 +v -0.166017 0.329470 -0.358809 +v -0.199929 0.324996 -0.340731 +v -0.225683 0.328235 -0.321492 +v -0.199425 0.377512 -0.331902 +v -0.222615 0.377692 -0.314014 +v -0.246420 0.395424 -0.285197 +v -0.271758 0.416095 -0.245054 +v -0.271695 0.449299 -0.224542 +v -0.292270 0.460993 -0.171087 +v -0.306353 0.444958 -0.140363 +v -0.316788 0.447398 -0.055372 +v -0.328130 0.317569 0.073177 +v -0.330638 0.283180 0.059971 +v -0.317380 0.357753 0.108674 +v -0.323205 0.374061 0.054046 +v -0.320437 0.422916 0.003327 +v -0.303968 0.498306 -0.065438 +v -0.299286 0.488718 -0.112560 +v -0.287494 0.511631 -0.121354 +v -0.272701 0.509149 -0.164125 +v -0.272391 0.482245 -0.195421 +v -0.243865 0.500232 -0.226402 +v -0.243134 0.453280 -0.262173 +v -0.219921 0.479624 -0.271628 +v -0.212805 0.428522 -0.304817 +v -0.165263 0.385818 -0.351090 +v -0.139177 0.328167 -0.368484 +v -0.129084 0.282221 -0.373086 +v -0.086549 0.278571 -0.379592 +v -0.058921 0.229581 -0.380592 +v -0.056478 0.180247 -0.371966 +v -0.059615 0.335282 -0.378254 +v -0.114448 0.347635 -0.372517 +v -0.098298 0.393586 -0.368764 +v -0.132182 0.394788 -0.363054 +v -0.156293 0.434417 -0.342582 +v -0.181995 0.432199 -0.327739 +v -0.192857 0.469851 -0.302023 +v -0.179867 0.511619 -0.286869 +v -0.213992 0.518188 -0.249494 +v -0.236890 0.538999 -0.198187 +v -0.259492 0.549311 -0.137185 +v -0.287430 0.538151 -0.067220 +v -0.311238 0.478297 -0.023132 +v -0.307144 0.449001 0.078125 +v -0.310174 0.332833 0.155844 +v -0.304009 0.393682 0.142874 +v -0.307616 0.485338 0.020191 +v -0.299902 0.518500 -0.012238 +v -0.292373 0.536035 -0.033927 +v -0.267311 0.562542 -0.090928 +v -0.230658 0.588163 -0.134998 +v -0.227685 0.563057 -0.181752 +v -0.200444 0.578110 -0.201323 +v -0.194031 0.550801 -0.240626 +v -0.163566 0.559903 -0.256853 +v -0.148723 0.519945 -0.300364 +v -0.157661 0.478340 -0.322243 +v -0.127243 0.494630 -0.325852 +v -0.128970 0.455237 -0.345443 +v -0.130261 0.427327 -0.355634 +v -0.074436 0.455778 -0.353690 +v -0.073864 0.483019 -0.342093 +v -0.070552 0.510736 -0.327043 +v -0.072400 0.563751 -0.288188 +v -0.125271 0.569909 -0.267300 +v -0.165331 0.587895 -0.223185 +v -0.166105 0.612269 -0.185392 +v -0.200327 0.602241 -0.163268 +v -0.234558 0.604366 -0.086229 +v -0.262273 0.583144 -0.048911 +v -0.287510 0.548522 -0.007775 +v -0.263614 0.586335 -0.005684 +v -0.279907 0.557082 0.032760 +v -0.290910 0.527862 0.051570 +v -0.297579 0.496269 0.069845 +v -0.290062 0.463646 0.141185 +v -0.284077 0.444759 0.178223 +v -0.279501 0.516495 0.119851 +v -0.270066 0.546685 0.105833 +v -0.261708 0.583926 0.038167 +v -0.245074 0.607041 -0.024302 +v -0.222567 0.625942 -0.018543 +v -0.217844 0.621718 -0.076443 +v -0.187010 0.640582 -0.076326 +v -0.185418 0.626686 -0.129421 +v -0.146373 0.637124 -0.151670 +v -0.128682 0.623215 -0.195657 +v -0.128183 0.598250 -0.233572 +v -0.089110 0.613064 -0.230465 +v -0.087221 0.584522 -0.265382 +v -0.059074 0.531366 -0.314238 +v -0.050112 0.624419 -0.219054 +v -0.091878 0.642638 -0.177466 +v -0.116050 0.650756 -0.144315 +v -0.148997 0.649450 -0.111501 +v -0.149527 0.660495 -0.055828 +v -0.186523 0.647210 -0.025034 +v -0.220988 0.625265 0.029313 +v -0.242778 0.603518 0.050827 +v -0.257704 0.576807 0.082576 +v -0.251588 0.568191 0.125027 +v -0.266651 0.406275 0.254340 +v -0.247972 0.505373 0.222461 +v -0.262797 0.515617 0.173831 +v -0.249655 0.542528 0.173602 +v -0.234654 0.570184 0.166802 +v -0.227126 0.597328 0.126412 +v -0.216691 0.620073 0.078571 +v -0.182599 0.642047 0.076935 +v -0.184938 0.647318 0.026225 +v -0.148142 0.664400 -0.003205 +v -0.108680 0.674640 -0.047302 +v -0.109079 0.668080 -0.088638 +v -0.070542 0.668576 -0.121122 +v -0.056987 0.686196 -0.052035 +v -0.066817 0.687134 -0.010137 +v -0.107138 0.677614 -0.006661 +v -0.107470 0.674354 0.047111 +v -0.146611 0.661874 0.049464 +v -0.140409 0.656507 0.097121 +v -0.165422 0.638673 0.128341 +v -0.197140 0.621700 0.126452 +v -0.208356 0.596413 0.170930 +v -0.229721 0.552285 0.205367 +v -0.223966 0.518578 0.254700 +v -0.244368 0.449735 0.275459 +v -0.288814 0.359594 0.218197 +v -0.298744 0.338078 0.197230 +v -0.305586 0.291054 0.186211 +v -0.319041 0.301043 0.129367 +v -0.324555 0.265806 0.101673 +v -0.313414 0.201656 0.145066 +v -0.314271 0.178676 0.110358 +v -0.307583 0.130067 0.147188 +v -0.308510 0.115767 0.087538 +v -0.305331 0.101235 0.128188 +v -0.303045 0.080385 0.065075 +v -0.300648 0.062238 0.058247 +v -0.291765 0.034151 0.070269 +v -0.299409 0.038637 0.043456 +v -0.307836 0.046449 0.026193 +v -0.305036 0.035969 0.021835 +v -0.284571 0.006273 0.026032 +v -0.295247 0.020424 0.006120 +v -0.289406 0.012958 -0.000944 +v -0.329305 0.049081 -0.022988 +v -0.303862 0.034543 -0.032795 +v -0.282065 0.002649 -0.013692 +v -0.300317 0.049163 -0.072331 +v -0.278960 0.003857 -0.048736 +v -0.310685 0.058369 -0.066293 +v -0.320941 0.060812 -0.059740 +v -0.344199 0.074688 -0.061263 +v -0.350999 0.089492 -0.073055 +v -0.357299 0.108161 -0.085610 +v -0.363442 0.109434 -0.077941 +v -0.364235 0.112367 -0.071641 +v -0.361641 0.114277 -0.063177 +v -0.358747 0.137455 -0.059092 +v -0.345620 0.081283 -0.040914 +v -0.350507 0.081163 -0.053725 +v -0.327418 0.051877 -0.011069 +v -0.321674 0.042554 -0.009275 +v -0.327289 0.052526 -0.040213 +v -0.368725 0.161252 -0.080909 +v -0.368551 0.152686 -0.089882 +v -0.361211 0.122097 -0.089815 +v -0.355514 0.126263 -0.095640 +v -0.334911 0.095965 -0.086699 +v -0.322433 0.091165 -0.092483 +v -0.314313 0.093231 -0.103930 +v -0.300469 0.068841 -0.109301 +v -0.255265 -0.026358 -0.123706 +v -0.226251 -0.084429 -0.145677 +v -0.233814 -0.095338 -0.108211 +v -0.219490 -0.139762 -0.115727 +v -0.214741 -0.076874 -0.179410 +v -0.211297 -0.123688 -0.149836 +v -0.205238 -0.164823 -0.140552 +v -0.201150 -0.106522 -0.180918 +v -0.193185 -0.143419 -0.172828 +v -0.188940 -0.174121 -0.170368 +v -0.187853 -0.206836 -0.169845 +v -0.173337 -0.148348 -0.195736 +v -0.170547 -0.176468 -0.193638 +v -0.177648 -0.219625 -0.185960 +v -0.158848 -0.207817 -0.204640 +v -0.162908 -0.242279 -0.206956 +v -0.179852 -0.272177 -0.201160 +v -0.137193 -0.226514 -0.221206 +v -0.156860 -0.278195 -0.221375 +v -0.132664 -0.276161 -0.232868 +v -0.111400 -0.255323 -0.236433 +v -0.144074 -0.315602 -0.241717 +v -0.111185 -0.327140 -0.256262 +v -0.095853 -0.303583 -0.252230 +v -0.083954 -0.240797 -0.239839 +v -0.120014 -0.126333 -0.228442 +v -0.135806 -0.090837 -0.234639 +v -0.153607 -0.064738 -0.239808 +v -0.181632 -0.046560 -0.234854 +v -0.211598 -0.016226 -0.230623 +v -0.236006 0.048670 -0.247262 +v -0.303261 0.132782 -0.179492 +v -0.323813 0.204681 -0.133068 +v -0.334758 0.222040 -0.109448 +v -0.346014 0.207751 -0.105348 +v -0.336190 0.193121 -0.110764 +v -0.339323 0.169116 -0.108759 +v -0.365327 0.167086 -0.099909 +v -0.357904 0.158294 -0.103060 +v -0.338193 0.132529 -0.103156 +v -0.328612 0.134239 -0.110628 +v -0.320012 0.130744 -0.120634 +v -0.309017 0.101313 -0.124493 +v -0.285965 0.057214 -0.159544 +v -0.235770 -0.029191 -0.181316 +v -0.242197 -0.029792 -0.165060 +v -0.189848 -0.088837 -0.204734 +v -0.154002 -0.129278 -0.214048 +v -0.146904 -0.161389 -0.212721 +v -0.142114 -0.191884 -0.214938 +v -0.116385 -0.159975 -0.226078 +v -0.109887 -0.191906 -0.228856 +v -0.172625 -0.088939 -0.218147 +v -0.213071 -0.055436 -0.199149 +v -0.262720 0.045925 -0.203546 +v -0.288733 0.096458 -0.192003 +v -0.295985 0.085930 -0.162132 +v -0.313148 0.131324 -0.135931 +v -0.318483 0.157017 -0.132221 +v -0.328682 0.189186 -0.118966 +v -0.194020 -0.312743 -0.208926 +v -0.197110 -0.269767 -0.177452 +v -0.199254 -0.248499 -0.160916 +v -0.201824 -0.205148 -0.141017 +v -0.213097 -0.183190 -0.111542 +v -0.249347 -0.067128 -0.075009 +v -0.264878 -0.040059 -0.018182 +v -0.285650 0.044096 0.169163 +v -0.295318 0.148975 0.226336 +v -0.310596 0.230138 0.167388 +v -0.289666 0.294892 0.241357 +v -0.276085 0.350453 0.261502 +v -0.260394 0.345639 0.301381 +v -0.249147 0.385657 0.305471 +v -0.229455 0.448833 0.300938 +v -0.209787 0.481127 0.304334 +v -0.196663 0.524553 0.284384 +v -0.199739 0.553728 0.250696 +v -0.203963 0.577583 0.212756 +v -0.175737 0.588627 0.231282 +v -0.178199 0.616952 0.172883 +v -0.139393 0.639549 0.160084 +v -0.098417 0.667644 0.099914 +v -0.067639 0.683339 0.044670 +v -0.055848 0.671601 0.115283 +v -0.098731 0.653706 0.150647 +v -0.135621 0.625400 0.198316 +v -0.160244 0.602779 0.222088 +v -0.137527 0.586314 0.265116 +v -0.168407 0.557356 0.278625 +v -0.133746 0.554501 0.305261 +v -0.160096 0.523840 0.317440 +v -0.188173 0.494368 0.316918 +v -0.181249 0.471216 0.338312 +v -0.211279 0.443952 0.326853 +v -0.227165 0.397688 0.332775 +v -0.246617 0.356244 0.322498 +v -0.269761 0.304665 0.292591 +v -0.295921 0.110349 0.197556 +v -0.290146 0.042926 0.130103 +v -0.267327 -0.040353 0.027189 +v -0.243125 -0.122672 0.023796 +v -0.238995 -0.130386 -0.023753 +v -0.228958 -0.141579 -0.076439 +v -0.220704 -0.184112 -0.078258 +v -0.220084 -0.211324 -0.073241 +v -0.214891 -0.229397 -0.104293 +v -0.207761 -0.234907 -0.133005 +v -0.217103 -0.257970 -0.119896 +v -0.213883 -0.278543 -0.150187 +v -0.217326 -0.303519 -0.169726 +v -0.207200 -0.306959 -0.189475 +v -0.225325 -0.336620 -0.196563 +v -0.247730 -0.356093 -0.196601 +v -0.235295 -0.330059 -0.168639 +v -0.224625 -0.287687 -0.128352 +v -0.226561 -0.186054 -0.028514 +v -0.227293 -0.186757 0.014408 +v -0.236350 -0.138422 0.059099 +v -0.253767 -0.068833 0.106922 +v -0.262694 -0.023837 0.171401 +v -0.282086 0.130518 0.280146 +v -0.284207 0.162946 0.276699 +v -0.280043 0.239674 0.277724 +v -0.271260 0.218030 0.299914 +v -0.260079 0.286993 0.316344 +v -0.240313 0.300827 0.345659 +v -0.229868 0.326295 0.353394 +v -0.214929 0.368497 0.356242 +v -0.199151 0.415258 0.352231 +v -0.167013 0.439348 0.363422 +v -0.151101 0.494079 0.344351 +v -0.132494 0.477144 0.362992 +v -0.124249 0.519603 0.340183 +v -0.099481 0.525968 0.344175 +v -0.099504 0.564524 0.309790 +v -0.101261 0.596843 0.269186 +v -0.115486 0.620082 0.223016 +v -0.069703 0.645167 0.190119 +v -0.066200 0.620006 0.245264 +v -0.064453 0.589303 0.291222 +v -0.065279 0.552720 0.329127 +v -0.066535 0.510841 0.360279 +v -0.099758 0.484241 0.370058 +v -0.135365 0.439000 0.377421 +v -0.152543 0.385944 0.387401 +v -0.185172 0.378379 0.374499 +v -0.202036 0.331839 0.375838 +v -0.221402 0.285028 0.366994 +v -0.233282 0.267805 0.354862 +v -0.244688 0.262484 0.341116 +v -0.255999 0.251779 0.324746 +v -0.263210 0.212764 0.314153 +v -0.269740 0.145336 0.316810 +v -0.277103 0.096018 0.275521 +v -0.266354 0.083878 0.303775 +v -0.268518 0.042767 0.244747 +v -0.256235 -0.025916 0.204919 +v -0.235106 -0.113888 0.130874 +v -0.224291 -0.209342 -0.022711 +v -0.224185 -0.234068 -0.037685 +v -0.226169 -0.257076 -0.060003 +v -0.231294 -0.280798 -0.077088 +v -0.237535 -0.299408 -0.089473 +v -0.243531 -0.316803 -0.109299 +v -0.248210 -0.332439 -0.135141 +v -0.273909 -0.355841 -0.120067 +v -0.263664 -0.354239 -0.158477 +v -0.283509 -0.375744 -0.179819 +v -0.299244 -0.380446 -0.153683 +v -0.376282 -0.428712 -0.157919 +v -0.394150 -0.449650 -0.193263 +v -0.414982 -0.455609 -0.154054 +v -0.460836 -0.501770 -0.179055 +v -0.440905 -0.481631 -0.177232 +v -0.453959 -0.519754 -0.228238 +v -0.458085 -0.534023 -0.229834 +v -0.439039 -0.537727 -0.263701 +v -0.418950 -0.545333 -0.284839 +v -0.443063 -0.548383 -0.256256 +v -0.394991 -0.535829 -0.301430 +v -0.389013 -0.550475 -0.307070 +v -0.366683 -0.533371 -0.315064 +v -0.391087 -0.561377 -0.299053 +v -0.425618 -0.557456 -0.268372 +v -0.411358 -0.565471 -0.264311 +v -0.433261 -0.563137 -0.220820 +v -0.449120 -0.554672 -0.226436 +v -0.466970 -0.542746 -0.197554 +v -0.471636 -0.530082 -0.185971 +v -0.469567 -0.516655 -0.181488 +v -0.467585 -0.500179 -0.141752 +v -0.455966 -0.486591 -0.133683 +v -0.439303 -0.472230 -0.106969 +v -0.400551 -0.442303 -0.114558 +v -0.313604 -0.385634 -0.115480 +v -0.288017 -0.366126 -0.092646 +v -0.260644 -0.338749 -0.093124 +v -0.266695 -0.345527 -0.054207 +v -0.246759 -0.316594 -0.037941 +v -0.231962 -0.281575 -0.015700 +v -0.243265 -0.318172 -0.008588 +v -0.226063 -0.255745 -0.002917 +v -0.221613 -0.228247 0.022212 +v -0.222835 -0.203923 0.030685 +v -0.216595 -0.213696 0.059784 +v -0.221143 -0.171986 0.092307 +v -0.206784 -0.187653 0.134381 +v -0.223007 -0.129753 0.166403 +v -0.243220 -0.059097 0.211611 +v -0.266146 0.111345 0.321553 +v -0.250113 0.136292 0.349847 +v -0.244749 0.193724 0.339658 +v -0.235231 0.223096 0.345849 +v -0.206987 0.227582 0.364879 +v -0.217980 0.260134 0.366546 +v -0.195194 0.263452 0.380595 +v -0.207800 0.286139 0.377597 +v -0.183773 0.306600 0.390466 +v -0.169367 0.340169 0.392175 +v -0.146843 0.323058 0.403888 +v -0.118393 0.378900 0.400195 +v -0.105901 0.443848 0.385229 +v -0.079758 0.407667 0.401533 +v -0.066223 0.468000 0.382901 +v -0.101528 0.327827 0.414658 +v -0.166983 0.289490 0.398634 +v -0.163352 0.270358 0.395625 +v -0.173028 0.249435 0.383496 +v -0.182459 0.202920 0.376372 +v -0.214785 0.196261 0.361336 +v -0.227496 0.144505 0.370745 +v -0.245287 0.100579 0.356687 +v -0.250490 0.068605 0.333491 +v -0.239923 0.011203 0.306542 +v -0.213676 -0.101798 0.259972 +v -0.210936 -0.140976 0.195631 +v -0.201629 -0.177732 0.165867 +v -0.203717 -0.232635 0.095673 +v -0.216335 -0.239026 0.045535 +v -0.224147 -0.277113 0.026626 +v -0.231492 -0.302878 0.019231 +v -0.251250 -0.344748 0.008125 +v -0.264819 -0.350521 -0.020876 +v -0.272056 -0.366433 -0.006739 +v -0.290855 -0.371530 -0.051320 +v -0.321078 -0.391916 -0.082976 +v -0.345235 -0.411284 -0.065643 +v -0.329833 -0.404425 -0.048276 +v -0.419141 -0.463177 -0.063140 +v -0.422831 -0.478383 -0.026178 +v -0.463392 -0.498518 -0.075848 +v -0.477547 -0.512737 -0.129107 +v -0.479656 -0.527036 -0.126005 +v -0.474912 -0.538969 -0.145754 +v -0.365860 -0.570606 -0.292545 +v -0.294481 -0.574643 -0.307220 +v -0.237667 -0.584088 -0.203976 +v -0.113617 -0.566963 0.070920 +v -0.110010 -0.564012 0.101703 +v -0.024083 -0.562556 0.136779 +v -0.151125 -0.574753 0.012146 +v -0.254056 -0.581210 -0.267839 +v -0.198307 -0.578476 -0.305639 +v -0.306986 -0.570067 -0.319534 +v -0.304261 -0.563397 -0.328133 +v -0.354141 -0.565509 -0.312039 +v -0.353211 -0.557400 -0.320072 +v -0.337309 -0.544642 -0.327862 +v -0.300514 -0.552291 -0.333242 +v -0.341780 -0.529427 -0.323620 +v -0.292713 -0.525496 -0.333658 +v -0.266556 -0.534675 -0.339205 +v -0.296842 -0.492211 -0.315519 +v -0.255115 -0.518566 -0.336534 +v -0.242306 -0.558458 -0.337518 +v -0.213083 -0.569267 -0.335052 +v -0.183083 -0.573860 -0.327988 +v -0.100966 -0.582825 -0.282112 +v -0.056960 -0.580059 0.027888 +v -0.118712 -0.580367 -0.033307 +v -0.069773 -0.586184 -0.115038 +v -0.076459 -0.587024 -0.157350 +v -0.062984 -0.580591 -0.299175 +v -0.134112 -0.577268 -0.315763 +v -0.159497 -0.553281 -0.342553 +v -0.216525 -0.546106 -0.343112 +v -0.177573 -0.524057 -0.345261 +v -0.209861 -0.493363 -0.334296 +v -0.189871 -0.464466 -0.322641 +v -0.242930 -0.477580 -0.320817 +v -0.144147 -0.563944 -0.337430 +v -0.058364 -0.575482 -0.315187 +v -0.413180 -0.486826 0.006008 +v -0.347594 -0.433848 -0.008320 +v -0.314713 -0.403226 -0.017935 +v -0.297366 -0.386111 -0.021439 +v -0.290588 -0.402286 0.015203 +v -0.268969 -0.380246 0.017306 +v -0.247790 -0.364694 0.034526 +v -0.237501 -0.335565 0.029337 +v -0.220215 -0.334581 0.062144 +v -0.208123 -0.291222 0.071891 +v -0.200259 -0.270069 0.089223 +v -0.191012 -0.245276 0.125391 +v -0.184077 -0.222029 0.163995 +v -0.183384 -0.195388 0.194901 +v -0.185655 -0.171740 0.223599 +v -0.195341 -0.127777 0.276094 +v -0.214812 -0.081114 0.288925 +v -0.211648 -0.060611 0.318425 +v -0.225032 0.015568 0.346733 +v -0.234793 0.067700 0.361446 +v -0.212895 0.071405 0.389297 +v -0.219340 0.111305 0.384599 +v -0.201933 0.140568 0.389756 +v -0.149349 0.215024 0.386627 +v -0.154774 0.232608 0.385272 +v -0.121555 0.254813 0.398842 +v -0.107428 0.277934 0.411823 +v -0.107500 0.296789 0.414964 +v -0.073285 0.282518 0.424841 +v -0.056831 0.349481 0.418615 +v -0.060398 0.299589 0.426155 +v -0.074580 0.263904 0.420558 +v -0.069564 0.244223 0.417429 +v -0.101362 0.233419 0.397543 +v -0.104085 0.214051 0.395573 +v -0.146183 0.192298 0.393826 +v -0.145091 0.150385 0.410556 +v -0.175781 0.139942 0.404282 +v -0.196223 0.100895 0.402066 +v -0.182751 0.024021 0.404957 +v -0.205312 0.001926 0.371715 +v -0.193859 0.008226 0.389565 +v -0.175261 -0.136813 0.316567 +v -0.159689 -0.129498 0.361984 +v -0.164220 -0.194907 0.239136 +v -0.169170 -0.251129 0.171779 +v -0.178267 -0.276858 0.139010 +v -0.187661 -0.296615 0.114807 +v -0.202803 -0.322305 0.089722 +v -0.225943 -0.368099 0.070520 +v -0.260623 -0.400656 0.050328 +v -0.322248 -0.435386 0.023309 +v -0.315842 -0.452294 0.052365 +v -0.123028 -0.227433 0.269189 +v -0.127483 -0.206464 0.300399 +v -0.196029 -0.071830 0.344150 +v -0.178639 -0.071043 0.375065 +v -0.178255 0.039927 0.410853 +v -0.158211 0.043323 0.422180 +v -0.162336 0.076889 0.418754 +v -0.158936 0.114917 0.414905 +v -0.139000 0.073508 0.428675 +v -0.104341 0.147245 0.422765 +v -0.098385 0.189498 0.403556 +v -0.082630 0.193656 0.408069 +v -0.067556 0.194508 0.418975 +v -0.082235 0.215159 0.403654 +v -0.045653 0.212705 0.436476 +v -0.049487 0.230905 0.430095 +v -0.045231 0.255647 0.431346 +v -0.029763 0.223330 0.444531 +v -0.033747 0.193772 0.457780 +v -0.071621 0.131199 0.450271 +v -0.097059 0.130510 0.431975 +v -0.092845 0.095370 0.443921 +v -0.105793 0.078038 0.440669 +v -0.150508 -0.012360 0.426172 +v -0.166137 -0.029747 0.408448 +v -0.171520 -0.060149 0.390612 +v -0.155092 -0.116424 0.380619 +v -0.127824 -0.132839 0.402015 +v -0.140054 -0.163920 0.355236 +v -0.127081 -0.186917 0.343599 +v -0.100327 -0.228556 0.307764 +v -0.099429 -0.245471 0.273196 +v -0.078366 -0.243294 0.306790 +v -0.094467 -0.209894 0.373048 +v -0.092675 -0.198340 0.396079 +v -0.109149 -0.169733 0.399147 +v -0.117895 -0.132226 0.411774 +v -0.119916 -0.107236 0.416569 +v -0.134553 -0.088151 0.411164 +v -0.130515 -0.060574 0.420325 +v -0.148619 -0.031176 0.422208 +v -0.103138 0.003779 0.450625 +v -0.079108 -0.023025 0.460619 +v -0.076684 -0.037262 0.457476 +v -0.108518 -0.057587 0.429386 +v -0.107635 -0.076415 0.427892 +v -0.070682 -0.053008 0.451876 +v -0.082656 -0.122756 0.442714 +v -0.071391 -0.095537 0.451737 +v -0.066812 -0.068291 0.451004 +v -0.081072 -0.222575 0.375141 +v -0.071241 -0.234685 0.358179 +v -0.058435 -0.246273 0.328267 +v -0.039301 -0.250625 0.329364 +v -0.040876 -0.095611 0.459919 +v -0.056198 -0.094290 0.457053 +v -0.046152 0.004898 0.466934 +v -0.038154 -0.132060 0.453681 +v -0.078536 0.093472 0.458760 +v -0.068660 0.085006 0.481844 +v -0.071014 0.115982 0.459043 +v -0.052470 0.154264 0.456557 +v -0.030899 0.156728 0.485232 +v -0.034077 0.113160 0.515602 +v -0.070032 0.070830 0.485025 +v -0.021207 0.095814 0.535572 +v -0.003807 -0.235680 0.429478 +v -0.000237 -0.225592 0.442942 +v 0.013273 0.043920 0.508457 +v 0.019567 0.053857 0.518606 +v 0.064586 0.040374 0.467087 +v 0.040741 0.039002 0.479601 +v 0.044656 0.026180 0.470742 +v 0.083361 0.066583 0.453851 +v 0.067745 0.053374 0.475130 +v 0.087028 0.030383 0.454393 +v 0.067684 0.015048 0.462634 +v 0.041383 -0.197634 0.452144 +v 0.059366 -0.180661 0.447551 +v 0.053519 -0.164688 0.452153 +v 0.026226 -0.210007 0.450625 +v 0.029310 -0.178031 0.455478 +v 0.135099 -0.260502 0.213519 +v 0.129059 -0.234078 0.249791 +v 0.154176 -0.257122 0.190796 +v 0.113212 -0.272372 0.227278 +v 0.036243 -0.480125 0.189528 +v 0.269436 0.470019 0.201642 +v 0.325762 0.221918 0.059754 +v 0.314242 0.178676 0.110382 +v 0.312447 0.092002 -0.005832 +v 0.307615 0.106611 -0.008497 +v 0.366210 0.218476 -0.058008 +v 0.354645 0.242650 -0.032568 +v 0.343350 0.124173 -0.044023 +v 0.325177 0.113870 -0.029700 +v 0.355799 0.260678 -0.048804 +v 0.349458 0.262317 -0.063448 +v 0.342576 0.270559 -0.041873 +v 0.357893 0.256988 -0.039294 +v 0.364020 0.243196 -0.052483 +v 0.364753 0.251618 -0.059154 +v 0.338894 0.264927 -0.075892 +v 0.238777 -0.574189 -0.057458 +v 0.278935 0.003864 -0.048739 +v 0.344588 -0.578928 -0.212439 +v 0.388383 -0.489806 -0.267965 +v 0.354242 -0.448999 -0.247322 +v 0.178611 -0.387940 -0.270319 +v 0.220429 -0.376333 -0.245094 +v 0.183022 -0.424437 -0.296285 +v 0.250181 -0.439819 -0.292908 +v 0.040452 -0.524916 -0.348658 +v 0.033363 -0.538652 -0.345479 +v -0.000015 -0.497699 -0.349797 +v 0.074592 0.067596 0.466670 +v 0.066046 0.059883 0.487609 +v 0.034916 -0.280712 0.264729 +v 0.043761 -0.098820 -0.248682 +v 0.036615 -0.070814 -0.259868 +v 0.033156 -0.383970 -0.293190 +v 0.032331 -0.322290 -0.268910 +v 0.017296 -0.277783 -0.256512 +v 0.030066 -0.212157 -0.240898 +v 0.026193 -0.161811 -0.237676 +v 0.036584 -0.133565 -0.239566 +v 0.030999 -0.041063 -0.274811 +v 0.072527 -0.032900 -0.274933 +v 0.098543 -0.072026 -0.251824 +v 0.112040 -0.024412 -0.273044 +v 0.169832 0.053314 -0.297595 +v 0.203957 0.099728 -0.301990 +v 0.227921 0.145516 -0.302556 +v 0.261100 0.170144 -0.275126 +v 0.263948 0.217039 -0.284035 +v 0.278333 0.202053 -0.259653 +v 0.292346 0.255281 -0.245260 +v 0.321720 0.298788 -0.168189 +v 0.328928 0.311101 -0.127557 +v 0.331968 0.324242 -0.093040 +v 0.334984 0.286292 -0.074825 +v 0.337301 0.276319 -0.060356 +v 0.362899 0.250965 -0.074041 +v 0.364535 0.238671 -0.084859 +v 0.369266 0.236476 -0.069838 +v 0.370273 0.217444 -0.085730 +v 0.369837 0.207084 -0.077105 +v 0.354603 0.178743 -0.048102 +v 0.337667 0.201675 -0.015137 +v 0.339388 0.168212 -0.029437 +v 0.329074 0.170415 -0.008831 +v 0.310539 0.144077 -0.002031 +v 0.322220 0.128570 -0.027071 +v 0.306439 0.126406 -0.006455 +v 0.323570 0.101845 -0.024086 +v 0.326204 0.073330 -0.012484 +v 0.338196 0.089284 -0.032212 +v 0.317504 0.064949 0.005474 +v 0.305726 0.092791 0.006873 +v 0.300396 0.109939 0.008887 +v 0.302458 0.133753 0.010794 +v 0.307551 0.139343 0.049742 +v 0.311670 0.162081 0.059732 +v 0.324516 0.181829 0.013919 +v 0.329449 0.205597 0.028105 +v 0.331777 0.193365 0.002015 +v 0.341111 0.234773 0.006978 +v 0.338238 0.222519 -0.008113 +v 0.350361 0.248831 -0.018893 +v 0.340782 0.271632 -0.021186 +v 0.345831 0.253772 -0.008349 +v 0.341326 0.264532 -0.009927 +v 0.336121 0.286525 -0.026996 +v 0.336035 0.285874 -0.044665 +v 0.330325 0.355402 -0.062467 +v 0.323418 0.352072 -0.142348 +v 0.311220 0.332967 -0.200085 +v 0.292298 0.312813 -0.246358 +v 0.271072 0.313269 -0.277723 +v 0.280889 0.260426 -0.265602 +v 0.252075 0.267869 -0.303015 +v 0.242685 0.210269 -0.306977 +v 0.225095 0.194980 -0.319429 +v 0.194245 0.184759 -0.338317 +v 0.191498 0.148529 -0.329313 +v 0.162782 0.107374 -0.327540 +v 0.131972 0.075779 -0.322398 +v 0.107706 0.070046 -0.325450 +v 0.036861 0.025448 -0.310559 +v 0.048320 0.095921 -0.344176 +v 0.040484 0.278202 -0.383049 +v 0.039217 0.682784 0.072578 +v 0.040285 0.632665 0.227104 +v 0.019205 0.259826 0.436133 +v 0.017271 0.216273 0.452566 +v 0.018715 0.114171 0.528157 +v 0.022361 0.079999 0.533812 +v 0.021180 0.095814 0.535573 +v 0.027625 0.062534 0.520806 +v 0.052796 0.053518 0.492350 +v 0.021810 0.031528 0.480623 +v 0.023140 0.017274 0.470789 +v 0.021247 -0.002949 0.467801 +v 0.044279 -0.025356 0.470577 +v 0.044453 -0.040430 0.468481 +v 0.030690 -0.054721 0.463112 +v 0.021011 -0.068494 0.460132 +v 0.015473 -0.100958 0.461641 +v 0.017093 -0.143245 0.456012 +v 0.082445 -0.161157 0.436840 +v 0.051661 -0.210865 0.441978 +v 0.073280 -0.197364 0.428876 +v 0.049014 -0.224632 0.430967 +v 0.033282 -0.222629 0.441554 +v 0.062227 -0.225297 0.413038 +v 0.044840 -0.238084 0.406888 +v 0.024064 -0.234350 0.428583 +v 0.025102 -0.240932 0.412811 +v 0.025759 -0.244971 0.393735 +v 0.020819 -0.258606 0.307479 +v 0.052037 -0.259413 0.293299 +v 0.074812 -0.259157 0.276620 +v 0.037106 -0.300397 0.244886 +v 0.066768 -0.280151 0.253320 +v 0.069075 -0.303157 0.233242 +v 0.095758 -0.263710 0.252491 +v 0.090808 -0.282777 0.236618 +v 0.092597 -0.308470 0.220010 +v 0.114225 -0.317401 0.201921 +v 0.131809 -0.311132 0.189744 +v 0.150660 -0.328397 0.164237 +v 0.164625 -0.302031 0.153480 +v 0.176858 -0.321970 0.131624 +v 0.179770 -0.347211 0.128412 +v 0.154226 -0.368336 0.155925 +v 0.184985 -0.387507 0.127665 +v 0.202576 -0.360382 0.100666 +v 0.213359 -0.397133 0.101653 +v 0.194601 -0.426699 0.130027 +v 0.233102 -0.441505 0.108296 +v 0.235495 -0.409581 0.085773 +v 0.261971 -0.428852 0.075952 +v 0.276428 -0.471879 0.102241 +v 0.229716 -0.469168 0.126848 +v 0.286466 -0.449562 0.074451 +v 0.262687 -0.492426 0.125969 +v 0.313601 -0.476271 0.082411 +v 0.313614 -0.504259 0.110845 +v 0.361776 -0.505682 0.081712 +v 0.383875 -0.501331 0.059617 +v 0.400359 -0.495323 0.035227 +v 0.412864 -0.513515 0.044171 +v 0.433114 -0.506794 0.009784 +v 0.440123 -0.520237 0.007606 +v 0.453319 -0.501003 -0.037852 +v 0.459474 -0.516317 -0.034208 +v 0.467716 -0.525676 -0.054682 +v 0.470861 -0.513571 -0.071283 +v 0.457717 -0.538665 -0.048805 +v 0.474092 -0.537905 -0.114779 +v 0.466158 -0.548203 -0.131189 +v 0.461015 -0.551140 -0.182676 +v 0.443659 -0.560358 -0.171057 +v 0.397655 -0.571369 -0.166321 +v 0.394533 -0.572528 -0.208781 +v 0.352435 -0.572883 -0.292773 +v 0.382511 -0.570016 -0.279228 +v 0.299215 -0.580051 -0.257065 +v 0.338002 -0.578859 -0.161444 +v 0.430264 -0.561475 -0.124625 +v 0.411021 -0.561576 -0.094799 +v 0.447594 -0.551866 -0.092281 +v 0.436490 -0.549670 -0.053319 +v 0.439615 -0.539731 -0.010155 +v 0.441780 -0.530725 0.000677 +v 0.395195 -0.530633 0.073093 +v 0.415828 -0.524929 0.045562 +v 0.392308 -0.519133 0.069858 +v 0.355468 -0.523592 0.099323 +v 0.297981 -0.519118 0.128479 +v 0.241434 -0.510820 0.144600 +v 0.202797 -0.455007 0.134337 +v 0.188341 -0.465812 0.145036 +v 0.158862 -0.441196 0.155593 +v 0.162612 -0.405839 0.149100 +v 0.123597 -0.368033 0.180133 +v 0.093280 -0.351516 0.202690 +v 0.069448 -0.336207 0.216693 +v 0.046212 -0.319358 0.229972 +v 0.022396 -0.342562 0.220996 +v 0.038880 -0.388936 0.203703 +v 0.032925 -0.424204 0.195895 +v 0.061722 -0.431988 0.191052 +v 0.107256 -0.504493 0.173636 +v 0.075211 -0.512387 0.176475 +v 0.118607 -0.531503 0.168961 +v 0.099186 -0.547068 0.162945 +v 0.160719 -0.546370 0.162446 +v 0.154069 -0.553459 0.157255 +v 0.171612 -0.533154 0.164328 +v 0.143984 -0.558488 0.149375 +v 0.134470 -0.561094 0.138386 +v 0.225731 -0.557922 0.133250 +v 0.231150 -0.552837 0.144090 +v 0.313127 -0.551534 0.055375 +v 0.265151 -0.557261 0.050448 +v 0.169759 -0.570781 0.027630 +v 0.197039 -0.579827 -0.081780 +v 0.376615 -0.572407 -0.135997 +v 0.243821 -0.583638 -0.166022 +v 0.408665 -0.552884 -0.032279 +v 0.410553 -0.542449 0.017985 +v 0.393075 -0.536674 0.061689 +v 0.336702 -0.544166 0.104674 +v 0.340966 -0.539962 0.110712 +v 0.327296 -0.533636 0.117847 +v 0.296106 -0.543404 0.131718 +v 0.277889 -0.531932 0.140633 +v 0.239426 -0.536850 0.152033 +v 0.229485 -0.526184 0.152984 +v 0.197150 -0.504528 0.154977 +v 0.163117 -0.483834 0.159324 +v 0.138920 -0.457769 0.165618 +v 0.124971 -0.422540 0.172302 +v 0.117055 -0.393521 0.179575 +v 0.090166 -0.412762 0.187912 +v 0.069987 -0.394646 0.198018 +v 0.047571 -0.346517 0.216991 +v 0.106288 -0.456042 0.177353 +v 0.139286 -0.500324 0.167659 +v 0.241320 -0.545939 0.148701 +v 0.276602 -0.551313 0.132177 +v 0.280666 -0.553661 0.120188 +v 0.327288 -0.548877 0.070318 +v 0.378678 -0.547178 0.025313 +v 0.234906 -0.584187 -0.197848 +v 0.205709 -0.559883 0.075971 +v 0.024886 -0.557365 0.149745 +v 0.054985 -0.530301 0.171153 +v 0.053497 -0.567436 -0.327788 +v 0.059936 -0.511594 -0.348637 +v 0.161795 -0.502487 -0.343170 +v 0.027978 -0.477535 -0.342111 +v 0.137942 -0.477485 -0.335398 +v 0.293157 -0.466104 -0.300122 +v 0.283789 -0.431654 -0.272959 +v 0.318145 -0.426718 -0.246927 +v 0.318688 -0.445926 -0.270503 +v 0.391055 -0.515359 -0.291263 +v 0.367859 -0.515144 -0.304558 +v 0.417726 -0.523581 -0.278449 +v 0.435568 -0.521352 -0.259000 +v 0.402683 -0.469191 -0.224067 +v 0.437561 -0.497917 -0.221949 +v 0.365011 -0.441167 -0.222229 +v 0.356586 -0.424986 -0.196261 +v 0.305369 -0.407578 -0.226659 +v 0.285199 -0.388082 -0.209749 +v 0.248694 -0.379287 -0.231033 +v 0.215004 -0.345441 -0.217870 +v 0.184691 -0.351084 -0.242313 +v 0.175785 -0.319132 -0.226920 +v 0.155645 -0.345480 -0.250955 +v 0.137948 -0.394154 -0.283596 +v 0.074348 -0.413779 -0.303921 +v 0.104079 -0.365415 -0.274415 +v 0.095640 -0.327316 -0.260171 +v 0.048501 -0.265489 -0.250343 +v 0.080069 -0.189093 -0.234211 +v 0.088401 -0.118052 -0.237506 +v 0.082742 -0.150340 -0.233043 +v 0.108915 -0.097933 -0.239392 +v 0.142305 -0.036940 -0.259230 +v 0.197747 0.044292 -0.278027 +v 0.222342 0.075424 -0.276649 +v 0.246025 0.101700 -0.265205 +v 0.266697 0.105301 -0.238428 +v 0.280593 0.133678 -0.229445 +v 0.293834 0.180571 -0.222015 +v 0.308878 0.279007 -0.210190 +v 0.317123 0.256969 -0.176339 +v 0.328309 0.230062 -0.121438 +v 0.334437 0.269584 -0.090245 +v 0.343339 0.249947 -0.089246 +v 0.332793 0.252802 -0.103181 +v 0.343718 0.230760 -0.099178 +v 0.355073 0.225584 -0.097821 +v 0.366164 0.216997 -0.096627 +v 0.357057 0.204433 -0.103792 +v 0.369765 0.187889 -0.091372 +v 0.363288 0.171773 -0.063254 +v 0.336692 0.155518 -0.031784 +v 0.349160 0.143983 -0.047864 +v 0.346110 0.107439 -0.043492 +v 0.327795 0.053456 -0.010845 +v 0.312177 0.058488 0.017904 +v 0.304968 0.077599 0.031568 +v 0.303850 0.092800 0.041668 +v 0.319584 0.186194 0.043074 +v 0.333152 0.242916 0.031337 +v 0.335898 0.272379 0.006174 +v 0.323872 0.406676 -0.019006 +v 0.324096 0.394048 -0.081990 +v 0.314134 0.440611 -0.099283 +v 0.315679 0.398779 -0.142852 +v 0.308037 0.406091 -0.171035 +v 0.310552 0.365047 -0.189446 +v 0.291165 0.422336 -0.207366 +v 0.292975 0.363949 -0.233564 +v 0.271948 0.364740 -0.266002 +v 0.249225 0.335845 -0.299273 +v 0.217480 0.271101 -0.333153 +v 0.213748 0.235023 -0.334266 +v 0.185262 0.270497 -0.353171 +v 0.193778 0.221713 -0.345866 +v 0.162530 0.197339 -0.356705 +v 0.155494 0.148426 -0.346390 +v 0.127689 0.179639 -0.363691 +v 0.116617 0.132901 -0.352097 +v 0.098632 0.176701 -0.368140 +v 0.105644 0.228097 -0.375428 +v 0.136369 0.227709 -0.369470 +v 0.161575 0.250589 -0.363546 +v 0.166001 0.329440 -0.358807 +v 0.199836 0.325121 -0.340755 +v 0.225638 0.328241 -0.321506 +v 0.199320 0.377579 -0.331937 +v 0.222602 0.377712 -0.313995 +v 0.271715 0.416067 -0.245090 +v 0.271626 0.449444 -0.224498 +v 0.292244 0.460975 -0.171101 +v 0.306325 0.444943 -0.140383 +v 0.316755 0.447452 -0.055261 +v 0.332817 0.323812 0.024257 +v 0.329196 0.312869 0.066966 +v 0.325887 0.335752 0.075222 +v 0.330609 0.283106 0.059971 +v 0.317446 0.357569 0.108331 +v 0.317304 0.424526 0.035019 +v 0.303954 0.498206 -0.065575 +v 0.295535 0.497418 -0.115597 +v 0.273315 0.522997 -0.144831 +v 0.272363 0.482244 -0.195421 +v 0.245236 0.474793 -0.245319 +v 0.245594 0.408781 -0.281285 +v 0.219892 0.479630 -0.271625 +v 0.212766 0.428529 -0.304823 +v 0.165230 0.385820 -0.351092 +v 0.130030 0.347095 -0.369645 +v 0.129054 0.282229 -0.373087 +v 0.086531 0.278589 -0.379590 +v 0.057867 0.231358 -0.380848 +v 0.055374 0.177048 -0.371147 +v 0.086944 0.328715 -0.377172 +v 0.111075 0.397389 -0.366974 +v 0.139114 0.388704 -0.361711 +v 0.156263 0.434424 -0.342581 +v 0.181964 0.432198 -0.327742 +v 0.192838 0.469848 -0.302017 +v 0.179838 0.511623 -0.286867 +v 0.213966 0.518189 -0.249491 +v 0.244888 0.519261 -0.206412 +v 0.243188 0.550354 -0.171146 +v 0.282937 0.541680 -0.081902 +v 0.311259 0.478079 -0.023317 +v 0.306343 0.445192 0.086505 +v 0.303086 0.347970 0.177022 +v 0.303682 0.391670 0.145515 +v 0.309673 0.471806 0.027723 +v 0.302487 0.510947 -0.002534 +v 0.292347 0.536208 -0.033506 +v 0.276420 0.562243 -0.049581 +v 0.244574 0.576084 -0.126343 +v 0.212400 0.573485 -0.191563 +v 0.212783 0.547836 -0.222313 +v 0.188691 0.550549 -0.245771 +v 0.163540 0.559905 -0.256850 +v 0.148695 0.519940 -0.300367 +v 0.157633 0.478340 -0.322243 +v 0.127200 0.494627 -0.325859 +v 0.128935 0.455248 -0.345440 +v 0.130235 0.427325 -0.355634 +v 0.061439 0.346228 -0.376948 +v 0.083490 0.425205 -0.362580 +v 0.074411 0.455784 -0.353688 +v 0.073791 0.483016 -0.342096 +v 0.096143 0.523358 -0.315199 +v 0.125241 0.569908 -0.267301 +v 0.176659 0.588254 -0.213904 +v 0.166068 0.612224 -0.185476 +v 0.200725 0.602222 -0.161573 +v 0.224151 0.592630 -0.139083 +v 0.218711 0.612406 -0.105358 +v 0.248523 0.591857 -0.079141 +v 0.257802 0.591933 -0.030916 +v 0.277096 0.566973 -0.007563 +v 0.288897 0.539976 0.031777 +v 0.276273 0.563507 0.034661 +v 0.294058 0.518843 0.051805 +v 0.288733 0.493136 0.115476 +v 0.285252 0.436156 0.180461 +v 0.288744 0.468887 0.140831 +v 0.273971 0.537453 0.109612 +v 0.250720 0.600472 0.020850 +v 0.244458 0.608031 -0.012245 +v 0.236499 0.611439 -0.050428 +v 0.222597 0.626122 -0.018612 +v 0.211815 0.626871 -0.070239 +v 0.187009 0.640670 -0.076409 +v 0.185402 0.626781 -0.129256 +v 0.146314 0.637139 -0.151706 +v 0.128658 0.623152 -0.195759 +v 0.128154 0.598249 -0.233572 +v 0.089083 0.613059 -0.230471 +v 0.087186 0.584523 -0.265383 +v 0.072400 0.563751 -0.288188 +v 0.061014 0.538145 -0.309111 +v 0.048970 0.511042 -0.327323 +v 0.050112 0.624419 -0.219054 +v 0.091829 0.642741 -0.177280 +v 0.116137 0.650756 -0.144347 +v 0.149120 0.649464 -0.111439 +v 0.149536 0.660601 -0.055765 +v 0.186503 0.647376 -0.025050 +v 0.221269 0.625324 0.029354 +v 0.247398 0.594851 0.066656 +v 0.255282 0.570418 0.109891 +v 0.276735 0.378663 0.243988 +v 0.255034 0.460998 0.244484 +v 0.247153 0.508272 0.221578 +v 0.264356 0.519247 0.165013 +v 0.243904 0.551660 0.175890 +v 0.227357 0.597447 0.126513 +v 0.216762 0.620255 0.078741 +v 0.182685 0.642204 0.077092 +v 0.184864 0.647538 0.026254 +v 0.148041 0.664573 -0.003062 +v 0.108944 0.674650 -0.047246 +v 0.109154 0.668121 -0.088593 +v 0.061737 0.670307 -0.120608 +v 0.044716 0.647034 -0.180103 +v 0.066300 0.682921 -0.064153 +v 0.057144 0.688575 -0.023925 +v 0.107010 0.677752 -0.006718 +v 0.107501 0.674461 0.047111 +v 0.146652 0.662020 0.049503 +v 0.140986 0.656775 0.096022 +v 0.165452 0.638960 0.128277 +v 0.197315 0.621917 0.126442 +v 0.201162 0.602416 0.171127 +v 0.222077 0.584312 0.169239 +v 0.217076 0.566117 0.209729 +v 0.231581 0.524125 0.236246 +v 0.212964 0.542943 0.245751 +v 0.210335 0.514943 0.277141 +v 0.235180 0.459998 0.284906 +v 0.255994 0.410470 0.277253 +v 0.288630 0.308566 0.241327 +v 0.302874 0.288746 0.196771 +v 0.319011 0.301039 0.129379 +v 0.324552 0.265929 0.101532 +v 0.313415 0.201740 0.144905 +v 0.307550 0.129984 0.147155 +v 0.308474 0.115691 0.087587 +v 0.305301 0.101231 0.128203 +v 0.303017 0.080384 0.065071 +v 0.300619 0.062236 0.058255 +v 0.291720 0.034112 0.070299 +v 0.303668 0.051966 0.039923 +v 0.293973 0.027296 0.041766 +v 0.307029 0.044353 0.026478 +v 0.305020 0.035977 0.021816 +v 0.284076 0.005358 0.025890 +v 0.295224 0.020429 0.006125 +v 0.289378 0.012958 -0.000947 +v 0.329278 0.049084 -0.022988 +v 0.303836 0.034541 -0.032789 +v 0.282044 0.002663 -0.013703 +v 0.300290 0.049165 -0.072332 +v 0.310659 0.058374 -0.066298 +v 0.320913 0.060811 -0.059739 +v 0.344174 0.074692 -0.061266 +v 0.350972 0.089491 -0.073055 +v 0.357271 0.108162 -0.085610 +v 0.363415 0.109437 -0.077942 +v 0.364207 0.112366 -0.071640 +v 0.361612 0.114283 -0.063172 +v 0.358731 0.137431 -0.059114 +v 0.345596 0.081288 -0.040917 +v 0.350483 0.081170 -0.053732 +v 0.321645 0.042552 -0.009279 +v 0.327262 0.052527 -0.040213 +v 0.368697 0.161236 -0.080907 +v 0.368524 0.152692 -0.089883 +v 0.361183 0.122095 -0.089814 +v 0.355486 0.126261 -0.095640 +v 0.334882 0.095964 -0.086699 +v 0.322405 0.091163 -0.092482 +v 0.314288 0.093237 -0.103932 +v 0.293075 0.048156 -0.096374 +v 0.255417 -0.026872 -0.121948 +v 0.233260 -0.090978 -0.116606 +v 0.229631 -0.073429 -0.148107 +v 0.212165 -0.122392 -0.148729 +v 0.219456 -0.139770 -0.115740 +v 0.214733 -0.076796 -0.179439 +v 0.205206 -0.164822 -0.140563 +v 0.201120 -0.106544 -0.180907 +v 0.193152 -0.143400 -0.172845 +v 0.188910 -0.174121 -0.170371 +v 0.187825 -0.206847 -0.169847 +v 0.173293 -0.148284 -0.195766 +v 0.170516 -0.176464 -0.193641 +v 0.177623 -0.219629 -0.185957 +v 0.158823 -0.207812 -0.204637 +v 0.162873 -0.242278 -0.206964 +v 0.179829 -0.272177 -0.201155 +v 0.156820 -0.278191 -0.221382 +v 0.137164 -0.226591 -0.221216 +v 0.132599 -0.276191 -0.232888 +v 0.144056 -0.315590 -0.241709 +v 0.111380 -0.255314 -0.236430 +v 0.095784 -0.303587 -0.252240 +v 0.083937 -0.240788 -0.239835 +v 0.119986 -0.126331 -0.228442 +v 0.135779 -0.090835 -0.234640 +v 0.153580 -0.064654 -0.239850 +v 0.181615 -0.046511 -0.234876 +v 0.206945 -0.017344 -0.233983 +v 0.236028 0.048794 -0.247277 +v 0.300962 0.122963 -0.179987 +v 0.304682 0.148860 -0.181671 +v 0.323786 0.204671 -0.133065 +v 0.334731 0.222043 -0.109446 +v 0.345986 0.207746 -0.105349 +v 0.336162 0.193129 -0.110765 +v 0.339304 0.169103 -0.108752 +v 0.365299 0.167079 -0.099908 +v 0.357876 0.158290 -0.103060 +v 0.338165 0.132528 -0.103156 +v 0.328585 0.134238 -0.110627 +v 0.319983 0.130744 -0.120636 +v 0.308989 0.101313 -0.124492 +v 0.302028 0.076481 -0.115831 +v 0.282574 0.049400 -0.158807 +v 0.242186 -0.029763 -0.165052 +v 0.270067 0.052138 -0.193851 +v 0.189833 -0.088797 -0.204746 +v 0.153973 -0.129276 -0.214049 +v 0.146875 -0.161386 -0.212722 +v 0.142087 -0.191895 -0.214938 +v 0.116354 -0.159977 -0.226080 +v 0.109858 -0.191911 -0.228857 +v 0.172606 -0.088930 -0.218147 +v 0.207377 -0.054520 -0.207524 +v 0.227147 -0.028000 -0.200078 +v 0.253964 0.041069 -0.214883 +v 0.288335 0.088992 -0.187136 +v 0.295958 0.085931 -0.162130 +v 0.313120 0.131326 -0.135932 +v 0.318456 0.157022 -0.132221 +v 0.328654 0.189186 -0.118966 +v 0.193990 -0.312744 -0.208929 +v 0.197058 -0.269810 -0.177510 +v 0.199220 -0.248474 -0.160918 +v 0.201792 -0.205145 -0.141027 +v 0.213054 -0.183106 -0.111608 +v 0.249349 -0.067041 -0.074997 +v 0.264857 -0.040000 -0.018308 +v 0.269322 -0.033051 0.050681 +v 0.285616 0.044076 0.169166 +v 0.295341 0.149068 0.226119 +v 0.310583 0.230121 0.167324 +v 0.271088 0.329998 0.283366 +v 0.258671 0.350820 0.302957 +v 0.236817 0.412737 0.311985 +v 0.209709 0.481045 0.304748 +v 0.182957 0.542574 0.282036 +v 0.186914 0.571437 0.244702 +v 0.184944 0.593501 0.213991 +v 0.178359 0.617240 0.172856 +v 0.144135 0.627956 0.186483 +v 0.127549 0.653975 0.124936 +v 0.077386 0.677423 0.073115 +v 0.067242 0.686226 0.016905 +v 0.049475 0.669378 0.125478 +v 0.088247 0.663570 0.125022 +v 0.117984 0.640579 0.175174 +v 0.151624 0.594127 0.244721 +v 0.152274 0.565081 0.282662 +v 0.133153 0.546184 0.313803 +v 0.167646 0.526149 0.310013 +v 0.148796 0.511938 0.333952 +v 0.181790 0.484948 0.329677 +v 0.183632 0.456307 0.344868 +v 0.211375 0.443841 0.327030 +v 0.213957 0.408677 0.342399 +v 0.246625 0.356493 0.322457 +v 0.295895 0.110355 0.197560 +v 0.290192 0.043255 0.130323 +v 0.263493 -0.051311 0.014634 +v 0.243102 -0.122654 0.023789 +v 0.228858 -0.140971 -0.077542 +v 0.220666 -0.184118 -0.078310 +v 0.220057 -0.211323 -0.073234 +v 0.214862 -0.229402 -0.104298 +v 0.207730 -0.234904 -0.133016 +v 0.217078 -0.257973 -0.119887 +v 0.213841 -0.278538 -0.150223 +v 0.217301 -0.303521 -0.169723 +v 0.207176 -0.306965 -0.189476 +v 0.225297 -0.336620 -0.196563 +v 0.235266 -0.330059 -0.168641 +v 0.247698 -0.356088 -0.196601 +v 0.224597 -0.287686 -0.128350 +v 0.226532 -0.186059 -0.028519 +v 0.238986 -0.130312 -0.023690 +v 0.227271 -0.186733 0.014424 +v 0.236319 -0.138424 0.059118 +v 0.255546 -0.063452 0.106707 +v 0.262677 -0.023821 0.171369 +v 0.282057 0.130537 0.280156 +v 0.284164 0.162954 0.276745 +v 0.280016 0.239683 0.277723 +v 0.271233 0.218029 0.299914 +v 0.260066 0.286855 0.316332 +v 0.240332 0.300632 0.345623 +v 0.235305 0.363460 0.336015 +v 0.214917 0.368193 0.356436 +v 0.183473 0.421572 0.360807 +v 0.149607 0.472245 0.357754 +v 0.115691 0.522110 0.342220 +v 0.117942 0.576775 0.288731 +v 0.089909 0.549387 0.327804 +v 0.115530 0.620212 0.223177 +v 0.094747 0.612805 0.247359 +v 0.069607 0.645268 0.190152 +v 0.053097 0.607502 0.269392 +v 0.082250 0.585900 0.291119 +v 0.046960 0.574826 0.310638 +v 0.048679 0.534474 0.345678 +v 0.083377 0.508293 0.359866 +v 0.115930 0.481143 0.367320 +v 0.115349 0.441540 0.383348 +v 0.151165 0.431953 0.373703 +v 0.152419 0.386213 0.387418 +v 0.185042 0.378646 0.374568 +v 0.202058 0.331807 0.375852 +v 0.223036 0.322842 0.360582 +v 0.221393 0.284974 0.366976 +v 0.233254 0.267805 0.354863 +v 0.244660 0.262483 0.341117 +v 0.255971 0.251788 0.324747 +v 0.263183 0.212753 0.314153 +v 0.269725 0.145289 0.316781 +v 0.277109 0.095941 0.275282 +v 0.266321 0.083869 0.303781 +v 0.268495 0.042738 0.244693 +v 0.256157 -0.026040 0.204959 +v 0.235123 -0.113766 0.130876 +v 0.224263 -0.209341 -0.022702 +v 0.224158 -0.234068 -0.037691 +v 0.226141 -0.257078 -0.060007 +v 0.231269 -0.280791 -0.077046 +v 0.236305 -0.298835 -0.095619 +v 0.243501 -0.316803 -0.109314 +v 0.248182 -0.332440 -0.135141 +v 0.273883 -0.355844 -0.120080 +v 0.263635 -0.354239 -0.158479 +v 0.283479 -0.375741 -0.179815 +v 0.299202 -0.380438 -0.153693 +v 0.376237 -0.428702 -0.157934 +v 0.394122 -0.449648 -0.193259 +v 0.414951 -0.455608 -0.154062 +v 0.440883 -0.481635 -0.177228 +v 0.460807 -0.501771 -0.179063 +v 0.453931 -0.519755 -0.228240 +v 0.458057 -0.534023 -0.229835 +v 0.439011 -0.537727 -0.263701 +v 0.418923 -0.545333 -0.284838 +v 0.443035 -0.548384 -0.256256 +v 0.394965 -0.535829 -0.301429 +v 0.388981 -0.550474 -0.307073 +v 0.366650 -0.533374 -0.315068 +v 0.391061 -0.561377 -0.299053 +v 0.425593 -0.557456 -0.268370 +v 0.411329 -0.565472 -0.264310 +v 0.433232 -0.563137 -0.220819 +v 0.449090 -0.554672 -0.226440 +v 0.466942 -0.542746 -0.197555 +v 0.471608 -0.530082 -0.185968 +v 0.469541 -0.516653 -0.181479 +v 0.467553 -0.500173 -0.141746 +v 0.455951 -0.486610 -0.133744 +v 0.439207 -0.472176 -0.106943 +v 0.400488 -0.442279 -0.114587 +v 0.313571 -0.385630 -0.115480 +v 0.287989 -0.366126 -0.092644 +v 0.260613 -0.338746 -0.093126 +v 0.266665 -0.345523 -0.054217 +v 0.246823 -0.316387 -0.053638 +v 0.240229 -0.307614 -0.012553 +v 0.231931 -0.281558 -0.015701 +v 0.226035 -0.255747 -0.002910 +v 0.221587 -0.228256 0.022201 +v 0.222796 -0.203979 0.030641 +v 0.216584 -0.213665 0.059736 +v 0.221340 -0.169233 0.095356 +v 0.218040 -0.154217 0.140160 +v 0.216810 -0.136301 0.180951 +v 0.240553 -0.065504 0.212612 +v 0.266125 0.111332 0.321537 +v 0.250089 0.136303 0.349841 +v 0.244720 0.193719 0.339661 +v 0.235205 0.223101 0.345848 +v 0.206961 0.227581 0.364878 +v 0.217952 0.260134 0.366546 +v 0.195167 0.263451 0.380595 +v 0.207772 0.286072 0.377590 +v 0.183772 0.306572 0.390469 +v 0.169472 0.340414 0.392123 +v 0.146754 0.323152 0.403939 +v 0.118129 0.378848 0.400300 +v 0.067728 0.410577 0.402710 +v 0.082671 0.466249 0.381731 +v 0.050309 0.491180 0.372870 +v 0.049075 0.448302 0.392101 +v 0.084013 0.356235 0.413066 +v 0.167006 0.289469 0.398612 +v 0.163342 0.270348 0.395616 +v 0.172998 0.249435 0.383496 +v 0.182426 0.202920 0.376374 +v 0.214752 0.196259 0.361339 +v 0.227467 0.144501 0.370747 +v 0.245255 0.100577 0.356693 +v 0.250461 0.068608 0.333497 +v 0.239790 0.010946 0.306585 +v 0.213652 -0.101810 0.259941 +v 0.194905 -0.165022 0.208197 +v 0.204481 -0.187023 0.143228 +v 0.203689 -0.232632 0.095680 +v 0.216309 -0.239023 0.045538 +v 0.224117 -0.277114 0.026635 +v 0.231463 -0.302876 0.019231 +v 0.250611 -0.337958 0.000876 +v 0.255197 -0.334029 -0.027192 +v 0.272039 -0.366443 -0.006741 +v 0.272931 -0.357295 -0.031171 +v 0.290827 -0.371529 -0.051321 +v 0.321049 -0.391915 -0.082982 +v 0.329803 -0.404424 -0.048277 +v 0.345208 -0.411284 -0.065645 +v 0.419126 -0.463185 -0.063146 +v 0.422805 -0.478382 -0.026181 +v 0.463367 -0.498519 -0.075859 +v 0.477518 -0.512736 -0.129106 +v 0.479628 -0.527036 -0.126001 +v 0.474885 -0.538969 -0.145755 +v 0.303059 -0.574051 -0.307404 +v 0.113588 -0.566963 0.070920 +v 0.109988 -0.564012 0.101700 +v 0.024060 -0.562556 0.136780 +v 0.151087 -0.574754 0.012147 +v 0.161083 -0.585171 -0.136359 +v 0.246741 -0.581175 -0.271706 +v 0.198282 -0.578476 -0.305640 +v 0.306959 -0.570067 -0.319534 +v 0.304238 -0.563398 -0.328132 +v 0.354129 -0.565508 -0.312032 +v 0.353188 -0.557398 -0.320071 +v 0.300487 -0.552290 -0.333243 +v 0.337256 -0.544638 -0.327866 +v 0.341756 -0.529432 -0.323621 +v 0.292685 -0.525496 -0.333658 +v 0.266528 -0.534675 -0.339205 +v 0.296813 -0.492208 -0.315518 +v 0.255088 -0.518565 -0.336534 +v 0.242277 -0.558458 -0.337518 +v 0.213056 -0.569268 -0.335052 +v 0.183058 -0.573860 -0.327988 +v 0.095138 -0.581563 -0.294443 +v 0.056934 -0.580059 0.027888 +v 0.118684 -0.580367 -0.033307 +v 0.069750 -0.586184 -0.115039 +v 0.076434 -0.587024 -0.157351 +v 0.134083 -0.577268 -0.315763 +v 0.159472 -0.553281 -0.342553 +v 0.216496 -0.546106 -0.343112 +v 0.177543 -0.524056 -0.345261 +v 0.209829 -0.493362 -0.334296 +v 0.189851 -0.464462 -0.322637 +v 0.242904 -0.477581 -0.320817 +v 0.144119 -0.563944 -0.337430 +v 0.068560 -0.575910 -0.314816 +v 0.413151 -0.486825 0.006007 +v 0.347630 -0.433884 -0.008341 +v 0.314683 -0.403224 -0.017933 +v 0.297338 -0.386110 -0.021441 +v 0.290614 -0.402307 0.015160 +v 0.268955 -0.380266 0.017309 +v 0.247773 -0.364699 0.034513 +v 0.237473 -0.335565 0.029337 +v 0.220189 -0.334591 0.062146 +v 0.208030 -0.291179 0.072027 +v 0.200225 -0.270062 0.089246 +v 0.190984 -0.245274 0.125392 +v 0.188619 -0.215663 0.158029 +v 0.171829 -0.238013 0.175503 +v 0.183345 -0.195425 0.194883 +v 0.190944 -0.141216 0.265059 +v 0.219699 -0.064698 0.291962 +v 0.224966 0.015475 0.346745 +v 0.234773 0.067807 0.361479 +v 0.212865 0.071403 0.389299 +v 0.219312 0.111310 0.384599 +v 0.201906 0.140565 0.389757 +v 0.149318 0.215023 0.386628 +v 0.154746 0.232608 0.385272 +v 0.121549 0.254816 0.398837 +v 0.107351 0.277953 0.411845 +v 0.107418 0.296738 0.414993 +v 0.075936 0.304747 0.423172 +v 0.050563 0.322358 0.424182 +v 0.072234 0.280741 0.424990 +v 0.074544 0.263887 0.420558 +v 0.069528 0.244224 0.417435 +v 0.101335 0.233419 0.397543 +v 0.104057 0.214051 0.395573 +v 0.146124 0.192323 0.393825 +v 0.145035 0.150373 0.410567 +v 0.175749 0.139937 0.404285 +v 0.196187 0.100891 0.402072 +v 0.182700 0.024005 0.404972 +v 0.205209 0.001740 0.371733 +v 0.193851 0.008252 0.389551 +v 0.198963 -0.069801 0.339964 +v 0.201492 -0.104751 0.295440 +v 0.159641 -0.129543 0.361969 +v 0.171445 -0.141800 0.317655 +v 0.170174 -0.185078 0.241395 +v 0.156836 -0.215672 0.224996 +v 0.178237 -0.276928 0.138981 +v 0.187625 -0.296581 0.114813 +v 0.202778 -0.322308 0.089718 +v 0.225918 -0.368106 0.070522 +v 0.260601 -0.400656 0.050322 +v 0.322252 -0.435422 0.023329 +v 0.315843 -0.452317 0.052371 +v 0.143762 -0.197668 0.281589 +v 0.178618 -0.070897 0.375132 +v 0.178225 0.039923 0.410854 +v 0.158182 0.043324 0.422181 +v 0.162318 0.076903 0.418749 +v 0.158880 0.114912 0.414914 +v 0.138968 0.073507 0.428677 +v 0.104316 0.147246 0.422764 +v 0.098356 0.189498 0.403556 +v 0.082603 0.193657 0.408068 +v 0.067529 0.194511 0.418973 +v 0.082205 0.215159 0.403655 +v 0.045628 0.212703 0.436473 +v 0.049459 0.230906 0.430095 +v 0.045223 0.255666 0.431337 +v 0.029738 0.223333 0.444529 +v 0.033719 0.193780 0.457777 +v 0.071597 0.131199 0.450268 +v 0.097030 0.130514 0.431975 +v 0.092816 0.095370 0.443921 +v 0.105766 0.078039 0.440669 +v 0.150484 -0.012353 0.426171 +v 0.166110 -0.029707 0.408461 +v 0.171464 -0.060270 0.390591 +v 0.155093 -0.116398 0.380600 +v 0.127796 -0.132839 0.402016 +v 0.140037 -0.163906 0.355235 +v 0.127049 -0.186955 0.343536 +v 0.120163 -0.217447 0.292195 +v 0.100305 -0.228552 0.307765 +v 0.099402 -0.245467 0.273201 +v 0.078339 -0.243293 0.306793 +v 0.094430 -0.209902 0.373055 +v 0.092648 -0.198337 0.396083 +v 0.109126 -0.169725 0.399148 +v 0.117860 -0.132229 0.411780 +v 0.119880 -0.107221 0.416579 +v 0.134535 -0.088148 0.411160 +v 0.148599 -0.031165 0.422206 +v 0.130465 -0.060557 0.420338 +v 0.103111 0.003784 0.450625 +v 0.079082 -0.023024 0.460619 +v 0.076653 -0.037265 0.457478 +v 0.108490 -0.057587 0.429386 +v 0.070655 -0.053008 0.451876 +v 0.107566 -0.076395 0.427917 +v 0.071369 -0.095546 0.451734 +v 0.082634 -0.122732 0.442713 +v 0.066786 -0.068292 0.451003 +v 0.081041 -0.222574 0.375151 +v 0.071214 -0.234686 0.358171 +v 0.058408 -0.246274 0.328264 +v 0.039274 -0.250625 0.329364 +v 0.040851 -0.095612 0.459919 +v 0.056155 -0.094304 0.457056 +v 0.046124 0.004898 0.466934 +v 0.057958 -0.132083 0.450846 +v 0.078508 0.093472 0.458760 +v 0.068632 0.085008 0.481843 +v 0.070988 0.115981 0.459042 +v 0.052447 0.154259 0.456555 +v 0.030871 0.156721 0.485238 +v 0.034049 0.113160 0.515603 +v 0.070004 0.070829 0.485024 +vn 0.0293 -0.9965 0.0782 +vn 0.0006 -0.9996 -0.0271 +vn -0.0029 -0.9997 -0.0244 +vn -0.0004 -0.9996 -0.0271 +vn 0.0028 -0.9997 -0.0247 +vn -0.3445 -0.7448 0.5715 +vn -0.3341 -0.7632 0.5530 +vn -0.8359 -0.4209 0.3522 +vn -0.5130 0.0648 0.8559 +vn -0.4559 -0.0997 0.8844 +vn -0.2706 -0.1345 0.9532 +vn -0.3302 -0.1354 0.9342 +vn -0.1309 -0.2076 0.9694 +vn -0.7387 -0.4788 0.4744 +vn -0.7065 -0.4779 0.5221 +vn -0.6369 -0.5593 0.5307 +vn -0.0613 -0.2899 0.9551 +vn -0.9885 -0.0890 0.1222 +vn -0.5716 0.1200 0.8117 +vn -0.5966 0.7842 -0.1703 +vn -0.6412 0.7524 0.1508 +vn -0.9020 0.4307 -0.0314 +vn -0.2490 0.6544 -0.7140 +vn -0.1738 0.5847 -0.7924 +vn -0.9203 -0.2813 0.2719 +vn 0.0213 -0.7772 0.6289 +vn -0.0202 0.4439 -0.8958 +vn -0.0864 0.3795 -0.9211 +vn -0.0896 -0.4115 -0.9070 +vn -0.1571 -0.4764 -0.8651 +vn -0.2295 -0.4690 -0.8529 +vn -0.3317 -0.4281 -0.8407 +vn -0.4322 -0.3898 -0.8132 +vn -0.7103 -0.2079 -0.6725 +vn -0.7472 -0.1676 -0.6431 +vn -0.8051 -0.1576 -0.5718 +vn -0.8318 -0.0922 -0.5474 +vn -0.8580 -0.0891 -0.5058 +vn -0.9974 0.0630 -0.0338 +vn -0.9827 0.1823 -0.0317 +vn -0.9189 0.3654 -0.1485 +vn -0.9210 0.3669 -0.1308 +vn -0.9308 0.2620 -0.2549 +vn -0.9624 0.1122 0.2475 +vn -0.9498 -0.0129 0.3126 +vn -0.9494 -0.0569 0.3090 +vn -0.9095 -0.0878 0.4064 +vn -0.8220 -0.1416 0.5516 +vn -0.8705 -0.1515 0.4683 +vn -0.8551 -0.2564 0.4506 +vn -0.7694 -0.3164 0.5548 +vn -0.7962 0.0280 0.6044 +vn -0.7153 0.3764 0.5888 +vn -0.8517 0.3415 0.3975 +vn -0.9370 0.2794 0.2095 +vn -0.9441 0.2777 0.1778 +vn -0.9586 -0.1034 0.2651 +vn -0.9512 -0.2378 0.1968 +vn -0.9554 -0.1174 0.2709 +vn -0.9196 -0.3717 0.1268 +vn -0.8854 -0.1701 0.4327 +vn -0.8961 0.3966 0.1991 +vn -0.9322 0.0391 0.3598 +vn -0.6613 0.7499 0.0185 +vn -0.9142 0.4011 0.0585 +vn -0.9968 0.0751 -0.0263 +vn -0.9871 0.0919 -0.1311 +vn -0.9798 0.0850 -0.1809 +vn -0.9694 0.1068 -0.2211 +vn -0.9192 0.0307 -0.3927 +vn -0.9064 -0.0073 -0.4223 +vn -0.8704 -0.0087 -0.4924 +vn -0.7930 0.0076 -0.6092 +vn -0.7423 -0.0752 -0.6658 +vn -0.7870 -0.0459 -0.6152 +vn -0.5623 -0.2381 -0.7920 +vn -0.2636 -0.4196 -0.8686 +vn -0.2272 -0.4454 -0.8660 +vn -0.0942 -0.4545 -0.8857 +vn -0.0915 -0.4789 -0.8731 +vn -0.0438 -0.4359 -0.8989 +vn 0.0000 -0.3841 -0.9233 +vn -0.0014 -0.0523 -0.9986 +vn -0.0091 0.3919 -0.9200 +vn 0.0059 0.4766 -0.8791 +vn 0.0000 0.7118 -0.7024 +vn 0.0000 0.7921 -0.6103 +vn -0.0284 0.9745 -0.2228 +vn 0.0068 0.9999 -0.0115 +vn 0.0003 0.9916 0.1291 +vn -0.0104 0.9735 0.2283 +vn -0.0136 0.2495 0.9683 +vn 0.0025 0.1677 0.9858 +vn -0.0754 0.3559 0.9315 +vn -0.1006 0.5335 0.8398 +vn 0.0018 0.6365 0.7713 +vn -0.0976 0.5793 0.8092 +vn -0.4815 0.2300 0.8457 +vn -0.4581 -0.3138 0.8317 +vn -0.5027 -0.4319 0.7488 +vn -0.3963 -0.7546 0.5229 +vn -0.0644 -0.9042 0.4223 +vn 0.0496 -0.5562 0.8296 +vn -0.0415 0.1924 0.9804 +vn -0.0888 -0.0648 0.9939 +vn -0.0849 0.0042 0.9964 +vn -0.0844 0.0026 0.9964 +vn -0.1004 0.0127 0.9949 +vn -0.3190 -0.0642 0.9456 +vn -0.5270 -0.2965 0.7965 +vn -0.4940 -0.7190 0.4889 +vn -0.4749 -0.8485 0.2336 +vn -0.1621 -0.9608 0.2249 +vn -0.1299 -0.9842 0.1207 +vn -0.0153 -0.9725 0.2325 +vn -0.3283 -0.8382 0.4354 +vn -0.1630 -0.7820 0.6016 +vn -0.2975 -0.6945 0.6551 +vn -0.3860 -0.6473 0.6573 +vn -0.4257 -0.5703 0.7025 +vn -0.5121 -0.4409 0.7372 +vn -0.6144 -0.3677 0.6981 +vn -0.6658 -0.2891 0.6878 +vn -0.7843 -0.1723 0.5960 +vn -0.7566 -0.0678 0.6504 +vn -0.6716 0.3929 0.6282 +vn -0.4336 0.4648 0.7720 +vn -0.4167 0.6479 0.6376 +vn -0.4297 0.6901 0.5824 +vn -0.4068 0.6511 0.6408 +vn -0.5529 0.6415 0.5317 +vn -0.5967 0.6450 0.4774 +vn -0.8312 0.3654 0.4190 +vn -0.9066 -0.0992 0.4103 +vn -0.8602 0.4160 0.2950 +vn -0.8179 -0.5406 0.1968 +vn -0.6504 -0.7440 0.1529 +vn -0.4831 -0.8627 0.1495 +vn -0.3591 -0.9295 0.0839 +vn -0.4708 -0.8822 0.0034 +vn -0.2247 -0.9744 -0.0005 +vn -0.1251 -0.9921 0.0121 +vn -0.1801 -0.9794 0.0915 +vn -0.2466 -0.9611 0.1244 +vn -0.4208 -0.8906 0.1725 +vn -0.7444 -0.5791 0.3326 +vn -0.6393 -0.6492 0.4121 +vn -0.8357 -0.2047 0.5096 +vn -0.7236 0.2655 0.6371 +vn -0.7948 0.2724 0.5424 +vn -0.5584 0.3516 0.7514 +vn -0.4975 0.5128 0.6996 +vn -0.4286 0.4670 0.7734 +vn -0.4465 0.5425 0.7116 +vn -0.3527 0.5966 0.7209 +vn -0.3380 0.4479 0.8277 +vn -0.4034 0.3121 0.8602 +vn -0.4422 0.2587 0.8588 +vn -0.4535 0.4575 0.7649 +vn -0.5540 0.0929 0.8273 +vn -0.5922 0.1924 0.7824 +vn -0.7003 0.0775 0.7096 +vn -0.6158 -0.1074 0.7805 +vn -0.6071 -0.2168 0.7645 +vn -0.7098 -0.2158 0.6706 +vn -0.4744 -0.2726 0.8371 +vn -0.2952 -0.3526 0.8880 +vn -0.5319 -0.3088 0.7885 +vn -0.1776 -0.4390 0.8808 +vn -0.3743 -0.4118 0.8308 +vn -0.0707 -0.5401 0.8386 +vn -0.0181 -0.1254 0.9919 +vn -0.0818 -0.0928 0.9923 +vn -0.1753 -0.1260 0.9764 +vn -0.1400 -0.2213 0.9651 +vn -0.1172 -0.1214 0.9857 +vn -0.0133 -0.5989 0.8007 +vn -0.0831 -0.9175 0.3889 +vn -0.1041 -0.9836 0.1474 +vn -0.1032 -0.9836 0.1481 +vn -0.1035 -0.9836 0.1476 +vn -0.1008 -0.9859 0.1339 +vn -0.1289 -0.9901 0.0555 +vn -0.1210 -0.9826 0.1412 +vn -0.2192 -0.9652 0.1424 +vn -0.2517 -0.9494 0.1877 +vn -0.3950 -0.8401 0.3716 +vn -0.2630 -0.8689 0.4193 +vn -0.4280 -0.0895 0.8993 +vn -0.5298 0.2215 0.8187 +vn -0.4172 -0.0429 0.9078 +vn -0.4166 0.1900 0.8890 +vn -0.2579 0.3005 0.9182 +vn -0.2984 0.3005 0.9059 +vn -0.2637 0.4400 0.8584 +vn -0.3942 0.0795 0.9156 +vn -0.4450 0.0063 0.8955 +vn -0.4866 0.1074 0.8670 +vn -0.5208 -0.1521 0.8400 +vn -0.5338 -0.1552 0.8312 +vn -0.2474 -0.2391 0.9389 +vn -0.1518 -0.2949 0.9434 +vn -0.0735 -0.3230 0.9435 +vn -0.2444 -0.2430 0.9387 +vn -0.3316 -0.1052 0.9375 +vn -0.3412 0.0908 0.9356 +vn -0.2209 0.1192 0.9680 +vn -0.1705 0.1652 0.9714 +vn -0.2974 -0.2448 0.9228 +vn -0.2618 -0.6018 0.7546 +vn -0.3315 -0.7874 0.5198 +vn -0.2313 -0.9370 0.2619 +vn -0.1887 -0.9786 0.0825 +vn -0.1220 -0.9873 0.1020 +vn -0.1023 -0.9916 0.0794 +vn -0.1445 -0.9741 0.1740 +vn -0.0910 -0.9863 0.1374 +vn -0.0789 -0.9969 0.0065 +vn -0.1301 -0.9638 0.2329 +vn -0.3139 0.0262 0.9491 +vn -0.2518 -0.0690 0.9653 +vn -0.0752 -0.2052 0.9758 +vn -0.0471 -0.9963 0.0712 +vn -0.0886 -0.9907 0.1036 +vn -0.0478 -0.9987 0.0175 +vn 0.0148 -0.7328 0.6802 +vn -0.0728 -0.2972 0.9520 +vn -0.0351 -0.9965 0.0763 +vn 0.0127 -0.3557 -0.9345 +vn 0.0100 -0.6467 -0.7627 +vn -0.0313 -0.2756 -0.9608 +vn -0.0164 -0.0483 -0.9987 +vn -0.0821 0.4853 -0.8705 +vn -0.0829 0.5299 -0.8440 +vn -0.1694 0.5620 -0.8096 +vn -0.3410 0.7326 -0.5891 +vn -0.4291 0.7051 -0.5645 +vn -0.4229 0.6343 -0.6472 +vn -0.4873 0.6156 -0.6193 +vn -0.5459 0.6758 -0.4953 +vn -0.5260 0.6909 -0.4959 +vn -0.5189 0.7272 -0.4495 +vn -0.4797 0.8061 -0.3466 +vn -0.4663 0.7708 -0.4340 +vn -0.3523 0.7173 -0.6011 +vn -0.4924 0.4853 -0.7226 +vn -0.2006 0.4943 -0.8458 +vn -0.0546 0.4635 -0.8844 +vn -0.0560 0.4591 -0.8866 +vn -0.0559 0.3306 -0.9421 +vn -0.0438 0.1062 -0.9934 +vn -0.0830 0.4813 -0.8726 +vn -0.0843 0.3181 -0.9443 +vn -0.0731 0.2830 -0.9563 +vn -0.0922 0.2228 -0.9705 +vn -0.0745 0.1187 -0.9901 +vn -0.0652 0.0007 -0.9979 +vn -0.0939 -0.3233 -0.9416 +vn -0.7817 -0.2547 -0.5693 +vn -0.8149 -0.2306 -0.5318 +vn -0.8333 -0.2001 -0.5153 +vn -0.8718 -0.1736 -0.4581 +vn -0.9753 -0.0604 -0.2123 +vn -0.9902 0.0083 -0.1393 +vn -0.9540 0.1860 -0.2352 +vn -0.4612 0.6200 -0.6348 +vn -0.4349 0.6677 -0.6042 +vn -0.7902 0.2940 -0.5378 +vn -0.3960 0.4283 -0.8123 +vn -0.4356 0.4546 -0.7769 +vn -0.9995 -0.0218 0.0243 +vn -0.7446 -0.1125 0.6580 +vn -0.6756 -0.2411 0.6967 +vn -0.6410 0.2656 0.7202 +vn -0.8502 0.1692 0.4986 +vn -0.9766 0.1286 0.1724 +vn -0.9932 0.1059 -0.0492 +vn -0.9271 -0.2780 0.2513 +vn -0.9787 -0.1121 0.1717 +vn -0.9307 -0.1222 0.3449 +vn -0.8334 0.4042 0.3768 +vn -0.9516 0.0112 0.3070 +vn -0.9298 0.3362 0.1497 +vn -0.9966 0.0822 0.0013 +vn -0.9897 0.1212 -0.0757 +vn -0.9775 0.1682 -0.1269 +vn -0.9627 0.1575 -0.2200 +vn -0.9193 0.2062 -0.3352 +vn -0.8353 0.1085 -0.5389 +vn -0.7617 0.1315 -0.6344 +vn -0.7353 0.0679 -0.6743 +vn -0.6589 0.0689 -0.7490 +vn -0.6539 -0.0444 -0.7553 +vn -0.5203 -0.0370 -0.8532 +vn -0.4414 -0.1743 -0.8802 +vn -0.4105 -0.2484 -0.8774 +vn -0.2348 -0.3550 -0.9049 +vn -0.1486 -0.3604 -0.9209 +vn -0.1028 -0.3982 -0.9115 +vn -0.0884 -0.4405 -0.8934 +vn -0.1042 -0.4259 -0.8987 +vn -0.0394 -0.3403 -0.9395 +vn -0.0501 -0.2914 -0.9553 +vn -0.0841 -0.2709 -0.9589 +vn -0.0988 -0.2423 -0.9652 +vn -0.1859 -0.2187 -0.9579 +vn -0.1900 -0.1339 -0.9726 +vn -0.2847 -0.1081 -0.9525 +vn -0.5266 0.0524 -0.8485 +vn -0.5901 0.0795 -0.8034 +vn -0.5812 0.1403 -0.8016 +vn -0.6513 0.1528 -0.7432 +vn -0.6930 0.1968 -0.6936 +vn -0.7256 0.1907 -0.6612 +vn -0.7554 0.2502 -0.6056 +vn -0.8282 0.2957 -0.4761 +vn -0.9146 0.2629 -0.3073 +vn -0.9568 0.2049 -0.2064 +vn -0.9803 0.1777 -0.0860 +vn -0.9892 0.1428 -0.0329 +vn -0.9937 0.1111 0.0119 +vn -0.9905 0.1322 0.0387 +vn -0.9858 0.0235 0.1664 +vn -0.9777 0.0967 0.1866 +vn -0.9803 0.1345 0.1447 +vn -0.9867 0.1408 0.0817 +vn -0.9855 0.1694 0.0073 +vn -0.9878 0.1524 -0.0333 +vn -0.8937 0.3544 -0.2750 +vn -0.8497 0.3957 -0.3486 +vn -0.8517 0.3458 -0.3937 +vn -0.7524 0.3918 -0.5296 +vn -0.7664 0.3387 -0.5458 +vn -0.6849 0.3027 -0.6628 +vn -0.6624 0.2553 -0.7043 +vn -0.5897 0.2526 -0.7671 +vn -0.5113 0.1472 -0.8467 +vn -0.3373 0.0376 -0.9407 +vn -0.1195 -0.0472 -0.9917 +vn -0.1253 -0.1009 -0.9870 +vn -0.0737 -0.1753 -0.9817 +vn -0.1476 0.0416 -0.9882 +vn -0.2017 0.0536 -0.9780 +vn -0.3684 0.1310 -0.9204 +vn -0.3852 0.2276 -0.8943 +vn -0.5011 0.2381 -0.8320 +vn -0.5880 0.3336 -0.7368 +vn -0.6388 0.3545 -0.6828 +vn -0.7125 0.4284 -0.5556 +vn -0.7701 0.4602 -0.4419 +vn -0.9007 0.3905 -0.1902 +vn -0.9708 0.2333 -0.0565 +vn -0.9809 0.1876 0.0515 +vn -0.9783 0.1732 0.1135 +vn -0.9644 0.2616 0.0381 +vn -0.9405 0.3353 -0.0554 +vn -0.8489 0.4750 -0.2317 +vn -0.7416 0.5702 -0.3534 +vn -0.7541 0.5420 -0.3710 +vn -0.6527 0.5681 -0.5012 +vn -0.6610 0.5448 -0.5160 +vn -0.6130 0.4621 -0.6409 +vn -0.4536 0.4892 -0.7449 +vn -0.3775 0.3794 -0.8447 +vn -0.3475 0.3358 -0.8755 +vn -0.1437 0.3453 -0.9274 +vn -0.1251 0.2276 -0.9657 +vn -0.1600 0.1361 -0.9777 +vn -0.0217 0.1465 -0.9890 +vn -0.0792 0.1088 -0.9909 +vn -0.1708 0.4447 -0.8792 +vn -0.1561 0.4857 -0.8601 +vn -0.1807 0.5773 -0.7963 +vn -0.2288 0.6096 -0.7589 +vn -0.4986 0.6535 -0.5695 +vn -0.4987 0.7238 -0.4770 +vn -0.6480 0.6441 -0.4065 +vn -0.7595 0.5802 -0.2943 +vn -0.7450 0.6130 -0.2632 +vn -0.8419 0.5357 -0.0656 +vn -0.9570 0.1959 0.2137 +vn -0.9518 0.2285 0.2048 +vn -0.8761 0.4467 0.1815 +vn -0.9126 0.3995 0.0868 +vn -0.8456 0.5318 0.0463 +vn -0.7738 0.6295 -0.0705 +vn -0.6231 0.7747 -0.1073 +vn -0.6704 0.7277 -0.1453 +vn -0.5069 0.8301 -0.2324 +vn -0.5867 0.7568 -0.2881 +vn -0.5996 0.7439 -0.2951 +vn -0.3673 0.7782 -0.5094 +vn -0.3663 0.7060 -0.6062 +vn -0.2537 0.7421 -0.6204 +vn -0.1518 0.5808 -0.7997 +vn -0.0670 0.7156 -0.6953 +vn -0.0560 0.7969 -0.6015 +vn -0.2626 0.8043 -0.5331 +vn -0.2086 0.8494 -0.4848 +vn -0.3811 0.9062 -0.1834 +vn -0.5187 0.8485 -0.1047 +vn -0.6476 0.7613 0.0321 +vn -0.7393 0.6698 0.0689 +vn -0.8510 0.4917 0.1846 +vn -0.8289 0.5128 0.2235 +vn -0.8998 0.3804 0.2137 +vn -0.9114 0.2971 0.2847 +vn -0.9158 0.2328 0.3274 +vn -0.8779 0.3384 0.3389 +vn -0.9097 0.3008 0.2864 +vn -0.8603 0.4226 0.2850 +vn -0.7501 0.6187 0.2336 +vn -0.6660 0.7195 0.1968 +vn -0.5115 0.8519 0.1121 +vn -0.5198 0.8538 0.0292 +vn -0.4175 0.9017 -0.1126 +vn -0.2962 0.9438 -0.1469 +vn -0.3255 0.9269 -0.1870 +vn -0.1785 0.9177 -0.3549 +vn -0.1069 0.9179 -0.3821 +vn -0.1080 0.8657 -0.4887 +vn -0.0766 0.9374 -0.3398 +vn -0.0895 0.9542 -0.2854 +vn -0.2142 0.9617 -0.1711 +vn -0.2240 0.9717 -0.0743 +vn -0.3109 0.9484 -0.0622 +vn -0.3020 0.9517 0.0558 +vn -0.3850 0.9102 0.1526 +vn -0.5259 0.8298 0.1865 +vn -0.6147 0.7559 0.2253 +vn -0.6073 0.7467 0.2714 +vn -0.7898 0.5116 0.3385 +vn -0.8425 0.4144 0.3441 +vn -0.7932 0.4558 0.4039 +vn -0.9285 0.1973 0.3146 +vn -0.9634 0.0802 0.2559 +vn -0.9727 0.0084 0.2317 +vn -0.9819 -0.0465 0.1833 +vn -0.9939 -0.0787 0.0766 +vn -0.9963 -0.0827 -0.0235 +vn -0.9936 -0.1058 0.0408 +vn -0.9937 -0.1043 0.0404 +vn -0.9909 -0.1340 0.0084 +vn -0.9656 -0.2490 0.0742 +vn -0.9592 -0.2750 0.0663 +vn -0.8936 -0.3537 0.2762 +vn -0.8094 -0.5190 0.2747 +vn -0.5393 -0.8229 -0.1792 +vn -0.8039 -0.5936 -0.0374 +vn -0.8219 -0.5694 -0.0128 +vn -0.8807 -0.4642 -0.0940 +vn -0.4674 -0.7626 -0.4472 +vn -0.3618 -0.6768 -0.6411 +vn -0.6950 -0.5478 -0.4658 +vn -0.9537 -0.0309 0.2992 +vn -0.9508 -0.0269 0.3086 +vn -0.8235 -0.0660 0.5634 +vn -0.7249 -0.1087 0.6803 +vn -0.7338 -0.0848 0.6740 +vn -0.6350 0.1291 0.7616 +vn -0.7238 0.0535 0.6880 +vn -0.9134 -0.2071 0.3503 +vn -0.8986 -0.3737 0.2299 +vn -0.9129 -0.2097 0.3502 +vn -0.8494 0.1416 0.5083 +vn -0.9564 -0.1895 0.2222 +vn -0.9852 -0.1566 0.0704 +vn -0.9689 0.1105 0.2215 +vn -0.9978 0.0368 0.0550 +vn -0.9855 -0.1319 -0.1066 +vn -0.9863 -0.1641 0.0185 +vn -0.8265 -0.4422 0.3484 +vn -0.7003 -0.6752 0.2317 +vn -0.9997 -0.0253 0.0047 +vn -0.8923 -0.2150 -0.3969 +vn -0.9926 -0.0970 0.0733 +vn -0.6107 -0.2261 -0.7589 +vn -0.5131 -0.3767 -0.7712 +vn -0.2829 -0.4434 -0.8505 +vn -0.8671 -0.4412 -0.2313 +vn -0.8063 -0.5235 -0.2753 +vn -0.9037 -0.4110 -0.1200 +vn -0.9320 -0.3256 -0.1593 +vn -0.8969 -0.3419 -0.2806 +vn -0.8815 -0.2969 -0.3672 +vn -0.8562 -0.2800 -0.4342 +vn -0.8509 -0.2317 -0.4714 +vn -0.7845 -0.1232 -0.6077 +vn -0.7858 -0.0360 -0.6175 +vn -0.7012 -0.0115 -0.7129 +vn -0.5727 0.2205 -0.7895 +vn -0.4288 0.3141 -0.8471 +vn -0.2999 0.2906 -0.9086 +vn -0.2625 0.3735 -0.8897 +vn -0.2063 0.2940 -0.9333 +vn -0.1657 0.2531 -0.9531 +vn -0.0955 -0.0977 -0.9906 +vn -0.1779 -0.0880 -0.9801 +vn -0.2296 -0.1726 -0.9579 +vn -0.3791 -0.4217 -0.8237 +vn -0.4296 -0.4487 -0.7836 +vn -0.5436 -0.4371 -0.7165 +vn -0.5721 -0.4208 -0.7040 +vn -0.6294 -0.4065 -0.6622 +vn -0.7679 -0.3310 -0.5484 +vn -0.8934 -0.1876 -0.4083 +vn -0.9398 -0.1046 -0.3253 +vn -0.9502 -0.0988 -0.2957 +vn -0.9707 -0.0673 -0.2307 +vn -0.6529 0.1904 -0.7331 +vn -0.2361 0.2733 -0.9325 +vn -0.8676 0.0176 -0.4969 +vn -0.2878 -0.0101 -0.9576 +vn -0.3135 -0.2364 -0.9197 +vn -0.5933 -0.1397 -0.7928 +vn -0.7688 -0.3388 -0.5424 +vn -0.5205 -0.3938 -0.7576 +vn -0.8936 -0.2888 -0.3436 +vn -0.7253 -0.3690 -0.5811 +vn -0.8401 -0.3950 -0.3716 +vn -0.8731 -0.4240 -0.2407 +vn -0.9001 -0.3532 -0.2551 +vn -0.8463 -0.3796 -0.3738 +vn -0.8158 -0.3506 -0.4600 +vn -0.7623 -0.2913 -0.5780 +vn -0.5890 -0.1629 -0.7916 +vn -0.5772 -0.1176 -0.8081 +vn -0.6396 0.0309 -0.7681 +vn -0.5116 -0.0179 -0.8591 +vn -0.4860 0.0883 -0.8695 +vn -0.4038 0.1180 -0.9072 +vn -0.3371 0.1860 -0.9229 +vn -0.1929 0.1366 -0.9717 +vn -0.1845 0.1199 -0.9755 +vn -0.1785 0.0173 -0.9838 +vn -0.3480 -0.3093 -0.8850 +vn -0.4048 -0.2755 -0.8719 +vn -0.3880 -0.4264 -0.8171 +vn -0.4092 -0.4086 -0.8158 +vn -0.5180 -0.4067 -0.7525 +vn -0.7265 -0.4133 -0.5489 +vn -0.8599 -0.3600 -0.3619 +vn -0.8807 -0.3373 -0.3327 +vn -0.9603 -0.1671 -0.2235 +vn -0.8971 -0.1079 -0.4283 +vn -0.9084 -0.1233 -0.3996 +vn -0.9030 -0.1283 -0.4101 +vn -0.9445 -0.2326 -0.2320 +vn -0.7999 -0.3716 -0.4713 +vn -0.4790 -0.3048 -0.8232 +vn -0.4011 0.0036 -0.9160 +vn -0.2963 0.0227 -0.9548 +vn -0.7197 0.0671 -0.6910 +vn -0.4295 0.3137 -0.8468 +vn -0.5849 0.3689 -0.7223 +vn -0.7816 0.2033 -0.5897 +vn -0.8052 0.0949 -0.5853 +vn -0.8864 0.1486 -0.4383 +vn -0.9001 -0.0368 -0.4341 +vn -0.9242 -0.2084 -0.3202 +vn -0.9355 -0.2615 -0.2376 +vn -0.8136 -0.5815 0.0017 +vn -0.7919 -0.6106 -0.0094 +vn -0.9430 -0.3284 0.0536 +vn -0.9619 -0.2466 0.1181 +vn -0.9717 -0.1782 0.1552 +vn -0.9422 0.1368 0.3058 +vn -0.9030 0.2013 0.3796 +vn -0.7528 0.4039 0.5198 +vn -0.6970 0.4624 0.5480 +vn -0.7095 0.5405 0.4521 +vn -0.7026 0.6069 0.3715 +vn -0.5361 0.7504 0.3866 +vn -0.5496 0.7782 0.3038 +vn -0.3992 0.8965 0.1922 +vn -0.2163 0.9746 0.0577 +vn -0.1368 0.9891 -0.0543 +vn -0.0824 0.9923 -0.0923 +vn -0.0847 0.9868 -0.1383 +vn -0.1290 0.9891 0.0710 +vn -0.1172 0.9764 0.1812 +vn -0.1818 0.9485 0.2595 +vn -0.2319 0.9046 0.3577 +vn -0.3808 0.8543 0.3537 +vn -0.4775 0.7825 0.3996 +vn -0.4000 0.7654 0.5041 +vn -0.5249 0.4877 0.6976 +vn -0.6413 0.4718 0.6051 +vn -0.6339 0.4140 0.6532 +vn -0.7426 0.3296 0.5830 +vn -0.8211 0.2912 0.4909 +vn -0.8039 0.2879 0.5204 +vn -0.8954 0.2116 0.3918 +vn -0.9174 0.1290 0.3765 +vn -0.9608 0.0339 0.2750 +vn -0.9766 -0.1186 0.1797 +vn -0.9630 -0.2175 0.1592 +vn -0.9373 -0.3473 -0.0274 +vn -0.9590 -0.2828 0.0209 +vn -0.9608 -0.1630 -0.2241 +vn -0.9725 -0.0635 -0.2240 +vn -0.9577 0.1208 -0.2610 +vn -0.9378 0.0974 -0.3333 +vn -0.9176 0.1666 -0.3608 +vn -0.8929 0.3226 -0.3140 +vn -0.8297 0.4104 -0.3784 +vn -0.7171 0.3642 -0.5942 +vn -0.6265 0.5213 -0.5795 +vn -0.6063 0.6983 -0.3805 +vn -0.7401 0.5460 -0.3925 +vn -0.5547 0.6393 -0.5325 +vn -0.7860 0.5183 -0.3370 +vn -0.9920 -0.0445 -0.1185 +vn -0.9753 -0.1841 -0.1220 +vn -0.9713 -0.2331 -0.0465 +vn -0.9581 -0.2802 0.0588 +vn -0.9481 -0.3083 0.0783 +vn -0.9396 -0.2718 0.2082 +vn -0.9551 -0.2288 0.1885 +vn -0.9723 -0.1298 0.1946 +vn -0.9734 -0.0397 0.2258 +vn -0.9457 0.0470 0.3216 +vn -0.9111 0.0506 0.4090 +vn -0.8571 0.1126 0.5027 +vn -0.8634 0.1951 0.4653 +vn -0.6604 0.2825 0.6958 +vn -0.6668 0.3154 0.6752 +vn -0.4672 0.4090 0.7839 +vn -0.5109 0.4865 0.7088 +vn -0.3756 0.6025 0.7042 +vn -0.2816 0.6385 0.7162 +vn -0.2958 0.7349 0.6102 +vn -0.2927 0.8148 0.5003 +vn -0.3304 0.8306 0.4482 +vn -0.2262 0.9043 0.3620 +vn -0.1085 0.9018 0.4183 +vn -0.1871 0.8892 0.4176 +vn -0.1632 0.8175 0.5524 +vn -0.1413 0.7139 0.6859 +vn -0.1155 0.5951 0.7953 +vn -0.2630 0.5098 0.8191 +vn -0.4350 0.4064 0.8035 +vn -0.3753 0.3525 0.8572 +vn -0.5052 0.2976 0.8101 +vn -0.5661 0.2581 0.7829 +vn -0.5629 0.2269 0.7948 +vn -0.7148 0.0822 0.6945 +vn -0.8353 0.0303 0.5489 +vn -0.8095 0.2268 0.5416 +vn -0.8965 0.0824 0.4352 +vn -0.9352 -0.2873 0.2069 +vn -0.9366 -0.3112 0.1612 +vn -0.9371 -0.3334 0.1029 +vn -0.9425 -0.3142 0.1140 +vn -0.9686 -0.2350 0.0810 +vn -0.9761 -0.2163 -0.0202 +vn -0.9946 -0.1016 -0.0186 +vn -0.9902 -0.1159 -0.0779 +vn -0.9953 0.0471 -0.0847 +vn -0.8335 0.5264 -0.1676 +vn -0.7018 0.7051 -0.1018 +vn -0.7147 0.6804 -0.1622 +vn -0.5612 0.7761 -0.2877 +vn -0.4622 0.7416 -0.4863 +vn -0.5250 0.8348 -0.1658 +vn -0.4821 0.8194 -0.3101 +vn -0.5148 0.8377 -0.1826 +vn -0.5490 0.8108 -0.2028 +vn -0.5990 0.7753 -0.2004 +vn -0.6065 0.7368 -0.2990 +vn -0.6156 0.7247 -0.3096 +vn -0.6647 0.6848 -0.2987 +vn -0.8146 0.2917 -0.5014 +vn -0.6761 0.3311 -0.6582 +vn -0.7442 -0.1703 -0.6459 +vn -0.5586 -0.5437 -0.6264 +vn -0.3898 -0.8602 -0.3288 +vn -0.4153 -0.8930 -0.1734 +vn -0.5887 -0.7585 -0.2795 +vn -0.8006 -0.5325 -0.2749 +vn -0.8878 -0.3200 -0.3308 +vn -0.9539 -0.0893 -0.2866 +vn -0.9048 0.2959 -0.3061 +vn -0.8368 0.5192 -0.1735 +vn -0.7027 0.6940 -0.1567 +vn -0.6150 0.7875 -0.0397 +vn -0.6393 0.7570 -0.1349 +vn -0.6157 0.7870 -0.0402 +vn -0.5458 0.8368 -0.0432 +vn -0.6048 0.7872 -0.1204 +vn -0.8014 0.5969 -0.0380 +vn -0.9284 0.3691 -0.0431 +vn -0.9200 0.3919 -0.0050 +vn -0.9360 0.3481 -0.0514 +vn -0.9762 0.2166 -0.0079 +vn -0.9963 0.0861 -0.0001 +vn -0.9906 0.1333 0.0296 +vn -0.9984 -0.0321 0.0460 +vn -0.9769 -0.2084 0.0478 +vn -0.9202 -0.3445 0.1858 +vn -0.9241 -0.3472 0.1596 +vn -0.9264 -0.3483 0.1432 +vn -0.9303 -0.2840 0.2319 +vn -0.9433 -0.1741 0.2825 +vn -0.9440 -0.0268 0.3287 +vn -0.8491 0.1027 0.5181 +vn -0.6690 0.0606 0.7408 +vn -0.6650 -0.2604 0.7000 +vn -0.5878 -0.2238 0.7775 +vn -0.7362 0.0316 0.6760 +vn -0.4565 -0.0234 0.8894 +vn -0.5345 0.0997 0.8393 +vn -0.6162 0.1296 0.7769 +vn -0.4737 0.1592 0.8662 +vn -0.4783 0.1979 0.8556 +vn -0.2894 0.2666 0.9193 +vn -0.2938 0.3575 0.8865 +vn -0.1702 0.5203 0.8369 +vn -0.0498 0.5971 0.8006 +vn -0.0819 0.7180 0.6912 +vn -0.1016 0.8254 0.5553 +vn -0.0332 0.4763 0.8787 +vn -0.0354 0.3604 0.9321 +vn -0.2259 0.2598 0.9389 +vn -0.3032 0.2159 0.9282 +vn -0.3855 0.1189 0.9150 +vn -0.3710 0.0777 0.9254 +vn -0.3363 -0.3511 0.8739 +vn -0.3292 -0.2614 0.9074 +vn -0.5633 0.0469 0.8249 +vn -0.5871 0.0197 0.8093 +vn -0.5780 0.2812 0.7661 +vn -0.7025 0.1874 0.6866 +vn -0.8620 -0.0204 0.5065 +vn -0.8623 -0.0196 0.5060 +vn -0.9027 -0.2289 0.3642 +vn -0.8952 -0.3135 0.3167 +vn -0.9148 -0.3003 0.2699 +vn -0.8885 -0.4077 0.2104 +vn -0.9792 -0.1088 0.1713 +vn -0.9922 0.0613 0.1086 +vn -0.9793 0.1668 0.1149 +vn -0.8471 0.5156 0.1285 +vn -0.8484 0.5144 0.1248 +vn -0.7299 0.6825 0.0391 +vn -0.6129 0.7901 0.0118 +vn -0.5456 0.8228 0.1590 +vn -0.5679 0.8145 0.1186 +vn -0.5664 0.8145 0.1259 +vn -0.5469 0.7920 0.2712 +vn -0.6421 0.7538 0.1397 +vn -0.7020 0.7099 0.0563 +vn -0.8765 0.4691 0.1077 +vn -0.9788 0.1693 0.1152 +vn -0.9838 0.1153 -0.1374 +vn -0.9631 -0.2355 -0.1305 +vn -0.7542 -0.6531 -0.0680 +vn -0.6872 -0.7148 -0.1295 +vn -0.4430 -0.8952 -0.0485 +vn -0.2260 -0.9722 -0.0616 +vn -0.1678 -0.9815 -0.0919 +vn -0.1058 -0.9624 -0.2501 +vn -0.0896 -0.9821 -0.1658 +vn -0.0515 -0.9923 -0.1126 +vn -0.0432 -0.9967 0.0691 +vn -0.0706 -0.9921 0.1034 +vn -0.0097 -0.9748 0.2229 +vn 0.0288 -0.9203 0.3902 +vn 0.0163 -0.7304 0.6828 +vn 0.0273 -0.4997 0.8658 +vn -0.0172 -0.9975 0.0678 +vn -0.0809 -0.9782 0.1912 +vn -0.0575 -0.9950 0.0819 +vn -0.0800 -0.9925 0.0928 +vn -0.0301 -0.9965 0.0784 +vn -0.0284 -0.9966 0.0778 +vn -0.0309 -0.9965 0.0778 +vn -0.0207 -0.9998 0.0085 +vn -0.0205 -0.9998 0.0055 +vn -0.0210 -0.9990 -0.0396 +vn -0.0374 -0.9912 -0.1269 +vn -0.1996 -0.6779 -0.7075 +vn -0.1426 -0.9332 -0.3297 +vn -0.2319 -0.8979 -0.3742 +vn -0.3158 -0.6492 -0.6920 +vn -0.3590 -0.1178 -0.9259 +vn -0.3431 0.1573 -0.9260 +vn -0.1716 0.1031 -0.9798 +vn -0.2135 0.4469 -0.8687 +vn -0.2965 0.5239 -0.7985 +vn -0.4434 0.4651 -0.7662 +vn -0.2819 0.5164 -0.8086 +vn -0.1515 0.4584 -0.8757 +vn -0.1186 0.2438 -0.9625 +vn -0.0961 -0.2371 -0.9667 +vn -0.1321 -0.5458 -0.8274 +vn -0.0311 -0.7850 -0.6187 +vn -0.0595 -0.9333 -0.3542 +vn -0.0430 -0.9500 -0.3093 +vn -0.0351 -0.9737 -0.2250 +vn -0.0684 -0.9949 0.0741 +vn -0.0813 -0.9846 0.1546 +vn -0.0484 -0.9719 0.2302 +vn -0.0311 -0.9981 0.0524 +vn -0.0163 -0.9996 0.0224 +vn 0.0363 -0.7603 -0.6486 +vn 0.0021 -0.4306 -0.9025 +vn -0.0377 -0.3181 -0.9473 +vn -0.0986 0.1671 -0.9810 +vn -0.1186 0.2971 -0.9475 +vn -0.1423 0.4533 -0.8799 +vn -0.2048 0.5559 -0.8056 +vn -0.1061 0.4342 -0.8945 +vn 0.0389 -0.7650 -0.6428 +vn 0.0240 -0.8449 -0.5344 +vn 0.0458 -0.8995 -0.4344 +vn 0.0248 -0.9544 -0.2974 +vn -0.0005 -0.9914 -0.1309 +vn 0.0304 -0.4293 -0.9027 +vn 0.0054 -0.2128 -0.9771 +vn -0.0106 -0.1609 -0.9869 +vn -0.0242 -0.0417 -0.9988 +vn -0.0419 0.1267 -0.9911 +vn -0.0202 -0.9963 0.0837 +vn -0.2698 -0.9253 -0.2665 +vn -0.6355 0.7343 0.2386 +vn -0.5370 0.7624 0.3610 +vn -0.6109 0.7424 0.2751 +vn -0.6413 0.7097 0.2915 +vn -0.5955 0.6884 0.4141 +vn -0.7785 0.5759 0.2496 +vn -0.8232 0.4582 0.3352 +vn -0.9196 0.1591 0.3592 +vn -0.9400 0.0239 0.3402 +vn -0.9530 -0.1559 0.2597 +vn -0.9178 -0.2930 0.2678 +vn -0.8728 -0.3598 0.3297 +vn -0.8519 -0.4353 0.2913 +vn -0.8391 -0.2875 0.4619 +vn -0.7250 -0.0361 0.6878 +vn -0.7377 0.0295 0.6745 +vn -0.5657 0.1953 0.8011 +vn -0.2905 -0.0159 0.9567 +vn -0.2629 -0.1853 0.9469 +vn -0.1993 -0.4209 0.8850 +vn -0.2430 -0.1603 0.9567 +vn -0.2482 -0.1963 0.9486 +vn -0.2657 0.0085 0.9640 +vn -0.1861 0.2086 0.9601 +vn -0.2131 0.0859 0.9733 +vn -0.3335 -0.1562 0.9297 +vn -0.3274 -0.1902 0.9256 +vn -0.3412 -0.2318 0.9109 +vn -0.1949 -0.0721 0.9782 +vn -0.1808 0.2740 0.9446 +vn -0.2997 0.3469 0.8887 +vn -0.5413 0.1771 0.8220 +vn -0.7793 -0.2323 0.5820 +vn -0.8218 -0.2920 0.4893 +vn -0.8365 -0.4182 0.3542 +vn -0.7929 -0.4996 0.3489 +vn -0.7820 -0.5194 0.3445 +vn -0.8274 -0.4161 0.3773 +vn -0.9207 -0.2383 0.3089 +vn -0.9361 -0.1286 0.3275 +vn -0.9033 -0.0155 0.4288 +vn -0.8661 0.2186 0.4495 +vn -0.8497 0.2920 0.4390 +vn -0.8580 0.2577 0.4444 +vn -0.6606 0.5223 0.5393 +vn -0.5581 0.6578 0.5058 +vn -0.6025 0.6790 0.4194 +vn -0.4871 0.6659 0.5650 +vn -0.5160 0.7296 0.4489 +vn -0.6110 0.5485 0.5708 +vn -0.6496 0.4880 0.5830 +vn -0.7957 0.2483 0.5525 +vn -0.7934 0.2717 0.5447 +vn -0.8582 0.0047 0.5133 +vn -0.8982 -0.0978 0.4285 +vn -0.8730 -0.1525 0.4633 +vn -0.8287 -0.2309 0.5099 +vn -0.8679 -0.2997 0.3962 +vn -0.8219 -0.2873 0.4919 +vn -0.6833 -0.2029 0.7014 +vn -0.4895 -0.0186 0.8718 +vn -0.6021 -0.1223 0.7890 +vn -0.3726 0.1264 0.9194 +vn -0.2646 0.2185 0.9393 +vn -0.3514 0.4281 0.8326 +vn -0.3175 0.1963 0.9277 +vn -0.6583 0.1217 0.7428 +vn -0.5868 -0.1235 0.8003 +vn -0.5117 0.0446 0.8580 +vn -0.3939 -0.2414 0.8869 +vn -0.1831 0.0527 0.9817 +vn -0.3839 -0.1816 0.9053 +vn -0.0839 0.1559 0.9842 +vn -0.3272 0.2952 0.8977 +vn -0.6106 0.3910 0.6887 +vn -0.7119 0.3180 0.6262 +vn -0.6580 0.3672 0.6574 +vn -0.5422 0.4001 0.7389 +vn -0.2307 0.3902 0.8914 +vn -0.5756 0.2005 0.7928 +vn -0.5068 0.2224 0.8329 +vn -0.3574 0.2103 0.9100 +vn -0.6285 -0.1079 0.7703 +vn -0.7055 -0.1734 0.6871 +vn -0.6614 -0.2890 0.6922 +vn -0.7550 -0.3219 0.5713 +vn -0.8083 -0.3626 0.4639 +vn -0.6963 -0.3176 0.6437 +vn -0.6455 -0.6734 0.3603 +vn -0.5835 -0.6576 0.4766 +vn -0.4904 -0.7878 0.3727 +vn -0.6073 -0.7198 0.3364 +vn -0.4661 -0.7030 0.5372 +vn -0.7136 -0.6357 0.2943 +vn -0.7295 -0.5882 0.3490 +vn -0.7463 -0.4793 0.4618 +vn -0.7349 -0.5074 0.4500 +vn -0.7026 -0.4013 0.5876 +vn -0.3208 -0.2560 0.9119 +vn -0.5247 -0.2459 0.8150 +vn -0.4588 0.0002 0.8886 +vn -0.3350 0.0505 0.9409 +vn -0.3410 0.0734 0.9372 +vn -0.4385 -0.3237 0.8384 +vn -0.2036 -0.6690 0.7148 +vn -0.5006 -0.0918 0.8608 +vn -0.4183 -0.4417 0.7937 +vn -0.4422 -0.2238 0.8686 +vn -0.5639 -0.0401 0.8249 +vn -0.5382 -0.8159 0.2113 +vn -0.3542 -0.9131 0.2021 +vn -0.2278 -0.9244 0.3059 +vn -0.1406 -0.9654 0.2198 +vn -0.2268 -0.9660 0.1244 +vn -0.5741 -0.7862 0.2286 +vn -0.7217 -0.6168 0.3140 +vn -0.7068 -0.3179 0.6319 +vn -0.5073 -0.1327 0.8515 +vn -0.2578 -0.1066 0.9603 +vn -0.3351 0.0816 0.9386 +vn -0.2681 -0.1224 0.9556 +vn -0.3298 -0.4255 0.8427 +vn -0.2818 -0.2523 0.9257 +vn 0.0157 0.1205 0.9926 +vn -0.2676 0.0991 0.9584 +vn -0.1771 0.1493 0.9728 +vn -0.1279 -0.1766 0.9759 +vn -0.1271 -0.1766 0.9760 +vn -0.3523 0.1284 0.9270 +vn -0.7197 0.0031 0.6943 +vn -0.8430 0.2759 0.4618 +vn -0.6878 0.2211 0.6914 +vn -0.7094 0.3821 0.5923 +vn -0.6518 0.3591 0.6680 +vn -0.7640 0.3452 0.5451 +vn -0.5346 0.5196 0.6665 +vn -0.7427 0.4186 0.5227 +vn -0.7853 0.3659 0.4994 +vn -0.7129 -0.0247 0.7008 +vn -0.7532 0.2125 0.6225 +vn -0.5726 -0.4001 0.7156 +vn -0.9659 0.1420 0.2164 +vn -0.4711 0.4129 0.7795 +vn -0.5819 0.6728 -0.4569 +vn -0.9789 -0.0072 0.2040 +vn -0.9470 0.1741 0.2698 +vn -0.9950 -0.0080 0.0994 +vn -0.9954 0.0271 0.0924 +vn -0.9944 0.0340 0.1002 +vn 0.0000 -0.3593 -0.9332 +vn -0.9788 -0.1264 0.1611 +vn -0.1696 0.5848 -0.7932 +vn -0.2611 0.6558 -0.7083 +vn -0.5293 -0.6198 0.5794 +vn -0.1141 -0.7827 0.6118 +vn -0.1277 -0.9162 0.3797 +vn -0.0933 -0.5611 0.8225 +vn -0.2883 -0.4328 0.8542 +vn -0.0731 -0.1702 0.9827 +vn -0.2249 -0.5390 0.8117 +vn -0.9167 0.0258 0.3988 +vn -0.6482 -0.5083 0.5670 +vn 0.1251 -0.8636 0.4884 +vn 0.4414 -0.4149 0.7956 +vn 0.8359 -0.4210 0.3521 +vn 0.5130 0.0648 0.8559 +vn 0.4559 -0.0998 0.8844 +vn 0.2706 -0.1345 0.9532 +vn 0.3617 -0.1335 0.9227 +vn 0.7069 -0.4766 0.5226 +vn 0.6361 -0.5694 0.5207 +vn 0.9850 -0.1317 0.1116 +vn 0.9885 -0.0889 0.1223 +vn 0.5434 0.8273 -0.1427 +vn 0.6412 0.7524 0.1507 +vn 0.8351 0.2950 0.4642 +vn 0.9306 -0.3287 -0.1609 +vn 0.2490 0.6544 -0.7139 +vn 0.1738 0.5847 -0.7924 +vn -0.0001 -0.2254 -0.9743 +vn 0.0434 0.0227 -0.9988 +vn 0.9203 -0.2813 0.2720 +vn 0.0901 -0.8049 0.5866 +vn 0.0295 -0.3924 -0.9193 +vn -0.0246 0.4460 -0.8947 +vn 0.0398 0.3867 -0.9213 +vn 0.0430 0.3664 -0.9294 +vn 0.0243 0.3080 -0.9511 +vn 0.0219 0.2222 -0.9747 +vn 0.0412 0.0944 -0.9947 +vn 0.0157 -0.0383 -0.9991 +vn 0.0199 -0.2239 -0.9744 +vn -0.0021 -0.3012 -0.9536 +vn 0.1062 -0.4524 -0.8855 +vn 0.1435 -0.4751 -0.8682 +vn 0.4370 -0.3977 -0.8068 +vn 0.5941 -0.3201 -0.7380 +vn 0.7472 -0.1675 -0.6431 +vn 0.8051 -0.1575 -0.5718 +vn 0.8318 -0.0922 -0.5474 +vn 0.9617 -0.0533 -0.2689 +vn 0.9939 0.0396 -0.1026 +vn 0.9960 0.0409 -0.0797 +vn 0.9974 0.0630 -0.0339 +vn 0.7539 0.6569 -0.0105 +vn 0.6423 0.6480 -0.4093 +vn 0.5901 0.8000 -0.1086 +vn 0.9126 0.3317 -0.2389 +vn 0.9308 0.2621 -0.2548 +vn 0.9984 0.0053 0.0569 +vn 0.9783 -0.0316 0.2047 +vn 0.9593 0.0221 0.2814 +vn 0.8540 -0.1234 0.5055 +vn 0.8434 -0.1274 0.5220 +vn 0.8111 -0.1941 0.5517 +vn 0.8777 -0.1494 0.4553 +vn 0.8507 -0.2646 0.4541 +vn 0.7695 -0.3162 0.5548 +vn 0.7688 -0.3173 0.5552 +vn 0.7947 -0.0154 0.6067 +vn 0.7146 0.2152 0.6656 +vn 0.7065 0.3462 0.6172 +vn 0.6607 0.3345 0.6721 +vn 0.7584 0.3669 0.5387 +vn 0.8414 0.3346 0.4243 +vn 0.9112 0.2443 0.3316 +vn 0.8359 0.3531 0.4202 +vn 0.9900 -0.0762 -0.1185 +vn 0.9135 -0.3309 0.2367 +vn 0.9512 -0.2380 0.1965 +vn 0.9757 -0.2190 -0.0079 +vn 0.9196 -0.3718 0.1268 +vn 0.8865 -0.2487 0.3901 +vn 0.8781 -0.1742 0.4457 +vn 0.8962 0.3965 0.1992 +vn 0.8058 0.3839 0.4510 +vn 0.9559 0.2932 -0.0154 +vn 0.9968 0.0758 -0.0237 +vn 0.9886 0.0893 -0.1210 +vn 0.9665 0.0987 -0.2368 +vn 0.9064 -0.0073 -0.4223 +vn 0.8281 0.0254 -0.5600 +vn 0.8703 -0.0087 -0.4924 +vn 0.7930 0.0076 -0.6092 +vn 0.7870 -0.0459 -0.6153 +vn 0.5634 -0.2393 -0.7908 +vn 0.4067 -0.3206 -0.8555 +vn 0.1103 -0.4680 -0.8768 +vn 0.0913 -0.4777 -0.8738 +vn 0.0086 -0.4475 -0.8942 +vn 0.0046 -0.4677 -0.8839 +vn 0.0000 -0.2466 -0.9691 +vn 0.0005 -0.1722 -0.9851 +vn 0.0015 -0.0464 -0.9989 +vn 0.0020 0.0871 -0.9962 +vn -0.0074 0.3277 -0.9448 +vn -0.0168 0.4768 -0.8789 +vn -0.0025 0.6310 -0.7757 +vn 0.0000 0.8648 -0.5021 +vn 0.0000 0.9024 -0.4308 +vn 0.0220 0.9033 0.4285 +vn 0.0657 0.3224 0.9443 +vn 0.0522 0.5940 0.8028 +vn 0.0976 0.5793 0.8092 +vn 0.2542 -0.0882 0.9631 +vn 0.4563 -0.3222 0.8294 +vn 0.4034 -0.6143 0.6782 +vn 0.0643 -0.9042 0.4223 +vn 0.3311 -0.7628 0.5554 +vn -0.0590 -0.5706 0.8191 +vn -0.0996 -0.1363 0.9856 +vn -0.0096 0.1132 0.9935 +vn 0.0699 -0.1366 0.9882 +vn 0.1057 -0.4373 0.8931 +vn 0.0135 -0.2205 0.9753 +vn -0.0140 0.0488 0.9987 +vn -0.0091 -0.1323 0.9912 +vn -0.0108 0.0346 0.9993 +vn 0.1204 -0.0939 0.9883 +vn 0.1236 0.0230 0.9921 +vn 0.6281 -0.3155 0.7113 +vn 0.6622 -0.5503 0.5085 +vn 0.2444 -0.8891 0.3871 +vn 0.0073 -0.9027 0.4303 +vn 0.1633 -0.8971 0.4105 +vn 0.2605 -0.6709 0.6943 +vn 0.2174 -0.6293 0.7461 +vn 0.4257 -0.5704 0.7025 +vn 0.3388 -0.4940 0.8007 +vn 0.5120 -0.4409 0.7372 +vn 0.6300 -0.3703 0.6827 +vn 0.7843 -0.1722 0.5961 +vn 0.7901 0.0910 0.6062 +vn 0.6985 0.2126 0.6833 +vn 0.5598 0.3607 0.7460 +vn 0.6716 0.3929 0.6282 +vn 0.5765 0.4417 0.6874 +vn 0.5178 0.5660 0.6415 +vn 0.4235 0.6255 0.6553 +vn 0.5529 0.6415 0.5317 +vn 0.5866 0.6719 0.4522 +vn 0.8150 0.4220 0.3970 +vn 0.9066 -0.0991 0.4102 +vn 0.8602 0.4159 0.2950 +vn 0.9387 0.1686 0.3008 +vn 0.4833 -0.8626 0.1494 +vn 0.3590 -0.9296 0.0838 +vn 0.3587 -0.9299 0.0811 +vn 0.2333 -0.9724 0.0054 +vn 0.2384 -0.9711 0.0090 +vn 0.1226 -0.9905 -0.0626 +vn 0.0895 -0.9938 -0.0660 +vn 0.1258 -0.9919 0.0176 +vn 0.1802 -0.9794 0.0915 +vn 0.2436 -0.9576 0.1540 +vn 0.2466 -0.9611 0.1244 +vn 0.4310 -0.8856 0.1727 +vn 0.7444 -0.5790 0.3327 +vn 0.6393 -0.6493 0.4121 +vn 0.7729 -0.3936 0.4977 +vn 0.7134 0.3499 0.6071 +vn 0.7947 0.2725 0.5424 +vn 0.5584 0.3516 0.7514 +vn 0.4975 0.5128 0.6997 +vn 0.4286 0.4670 0.7735 +vn 0.4465 0.5425 0.7116 +vn 0.3524 0.5380 0.7658 +vn 0.3049 0.4805 0.8223 +vn 0.4122 0.5570 0.7210 +vn 0.5512 0.2793 0.7862 +vn 0.5921 0.1924 0.7826 +vn 0.7098 -0.2157 0.6706 +vn 0.4744 -0.2724 0.8371 +vn 0.5319 -0.3088 0.7885 +vn 0.1848 -0.4383 0.8796 +vn 0.3744 -0.4118 0.8308 +vn 0.2522 -0.5205 0.8158 +vn 0.0399 -0.7072 0.7059 +vn 0.0448 -0.4641 0.8847 +vn 0.0216 -0.3426 0.9392 +vn 0.0193 -0.2192 0.9755 +vn 0.0255 -0.1116 0.9934 +vn 0.0231 -0.1137 0.9932 +vn 0.1375 -0.1040 0.9850 +vn 0.1760 -0.1241 0.9765 +vn 0.1400 -0.2214 0.9651 +vn 0.0789 -0.2040 0.9758 +vn 0.0450 -0.9801 0.1936 +vn 0.0851 -0.8857 0.4564 +vn 0.0938 -0.9917 0.0874 +vn 0.1032 -0.9836 0.1481 +vn 0.1041 -0.9836 0.1474 +vn 0.1033 -0.9836 0.1477 +vn 0.1008 -0.9859 0.1339 +vn 0.0799 -0.9884 0.1294 +vn 0.1259 -0.9802 0.1526 +vn 0.1210 -0.9826 0.1412 +vn 0.3950 -0.8402 0.3716 +vn 0.4280 -0.0895 0.8993 +vn 0.5298 0.2215 0.8187 +vn 0.4166 0.1901 0.8890 +vn 0.4506 0.2920 0.8436 +vn 0.2689 0.2918 0.9179 +vn 0.2579 0.3005 0.9183 +vn 0.2984 0.3005 0.9059 +vn 0.4034 0.3126 0.8600 +vn 0.4012 0.1544 0.9029 +vn 0.4986 0.2398 0.8330 +vn 0.4867 0.1076 0.8669 +vn 0.5208 -0.1521 0.8400 +vn 0.4357 -0.2320 0.8697 +vn 0.2547 -0.2390 0.9370 +vn 0.3443 -0.2829 0.8952 +vn 0.1540 -0.2995 0.9416 +vn 0.1004 -0.3161 0.9434 +vn 0.1381 -0.2111 0.9677 +vn 0.4063 -0.0125 0.9137 +vn 0.3412 0.0909 0.9356 +vn 0.2208 0.1192 0.9680 +vn 0.1821 0.0824 0.9798 +vn 0.1614 -0.3099 0.9370 +vn 0.2618 0.1591 0.9519 +vn 0.1965 -0.7284 0.6564 +vn 0.3052 -0.2710 0.9129 +vn 0.2617 -0.6018 0.7546 +vn 0.2313 -0.9370 0.2619 +vn 0.1887 -0.9786 0.0825 +vn 0.1219 -0.9873 0.1020 +vn 0.1023 -0.9916 0.0794 +vn 0.1445 -0.9741 0.1740 +vn 0.0896 -0.9959 -0.0118 +vn 0.1301 -0.9638 0.2328 +vn 0.1312 -0.6630 0.7371 +vn 0.1740 0.0716 0.9821 +vn 0.1919 -0.0713 0.9788 +vn 0.2590 -0.0687 0.9634 +vn 0.2122 -0.1563 0.9647 +vn 0.0886 -0.9907 0.1036 +vn 0.1000 -0.9861 0.1324 +vn 0.0478 -0.9987 0.0175 +vn 0.0377 -0.9978 0.0542 +vn -0.0062 -0.8393 0.5436 +vn 0.0895 -0.2788 0.9562 +vn 0.0328 -0.3822 0.9235 +vn 0.0019 -0.5739 0.8189 +vn 0.0073 -0.6624 0.7491 +vn -0.0106 -0.8688 0.4950 +vn 0.0350 -0.9965 0.0758 +vn 0.0004 -0.6093 -0.7929 +vn 0.1479 0.5613 -0.8143 +vn 0.1694 0.5620 -0.8096 +vn 0.4445 0.6717 -0.5927 +vn 0.4229 0.6343 -0.6472 +vn 0.5313 0.6355 -0.5603 +vn 0.5389 0.7822 -0.3126 +vn 0.4664 0.7708 -0.4340 +vn 0.4815 0.8029 -0.3514 +vn 0.4042 0.6437 -0.6499 +vn 0.4923 0.4853 -0.7226 +vn 0.3340 0.4830 -0.8094 +vn 0.3341 0.5347 -0.7762 +vn 0.1079 0.5249 -0.8443 +vn 0.0827 0.5066 -0.8582 +vn 0.0198 0.3320 -0.9431 +vn 0.0114 0.4686 -0.8834 +vn 0.1099 0.4676 -0.8771 +vn 0.1556 0.3636 -0.9185 +vn 0.0741 0.2907 -0.9539 +vn 0.0654 0.0023 -0.9979 +vn 0.0843 -0.1509 -0.9849 +vn 0.1355 -0.3840 -0.9133 +vn 0.1360 -0.4352 -0.8900 +vn 0.2115 -0.4473 -0.8690 +vn 0.6906 -0.3458 -0.6352 +vn 0.7817 -0.2547 -0.5693 +vn 0.8333 -0.2001 -0.5153 +vn 0.9773 -0.0236 -0.2107 +vn 0.7843 0.3288 -0.5262 +vn 0.4349 0.6677 -0.6042 +vn 0.3960 0.4283 -0.8123 +vn 0.4356 0.4546 -0.7769 +vn 0.3181 0.2851 -0.9042 +vn 0.9995 -0.0218 0.0243 +vn 0.8389 -0.1123 0.5325 +vn 0.7447 -0.1121 0.6579 +vn 0.6756 -0.2410 0.6967 +vn 0.8058 0.1128 0.5814 +vn 0.8737 0.1091 0.4741 +vn 0.9560 0.2359 0.1745 +vn 0.9955 -0.0676 -0.0665 +vn 0.9854 -0.1301 -0.1102 +vn 0.9504 -0.3111 0.0012 +vn 0.9652 -0.2273 0.1295 +vn 0.9273 -0.2776 0.2512 +vn 0.9272 -0.3141 0.2039 +vn 0.9787 -0.1120 0.1718 +vn 0.9306 -0.1222 0.3449 +vn 0.9570 0.1316 0.2585 +vn 0.9516 0.0113 0.3071 +vn 0.9950 0.0998 -0.0085 +vn 0.9898 0.1230 -0.0722 +vn 0.9787 0.1632 -0.1245 +vn 0.9626 0.1577 -0.2201 +vn 0.9194 0.2065 -0.3347 +vn 0.9099 0.1930 -0.3673 +vn 0.8354 0.1085 -0.5389 +vn 0.7617 0.1315 -0.6344 +vn 0.7353 0.0679 -0.6743 +vn 0.6589 0.0689 -0.7490 +vn 0.6539 -0.0443 -0.7553 +vn 0.5279 -0.0284 -0.8488 +vn 0.4120 -0.2444 -0.8778 +vn 0.3059 -0.2384 -0.9217 +vn 0.2509 -0.2886 -0.9240 +vn 0.1118 -0.4013 -0.9091 +vn 0.0897 -0.4406 -0.8932 +vn 0.0637 -0.3201 -0.9452 +vn 0.1652 -0.1605 -0.9731 +vn 0.2730 -0.1633 -0.9480 +vn 0.3248 -0.1148 -0.9388 +vn 0.4405 -0.0573 -0.8959 +vn 0.4766 0.0720 -0.8761 +vn 0.5263 0.0525 -0.8487 +vn 0.6024 0.1558 -0.7829 +vn 0.6993 0.2038 -0.6851 +vn 0.7196 0.1998 -0.6650 +vn 0.7613 0.2476 -0.5993 +vn 0.9146 0.2628 -0.3072 +vn 0.9568 0.2049 -0.2063 +vn 0.9803 0.1778 -0.0860 +vn 0.9814 0.0787 0.1751 +vn 0.9885 0.1032 0.1102 +vn 0.9849 0.1409 0.1009 +vn 0.9698 0.2257 -0.0928 +vn 0.9481 0.2724 -0.1639 +vn 0.9152 0.3099 -0.2578 +vn 0.8643 0.3955 -0.3109 +vn 0.8425 0.3466 -0.4124 +vn 0.7742 0.4100 -0.4821 +vn 0.7621 0.3415 -0.5502 +vn 0.7075 0.3411 -0.6189 +vn 0.7333 0.3283 -0.5953 +vn 0.6737 0.3343 -0.6591 +vn 0.6601 0.2553 -0.7065 +vn 0.5897 0.2526 -0.7671 +vn 0.5113 0.1474 -0.8466 +vn 0.3062 0.0400 -0.9511 +vn 0.3765 0.0345 -0.9258 +vn 0.1531 -0.0235 -0.9879 +vn 0.1099 -0.0403 -0.9931 +vn 0.1011 -0.1532 -0.9830 +vn 0.0750 -0.0190 -0.9970 +vn 0.0744 0.0475 -0.9961 +vn 0.1471 0.0465 -0.9880 +vn 0.1292 0.1011 -0.9865 +vn 0.2231 0.1354 -0.9653 +vn 0.3341 0.1057 -0.9366 +vn 0.3882 0.2280 -0.8929 +vn 0.6006 0.4311 -0.6734 +vn 0.6861 0.4386 -0.5804 +vn 0.7141 0.4642 -0.5241 +vn 0.7665 0.4263 -0.4804 +vn 0.9109 0.3805 -0.1593 +vn 0.9707 0.2333 -0.0567 +vn 0.9829 0.1768 0.0523 +vn 0.9766 0.1596 0.1439 +vn 0.9639 0.1469 0.2220 +vn 0.9700 0.1080 0.2179 +vn 0.9647 0.1719 0.1994 +vn 0.9745 0.1783 0.1359 +vn 0.9445 0.3254 -0.0438 +vn 0.9601 0.2776 -0.0338 +vn 0.8779 0.4641 -0.1182 +vn 0.9110 0.3904 -0.1330 +vn 0.8567 0.4439 -0.2628 +vn 0.8001 0.5306 -0.2799 +vn 0.7076 0.5466 -0.4478 +vn 0.6982 0.4987 -0.5136 +vn 0.6153 0.5459 -0.5687 +vn 0.4994 0.5727 -0.6501 +vn 0.4536 0.4892 -0.7450 +vn 0.5255 0.4298 -0.7342 +vn 0.3583 0.4937 -0.7924 +vn 0.3308 0.4318 -0.8391 +vn 0.3475 0.3358 -0.8755 +vn 0.1259 0.3113 -0.9420 +vn 0.1288 0.2781 -0.9519 +vn 0.0313 0.1619 -0.9863 +vn 0.0743 0.1209 -0.9899 +vn 0.1414 0.3905 -0.9097 +vn 0.1554 0.4869 -0.8595 +vn 0.4497 0.5761 -0.6825 +vn 0.4958 0.6474 -0.5788 +vn 0.5790 0.6261 -0.5222 +vn 0.7031 0.6028 -0.3773 +vn 0.6990 0.6587 -0.2786 +vn 0.7915 0.5567 -0.2522 +vn 0.8694 0.4892 -0.0691 +vn 0.9166 0.3991 0.0254 +vn 0.9185 0.3340 0.2117 +vn 0.8096 0.5835 0.0637 +vn 0.7699 0.6382 0.0001 +vn 0.6477 0.7589 -0.0673 +vn 0.6252 0.7572 -0.1891 +vn 0.5167 0.8226 -0.2374 +vn 0.4273 0.8287 -0.3615 +vn 0.3678 0.7739 -0.5155 +vn 0.3523 0.7013 -0.6197 +vn 0.2282 0.7356 -0.6378 +vn 0.0824 0.5208 -0.8497 +vn 0.2086 0.8494 -0.4848 +vn 0.2618 0.8754 -0.4063 +vn 0.3316 0.8954 -0.2972 +vn 0.3578 0.8391 -0.4098 +vn 0.3989 0.8687 -0.2937 +vn 0.5035 0.8573 -0.1070 +vn 0.5896 0.7999 -0.1115 +vn 0.6320 0.7744 0.0304 +vn 0.7920 0.5963 0.1306 +vn 0.8798 0.3388 0.3333 +vn 0.9031 0.3362 0.2672 +vn 0.8501 0.4385 0.2916 +vn 0.6616 0.7374 0.1361 +vn 0.5320 0.8360 0.1344 +vn 0.5056 0.8627 0.0134 +vn 0.4131 0.9106 0.0103 +vn 0.3253 0.9268 -0.1878 +vn 0.2874 0.9241 -0.2518 +vn 0.2219 0.9385 -0.2645 +vn 0.1759 0.9188 -0.3535 +vn 0.1096 0.8661 -0.4877 +vn 0.0495 0.9253 -0.3761 +vn 0.1265 0.9659 -0.2260 +vn 0.1985 0.9530 -0.2290 +vn 0.2324 0.9706 -0.0632 +vn 0.3157 0.9471 -0.0574 +vn 0.2994 0.9526 0.0542 +vn 0.3829 0.9222 0.0549 +vn 0.4704 0.8565 0.2126 +vn 0.5258 0.8302 0.1854 +vn 0.6401 0.7085 0.2969 +vn 0.7483 0.6024 0.2777 +vn 0.8107 0.4523 0.3719 +vn 0.7665 0.5107 0.3895 +vn 0.8395 0.3438 0.4208 +vn 0.9099 0.2378 0.3400 +vn 0.9594 0.0861 0.2684 +vn 0.9720 0.0100 0.2346 +vn 0.9820 -0.0466 0.1831 +vn 0.9939 -0.0788 0.0765 +vn 0.9936 -0.1057 0.0408 +vn 0.9937 -0.1044 0.0405 +vn 0.9657 -0.2489 0.0742 +vn 0.9466 -0.1872 0.2623 +vn 0.8656 -0.3109 0.3924 +vn 0.8272 -0.5606 0.0391 +vn 0.8039 -0.5936 -0.0374 +vn 0.7247 -0.6226 -0.2952 +vn 0.4674 -0.7626 -0.4472 +vn 0.3618 -0.6768 -0.6411 +vn 0.9537 -0.0309 0.2993 +vn 0.7546 -0.0211 0.6558 +vn 0.8560 -0.0516 0.5143 +vn 0.7249 -0.1089 0.6802 +vn 0.8125 -0.0011 0.5830 +vn 0.9135 -0.2071 0.3503 +vn 0.9137 -0.2059 0.3503 +vn 0.8763 -0.0274 0.4809 +vn 0.9852 -0.1566 0.0704 +vn 0.9855 -0.1318 -0.1064 +vn 0.9730 -0.2292 0.0257 +vn 0.8373 -0.4051 0.3671 +vn 0.6379 -0.7700 -0.0117 +vn 0.5474 -0.8060 -0.2252 +vn 0.6630 -0.7154 -0.2206 +vn 0.7749 -0.6217 -0.1144 +vn 0.9997 -0.0253 0.0048 +vn 0.8152 -0.1968 -0.5447 +vn 0.9926 -0.0970 0.0732 +vn 0.6107 -0.2262 -0.7589 +vn 0.5132 -0.3767 -0.7712 +vn 0.2829 -0.4434 -0.8505 +vn 0.6112 -0.5801 -0.5384 +vn 0.8623 -0.4460 -0.2401 +vn 0.8297 -0.5097 -0.2276 +vn 0.9030 -0.4135 -0.1165 +vn 0.9030 -0.3988 -0.1598 +vn 0.9276 -0.3345 -0.1663 +vn 0.8970 -0.3341 -0.2895 +vn 0.8721 -0.3062 -0.3817 +vn 0.8516 -0.2303 -0.4708 +vn 0.8863 -0.1575 -0.4354 +vn 0.7627 -0.1559 -0.6276 +vn 0.7858 -0.0359 -0.6175 +vn 0.7391 0.1314 -0.6607 +vn 0.6164 0.2017 -0.7612 +vn 0.5361 0.1194 -0.8357 +vn 0.5724 0.2206 -0.7897 +vn 0.4343 0.1683 -0.8849 +vn 0.2800 0.2866 -0.9162 +vn 0.2453 0.3867 -0.8890 +vn 0.1725 0.2613 -0.9497 +vn 0.1348 0.2178 -0.9666 +vn 0.2296 -0.1726 -0.9579 +vn 0.1404 -0.2334 -0.9622 +vn 0.1943 -0.2861 -0.9383 +vn 0.1924 -0.4265 -0.8838 +vn 0.4294 -0.4487 -0.7837 +vn 0.5309 -0.4388 -0.7250 +vn 0.5705 -0.4194 -0.7061 +vn 0.6801 -0.4199 -0.6010 +vn 0.7840 -0.3447 -0.5163 +vn 0.8512 -0.2754 -0.4467 +vn 0.9372 -0.1032 -0.3331 +vn 0.9512 -0.0950 -0.2935 +vn 0.9592 -0.0973 -0.2653 +vn 0.9707 -0.0673 -0.2307 +vn 0.9085 0.1376 -0.3945 +vn 0.4098 0.0618 -0.9101 +vn 0.2164 0.2840 -0.9341 +vn 0.1566 0.0597 -0.9859 +vn 0.5800 0.0434 -0.8134 +vn 0.3983 -0.0075 -0.9172 +vn 0.2877 -0.0101 -0.9577 +vn 0.2064 -0.1543 -0.9662 +vn 0.5933 -0.1398 -0.7927 +vn 0.5206 -0.3938 -0.7576 +vn 0.7254 -0.3689 -0.5811 +vn 0.8557 -0.3671 -0.3647 +vn 0.8497 -0.4071 -0.3351 +vn 0.8996 -0.3565 -0.2522 +vn 0.8444 -0.3762 -0.3815 +vn 0.7625 -0.2912 -0.5778 +vn 0.5910 -0.2786 -0.7570 +vn 0.5771 -0.1178 -0.8081 +vn 0.5117 -0.0179 -0.8590 +vn 0.5688 0.0676 -0.8197 +vn 0.3372 0.1861 -0.9229 +vn 0.1929 0.1366 -0.9717 +vn 0.1845 0.1199 -0.9755 +vn 0.0800 0.1137 -0.9903 +vn 0.2140 0.0413 -0.9760 +vn 0.3781 -0.1215 -0.9177 +vn 0.3480 -0.3093 -0.8850 +vn 0.2361 -0.2681 -0.9340 +vn 0.3880 -0.4264 -0.8171 +vn 0.4092 -0.4087 -0.8158 +vn 0.5578 -0.4252 -0.7128 +vn 0.7029 -0.4070 -0.5834 +vn 0.9104 -0.2730 -0.3109 +vn 0.9569 -0.1641 -0.2395 +vn 0.4924 -0.0085 -0.8703 +vn 0.9084 -0.1234 -0.3995 +vn 0.9030 -0.1283 -0.4100 +vn 0.9437 -0.2382 -0.2294 +vn 0.8826 -0.3517 -0.3121 +vn 0.8163 -0.4052 -0.4117 +vn 0.7196 0.0671 -0.6911 +vn 0.4222 0.4133 -0.8068 +vn 0.4297 0.3136 -0.8468 +vn 0.5847 0.3686 -0.7226 +vn 0.7815 0.2034 -0.5898 +vn 0.6584 0.2345 -0.7152 +vn 0.8584 0.2578 -0.4434 +vn 0.8053 0.0949 -0.5853 +vn 0.8843 -0.0695 -0.4618 +vn 0.9490 -0.0768 -0.3057 +vn 0.9423 -0.2536 -0.2184 +vn 0.9269 -0.3632 -0.0947 +vn 0.8159 -0.5781 -0.0025 +vn 0.8366 -0.5141 0.1889 +vn 0.9619 -0.2463 0.1185 +vn 0.9826 -0.0750 0.1700 +vn 0.9372 0.1471 0.3161 +vn 0.9415 0.1483 0.3025 +vn 0.9086 0.1938 0.3699 +vn 0.6444 0.5431 0.5384 +vn 0.6452 0.6069 0.4641 +vn 0.6282 0.6777 0.3821 +vn 0.3938 0.8661 0.3080 +vn 0.3383 0.9085 0.2452 +vn 0.2459 0.9531 0.1763 +vn 0.2404 0.9690 0.0570 +vn 0.2107 0.9775 0.0041 +vn 0.1224 0.9921 0.0268 +vn 0.1007 0.9814 -0.1637 +vn 0.1131 0.9866 0.1179 +vn 0.1469 0.9629 0.2262 +vn 0.2349 0.9503 0.2045 +vn 0.3301 0.8981 0.2907 +vn 0.4568 0.7101 0.5358 +vn 0.4349 0.6263 0.6470 +vn 0.4705 0.6241 0.6239 +vn 0.6115 0.4968 0.6159 +vn 0.6169 0.4245 0.6627 +vn 0.7415 0.3689 0.5605 +vn 0.8024 0.2415 0.5458 +vn 0.8574 0.2348 0.4580 +vn 0.9328 0.0701 0.3535 +vn 0.9766 -0.1186 0.1796 +vn 0.9436 -0.3233 0.0717 +vn 0.9436 -0.3226 0.0741 +vn 0.9437 -0.3226 0.0732 +vn 0.9471 -0.3207 0.0093 +vn 0.9609 -0.2766 -0.0153 +vn 0.9595 -0.2791 -0.0382 +vn 0.9608 -0.2720 -0.0536 +vn 0.9568 -0.2608 -0.1283 +vn 0.9544 -0.1769 -0.2405 +vn 0.9851 0.0114 -0.1714 +vn 0.9382 -0.0180 -0.3457 +vn 0.9577 0.1208 -0.2610 +vn 0.9176 0.1665 -0.3609 +vn 0.7285 0.3816 -0.5690 +vn 0.6354 0.5551 -0.5368 +vn 0.6063 0.6983 -0.3805 +vn 0.7401 0.5460 -0.3925 +vn 0.5547 0.6393 -0.5326 +vn 0.8901 0.3505 -0.2911 +vn 0.8972 0.3152 -0.3094 +vn 0.9353 0.2226 -0.2749 +vn 0.9891 -0.0420 -0.1410 +vn 0.9686 -0.2041 -0.1418 +vn 0.9402 -0.3266 0.0965 +vn 0.9420 -0.3144 0.1176 +vn 0.9734 -0.0395 0.2257 +vn 0.9749 -0.0227 0.2215 +vn 0.9171 0.0661 0.3931 +vn 0.8492 0.1258 0.5129 +vn 0.5891 0.3596 0.7237 +vn 0.4895 0.4330 0.7569 +vn 0.3616 0.5899 0.7220 +vn 0.4209 0.5951 0.6847 +vn 0.2694 0.6870 0.6748 +vn 0.3475 0.6917 0.6331 +vn 0.2911 0.8157 0.5000 +vn 0.1018 0.9421 0.3195 +vn 0.0505 0.9704 0.2361 +vn 0.0806 0.9221 0.3785 +vn 0.2268 0.7136 0.6629 +vn 0.1520 0.7162 0.6811 +vn 0.1799 0.5872 0.7892 +vn 0.2347 0.5088 0.8283 +vn 0.3501 0.4908 0.7978 +vn 0.3310 0.3583 0.8729 +vn 0.4683 0.3408 0.8152 +vn 0.4084 0.2922 0.8648 +vn 0.5786 0.2681 0.7703 +vn 0.7165 0.1896 0.6714 +vn 0.7959 0.1533 0.5857 +vn 0.7719 0.0125 0.6356 +vn 0.8354 0.0299 0.5488 +vn 0.9003 0.0251 0.4345 +vn 0.8966 0.0823 0.4352 +vn 0.9331 0.0459 0.3568 +vn 0.9650 -0.1666 0.2024 +vn 0.9352 -0.2873 0.2069 +vn 0.9378 -0.3324 0.1003 +vn 0.9686 -0.2350 0.0811 +vn 0.9953 0.0471 -0.0847 +vn 0.7017 0.7051 -0.1018 +vn 0.5580 0.7842 -0.2713 +vn 0.5612 0.7761 -0.2877 +vn 0.4622 0.7416 -0.4863 +vn 0.4325 0.8030 -0.4100 +vn 0.4822 0.8194 -0.3101 +vn 0.5490 0.8109 -0.2029 +vn 0.5990 0.7752 -0.2005 +vn 0.6156 0.7247 -0.3096 +vn 0.6647 0.6848 -0.2987 +vn 0.7082 0.6253 -0.3277 +vn 0.6605 0.6180 -0.4264 +vn 0.8131 0.3120 -0.4914 +vn 0.6761 0.3311 -0.6582 +vn 0.6166 0.2529 -0.7455 +vn 0.4396 0.1609 -0.8837 +vn 0.6397 -0.3725 -0.6723 +vn 0.3898 -0.8602 -0.3288 +vn 0.4437 -0.8787 -0.1763 +vn 0.5886 -0.7585 -0.2795 +vn 0.8878 -0.3200 -0.3308 +vn 0.8721 -0.0124 -0.4891 +vn 0.9048 0.2959 -0.3061 +vn 0.8001 0.5186 -0.3015 +vn 0.8368 0.5193 -0.1735 +vn 0.7029 0.6938 -0.1568 +vn 0.6393 0.7570 -0.1349 +vn 0.6156 0.7870 -0.0402 +vn 0.5458 0.8368 -0.0433 +vn 0.6009 0.7992 -0.0094 +vn 0.6048 0.7872 -0.1205 +vn 0.7046 0.7027 -0.0989 +vn 0.8259 0.5630 -0.0304 +vn 0.9463 0.3117 0.0853 +vn 0.9984 -0.0321 0.0460 +vn 0.9514 -0.2481 0.1824 +vn 0.9199 -0.3463 0.1838 +vn 0.9228 -0.3556 0.1487 +vn 0.7251 0.1854 0.6632 +vn 0.6690 0.0606 0.7408 +vn 0.8128 -0.0079 0.5824 +vn 0.7811 -0.1137 0.6140 +vn 0.5371 -0.1442 0.8311 +vn 0.5138 -0.0860 0.8536 +vn 0.5347 0.0987 0.8393 +vn 0.6145 0.1065 0.7817 +vn 0.3183 0.2134 0.9237 +vn 0.2836 0.2826 0.9164 +vn 0.2292 0.3623 0.9034 +vn 0.1766 0.4518 0.8745 +vn 0.1105 0.4571 0.8825 +vn 0.0375 0.6561 0.7537 +vn 0.0540 0.7788 0.6250 +vn 0.0659 0.8662 0.4954 +vn 0.0315 0.5325 0.8459 +vn 0.0225 0.4086 0.9124 +vn 0.0639 0.4069 0.9113 +vn 0.1966 0.2401 0.9506 +vn 0.2356 0.1834 0.9544 +vn 0.3711 0.0768 0.9254 +vn 0.4457 -0.1308 0.8856 +vn 0.4910 -0.2091 0.8457 +vn 0.4222 0.0026 0.9065 +vn 0.5633 0.0469 0.8249 +vn 0.7025 0.1874 0.6866 +vn 0.8624 -0.0196 0.5059 +vn 0.8686 -0.1904 0.4574 +vn 0.9027 -0.2290 0.3642 +vn 0.9112 -0.3216 0.2574 +vn 0.9149 -0.3121 0.2561 +vn 0.8715 -0.4108 0.2676 +vn 0.9141 -0.3604 0.1859 +vn 0.9138 -0.3601 0.1880 +vn 0.9370 -0.2597 0.2335 +vn 0.9790 -0.1076 0.1732 +vn 0.9539 0.2104 0.2141 +vn 0.9793 0.1667 0.1149 +vn 0.9246 0.3195 0.2074 +vn 0.7915 0.5623 0.2394 +vn 0.7994 0.5946 0.0864 +vn 0.7153 0.6817 0.1538 +vn 0.7299 0.6825 0.0391 +vn 0.6024 0.7957 0.0627 +vn 0.6129 0.7901 0.0118 +vn 0.5679 0.8145 0.1186 +vn 0.9788 0.1694 0.1151 +vn 0.9838 0.1153 -0.1374 +vn 0.9748 -0.1874 -0.1209 +vn 0.4430 -0.8952 -0.0484 +vn 0.2184 -0.9741 -0.0577 +vn 0.0572 -0.9919 -0.1138 +vn 0.0451 -0.9988 -0.0208 +vn 0.0432 -0.9967 0.0691 +vn -0.0076 -0.9281 0.3721 +vn 0.0230 -0.9948 0.0989 +vn 0.0172 -0.9975 0.0679 +vn 0.0575 -0.9950 0.0819 +vn 0.0800 -0.9925 0.0928 +vn 0.0205 -0.9998 0.0055 +vn 0.0221 -0.9991 -0.0372 +vn 0.0395 -0.9900 -0.1352 +vn 0.1739 -0.7507 -0.6373 +vn 0.1996 -0.6780 -0.7074 +vn 0.2355 -0.9144 -0.3294 +vn 0.3158 -0.6492 -0.6919 +vn 0.2012 -0.3153 -0.9274 +vn 0.3590 -0.1174 -0.9259 +vn 0.3783 0.0622 -0.9236 +vn 0.4215 0.4318 -0.7974 +vn 0.4957 0.4604 -0.7364 +vn 0.2819 0.5164 -0.8086 +vn 0.1969 0.4015 -0.8944 +vn 0.1186 0.2438 -0.9625 +vn 0.0961 -0.2371 -0.9667 +vn 0.0312 -0.7850 -0.6188 +vn 0.0414 -0.9531 -0.2998 +vn 0.0373 -0.9733 -0.2264 +vn 0.0150 -0.9965 -0.0828 +vn 0.0684 -0.9949 0.0741 +vn 0.0163 -0.9996 0.0224 +vn 0.0129 -0.9997 -0.0194 +vn -0.0021 -0.4306 -0.9025 +vn 0.0377 -0.3181 -0.9473 +vn 0.0350 -0.3508 -0.9358 +vn 0.0930 0.2268 -0.9695 +vn 0.1186 0.2971 -0.9475 +vn 0.2218 0.5182 -0.8260 +vn 0.1101 0.4363 -0.8930 +vn 0.0979 0.3794 -0.9200 +vn -0.0453 -0.9005 -0.4326 +vn -0.0236 -0.9708 -0.2386 +vn 0.0087 -0.9969 -0.0780 +vn -0.0034 -0.2238 -0.9746 +vn 0.0112 -0.1654 -0.9862 +vn 0.0251 -0.0530 -0.9983 +vn 0.0419 0.1267 -0.9911 +vn -0.0012 -0.8379 -0.5459 +vn -0.0023 -0.9525 -0.3046 +vn 0.2874 -0.9173 -0.2758 +vn 0.5052 0.7439 0.4375 +vn 0.5370 0.7624 0.3610 +vn 0.6108 0.7425 0.2750 +vn 0.6413 0.7097 0.2915 +vn 0.5955 0.6884 0.4140 +vn 0.6958 0.6008 0.3937 +vn 0.6631 0.6091 0.4351 +vn 0.7371 0.4973 0.4575 +vn 0.9438 0.2428 0.2241 +vn 0.9400 0.0237 0.3405 +vn 0.9279 0.0409 0.3705 +vn 0.9176 -0.2944 0.2671 +vn 0.8850 -0.3111 0.3464 +vn 0.8905 -0.3329 0.3102 +vn 0.8519 -0.4449 0.2762 +vn 0.8549 -0.4620 0.2359 +vn 0.9032 -0.3551 0.2410 +vn 0.8563 -0.2046 0.4742 +vn 0.7665 -0.1541 0.6235 +vn 0.6973 0.1234 0.7061 +vn 0.5657 0.1953 0.8011 +vn 0.4530 0.3201 0.8321 +vn 0.5454 0.2777 0.7908 +vn 0.2905 -0.0159 0.9567 +vn 0.3685 0.0657 0.9273 +vn 0.2629 -0.1853 0.9468 +vn 0.2307 -0.3654 0.9018 +vn 0.2483 -0.1962 0.9486 +vn 0.2214 0.1543 0.9629 +vn 0.2592 0.0331 0.9653 +vn 0.3299 -0.1975 0.9231 +vn 0.4181 -0.3285 0.8469 +vn 0.2895 -0.3279 0.8993 +vn 0.1808 0.2740 0.9446 +vn 0.3186 0.3556 0.8787 +vn 0.4722 0.1985 0.8588 +vn 0.8680 -0.3654 0.3361 +vn 0.7955 -0.5326 0.2890 +vn 0.8017 -0.5294 0.2775 +vn 0.7829 -0.5224 0.3379 +vn 0.8036 -0.4086 0.4327 +vn 0.8089 -0.4103 0.4210 +vn 0.8465 -0.2836 0.4507 +vn 0.9242 -0.2472 0.2912 +vn 0.9382 0.0372 0.3440 +vn 0.8580 0.2577 0.4444 +vn 0.7382 0.4743 0.4797 +vn 0.6025 0.6791 0.4194 +vn 0.5298 0.7130 0.4592 +vn 0.4871 0.6660 0.5649 +vn 0.4622 0.7184 0.5199 +vn 0.5160 0.7296 0.4489 +vn 0.4573 0.7411 0.4916 +vn 0.7795 0.3667 0.5078 +vn 0.7934 0.2718 0.5447 +vn 0.8638 0.1350 0.4853 +vn 0.8933 0.0985 0.4385 +vn 0.7833 -0.2792 0.5554 +vn 0.8172 -0.1172 0.5643 +vn 0.8254 0.0235 0.5641 +vn 0.7987 0.1624 0.5794 +vn 0.6712 0.3874 0.6320 +vn 0.7452 -0.5936 0.3037 +vn 0.7657 -0.5648 0.3077 +vn 0.4895 -0.0186 0.8718 +vn 0.5566 -0.0445 0.8296 +vn 0.3725 0.1265 0.9194 +vn 0.2287 0.4294 0.8737 +vn 0.3515 0.4281 0.8326 +vn 0.5837 0.1736 0.7932 +vn 0.3175 0.1963 0.9277 +vn 0.6626 0.0814 0.7446 +vn 0.5868 -0.1235 0.8003 +vn 0.4686 -0.2032 0.8597 +vn 0.3937 -0.2413 0.8870 +vn 0.1881 0.0455 0.9811 +vn 0.1886 0.0452 0.9810 +vn 0.2169 0.2779 0.9358 +vn 0.7119 0.3180 0.6262 +vn 0.6580 0.3672 0.6574 +vn 0.5514 0.4026 0.7306 +vn 0.2307 0.3902 0.8914 +vn 0.5756 0.2005 0.7928 +vn 0.5068 0.2224 0.8329 +vn 0.3851 0.0468 0.9217 +vn 0.4913 -0.0055 0.8710 +vn 0.6598 -0.1560 0.7351 +vn 0.7055 -0.1733 0.6872 +vn 0.7748 -0.2443 0.5831 +vn 0.8110 -0.3643 0.4578 +vn 0.6963 -0.3177 0.6436 +vn 0.7822 -0.5150 0.3508 +vn 0.7537 -0.5808 0.3075 +vn 0.6890 -0.6294 0.3594 +vn 0.5860 -0.7050 0.3994 +vn 0.4904 -0.7878 0.3727 +vn 0.4661 -0.7030 0.5372 +vn 0.4337 -0.7388 0.5159 +vn 0.7348 -0.5074 0.4500 +vn 0.7027 -0.4013 0.5875 +vn 0.7337 -0.4578 0.5021 +vn 0.6014 -0.2462 0.7600 +vn 0.3210 -0.2560 0.9118 +vn 0.6053 -0.2929 0.7401 +vn 0.6135 -0.2212 0.7581 +vn 0.4505 -0.2271 0.8634 +vn 0.3760 0.0967 0.9216 +vn 0.4173 0.0706 0.9060 +vn 0.3350 0.0505 0.9409 +vn 0.4412 -0.0616 0.8953 +vn 0.4425 -0.2645 0.8569 +vn 0.2041 -0.6686 0.7150 +vn 0.5006 -0.0918 0.8608 +vn 0.4183 -0.4418 0.7936 +vn 0.3687 -0.0904 0.9251 +vn 0.5639 -0.0400 0.8249 +vn 0.5059 0.1080 0.8558 +vn 0.6039 -0.1233 0.7875 +vn 0.6379 -0.3752 0.6726 +vn 0.7293 -0.5871 0.3513 +vn 0.6853 -0.6816 0.2564 +vn 0.5695 -0.7884 0.2327 +vn 0.3542 -0.9131 0.2021 +vn 0.4065 -0.8772 0.2556 +vn 0.2278 -0.9244 0.3059 +vn 0.2272 -0.9638 0.1396 +vn 0.4206 -0.8920 0.1655 +vn 0.5239 -0.8212 0.2263 +vn 0.5741 -0.7862 0.2286 +vn 0.7217 -0.6168 0.3141 +vn 0.7489 -0.4802 0.4566 +vn 0.7068 -0.3179 0.6319 +vn 0.7445 -0.3623 0.5608 +vn 0.3618 -0.1551 0.9193 +vn 0.2575 -0.1178 0.9591 +vn 0.1901 -0.6852 0.7031 +vn 0.1686 0.1575 0.9730 +vn 0.3350 0.0815 0.9387 +vn 0.2490 -0.5414 0.8030 +vn 0.3318 -0.1262 0.9349 +vn 0.2676 0.0991 0.9584 +vn 0.1932 0.1337 0.9720 +vn 0.1068 -0.1926 0.9755 +vn 0.1578 -0.1244 0.9796 +vn 0.3311 -0.0543 0.9420 +vn 0.0969 -0.9586 0.2679 +vn 0.4379 0.1612 0.8844 +vn 0.8262 0.0466 0.5615 +vn 0.9220 -0.0213 0.3866 +vn 0.6878 0.2211 0.6914 +vn 0.7640 0.3452 0.5451 +vn 0.4696 0.5021 0.7263 +vn 0.5688 0.4979 0.6547 +vn 0.7427 0.4186 0.5227 +vn 0.7532 0.2125 0.6225 +vn 0.5073 -0.5840 0.6338 +vn 0.3092 0.4234 0.8516 +vn 0.9767 -0.0417 0.2104 +vn 0.9339 -0.0971 0.3441 +vn 0.9790 -0.0072 0.2039 +vn 0.9662 0.0784 0.2457 +vn 0.9407 0.1790 0.2881 +vn 0.8752 0.3212 0.3618 +vn 0.9950 -0.0078 0.0994 +vn 0.9953 0.0271 0.0925 +vn 0.6216 -0.0306 0.7827 +vn 0.1696 0.5848 -0.7932 +vn 0.2611 0.6558 -0.7083 +vn 0.0503 -0.9987 0.0078 +vn 0.0559 -0.9347 0.3511 +vn 0.0571 -0.7606 0.6467 +vn 0.0831 -0.5505 0.8307 +vn 0.2726 -0.4564 0.8470 +vn 0.0826 -0.1532 0.9847 +vn 0.2249 -0.5390 0.8117 +vn 0.9048 -0.0052 0.4258 +vn 0.6482 -0.5083 0.5670 +vn -0.1253 -0.8636 0.4884 +vn -0.4414 -0.4149 0.7956 +vn -0.8218 -0.0585 0.5668 +vn -0.4824 0.0619 0.8738 +vn -0.2619 -0.1524 0.9530 +vn -0.1339 -0.2089 0.9687 +vn -0.5821 -0.5555 0.5938 +vn -0.9850 -0.1318 0.1116 +vn -0.5435 0.8272 -0.1425 +vn -0.8351 0.2950 0.4643 +vn -0.9306 -0.3283 -0.1618 +vn -0.9225 -0.2680 0.2777 +vn -0.1020 -0.2511 0.9626 +vn -0.0684 -0.2845 -0.9562 +vn -0.0624 -0.3087 -0.9491 +vn -0.0857 -0.4224 -0.9023 +vn -0.1989 -0.4580 -0.8664 +vn -0.4458 -0.3968 -0.8024 +vn -0.5494 -0.2972 -0.7809 +vn -0.5940 -0.3200 -0.7380 +vn -0.6666 -0.3046 -0.6804 +vn -0.6770 -0.2027 -0.7075 +vn -0.7209 -0.2523 -0.6454 +vn -0.8929 -0.1175 -0.4347 +vn -0.9627 -0.0485 -0.2661 +vn -0.9524 0.0183 -0.3043 +vn -0.9857 -0.0058 -0.1684 +vn -0.9925 0.0288 -0.1186 +vn -0.9829 0.1815 -0.0323 +vn -0.7537 0.6571 -0.0105 +vn -0.6421 0.6481 -0.4094 +vn -0.5903 0.7999 -0.1087 +vn -0.9126 0.3318 -0.2390 +vn -0.9005 0.2578 -0.3502 +vn -0.9984 0.0053 0.0569 +vn -0.9792 -0.0305 0.2005 +vn -0.8444 -0.1183 0.5225 +vn -0.8759 -0.1482 0.4592 +vn -0.9509 -0.2168 0.2210 +vn -0.7588 -0.4016 0.5128 +vn -0.7689 -0.3173 0.5551 +vn -0.8975 -0.2911 0.3312 +vn -0.7916 0.0375 0.6099 +vn -0.9441 -0.0402 0.3273 +vn -0.7745 0.0430 0.6312 +vn -0.6571 0.3314 0.6770 +vn -0.7159 0.3537 0.6021 +vn -0.8103 0.4087 0.4200 +vn -0.7595 0.3318 0.5594 +vn -0.9901 -0.0759 -0.1185 +vn -0.8815 -0.4187 0.2185 +vn -0.8830 -0.4189 0.2116 +vn -0.7973 -0.4603 0.3905 +vn -0.9133 -0.3311 0.2370 +vn -0.9154 -0.2551 0.3114 +vn -0.9759 -0.2183 -0.0085 +vn -0.9276 0.0661 0.3676 +vn -0.8882 -0.2517 0.3845 +vn -0.8111 -0.3097 0.4961 +vn -0.8901 0.4038 0.2115 +vn -0.8058 0.3839 0.4509 +vn -0.9163 0.3572 0.1809 +vn -0.9559 0.2932 -0.0154 +vn -0.9875 0.1250 -0.0957 +vn -0.9627 0.0927 -0.2541 +vn -0.9054 0.0917 -0.4145 +vn -0.8280 0.0254 -0.5601 +vn -0.6335 -0.1045 -0.7666 +vn -0.5535 -0.1600 -0.8174 +vn -0.5635 -0.2392 -0.7907 +vn -0.3979 -0.3147 -0.8618 +vn -0.4469 -0.3475 -0.8243 +vn -0.1074 -0.4895 -0.8653 +vn -0.0064 -0.4662 -0.8846 +vn 0.0163 0.2921 -0.9562 +vn 0.0325 0.5140 -0.8572 +vn 0.0015 0.6272 -0.7789 +vn -0.0071 0.7039 -0.7103 +vn -0.0279 0.9768 -0.2124 +vn -0.0211 0.9977 -0.0650 +vn -0.0347 0.9968 0.0723 +vn -0.0539 0.9818 0.1820 +vn -0.0482 0.9607 0.2735 +vn -0.0087 0.9333 0.3589 +vn -0.0101 0.3523 0.9358 +vn -0.0396 0.1566 0.9869 +vn -0.1482 0.6001 0.7861 +vn -0.2960 0.3917 0.8712 +vn -0.2544 -0.0883 0.9631 +vn -0.3743 -0.6273 0.6829 +vn -0.3848 -0.7936 0.4713 +vn -0.1020 -0.9072 0.4081 +vn -0.3311 -0.7628 0.5554 +vn 0.0591 -0.5706 0.8191 +vn 0.1012 -0.1196 0.9877 +vn -0.0699 -0.1366 0.9882 +vn -0.1052 -0.4203 0.9013 +vn -0.1708 0.0129 0.9852 +vn -0.5980 -0.2993 0.7435 +vn -0.6230 -0.3152 0.7159 +vn -0.3793 -0.6989 0.6064 +vn -0.6833 -0.5455 0.4852 +vn -0.4508 -0.8017 0.3924 +vn -0.1015 -0.9860 0.1323 +vn -0.0176 -0.9945 0.1035 +vn -0.1544 -0.8951 0.4182 +vn -0.1114 -0.8491 0.5164 +vn -0.2468 -0.7607 0.6004 +vn -0.1087 -0.7330 0.6715 +vn -0.2251 -0.6277 0.7452 +vn -0.3388 -0.4940 0.8007 +vn -0.4713 -0.4248 0.7730 +vn -0.6300 -0.3703 0.6826 +vn -0.7786 0.0099 0.6275 +vn -0.7901 0.0910 0.6062 +vn -0.6988 0.2233 0.6796 +vn -0.6985 0.2126 0.6833 +vn -0.5598 0.3608 0.7460 +vn -0.5765 0.4417 0.6874 +vn -0.4906 0.5685 0.6604 +vn -0.5178 0.5660 0.6415 +vn -0.4234 0.6256 0.6552 +vn -0.5581 0.5963 0.5771 +vn -0.5866 0.6719 0.4522 +vn -0.6219 0.6913 0.3678 +vn -0.8150 0.4221 0.3970 +vn -0.9025 -0.1289 0.4110 +vn -0.8334 0.4917 0.2525 +vn -0.9387 0.1686 0.3008 +vn -0.9369 -0.3096 0.1624 +vn -0.7584 -0.6505 0.0418 +vn -0.3588 -0.9299 0.0810 +vn -0.2333 -0.9724 0.0054 +vn -0.1135 -0.9919 -0.0569 +vn -0.0576 -0.9968 -0.0552 +vn -0.1256 -0.9920 0.0125 +vn -0.2369 -0.9705 0.0448 +vn -0.2436 -0.9576 0.1539 +vn -0.4311 -0.8856 0.1727 +vn -0.8135 -0.4733 0.3379 +vn -0.4742 -0.8114 0.3417 +vn -0.7729 -0.3935 0.4977 +vn -0.7134 0.3499 0.6071 +vn -0.7860 0.3245 0.5263 +vn -0.5050 0.5975 0.6229 +vn -0.3524 0.5380 0.7658 +vn -0.3049 0.4805 0.8223 +vn -0.4121 0.5570 0.7211 +vn -0.5512 0.2793 0.7863 +vn -0.6831 0.0200 0.7300 +vn -0.1994 -0.3971 0.8959 +vn -0.2729 -0.5585 0.7833 +vn -0.1388 -0.6199 0.7723 +vn -0.1048 -0.4670 0.8780 +vn -0.0259 -0.3142 0.9490 +vn -0.0117 -0.3732 0.9277 +vn -0.0789 -0.2040 0.9758 +vn -0.0450 -0.9801 0.1936 +vn -0.0851 -0.8857 0.4564 +vn -0.0938 -0.9917 0.0874 +vn -0.0727 -0.9918 0.1051 +vn -0.0799 -0.9884 0.1294 +vn -0.1259 -0.9802 0.1526 +vn -0.1759 -0.9782 0.1102 +vn -0.1873 -0.9632 0.1930 +vn -0.2781 -0.9403 0.1964 +vn -0.1891 -0.9608 0.2026 +vn -0.3290 -0.9101 0.2518 +vn -0.3795 -0.8632 0.3330 +vn -0.5505 -0.0043 0.8348 +vn -0.4506 0.2920 0.8436 +vn -0.3043 0.3596 0.8821 +vn -0.2689 0.2918 0.9179 +vn -0.4034 0.3126 0.8600 +vn -0.4012 0.1543 0.9029 +vn -0.4986 0.2398 0.8330 +vn -0.5433 -0.0617 0.8373 +vn -0.6201 -0.0041 0.7845 +vn -0.4356 -0.2320 0.8697 +vn -0.3350 -0.2966 0.8943 +vn -0.0810 -0.4454 0.8917 +vn -0.0645 -0.3332 0.9407 +vn -0.3758 -0.1275 0.9179 +vn -0.4062 -0.0125 0.9137 +vn -0.2226 0.1642 0.9610 +vn -0.1821 0.0824 0.9798 +vn -0.1614 -0.3098 0.9370 +vn -0.2618 0.1591 0.9519 +vn -0.1965 -0.7284 0.6564 +vn -0.3052 -0.2711 0.9129 +vn -0.2578 -0.9507 0.1725 +vn -0.1476 -0.9737 0.1733 +vn -0.1356 -0.9841 0.1143 +vn -0.1026 -0.9841 0.1452 +vn -0.0896 -0.9959 -0.0118 +vn -0.1274 -0.9209 0.3683 +vn -0.1276 -0.6743 0.7273 +vn -0.1312 -0.6630 0.7371 +vn -0.1580 -0.2656 0.9511 +vn -0.0820 -0.1364 0.9872 +vn -0.1739 0.0716 0.9822 +vn -0.1920 -0.0712 0.9788 +vn -0.2069 -0.1585 0.9654 +vn -0.0785 -0.2017 0.9763 +vn -0.0476 -0.9989 0.0041 +vn -0.1000 -0.9861 0.1324 +vn -0.0728 -0.9951 0.0673 +vn -0.1033 -0.9848 0.1393 +vn -0.0377 -0.9978 0.0542 +vn -0.0006 -0.8433 0.5375 +vn -0.0249 -0.3873 0.9216 +vn -0.0702 -0.3526 0.9332 +vn -0.0652 -0.8892 0.4528 +vn -0.0001 -0.9996 0.0288 +vn -0.0017 -0.9996 0.0287 +vn 0.0024 -0.4189 -0.9080 +vn -0.1479 0.5613 -0.8143 +vn -0.1674 0.5649 -0.8080 +vn -0.2710 0.6403 -0.7187 +vn -0.3109 0.6392 -0.7034 +vn -0.4445 0.6718 -0.5926 +vn -0.3636 0.5937 -0.7179 +vn -0.3844 0.6452 -0.6602 +vn -0.5313 0.6355 -0.5603 +vn -0.5389 0.7822 -0.3126 +vn -0.4680 0.7664 -0.4399 +vn -0.4815 0.8029 -0.3514 +vn -0.4236 0.7766 -0.4663 +vn -0.3684 0.7189 -0.5895 +vn -0.3932 0.6457 -0.6546 +vn -0.4041 0.6436 -0.6500 +vn -0.5049 0.5140 -0.6934 +vn -0.3340 0.4831 -0.8093 +vn -0.3203 0.5391 -0.7790 +vn -0.3341 0.5347 -0.7762 +vn -0.1926 0.4973 -0.8459 +vn -0.1103 0.5198 -0.8471 +vn -0.0814 0.5046 -0.8595 +vn -0.0475 0.2770 -0.9597 +vn -0.0315 0.0524 -0.9981 +vn -0.1522 0.3904 -0.9080 +vn -0.1003 0.3086 -0.9459 +vn -0.0728 0.2075 -0.9755 +vn -0.0725 0.1155 -0.9907 +vn -0.0958 -0.1812 -0.9788 +vn -0.0850 -0.1511 -0.9849 +vn -0.1396 -0.3922 -0.9092 +vn -0.2009 -0.4524 -0.8689 +vn -0.3252 -0.4495 -0.8320 +vn -0.3235 -0.4492 -0.8328 +vn -0.5305 -0.3857 -0.7549 +vn -0.6905 -0.3458 -0.6353 +vn -0.9239 -0.0968 -0.3701 +vn -0.9773 -0.0236 -0.2107 +vn -0.9967 0.0359 -0.0722 +vn -0.9032 0.4021 -0.1504 +vn -0.7840 0.3301 -0.5257 +vn -0.5010 0.8279 -0.2520 +vn -0.3347 0.5299 -0.7793 +vn -0.3181 0.2851 -0.9042 +vn -0.9337 0.0516 -0.3542 +vn -0.8047 0.0035 0.5937 +vn -0.8064 -0.2791 0.5214 +vn -0.6239 -0.0144 0.7813 +vn -0.5837 0.2294 0.7789 +vn -0.7816 0.0459 0.6221 +vn -0.8096 0.1244 0.5737 +vn -0.7038 0.2324 0.6714 +vn -0.8435 0.3069 0.4409 +vn -0.9896 -0.0556 -0.1330 +vn -0.9955 -0.0676 -0.0667 +vn -0.9853 -0.1304 -0.1101 +vn -0.9502 -0.3115 0.0014 +vn -0.9652 -0.2275 0.1293 +vn -0.9272 -0.3142 0.2038 +vn -0.9570 0.1315 0.2585 +vn -0.9984 0.0499 0.0280 +vn -0.9950 0.0997 -0.0085 +vn -0.9790 0.1650 -0.1197 +vn -0.9644 0.1520 -0.2162 +vn -0.9099 0.1930 -0.3673 +vn -0.9184 0.1418 -0.3695 +vn -0.8213 0.2185 -0.5271 +vn -0.8222 0.1279 -0.5546 +vn -0.6758 0.0533 -0.7352 +vn -0.6593 -0.0559 -0.7498 +vn -0.5280 -0.0284 -0.8488 +vn -0.5658 -0.1407 -0.8125 +vn -0.4010 -0.1096 -0.9095 +vn -0.4110 -0.2492 -0.8769 +vn -0.2923 -0.2205 -0.9306 +vn -0.2632 -0.3708 -0.8907 +vn -0.2448 -0.2391 -0.9396 +vn -0.3000 -0.1163 -0.9468 +vn -0.2584 -0.0256 -0.9657 +vn -0.4406 -0.0572 -0.8959 +vn -0.4766 0.0721 -0.8762 +vn -0.6026 0.1557 -0.7827 +vn -0.7372 0.2856 -0.6124 +vn -0.8461 0.2398 -0.4761 +vn -0.9215 0.2648 -0.2842 +vn -0.9654 0.2043 -0.1619 +vn -0.9886 0.0144 0.1502 +vn -0.9862 0.1235 0.1107 +vn -0.9856 0.1689 0.0109 +vn -0.9697 0.2258 -0.0929 +vn -0.9536 0.2587 -0.1539 +vn -0.9580 0.2471 -0.1455 +vn -0.9150 0.3111 -0.2571 +vn -0.8605 0.3960 -0.3206 +vn -0.8425 0.3467 -0.4123 +vn -0.7774 0.4073 -0.4793 +vn -0.6605 0.3421 -0.6684 +vn -0.5971 0.2484 -0.7627 +vn -0.4795 0.1254 -0.8685 +vn -0.3064 0.0280 -0.9515 +vn -0.3765 0.0345 -0.9258 +vn -0.1525 -0.0159 -0.9882 +vn -0.0734 -0.0210 -0.9971 +vn -0.0731 0.0582 -0.9956 +vn -0.0892 0.0658 -0.9938 +vn -0.2392 0.1038 -0.9654 +vn -0.3322 0.1112 -0.9366 +vn -0.3865 0.2267 -0.8940 +vn -0.5184 0.2280 -0.8242 +vn -0.5283 0.3697 -0.7644 +vn -0.6006 0.4311 -0.6734 +vn -0.6996 0.4261 -0.5736 +vn -0.7818 0.4690 -0.4109 +vn -0.8207 0.4790 -0.3116 +vn -0.9133 0.3717 -0.1664 +vn -0.9753 0.1602 0.1518 +vn -0.9637 0.1454 0.2240 +vn -0.9738 0.1026 0.2029 +vn -0.9578 0.1494 0.2456 +vn -0.9653 0.1736 0.1950 +vn -0.9787 0.1701 0.1146 +vn -0.9596 0.2794 -0.0327 +vn -0.9195 0.3766 -0.1126 +vn -0.8231 0.4939 -0.2804 +vn -0.7773 0.5062 -0.3736 +vn -0.6620 0.5787 -0.4763 +vn -0.6975 0.4995 -0.5139 +vn -0.5807 0.5247 -0.6225 +vn -0.5122 0.5715 -0.6411 +vn -0.5255 0.4298 -0.7342 +vn -0.3584 0.4937 -0.7924 +vn -0.3308 0.4318 -0.8391 +vn -0.4905 0.3536 -0.7965 +vn -0.1207 0.3014 -0.9458 +vn -0.1505 0.2722 -0.9504 +vn -0.0258 0.1964 -0.9802 +vn -0.1415 0.3903 -0.9098 +vn -0.2178 0.5945 -0.7740 +vn -0.3532 0.6259 -0.6953 +vn -0.4498 0.5761 -0.6825 +vn -0.5214 0.6586 -0.5426 +vn -0.5101 0.7271 -0.4595 +vn -0.6600 0.6436 -0.3876 +vn -0.7578 0.6172 -0.2118 +vn -0.8323 0.5306 -0.1604 +vn -0.8530 0.5139 -0.0908 +vn -0.8470 0.5235 -0.0925 +vn -0.9237 0.3829 -0.0111 +vn -0.9256 0.3708 0.0761 +vn -0.9476 0.3070 0.0885 +vn -0.9519 0.2770 0.1314 +vn -0.9643 0.2208 0.1463 +vn -0.9452 0.1804 0.2723 +vn -0.9477 0.1950 0.2526 +vn -0.9339 0.2776 0.2253 +vn -0.9261 0.3154 0.2072 +vn -0.8320 0.5507 0.0664 +vn -0.7215 0.6755 -0.1517 +vn -0.6111 0.7371 -0.2886 +vn -0.4731 0.7999 -0.3693 +vn -0.4273 0.8289 -0.3610 +vn -0.3608 0.7767 -0.5162 +vn -0.3523 0.7013 -0.6197 +vn -0.2282 0.7356 -0.6378 +vn -0.2127 0.6496 -0.7299 +vn -0.1021 0.7675 -0.6329 +vn -0.2618 0.8758 -0.4054 +vn -0.2957 0.8702 -0.3941 +vn -0.3309 0.8959 -0.2965 +vn -0.3578 0.8393 -0.4093 +vn -0.3991 0.8689 -0.2927 +vn -0.4264 0.8719 -0.2410 +vn -0.5193 0.8482 -0.1042 +vn -0.6776 0.7334 0.0551 +vn -0.7191 0.6928 0.0545 +vn -0.7611 0.6262 0.1690 +vn -0.8317 0.5316 0.1602 +vn -0.9166 0.2762 0.2889 +vn -0.8374 0.4688 0.2810 +vn -0.8731 0.4013 0.2767 +vn -0.8049 0.5108 0.3019 +vn -0.7306 0.6515 0.2046 +vn -0.6296 0.7650 0.1356 +vn -0.5323 0.8358 0.1345 +vn -0.5063 0.8623 0.0138 +vn -0.4139 0.9103 0.0109 +vn -0.3801 0.9231 -0.0585 +vn -0.2872 0.9242 -0.2516 +vn -0.2331 0.9364 -0.2622 +vn -0.1095 0.8661 -0.4877 +vn -0.0865 0.9230 -0.3750 +vn -0.2034 0.9525 -0.2268 +vn -0.2288 0.9618 -0.1504 +vn -0.2344 0.9702 -0.0621 +vn -0.3160 0.9470 -0.0573 +vn -0.3003 0.9523 0.0544 +vn -0.3837 0.9218 0.0553 +vn -0.4119 0.9042 0.1130 +vn -0.4708 0.8560 0.2135 +vn -0.6959 0.6494 0.3067 +vn -0.7433 0.6111 0.2722 +vn -0.8118 0.4510 0.3709 +vn -0.8104 0.3757 0.4496 +vn -0.8677 0.3122 0.3868 +vn -0.8852 0.2697 0.3790 +vn -0.9507 -0.1989 0.2378 +vn -0.8255 -0.5628 0.0426 +vn -0.6917 -0.6550 -0.3042 +vn -0.7247 -0.6226 -0.2952 +vn -0.5163 -0.6397 -0.5694 +vn -0.3193 -0.6533 -0.6865 +vn -0.4301 -0.7723 -0.4675 +vn -0.8030 -0.5545 -0.2185 +vn -0.7530 -0.3746 -0.5410 +vn -0.6449 -0.0538 0.7624 +vn -0.8126 -0.0012 0.5829 +vn -0.9192 -0.2148 0.3301 +vn -0.8634 -0.0230 0.5039 +vn -0.8866 -0.0298 0.4616 +vn -0.9080 -0.0111 0.4189 +vn -0.9005 -0.0111 0.4346 +vn -0.9908 -0.1313 -0.0343 +vn -0.9730 -0.2292 0.0257 +vn -0.8391 -0.3843 0.3850 +vn -0.6381 -0.7699 -0.0114 +vn -0.4400 -0.7663 -0.4682 +vn -0.5474 -0.8060 -0.2252 +vn -0.6629 -0.7154 -0.2207 +vn -0.7749 -0.6217 -0.1144 +vn -0.8458 -0.4740 0.2450 +vn -0.8122 -0.5430 -0.2133 +vn -0.9256 -0.3741 0.0577 +vn -0.9986 -0.0335 0.0401 +vn -0.8152 -0.1968 -0.5448 +vn -0.9915 -0.1054 -0.0757 +vn -0.5931 -0.2234 -0.7735 +vn -0.2309 -0.4165 -0.8793 +vn -0.3622 -0.6012 -0.7123 +vn -0.6113 -0.5801 -0.5383 +vn -0.5117 -0.6416 -0.5714 +vn -0.7279 -0.5677 -0.3844 +vn -0.8858 -0.2999 -0.3541 +vn -0.8864 -0.1575 -0.4354 +vn -0.7629 -0.1558 -0.6275 +vn -0.8304 0.0377 -0.5559 +vn -0.7391 0.1315 -0.6607 +vn -0.6164 0.2018 -0.7612 +vn -0.5362 0.1193 -0.8356 +vn -0.4344 0.1682 -0.8849 +vn -0.2459 0.3168 -0.9161 +vn -0.2595 0.3758 -0.8896 +vn -0.1546 0.3999 -0.9034 +vn -0.1591 0.3366 -0.9281 +vn -0.1427 0.2070 -0.9679 +vn -0.0757 0.0248 -0.9968 +vn -0.1455 -0.2384 -0.9602 +vn -0.1943 -0.2861 -0.9383 +vn -0.2439 -0.3073 -0.9198 +vn -0.1838 -0.4264 -0.8857 +vn -0.4184 -0.4467 -0.7908 +vn -0.7258 -0.4139 -0.5495 +vn -0.7895 -0.3496 -0.5044 +vn -0.8298 -0.3232 -0.4549 +vn -0.8485 -0.2718 -0.4540 +vn -0.8880 -0.2152 -0.4063 +vn -0.9612 -0.1120 -0.2521 +vn -0.9580 -0.1003 -0.2687 +vn -0.9408 -0.0125 -0.3386 +vn -0.9085 0.1376 -0.3946 +vn -0.6054 0.2613 -0.7518 +vn -0.4098 0.0617 -0.9101 +vn -0.2165 0.2840 -0.9341 +vn -0.1566 0.0599 -0.9858 +vn -0.5801 0.0434 -0.8134 +vn -0.3983 -0.0075 -0.9172 +vn -0.9286 -0.0483 -0.3680 +vn -0.2065 -0.1543 -0.9662 +vn -0.5530 -0.1257 -0.8236 +vn -0.5066 -0.3913 -0.7683 +vn -0.8736 -0.2736 -0.4024 +vn -0.9255 -0.3304 -0.1854 +vn -0.9220 -0.3386 -0.1878 +vn -0.9002 -0.4022 -0.1668 +vn -0.8471 -0.4056 -0.3435 +vn -0.8458 -0.4033 -0.3493 +vn -0.8515 -0.3895 -0.3511 +vn -0.8192 -0.3526 -0.4523 +vn -0.7523 -0.2976 -0.5878 +vn -0.5911 -0.2785 -0.7570 +vn -0.5688 0.0676 -0.8197 +vn -0.3416 0.1911 -0.9202 +vn -0.1689 0.2570 -0.9515 +vn -0.1867 0.1210 -0.9749 +vn -0.0742 0.1134 -0.9908 +vn -0.2142 0.0413 -0.9759 +vn -0.3945 -0.1067 -0.9127 +vn -0.3781 -0.1215 -0.9177 +vn -0.2361 -0.2681 -0.9340 +vn -0.5586 -0.4219 -0.7141 +vn -0.8080 -0.4140 -0.4192 +vn -0.7710 -0.3992 -0.4962 +vn -0.9141 -0.2579 -0.3128 +vn -0.9506 -0.2158 -0.2230 +vn -0.7280 0.0378 -0.6846 +vn -0.4924 -0.0084 -0.8703 +vn -0.7713 -0.0965 -0.6292 +vn -0.6939 -0.3802 -0.6115 +vn -0.8752 -0.0218 -0.4833 +vn -0.4221 0.4133 -0.8068 +vn -0.5685 0.3418 -0.7483 +vn -0.6584 0.2345 -0.7152 +vn -0.8585 0.2580 -0.4432 +vn -0.8843 -0.0696 -0.4617 +vn -0.9490 -0.0768 -0.3057 +vn -0.9189 -0.1696 -0.3563 +vn -0.9305 -0.2557 -0.2623 +vn -0.9409 -0.2533 -0.2249 +vn -0.9547 -0.2653 -0.1346 +vn -0.9303 -0.3569 -0.0843 +vn -0.9269 -0.3631 -0.0946 +vn -0.8601 -0.4829 0.1644 +vn -0.9826 -0.0750 0.1702 +vn -0.9812 -0.0492 0.1866 +vn -0.9594 0.0759 0.2717 +vn -0.9434 0.1183 0.3098 +vn -0.8789 0.2545 0.4035 +vn -0.7928 0.3789 0.4774 +vn -0.6764 0.5251 0.5165 +vn -0.6906 0.5751 0.4385 +vn -0.6872 0.6327 0.3570 +vn -0.5464 0.7407 0.3909 +vn -0.4662 0.8405 0.2761 +vn -0.4018 0.8632 0.3057 +vn -0.3355 0.9083 0.2500 +vn -0.2927 0.9452 0.1445 +vn -0.2631 0.9504 0.1659 +vn -0.2241 0.9725 0.0640 +vn -0.1227 0.9902 0.0667 +vn -0.1185 0.9761 0.1820 +vn -0.1578 0.9695 0.1875 +vn -0.2080 0.9652 0.1583 +vn -0.2649 0.9302 0.2539 +vn -0.2637 0.9303 0.2548 +vn -0.3952 0.8342 0.3845 +vn -0.5746 0.6625 0.4805 +vn -0.4364 0.7182 0.5420 +vn -0.5404 0.6612 0.5204 +vn -0.5730 0.5929 0.5658 +vn -0.4392 0.6313 0.6392 +vn -0.3977 0.7005 0.5926 +vn -0.5425 0.5749 0.6125 +vn -0.5645 0.5265 0.6358 +vn -0.6306 0.4205 0.6523 +vn -0.7225 0.3792 0.5781 +vn -0.8051 0.2438 0.5408 +vn -0.9336 0.1039 0.3429 +vn -0.9343 0.0677 0.3501 +vn -0.9712 0.0062 0.2383 +vn -0.9417 -0.3287 0.0718 +vn -0.9418 -0.3280 0.0736 +vn -0.9417 -0.3281 0.0747 +vn -0.9376 -0.3458 0.0375 +vn -0.9283 -0.3681 -0.0525 +vn -0.9591 -0.2804 -0.0378 +vn -0.9608 -0.2720 -0.0536 +vn -0.9573 -0.2597 -0.1272 +vn -0.9553 -0.1752 -0.2383 +vn -0.9851 0.0114 -0.1714 +vn -0.9382 -0.0178 -0.3456 +vn -0.8576 0.2590 -0.4443 +vn -0.7989 0.3700 -0.4741 +vn -0.7284 0.3816 -0.5690 +vn -0.6354 0.5551 -0.5368 +vn -0.7420 0.4833 -0.4646 +vn -0.6785 0.6633 -0.3158 +vn -0.8775 0.4276 -0.2173 +vn -0.8902 0.3505 -0.2912 +vn -0.8972 0.3152 -0.3094 +vn -0.9449 0.2851 -0.1612 +vn -0.9353 0.2225 -0.2750 +vn -0.9760 0.1575 -0.1501 +vn -0.9891 -0.0420 -0.1410 +vn -0.9687 -0.2043 -0.1411 +vn -0.9450 -0.3208 0.0637 +vn -0.9449 -0.3211 0.0641 +vn -0.9402 -0.3263 0.0975 +vn -0.9419 -0.3144 0.1178 +vn -0.9749 -0.0227 0.2214 +vn -0.8898 0.1083 0.4433 +vn -0.8491 0.1263 0.5129 +vn -0.7627 0.2059 0.6131 +vn -0.7305 0.2149 0.6482 +vn -0.6044 0.3690 0.7061 +vn -0.5364 0.3613 0.7627 +vn -0.4028 0.5439 0.7362 +vn -0.2798 0.6400 0.7156 +vn -0.2839 0.7442 0.6046 +vn -0.3245 0.8135 0.4826 +vn -0.2231 0.8996 0.3755 +vn -0.1400 0.9415 0.3066 +vn -0.1086 0.9435 0.3132 +vn -0.0612 0.9594 0.2755 +vn -0.0734 0.9304 0.3592 +vn -0.2208 0.8422 0.4919 +vn -0.2120 0.7601 0.6142 +vn -0.1887 0.6536 0.7330 +vn -0.2846 0.4960 0.8204 +vn -0.3471 0.4952 0.7964 +vn -0.4084 0.2795 0.8690 +vn -0.6389 0.1756 0.7489 +vn -0.7999 0.1517 0.5807 +vn -0.7719 0.0128 0.6356 +vn -0.8328 0.0353 0.5524 +vn -0.8613 0.0213 0.5077 +vn -0.9003 0.0251 0.4346 +vn -0.9331 0.0459 0.3567 +vn -0.9651 -0.1665 0.2023 +vn -0.9704 -0.2414 0.0112 +vn -0.9719 0.2016 -0.1215 +vn -0.9397 0.3088 -0.1469 +vn -0.8938 0.4142 -0.1719 +vn -0.8709 0.4687 -0.1479 +vn -0.7436 0.6231 -0.2424 +vn -0.6767 0.7054 -0.2108 +vn -0.5975 0.7708 -0.2210 +vn -0.5580 0.7842 -0.2713 +vn -0.4325 0.8031 -0.4099 +vn -0.5321 0.7092 -0.4625 +vn -0.5733 0.7986 -0.1832 +vn -0.7083 0.6253 -0.3277 +vn -0.6605 0.6180 -0.4264 +vn -0.8131 0.3120 -0.4914 +vn -0.6570 0.2455 -0.7128 +vn -0.6166 0.2529 -0.7455 +vn -0.5868 0.0711 -0.8066 +vn -0.4396 0.1609 -0.8837 +vn -0.3794 -0.5002 -0.7784 +vn -0.6396 -0.3727 -0.6723 +vn -0.6554 -0.4567 -0.6016 +vn -0.4437 -0.8787 -0.1763 +vn -0.8721 -0.0124 -0.4891 +vn -0.9138 0.2481 -0.3215 +vn -0.8001 0.5186 -0.3015 +vn -0.8473 0.5042 -0.1668 +vn -0.7683 0.6394 0.0297 +vn -0.6943 0.7016 -0.1602 +vn -0.5739 0.8163 -0.0653 +vn -0.5258 0.8466 -0.0827 +vn -0.6009 0.7992 -0.0094 +vn -0.5993 0.7904 -0.1269 +vn -0.7046 0.7027 -0.0989 +vn -0.8162 0.5772 -0.0264 +vn -0.9402 0.3154 0.1288 +vn -0.9745 0.2243 -0.0034 +vn -0.9975 -0.0617 0.0335 +vn -0.9603 -0.2509 0.1216 +vn -0.9510 -0.2479 0.1849 +vn -0.9388 -0.3163 0.1365 +vn -0.9202 -0.3607 0.1523 +vn -0.9296 -0.3327 0.1585 +vn -0.9428 -0.1761 0.2831 +vn -0.9398 -0.0522 0.3376 +vn -0.8426 0.1024 0.5286 +vn -0.7251 0.1854 0.6632 +vn -0.8128 -0.0079 0.5824 +vn -0.7463 -0.1001 0.6580 +vn -0.7810 -0.1137 0.6140 +vn -0.5371 -0.1441 0.8311 +vn -0.6353 -0.1011 0.7656 +vn -0.5138 -0.0860 0.8536 +vn -0.6371 0.1097 0.7629 +vn -0.4047 0.2407 0.8822 +vn -0.3183 0.2133 0.9237 +vn -0.2877 0.2673 0.9197 +vn -0.3845 0.2894 0.8766 +vn -0.2725 0.3743 0.8864 +vn -0.1711 0.3691 0.9135 +vn -0.1122 0.4634 0.8790 +vn -0.0620 0.5855 0.8083 +vn -0.0506 0.6546 0.7543 +vn -0.0844 0.7151 0.6939 +vn -0.0726 0.7756 0.6271 +vn -0.0873 0.8385 0.5379 +vn -0.0803 0.8861 0.4566 +vn -0.0437 0.4662 0.8836 +vn -0.1135 0.3163 0.9418 +vn -0.1403 0.3207 0.9367 +vn -0.2458 0.1881 0.9509 +vn -0.3788 -0.2135 0.9005 +vn -0.4457 -0.1306 0.8856 +vn -0.3056 -0.2960 0.9050 +vn -0.4910 -0.2091 0.8457 +vn -0.4222 0.0026 0.9065 +vn -0.8686 -0.1904 0.4574 +vn -0.9112 -0.3216 0.2574 +vn -0.9178 -0.3090 0.2492 +vn -0.9174 -0.3093 0.2505 +vn -0.8982 -0.3885 0.2057 +vn -0.8912 -0.3995 0.2147 +vn -0.8788 -0.3946 0.2682 +vn -0.9397 -0.2575 0.2248 +vn -0.9443 -0.2601 0.2016 +vn -0.9789 -0.1078 0.1737 +vn -0.9650 0.0728 0.2520 +vn -0.9539 0.2105 0.2141 +vn -0.9216 0.3257 0.2110 +vn -0.8419 0.4450 0.3053 +vn -0.7716 0.5810 0.2590 +vn -0.7172 0.6822 0.1426 +vn -0.7070 0.7071 0.0132 +vn -0.6354 0.7636 0.1149 +vn -0.6024 0.7957 0.0627 +vn -0.5796 0.7893 0.2024 +vn -0.5454 0.8374 0.0364 +vn -0.6465 0.7309 0.2187 +vn -0.5942 0.7969 0.1087 +vn -0.7679 0.6397 0.0333 +vn -0.9858 -0.0295 0.1652 +vn -0.9056 -0.4224 0.0377 +vn -0.9656 0.2030 -0.1623 +vn -0.9748 -0.1875 -0.1208 +vn -0.7027 -0.7109 -0.0296 +vn -0.4576 -0.8880 -0.0463 +vn -0.1390 -0.9869 -0.0820 +vn -0.0440 -0.9985 -0.0337 +vn -0.0898 -0.9752 0.2025 +vn 0.0028 -0.9436 0.3311 +vn 0.0292 -0.6754 0.7369 +vn -0.0115 -0.9933 0.1150 +vn -0.0353 -0.9944 0.0995 +vn -0.1739 -0.7507 -0.6374 +vn -0.1941 -0.3552 -0.9144 +vn -0.1527 -0.0399 -0.9875 +vn -0.2012 -0.3155 -0.9274 +vn -0.2119 0.2042 -0.9557 +vn -0.3784 0.0624 -0.9235 +vn -0.3531 0.4496 -0.8205 +vn -0.4216 0.4318 -0.7974 +vn -0.4958 0.4604 -0.7364 +vn -0.1969 0.4015 -0.8944 +vn -0.1064 -0.1287 -0.9860 +vn -0.1081 -0.3852 -0.9165 +vn -0.0199 -0.9947 -0.1013 +vn -0.0371 -0.9981 0.0488 +vn -0.0494 -0.9718 0.2307 +vn -0.0080 -0.9784 0.2064 +vn -0.0192 -0.9863 0.1636 +vn -0.0362 -0.9985 0.0415 +vn -0.0126 -0.9997 -0.0194 +vn -0.0084 -0.9889 -0.1481 +vn -0.0170 -0.9974 -0.0699 +vn -0.0075 -0.9862 -0.1653 +vn -0.0151 -0.9771 -0.2121 +vn -0.0350 -0.3508 -0.9358 +vn -0.0713 0.0288 -0.9970 +vn -0.0930 0.2268 -0.9695 +vn -0.1770 0.3979 -0.9002 +vn -0.2218 0.5182 -0.8260 +vn -0.0986 0.3768 -0.9210 +vn 0.0562 -0.6397 -0.7666 +vn -0.0125 -0.9997 0.0218 +vn -0.0279 -0.9987 0.0424 +vn -0.0053 -0.9992 0.0407 +vn -0.0066 -0.9980 -0.0621 +vn 0.0318 -0.3954 -0.9180 +vn -0.0247 -0.0537 -0.9983 +vn -0.6406 0.6812 0.3545 +vn -0.5053 0.7438 0.4375 +vn -0.5376 0.7752 0.3318 +vn -0.6166 0.7521 0.2327 +vn -0.6958 0.6007 0.3939 +vn -0.6715 0.7130 0.2016 +vn -0.6632 0.6090 0.4350 +vn -0.7836 0.4899 0.3821 +vn -0.7377 0.4887 0.4659 +vn -0.8449 0.3677 0.3885 +vn -0.9332 0.2510 0.2570 +vn -0.9278 0.0412 0.3707 +vn -0.9570 -0.1328 0.2578 +vn -0.8925 -0.2972 0.3393 +vn -0.8935 -0.3651 0.2613 +vn -0.8569 -0.4376 0.2724 +vn -0.8459 -0.4752 0.2419 +vn -0.8571 -0.4554 0.2408 +vn -0.8576 -0.4338 0.2763 +vn -0.9021 -0.3671 0.2269 +vn -0.9015 -0.3040 0.3080 +vn -0.8732 -0.3294 0.3593 +vn -0.8563 -0.2047 0.4742 +vn -0.7666 -0.1545 0.6233 +vn -0.6972 0.1234 0.7061 +vn -0.4541 0.3203 0.8314 +vn -0.4530 0.3201 0.8321 +vn -0.5454 0.2777 0.7908 +vn -0.3648 0.2346 0.9010 +vn -0.3685 0.0656 0.9273 +vn -0.2126 -0.2579 0.9425 +vn -0.2307 -0.3654 0.9018 +vn -0.2364 0.0551 0.9701 +vn -0.2543 0.0585 0.9654 +vn -0.1890 0.2093 0.9594 +vn -0.0912 0.2473 0.9646 +vn -0.0602 0.2400 0.9689 +vn -0.0827 0.1547 0.9845 +vn -0.1632 0.1588 0.9737 +vn -0.4180 -0.3286 0.8469 +vn -0.2896 -0.3279 0.8992 +vn -0.3444 -0.0470 0.9376 +vn -0.1772 0.2669 0.9473 +vn -0.2240 0.0060 0.9746 +vn -0.3186 0.3556 0.8786 +vn -0.3694 0.1413 0.9185 +vn -0.4723 0.1985 0.8588 +vn -0.6039 -0.0037 0.7970 +vn -0.7478 -0.2384 0.6197 +vn -0.8869 -0.2896 0.3600 +vn -0.8553 -0.3780 0.3544 +vn -0.8403 -0.4383 0.3190 +vn -0.8366 -0.4175 0.3547 +vn -0.8068 -0.5177 0.2847 +vn -0.7994 -0.4072 0.4417 +vn -0.7967 -0.2625 0.5443 +vn -0.8314 -0.3010 0.4671 +vn -0.9306 -0.1440 0.3366 +vn -0.9384 0.0365 0.3436 +vn -0.7798 0.3659 0.5079 +vn -0.7382 0.4742 0.4797 +vn -0.6357 0.5744 0.5157 +vn -0.5298 0.7131 0.4592 +vn -0.5628 0.6267 0.5390 +vn -0.4878 0.6644 0.5662 +vn -0.4622 0.7184 0.5199 +vn -0.4376 0.6922 0.5740 +vn -0.4573 0.7410 0.4916 +vn -0.7795 0.3667 0.5078 +vn -0.8639 0.1350 0.4853 +vn -0.8934 0.0986 0.4384 +vn -0.8760 -0.2301 0.4238 +vn -0.7965 -0.2659 0.5431 +vn -0.8171 -0.1175 0.5644 +vn -0.8253 0.0234 0.5642 +vn -0.7987 0.1623 0.5794 +vn -0.6712 0.3874 0.6320 +vn -0.7402 -0.5177 0.4290 +vn -0.7700 -0.5249 0.3627 +vn -0.7916 -0.5376 0.2905 +vn -0.7697 -0.5585 0.3093 +vn -0.8329 -0.2826 0.4757 +vn -0.4177 0.0413 0.9076 +vn -0.5567 -0.0445 0.8296 +vn -0.3650 0.1260 0.9224 +vn -0.2664 0.2191 0.9386 +vn -0.3289 0.3414 0.8805 +vn -0.2287 0.4294 0.8737 +vn -0.1655 0.3619 0.9174 +vn -0.5837 0.1736 0.7932 +vn -0.3483 0.2157 0.9122 +vn -0.6626 0.0814 0.7446 +vn -0.4686 -0.2032 0.8597 +vn -0.1744 0.0560 0.9831 +vn -0.2198 0.2765 0.9355 +vn -0.5751 0.0577 0.8160 +vn -0.5363 0.1764 0.8254 +vn -0.7255 0.2606 0.6370 +vn -0.5514 0.4026 0.7307 +vn -0.7254 0.3218 0.6085 +vn -0.3011 0.1672 0.9388 +vn -0.3851 0.0468 0.9217 +vn -0.4913 -0.0055 0.8710 +vn -0.6598 -0.1562 0.7350 +vn -0.7456 -0.2340 0.6239 +vn -0.7747 -0.2442 0.5833 +vn -0.7931 -0.2647 0.5486 +vn -0.8091 -0.3683 0.4580 +vn -0.7821 -0.5149 0.3511 +vn -0.7514 -0.5816 0.3116 +vn -0.7020 -0.6464 0.2990 +vn -0.6693 -0.6574 0.3461 +vn -0.6116 -0.6601 0.4362 +vn -0.5106 -0.7842 0.3527 +vn -0.4339 -0.7386 0.5160 +vn -0.6508 -0.7126 0.2621 +vn -0.7337 -0.4578 0.5022 +vn -0.6821 -0.1906 0.7060 +vn -0.6016 -0.2462 0.7599 +vn -0.6053 -0.2929 0.7402 +vn -0.6135 -0.2213 0.7581 +vn -0.4505 -0.2271 0.8634 +vn -0.3760 0.0967 0.9216 +vn -0.4173 0.0706 0.9060 +vn -0.4412 -0.0617 0.8953 +vn -0.4425 -0.2645 0.8569 +vn -0.4605 -0.1663 0.8719 +vn -0.3688 -0.0907 0.9251 +vn -0.4836 -0.1255 0.8662 +vn -0.5060 0.1080 0.8557 +vn -0.5901 -0.0606 0.8050 +vn -0.6039 -0.1233 0.7875 +vn -0.6666 -0.1921 0.7203 +vn -0.6379 -0.3752 0.6726 +vn -0.7293 -0.5871 0.3513 +vn -0.6853 -0.6816 0.2564 +vn -0.5695 -0.7883 0.2327 +vn -0.4065 -0.8772 0.2556 +vn -0.3122 -0.8554 0.4133 +vn -0.4193 -0.8925 0.1663 +vn -0.4822 -0.8470 0.2239 +vn -0.7203 -0.5352 0.4413 +vn -0.6676 -0.6433 0.3747 +vn -0.7489 -0.4803 0.4565 +vn -0.7445 -0.3624 0.5608 +vn -0.3988 -0.1356 0.9070 +vn -0.1908 -0.6826 0.7055 +vn -0.1684 0.1576 0.9730 +vn -0.2394 -0.5540 0.7974 +vn -0.3319 -0.1262 0.9349 +vn -0.1930 0.0058 0.9812 +vn -0.1754 -0.3555 0.9181 +vn -0.0589 0.0582 0.9966 +vn -0.0739 -0.1178 0.9903 +vn -0.1626 -0.1248 0.9788 +vn -0.3577 -0.0659 0.9315 +vn -0.0931 -0.9593 0.2667 +vn -0.4379 0.1612 0.8844 +vn -0.8262 0.0466 0.5615 +vn -0.9220 -0.0214 0.3866 +vn -0.4366 0.5137 0.7385 +vn -0.5688 0.4978 0.6547 +vn -0.7554 0.1686 0.6332 +vn -0.7043 -0.0883 0.7044 +vn -0.5091 -0.6002 0.6169 +vn -0.6037 0.3670 0.7078 +vn -0.2360 0.5365 0.8103 +vn -0.5526 0.7647 -0.3315 +vn -0.9790 -0.0409 0.1997 +vn -0.9231 -0.1009 0.3710 +vn -0.9680 0.0690 0.2413 +vn -0.9287 0.1986 0.3130 +vn -0.9908 -0.0311 0.1316 +vn -0.6216 -0.0305 0.7827 +vn -0.1429 0.4500 -0.8815 +vn -0.0459 -0.0973 -0.9942 +vn -0.0502 -0.9987 0.0094 +vn -0.7483 -0.3212 0.5805 +vn -0.1121 -0.7808 0.6147 +vn -0.1155 -0.9120 0.3936 +vn -0.1133 -0.5850 0.8031 +vn -0.2659 -0.5000 0.8242 +vn -0.0728 -0.1708 0.9826 +vn -0.2424 -0.5003 0.8312 +vn -0.5124 -0.7308 0.4509 +vn 0.3092 -0.7683 0.5604 +vn 0.3341 -0.7632 0.5530 +vn 0.8218 -0.0585 0.5668 +vn 0.4824 0.0619 0.8738 +vn 0.2619 -0.1524 0.9530 +vn 0.1697 -0.0628 0.9835 +vn 0.7395 -0.4795 0.4725 +vn 0.5633 -0.5794 0.5890 +vn 0.5717 0.1201 0.8117 +vn 0.5965 0.7843 -0.1705 +vn 0.9019 0.4308 -0.0315 +vn 0.0199 -0.1898 -0.9816 +vn 0.9225 -0.2680 0.2778 +vn 0.1018 -0.2482 0.9633 +vn 0.0179 -0.3669 -0.9301 +vn 0.0050 0.3857 -0.9226 +vn 0.0739 0.2907 -0.9540 +vn 0.0313 0.2256 -0.9737 +vn 0.0168 0.0605 -0.9980 +vn 0.0037 -0.0681 -0.9977 +vn 0.0371 -0.2608 -0.9647 +vn 0.0828 -0.4348 -0.8967 +vn 0.2012 -0.4648 -0.8623 +vn 0.2179 -0.4658 -0.8576 +vn 0.2887 -0.4478 -0.8463 +vn 0.4308 -0.3936 -0.8121 +vn 0.5492 -0.2971 -0.7811 +vn 0.6666 -0.3045 -0.6803 +vn 0.6770 -0.2027 -0.7075 +vn 0.7209 -0.2523 -0.6455 +vn 0.7103 -0.2079 -0.6725 +vn 0.8928 -0.1175 -0.4348 +vn 0.8580 -0.0891 -0.5058 +vn 0.9543 0.0145 -0.2986 +vn 0.9793 0.0622 -0.1926 +vn 0.9850 -0.0069 -0.1727 +vn 0.9829 0.1815 -0.0323 +vn 0.9827 0.1823 -0.0317 +vn 0.9189 0.3654 -0.1484 +vn 0.9211 0.3668 -0.1308 +vn 0.9005 0.2578 -0.3502 +vn 0.9624 0.1122 0.2475 +vn 0.9508 -0.2167 0.2215 +vn 0.7589 -0.4017 0.5126 +vn 0.8975 -0.2911 0.3312 +vn 0.9245 0.0156 0.3809 +vn 0.7631 0.0381 0.6451 +vn 0.7249 0.3160 0.6121 +vn 0.9428 0.2702 0.1952 +vn 0.9584 -0.1043 0.2656 +vn 0.8815 -0.4186 0.2184 +vn 0.8829 -0.4189 0.2120 +vn 0.7973 -0.4602 0.3906 +vn 0.9154 -0.2550 0.3114 +vn 0.9553 -0.1175 0.2711 +vn 0.9276 0.0661 0.3676 +vn 0.8901 0.4037 0.2116 +vn 0.9441 -0.0619 0.3238 +vn 0.9162 0.3574 0.1811 +vn 0.6613 0.7499 0.0185 +vn 0.9142 0.4011 0.0585 +vn 0.9869 0.1270 -0.0994 +vn 0.9694 0.0815 -0.2318 +vn 0.9073 0.0907 -0.4105 +vn 0.9199 0.0332 -0.3906 +vn 0.7423 -0.0752 -0.6658 +vn 0.6335 -0.1045 -0.7666 +vn 0.5535 -0.1600 -0.8174 +vn 0.5621 -0.2381 -0.7920 +vn 0.4470 -0.3474 -0.8243 +vn 0.2539 -0.3916 -0.8844 +vn 0.3009 -0.4318 -0.8503 +vn 0.0170 -0.4745 -0.8801 +vn 0.0438 -0.4359 -0.8989 +vn 0.0022 -0.1760 -0.9844 +vn 0.0027 0.0885 -0.9961 +vn -0.0009 0.1278 -0.9918 +vn 0.0091 0.3918 -0.9200 +vn -0.0036 0.4638 -0.8859 +vn 0.0041 0.5565 -0.8308 +vn -0.0009 0.6330 -0.7742 +vn 0.0071 0.7039 -0.7103 +vn 0.0240 0.9862 -0.1636 +vn 0.0379 0.9892 0.1415 +vn 0.0170 0.9507 0.3096 +vn 0.0294 0.9189 0.3934 +vn 0.0007 0.2438 0.9698 +vn 0.0237 0.3520 0.9357 +vn 0.1347 0.5948 0.7925 +vn 0.2958 0.3917 0.8713 +vn 0.4813 0.2301 0.8459 +vn 0.4929 -0.4192 0.7625 +vn 0.4006 -0.5566 0.7279 +vn 0.4871 -0.6107 0.6243 +vn 0.3930 -0.7670 0.5072 +vn 0.3848 -0.7936 0.4713 +vn 0.1019 -0.9072 0.4081 +vn -0.0496 -0.5562 0.8296 +vn -0.0055 -0.0279 0.9996 +vn 0.0365 0.2424 0.9695 +vn 0.0888 -0.0648 0.9939 +vn 0.0095 -0.6148 0.7886 +vn -0.0087 -0.2916 0.9565 +vn 0.0227 0.0898 0.9957 +vn -0.0020 -0.1455 0.9894 +vn 0.0006 -0.1198 0.9928 +vn 0.0033 0.0234 0.9997 +vn 0.0665 0.0080 0.9978 +vn 0.1183 0.0263 0.9926 +vn 0.5935 -0.2899 0.7508 +vn 0.4735 -0.2752 0.8367 +vn 0.6564 -0.5441 0.5225 +vn 0.3817 -0.6210 0.6846 +vn 0.4618 -0.8060 0.3703 +vn 0.2924 -0.8555 0.4273 +vn 0.2009 -0.9569 0.2095 +vn 0.0854 -0.9908 0.1050 +vn 0.0328 -0.9924 0.1182 +vn 0.0080 -0.9773 0.2118 +vn -0.0276 -0.8819 0.4706 +vn 0.0760 -0.8207 0.5663 +vn 0.3257 -0.8411 0.4318 +vn 0.2305 -0.7612 0.6062 +vn 0.3859 -0.6474 0.6573 +vn 0.4712 -0.4247 0.7731 +vn 0.6145 -0.3677 0.6980 +vn 0.6658 -0.2891 0.6878 +vn 0.7786 0.0100 0.6274 +vn 0.7566 -0.0678 0.6504 +vn 0.6988 0.2233 0.6796 +vn 0.4907 0.5684 0.6604 +vn 0.4337 0.4647 0.7720 +vn 0.4167 0.6480 0.6376 +vn 0.4297 0.6901 0.5823 +vn 0.4068 0.6511 0.6407 +vn 0.5581 0.5963 0.5770 +vn 0.5967 0.6449 0.4775 +vn 0.6219 0.6913 0.3678 +vn 0.8312 0.3655 0.4189 +vn 0.9024 -0.1290 0.4110 +vn 0.8334 0.4917 0.2525 +vn 0.8179 -0.5406 0.1968 +vn 0.9369 -0.3096 0.1624 +vn 0.6501 -0.7443 0.1528 +vn 0.7582 -0.6507 0.0418 +vn 0.4708 -0.8822 0.0033 +vn 0.1310 -0.9897 -0.0576 +vn 0.1260 -0.9919 0.0178 +vn 0.2369 -0.9705 0.0450 +vn 0.4207 -0.8906 0.1725 +vn 0.8135 -0.4733 0.3380 +vn 0.4741 -0.8114 0.3417 +vn 0.8357 -0.2047 0.5096 +vn 0.7236 0.2655 0.6371 +vn 0.7860 0.3245 0.5263 +vn 0.5050 0.5975 0.6228 +vn 0.3527 0.5966 0.7209 +vn 0.3380 0.4479 0.8277 +vn 0.4034 0.3120 0.8602 +vn 0.4422 0.2588 0.8588 +vn 0.4535 0.4575 0.7649 +vn 0.5540 0.0932 0.8273 +vn 0.6831 0.0199 0.7300 +vn 0.7003 0.0775 0.7097 +vn 0.6158 -0.1075 0.7805 +vn 0.6068 -0.2167 0.7647 +vn 0.2953 -0.3526 0.8880 +vn 0.2069 -0.4134 0.8867 +vn 0.2163 -0.5373 0.8152 +vn 0.0687 -0.6256 0.7771 +vn 0.0115 -0.4959 0.8683 +vn 0.0128 -0.3370 0.9414 +vn 0.0356 -0.1990 0.9794 +vn 0.1172 -0.1214 0.9857 +vn 0.0268 -0.3124 0.9496 +vn 0.0117 -0.3732 0.9277 +vn 0.0133 -0.5989 0.8007 +vn 0.0831 -0.9175 0.3889 +vn 0.0727 -0.9918 0.1051 +vn 0.1289 -0.9901 0.0555 +vn 0.1760 -0.9782 0.1102 +vn 0.2192 -0.9652 0.1425 +vn 0.1872 -0.9632 0.1929 +vn 0.2515 -0.9495 0.1877 +vn 0.2781 -0.9403 0.1964 +vn 0.1891 -0.9608 0.2026 +vn 0.3290 -0.9101 0.2518 +vn 0.3795 -0.8632 0.3330 +vn 0.2630 -0.8689 0.4193 +vn 0.5505 -0.0043 0.8349 +vn 0.4172 -0.0430 0.9078 +vn 0.3043 0.3596 0.8821 +vn 0.2637 0.4400 0.8584 +vn 0.3942 0.0795 0.9156 +vn 0.4450 0.0063 0.8955 +vn 0.5434 -0.0616 0.8372 +vn 0.6200 -0.0041 0.7846 +vn 0.5339 -0.1552 0.8312 +vn 0.1136 -0.3181 0.9412 +vn 0.1002 -0.2312 0.9677 +vn 0.2606 -0.2325 0.9370 +vn 0.3758 -0.1275 0.9179 +vn 0.3316 -0.1052 0.9375 +vn 0.2225 0.1642 0.9610 +vn 0.1705 0.1652 0.9714 +vn 0.2974 -0.2448 0.9228 +vn 0.3315 -0.7874 0.5198 +vn 0.2579 -0.9507 0.1725 +vn 0.1476 -0.9737 0.1733 +vn 0.1356 -0.9842 0.1143 +vn 0.0910 -0.9863 0.1374 +vn 0.1026 -0.9841 0.1452 +vn 0.0789 -0.9969 0.0066 +vn 0.1274 -0.9209 0.3683 +vn 0.1276 -0.6743 0.7273 +vn 0.1580 -0.2656 0.9511 +vn 0.0820 -0.1364 0.9872 +vn 0.3139 0.0261 0.9491 +vn 0.0484 -0.9988 0.0037 +vn 0.0471 -0.9963 0.0712 +vn 0.0728 -0.9951 0.0673 +vn 0.1033 -0.9848 0.1393 +vn -0.0166 -0.7408 0.6715 +vn 0.0245 -0.3868 0.9218 +vn 0.0637 -0.3496 0.9347 +vn 0.0008 -0.4748 0.8801 +vn 0.0588 -0.9280 0.3679 +vn -0.0206 -0.9672 0.2531 +vn -0.0020 -0.9991 0.0428 +vn -0.0006 -0.8404 -0.5420 +vn -0.0126 -0.4190 -0.9079 +vn 0.0570 0.3456 -0.9367 +vn 0.0761 0.5264 -0.8468 +vn 0.1674 0.5649 -0.8080 +vn 0.2710 0.6404 -0.7187 +vn 0.3411 0.7326 -0.5890 +vn 0.3109 0.6392 -0.7034 +vn 0.4291 0.7051 -0.5646 +vn 0.3636 0.5936 -0.7179 +vn 0.3844 0.6452 -0.6602 +vn 0.4873 0.6156 -0.6193 +vn 0.5459 0.6758 -0.4952 +vn 0.5261 0.6910 -0.4958 +vn 0.5189 0.7270 -0.4496 +vn 0.4682 0.7663 -0.4401 +vn 0.4797 0.8061 -0.3466 +vn 0.4236 0.7766 -0.4663 +vn 0.3684 0.7189 -0.5895 +vn 0.3524 0.7174 -0.6010 +vn 0.3931 0.6458 -0.6545 +vn 0.5049 0.5141 -0.6934 +vn 0.3203 0.5391 -0.7790 +vn 0.2006 0.4943 -0.8458 +vn 0.1926 0.4973 -0.8459 +vn 0.0536 0.4694 -0.8814 +vn 0.0478 0.2971 -0.9537 +vn 0.0147 -0.0200 -0.9997 +vn 0.0438 0.1062 -0.9934 +vn 0.1445 0.3744 -0.9159 +vn 0.1094 0.2099 -0.9716 +vn 0.0733 0.1279 -0.9891 +vn 0.1203 -0.2751 -0.9539 +vn 0.1144 -0.3432 -0.9323 +vn 0.3207 -0.4517 -0.8325 +vn 0.5305 -0.3857 -0.7548 +vn 0.8148 -0.2307 -0.5318 +vn 0.8718 -0.1736 -0.4581 +vn 0.9239 -0.0967 -0.3701 +vn 0.9749 -0.0657 -0.2126 +vn 0.9906 0.0085 -0.1368 +vn 0.9033 0.4019 -0.1501 +vn 0.9538 0.1863 -0.2357 +vn 0.4612 0.6200 -0.6348 +vn 0.5010 0.8280 -0.2516 +vn 0.7902 0.2940 -0.5378 +vn 0.3347 0.5298 -0.7793 +vn 0.9337 0.0516 -0.3542 +vn 0.8152 -0.2680 0.5134 +vn 0.6239 -0.0142 0.7814 +vn 0.5837 0.2295 0.7789 +vn 0.6291 0.2582 0.7331 +vn 0.7815 0.0459 0.6222 +vn 0.8061 0.1125 0.5809 +vn 0.8026 0.3140 0.5072 +vn 0.9222 0.0768 0.3791 +vn 0.9895 -0.0555 -0.1333 +vn 0.9932 0.1058 -0.0493 +vn 0.8334 0.4042 0.3768 +vn 0.9298 0.3362 0.1496 +vn 0.9984 0.0500 0.0280 +vn 0.9966 0.0822 0.0013 +vn 0.9796 0.1610 -0.1199 +vn 0.9628 0.1572 -0.2197 +vn 0.9184 0.1416 -0.3694 +vn 0.8213 0.2185 -0.5271 +vn 0.8222 0.1279 -0.5546 +vn 0.6758 0.0534 -0.7352 +vn 0.6593 -0.0559 -0.7498 +vn 0.5203 -0.0369 -0.8532 +vn 0.5658 -0.1407 -0.8125 +vn 0.4010 -0.1097 -0.9095 +vn 0.4413 -0.1743 -0.8803 +vn 0.4156 -0.2489 -0.8748 +vn 0.1726 -0.2755 -0.9457 +vn 0.2748 -0.3605 -0.8914 +vn 0.2080 -0.4066 -0.8896 +vn 0.0631 -0.3198 -0.9454 +vn 0.1864 -0.1504 -0.9709 +vn 0.2544 -0.0300 -0.9666 +vn 0.5899 0.0796 -0.8036 +vn 0.5811 0.1405 -0.8016 +vn 0.6513 0.1528 -0.7433 +vn 0.8461 0.2398 -0.4761 +vn 0.8281 0.2960 -0.4761 +vn 0.9215 0.2648 -0.2842 +vn 0.9654 0.2043 -0.1619 +vn 0.9889 0.1463 -0.0258 +vn 0.9934 0.1140 0.0130 +vn 0.9888 0.0119 0.1490 +vn 0.9859 0.0243 0.1655 +vn 0.9778 0.0963 0.1859 +vn 0.9869 0.1443 0.0723 +vn 0.9847 0.1741 0.0025 +vn 0.9885 0.1491 -0.0264 +vn 0.9536 0.2587 -0.1539 +vn 0.8490 0.4036 -0.3411 +vn 0.8517 0.3458 -0.3938 +vn 0.5971 0.2483 -0.7628 +vn 0.4794 0.1254 -0.8686 +vn 0.3216 0.0453 -0.9458 +vn 0.1897 -0.0396 -0.9810 +vn 0.0668 -0.1784 -0.9817 +vn 0.0325 -0.3178 -0.9476 +vn 0.1511 0.0501 -0.9873 +vn 0.3858 0.1303 -0.9134 +vn 0.3856 0.2293 -0.8937 +vn 0.5010 0.2381 -0.8320 +vn 0.5183 0.2280 -0.8243 +vn 0.5283 0.3697 -0.7644 +vn 0.5880 0.3336 -0.7369 +vn 0.6389 0.3545 -0.6828 +vn 0.7903 0.4698 -0.3933 +vn 0.7821 0.4856 -0.3905 +vn 0.9836 0.1686 0.0639 +vn 0.9731 0.2232 0.0577 +vn 0.7764 0.5359 -0.3318 +vn 0.7080 0.5466 -0.4471 +vn 0.5806 0.5250 -0.6223 +vn 0.6130 0.4621 -0.6409 +vn 0.3775 0.3794 -0.8447 +vn 0.4905 0.3536 -0.7965 +vn 0.1437 0.3453 -0.9274 +vn 0.2415 0.2048 -0.9486 +vn 0.0434 0.0760 -0.9962 +vn 0.0029 0.2834 -0.9590 +vn 0.0322 0.1870 -0.9818 +vn 0.1706 0.4447 -0.8793 +vn 0.2575 0.5683 -0.7815 +vn 0.2512 0.6134 -0.7487 +vn 0.3532 0.6259 -0.6953 +vn 0.5823 0.6279 -0.5165 +vn 0.5653 0.6944 -0.4453 +vn 0.6417 0.6630 -0.3856 +vn 0.6090 0.7232 -0.3258 +vn 0.7129 0.5984 -0.3657 +vn 0.7025 0.6575 -0.2723 +vn 0.8037 0.5620 -0.1955 +vn 0.7910 0.5923 -0.1531 +vn 0.9033 0.4284 0.0230 +vn 0.8827 0.4667 0.0555 +vn 0.9410 0.3236 0.0989 +vn 0.9588 0.2616 0.1107 +vn 0.9539 0.2495 0.1670 +vn 0.9461 0.1809 0.2687 +vn 0.9483 0.1985 0.2475 +vn 0.9519 0.2217 0.2116 +vn 0.9545 0.1982 0.2228 +vn 0.8996 0.3953 0.1853 +vn 0.9578 0.2295 0.1728 +vn 0.8755 0.4474 0.1824 +vn 0.7883 0.6153 0.0063 +vn 0.7150 0.6937 -0.0871 +vn 0.8223 0.5639 -0.0767 +vn 0.6712 0.7124 -0.2048 +vn 0.7363 0.6614 -0.1427 +vn 0.5208 0.8216 -0.2318 +vn 0.4848 0.7917 -0.3717 +vn 0.5585 0.7663 -0.3175 +vn 0.5129 0.7409 -0.4336 +vn 0.3683 0.7742 -0.5148 +vn 0.3849 0.7118 -0.5875 +vn 0.2537 0.7421 -0.6204 +vn 0.2127 0.6497 -0.7298 +vn 0.1141 0.5977 -0.7936 +vn 0.2214 0.6287 -0.7455 +vn 0.0778 0.5320 -0.8432 +vn 0.0671 0.7156 -0.6953 +vn 0.1022 0.7675 -0.6329 +vn 0.0561 0.7969 -0.6015 +vn 0.2625 0.8042 -0.5332 +vn 0.2962 0.8697 -0.3948 +vn 0.4261 0.8717 -0.2421 +vn 0.3806 0.9062 -0.1841 +vn 0.5177 0.8502 -0.0958 +vn 0.6524 0.7563 0.0493 +vn 0.8060 0.5660 0.1729 +vn 0.8541 0.4826 0.1940 +vn 0.9116 0.2973 0.2839 +vn 0.9239 0.2510 0.2887 +vn 0.9181 0.2247 0.3264 +vn 0.9082 0.3062 0.2855 +vn 0.8366 0.4719 0.2784 +vn 0.7431 0.6315 0.2214 +vn 0.6703 0.7164 0.1934 +vn 0.6639 0.7349 0.1383 +vn 0.5107 0.8525 0.1113 +vn 0.5189 0.8544 0.0286 +vn 0.3792 0.9234 -0.0588 +vn 0.4170 0.9018 -0.1136 +vn 0.2957 0.9438 -0.1475 +vn 0.1067 0.9152 -0.3885 +vn 0.1082 0.8657 -0.4887 +vn 0.0417 0.9383 -0.3432 +vn 0.2452 0.9578 -0.1500 +vn 0.2224 0.9712 -0.0859 +vn 0.3103 0.9486 -0.0627 +vn 0.3009 0.9521 0.0555 +vn 0.4113 0.9045 0.1125 +vn 0.3860 0.9103 0.1497 +vn 0.6151 0.7558 0.2244 +vn 0.6071 0.7461 0.2733 +vn 0.7657 0.5718 0.2945 +vn 0.7452 0.5690 0.3477 +vn 0.8430 0.4187 0.3376 +vn 0.7422 0.5219 0.4205 +vn 0.7313 0.4774 0.4871 +vn 0.8093 0.3832 0.4451 +vn 0.9049 0.2364 0.3540 +vn 0.9963 -0.0827 -0.0235 +vn 0.9909 -0.1341 0.0084 +vn 0.9591 -0.2751 0.0663 +vn 0.9231 -0.3511 0.1568 +vn 0.8686 -0.3832 0.3142 +vn 0.8182 -0.5301 0.2224 +vn 0.5393 -0.8228 -0.1790 +vn 0.8219 -0.5695 -0.0129 +vn 0.6917 -0.6550 -0.3043 +vn 0.8807 -0.4642 -0.0941 +vn 0.5163 -0.6397 -0.5694 +vn 0.3193 -0.6533 -0.6865 +vn 0.4301 -0.7723 -0.4675 +vn 0.8030 -0.5545 -0.2185 +vn 0.6950 -0.5477 -0.4658 +vn 0.7530 -0.3746 -0.5410 +vn 0.9511 -0.0273 0.3077 +vn 0.7340 -0.0847 0.6738 +vn 0.6356 0.1290 0.7612 +vn 0.7240 0.0535 0.6877 +vn 0.9191 -0.2147 0.3303 +vn 0.8998 -0.3639 0.2409 +vn 0.8798 0.1452 0.4526 +vn 0.9364 -0.1481 0.3180 +vn 0.9867 0.0029 0.1624 +vn 0.9908 -0.1312 -0.0343 +vn 0.9978 0.0368 0.0550 +vn 0.9862 -0.1643 0.0186 +vn 0.8345 -0.4193 0.3575 +vn 0.7004 -0.6751 0.2317 +vn 0.4400 -0.7663 -0.4682 +vn 0.8557 -0.4447 0.2646 +vn 0.8122 -0.5430 -0.2133 +vn 0.9256 -0.3741 0.0576 +vn 0.9986 -0.0335 0.0401 +vn 0.8923 -0.2150 -0.3970 +vn 0.9916 -0.1054 -0.0756 +vn 0.5930 -0.2235 -0.7736 +vn 0.2309 -0.4165 -0.8793 +vn 0.3622 -0.6012 -0.7123 +vn 0.5117 -0.6416 -0.5714 +vn 0.8577 -0.4464 -0.2551 +vn 0.7279 -0.5677 -0.3844 +vn 0.9048 -0.3192 -0.2820 +vn 0.9041 -0.2618 -0.3377 +vn 0.8573 -0.2799 -0.4321 +vn 0.7845 -0.1231 -0.6077 +vn 0.8304 0.0378 -0.5559 +vn 0.7012 -0.0115 -0.7129 +vn 0.4284 0.3143 -0.8472 +vn 0.2552 0.3051 -0.9175 +vn 0.2595 0.3758 -0.8896 +vn 0.1542 0.3124 -0.9374 +vn 0.0766 0.0248 -0.9968 +vn 0.1021 -0.1037 -0.9893 +vn 0.1778 -0.0880 -0.9801 +vn 0.2494 -0.3360 -0.9082 +vn 0.3792 -0.4217 -0.8236 +vn 0.4128 -0.4457 -0.7943 +vn 0.6271 -0.4090 -0.6630 +vn 0.7464 -0.4090 -0.5250 +vn 0.7680 -0.3309 -0.5484 +vn 0.8252 -0.3313 -0.4575 +vn 0.8285 -0.3280 -0.4538 +vn 0.8933 -0.1875 -0.4085 +vn 0.9164 -0.1556 -0.3687 +vn 0.9619 -0.1120 -0.2495 +vn 0.9408 -0.0125 -0.3387 +vn 0.6055 0.2612 -0.7518 +vn 0.6529 0.1904 -0.7331 +vn 0.2361 0.2733 -0.9325 +vn 0.8676 0.0176 -0.4969 +vn 0.9286 -0.0483 -0.3680 +vn 0.3135 -0.2364 -0.9197 +vn 0.5529 -0.1257 -0.8237 +vn 0.7687 -0.3388 -0.5424 +vn 0.5066 -0.3913 -0.7683 +vn 0.8736 -0.2736 -0.4024 +vn 0.8936 -0.2888 -0.3437 +vn 0.9267 -0.3253 -0.1879 +vn 0.9157 -0.3530 -0.1921 +vn 0.9043 -0.3957 -0.1600 +vn 0.8724 -0.4262 -0.2392 +vn 0.8490 -0.3847 -0.3623 +vn 0.8183 -0.3521 -0.4543 +vn 0.8046 -0.3443 -0.4839 +vn 0.7520 -0.2979 -0.5881 +vn 0.5889 -0.1629 -0.7916 +vn 0.6396 0.0309 -0.7681 +vn 0.4860 0.0884 -0.8695 +vn 0.4038 0.1181 -0.9072 +vn 0.3416 0.1910 -0.9202 +vn 0.1689 0.2570 -0.9515 +vn 0.1867 0.1209 -0.9749 +vn 0.1785 0.0173 -0.9838 +vn 0.3945 -0.1067 -0.9127 +vn 0.4048 -0.2755 -0.8719 +vn 0.4965 -0.4077 -0.7664 +vn 0.7561 -0.4039 -0.5150 +vn 0.8663 -0.3678 -0.3379 +vn 0.8784 -0.2395 -0.4136 +vn 0.9511 -0.2154 -0.2216 +vn 0.9597 -0.1532 -0.2358 +vn 0.8971 -0.1079 -0.4283 +vn 0.7279 0.0379 -0.6846 +vn 0.7712 -0.0964 -0.6292 +vn 0.8285 -0.4145 -0.3766 +vn 0.7476 -0.4109 -0.5217 +vn 0.6789 -0.3975 -0.6173 +vn 0.4790 -0.3048 -0.8232 +vn 0.4011 0.0036 -0.9160 +vn 0.2961 0.0228 -0.9549 +vn 0.8752 -0.0218 -0.4833 +vn 0.5685 0.3418 -0.7483 +vn 0.8864 0.1486 -0.4384 +vn 0.9001 -0.0368 -0.4341 +vn 0.9189 -0.1697 -0.3562 +vn 0.9246 -0.2125 -0.3163 +vn 0.9334 -0.2683 -0.2382 +vn 0.9547 -0.2655 -0.1345 +vn 0.9303 -0.3570 -0.0842 +vn 0.7943 -0.6073 -0.0151 +vn 0.9717 -0.1783 0.1552 +vn 0.9812 -0.0493 0.1864 +vn 0.9060 0.2012 0.3725 +vn 0.8465 0.2864 0.4487 +vn 0.8552 0.2934 0.4273 +vn 0.7469 0.4115 0.5223 +vn 0.7656 0.4156 0.4911 +vn 0.6210 0.5869 0.5195 +vn 0.6174 0.6571 0.4325 +vn 0.6251 0.6826 0.3785 +vn 0.5276 0.7696 0.3597 +vn 0.4657 0.8411 0.2750 +vn 0.5360 0.7897 0.2985 +vn 0.4001 0.8962 0.1915 +vn 0.2919 0.9458 0.1422 +vn 0.1967 0.9735 0.1170 +vn 0.1325 0.9853 -0.1083 +vn 0.0877 0.9961 -0.0083 +vn 0.1362 0.9822 0.1293 +vn 0.1327 0.9666 0.2192 +vn 0.2328 0.9517 0.2003 +vn 0.2275 0.9292 0.2911 +vn 0.3067 0.9035 0.2993 +vn 0.4159 0.8149 0.4037 +vn 0.4030 0.8129 0.4204 +vn 0.4701 0.7308 0.4949 +vn 0.4961 0.6607 0.5634 +vn 0.5989 0.5030 0.6231 +vn 0.6178 0.4897 0.6152 +vn 0.6429 0.3908 0.6588 +vn 0.7536 0.3462 0.5587 +vn 0.7380 0.3150 0.5968 +vn 0.8559 0.2366 0.4599 +vn 0.9562 0.0340 0.2906 +vn 0.9710 0.0076 0.2391 +vn 0.9630 -0.2175 0.1592 +vn 0.9427 -0.3298 0.0514 +vn 0.9425 -0.3302 0.0516 +vn 0.9423 -0.3309 0.0506 +vn 0.9283 -0.3711 -0.0224 +vn 0.9273 -0.3642 -0.0867 +vn 0.9607 -0.1633 -0.2246 +vn 0.9725 -0.0636 -0.2242 +vn 0.9378 0.0974 -0.3334 +vn 0.8930 0.3226 -0.3140 +vn 0.8575 0.2589 -0.4446 +vn 0.8294 0.4106 -0.3788 +vn 0.7989 0.3702 -0.4740 +vn 0.7171 0.3642 -0.5943 +vn 0.6265 0.5213 -0.5795 +vn 0.7420 0.4833 -0.4646 +vn 0.6785 0.6633 -0.3158 +vn 0.7860 0.5183 -0.3370 +vn 0.8775 0.4276 -0.2173 +vn 0.9449 0.2850 -0.1612 +vn 0.9760 0.1576 -0.1501 +vn 0.9920 -0.0443 -0.1186 +vn 0.9755 -0.1830 -0.1221 +vn 0.9713 -0.2332 -0.0466 +vn 0.9576 -0.2823 0.0578 +vn 0.9513 -0.2998 0.0713 +vn 0.9434 -0.3273 0.0544 +vn 0.9396 -0.2718 0.2082 +vn 0.9551 -0.2288 0.1884 +vn 0.9723 -0.1296 0.1946 +vn 0.9533 0.0477 0.2981 +vn 0.8889 0.1122 0.4441 +vn 0.8596 0.1101 0.4990 +vn 0.7139 0.2444 0.6562 +vn 0.7520 0.2701 0.6013 +vn 0.6108 0.3279 0.7207 +vn 0.4736 0.4600 0.7510 +vn 0.5051 0.5068 0.6986 +vn 0.2789 0.6414 0.7147 +vn 0.2862 0.7962 0.5330 +vn 0.3538 0.7456 0.5647 +vn 0.3387 0.8319 0.4396 +vn 0.2080 0.8966 0.3910 +vn 0.2589 0.8843 0.3885 +vn 0.1446 0.9411 0.3058 +vn 0.1877 0.9303 0.3152 +vn 0.0394 0.9728 0.2281 +vn 0.0476 0.9538 0.2966 +vn 0.1629 0.8871 0.4318 +vn 0.1404 0.8690 0.4745 +vn 0.1784 0.8874 0.4251 +vn 0.1892 0.8115 0.5529 +vn 0.2405 0.7947 0.5573 +vn 0.1046 0.7719 0.6271 +vn 0.0882 0.6552 0.7503 +vn 0.3552 0.4861 0.7985 +vn 0.3402 0.3485 0.8734 +vn 0.4447 0.3719 0.8148 +vn 0.4305 0.2700 0.8612 +vn 0.5679 0.2816 0.7734 +vn 0.5620 0.2260 0.7957 +vn 0.6283 0.1816 0.7565 +vn 0.7541 0.1578 0.6376 +vn 0.7130 0.0866 0.6958 +vn 0.8328 0.0353 0.5524 +vn 0.8612 0.0213 0.5078 +vn 0.8094 0.2268 0.5417 +vn 0.9366 -0.3112 0.1612 +vn 0.9390 -0.3178 0.1319 +vn 0.9409 -0.3171 0.1194 +vn 0.9704 -0.2414 0.0111 +vn 0.9761 -0.2163 -0.0202 +vn 0.9946 -0.1016 -0.0187 +vn 0.9902 -0.1160 -0.0779 +vn 0.9719 0.2016 -0.1215 +vn 0.9326 0.3501 -0.0873 +vn 0.9397 0.3087 -0.1469 +vn 0.8882 0.4257 -0.1727 +vn 0.8716 0.4658 -0.1531 +vn 0.8336 0.5264 -0.1676 +vn 0.7437 0.6231 -0.2424 +vn 0.7147 0.6804 -0.1623 +vn 0.6767 0.7054 -0.2108 +vn 0.5975 0.7708 -0.2211 +vn 0.5319 0.7093 -0.4625 +vn 0.5250 0.8348 -0.1659 +vn 0.5147 0.8376 -0.1827 +vn 0.5733 0.7986 -0.1833 +vn 0.6065 0.7368 -0.2989 +vn 0.8146 0.2916 -0.5014 +vn 0.6570 0.2455 -0.7128 +vn 0.7442 -0.1702 -0.6459 +vn 0.5868 0.0711 -0.8066 +vn 0.3794 -0.5002 -0.7784 +vn 0.5585 -0.5437 -0.6264 +vn 0.6553 -0.4567 -0.6016 +vn 0.4152 -0.8930 -0.1734 +vn 0.8005 -0.5325 -0.2749 +vn 0.9539 -0.0892 -0.2867 +vn 0.9138 0.2481 -0.3215 +vn 0.8473 0.5043 -0.1668 +vn 0.7682 0.6395 0.0299 +vn 0.6943 0.7016 -0.1603 +vn 0.6151 0.7874 -0.0398 +vn 0.5739 0.8163 -0.0654 +vn 0.5258 0.8466 -0.0827 +vn 0.5993 0.7904 -0.1269 +vn 0.8059 0.5897 -0.0525 +vn 0.9109 0.4086 -0.0574 +vn 0.8786 0.4760 0.0394 +vn 0.9175 0.3977 -0.0049 +vn 0.9745 0.2243 -0.0034 +vn 0.9762 0.2167 -0.0078 +vn 0.9963 0.0860 -0.0002 +vn 0.9906 0.1333 0.0296 +vn 0.9975 -0.0614 0.0336 +vn 0.9768 -0.2085 0.0478 +vn 0.9604 -0.2509 0.1215 +vn 0.8990 -0.3903 0.1986 +vn 0.9284 -0.3364 0.1580 +vn 0.9261 -0.3487 0.1444 +vn 0.9304 -0.2839 0.2319 +vn 0.9430 -0.1759 0.2824 +vn 0.9399 -0.0524 0.3375 +vn 0.9433 -0.1747 0.2821 +vn 0.9441 -0.0268 0.3287 +vn 0.8428 0.1023 0.5284 +vn 0.8492 0.1025 0.5181 +vn 0.7463 -0.1001 0.6580 +vn 0.6650 -0.2604 0.7000 +vn 0.6353 -0.1013 0.7656 +vn 0.5877 -0.2242 0.7774 +vn 0.7361 0.0316 0.6761 +vn 0.4566 -0.0242 0.8894 +vn 0.6165 0.1050 0.7803 +vn 0.4742 0.1580 0.8661 +vn 0.4036 0.2409 0.8827 +vn 0.4792 0.1975 0.8552 +vn 0.3155 0.2607 0.9124 +vn 0.1830 0.3033 0.9352 +vn 0.0565 0.5326 0.8445 +vn 0.1271 0.5975 0.7917 +vn 0.0102 0.5832 0.8122 +vn 0.0396 0.6536 0.7558 +vn 0.0255 0.7162 0.6975 +vn 0.0558 0.7767 0.6274 +vn 0.0408 0.8328 0.5521 +vn -0.0004 0.4735 0.8808 +vn 0.1177 0.3223 0.9393 +vn 0.2111 0.2639 0.9412 +vn 0.3024 0.2161 0.9284 +vn 0.3859 0.1184 0.9149 +vn 0.3787 -0.2136 0.9005 +vn 0.3364 -0.3511 0.8738 +vn 0.3056 -0.2960 0.9050 +vn 0.3292 -0.2614 0.9074 +vn 0.5871 0.0197 0.8093 +vn 0.5780 0.2812 0.7661 +vn 0.8620 -0.0205 0.5065 +vn 0.8952 -0.3135 0.3168 +vn 0.9155 -0.2959 0.2726 +vn 0.9188 -0.3097 0.2446 +vn 0.8957 -0.3932 0.2075 +vn 0.9441 -0.2618 0.2005 +vn 0.9792 -0.1084 0.1715 +vn 0.9650 0.0728 0.2518 +vn 0.9922 0.0613 0.1087 +vn 0.8959 0.3958 0.2018 +vn 0.8075 0.5515 0.2091 +vn 0.8224 0.5579 0.1117 +vn 0.7601 0.5989 0.2520 +vn 0.7070 0.7071 0.0132 +vn 0.6354 0.7636 0.1149 +vn 0.5456 0.8228 0.1589 +vn 0.5796 0.7893 0.2024 +vn 0.5454 0.8374 0.0364 +vn 0.5664 0.8145 0.1258 +vn 0.5469 0.7920 0.2712 +vn 0.6466 0.7308 0.2187 +vn 0.5940 0.7971 0.1087 +vn 0.6421 0.7538 0.1397 +vn 0.7019 0.7100 0.0561 +vn 0.7678 0.6398 0.0332 +vn 0.8765 0.4691 0.1077 +vn 0.9858 -0.0295 0.1652 +vn 0.9056 -0.4224 0.0377 +vn 0.9657 0.2029 -0.1623 +vn 0.7027 -0.7109 -0.0297 +vn 0.9631 -0.2354 -0.1305 +vn 0.7542 -0.6532 -0.0680 +vn 0.6872 -0.7149 -0.1295 +vn 0.4576 -0.8880 -0.0463 +vn 0.1893 -0.9796 -0.0672 +vn 0.1054 -0.9543 -0.2796 +vn 0.0531 -0.9920 -0.1142 +vn 0.0314 -0.9989 -0.0357 +vn 0.0898 -0.9752 0.2025 +vn 0.0706 -0.9921 0.1034 +vn 0.0097 -0.9748 0.2229 +vn -0.0294 -0.7012 0.7123 +vn -0.0272 -0.4965 0.8676 +vn 0.0115 -0.9933 0.1148 +vn 0.0231 -0.9949 0.0981 +vn 0.0809 -0.9782 0.1912 +vn 0.0208 -0.9997 0.0114 +vn 0.1941 -0.3552 -0.9144 +vn 0.1442 -0.9283 -0.3428 +vn 0.2131 -0.9260 -0.3116 +vn 0.1527 -0.0399 -0.9875 +vn 0.2119 0.2041 -0.9558 +vn 0.3431 0.1570 -0.9261 +vn 0.1716 0.1031 -0.9798 +vn 0.2135 0.4469 -0.8687 +vn 0.3531 0.4496 -0.8205 +vn 0.2965 0.5238 -0.7986 +vn 0.4434 0.4652 -0.7662 +vn 0.1515 0.4584 -0.8757 +vn 0.1064 -0.1287 -0.9860 +vn 0.1081 -0.3852 -0.9165 +vn 0.1321 -0.5458 -0.8274 +vn 0.0594 -0.9331 -0.3547 +vn 0.0183 -0.9943 -0.1052 +vn 0.0371 -0.9981 0.0488 +vn 0.0386 -0.9980 0.0496 +vn 0.0813 -0.9846 0.1546 +vn 0.0482 -0.9715 0.2321 +vn 0.0496 -0.9712 0.2331 +vn 0.0190 -0.9864 0.1632 +vn 0.0356 -0.9985 0.0409 +vn 0.0305 -0.9982 0.0522 +vn 0.0311 -0.9981 0.0522 +vn 0.0309 -0.9981 0.0525 +vn 0.0099 -0.9836 -0.1801 +vn 0.0151 -0.9771 -0.2121 +vn -0.0363 -0.7603 -0.6486 +vn 0.0713 0.0288 -0.9970 +vn 0.0986 0.1671 -0.9810 +vn 0.1423 0.4533 -0.8799 +vn 0.1770 0.3979 -0.9002 +vn 0.2048 0.5559 -0.8056 +vn -0.0389 -0.7650 -0.6429 +vn -0.0559 -0.6473 -0.7601 +vn -0.0252 -0.8501 -0.5260 +vn 0.0125 -0.9997 0.0218 +vn 0.0276 -0.9987 0.0423 +vn 0.0062 -0.9992 0.0385 +vn -0.0023 -0.9919 -0.1266 +vn -0.0280 -0.4223 -0.9060 +vn -0.0282 -0.3996 -0.9162 +vn 0.0250 -0.0350 -0.9991 +vn 0.0022 -0.9629 -0.2700 +vn 0.6406 0.6812 0.3545 +vn 0.6356 0.7342 0.2386 +vn 0.5376 0.7752 0.3318 +vn 0.6166 0.7521 0.2327 +vn 0.6715 0.7131 0.2016 +vn 0.7849 0.4884 0.3814 +vn 0.8558 0.3673 0.3642 +vn 0.8300 0.4373 0.3464 +vn 0.9196 0.1590 0.3591 +vn 0.9570 -0.1329 0.2578 +vn 0.9530 -0.1557 0.2597 +vn 0.8603 -0.3848 0.3344 +vn 0.8684 -0.4329 0.2418 +vn 0.8519 -0.4580 0.2539 +vn 0.8731 -0.3293 0.3594 +vn 0.8390 -0.2873 0.4621 +vn 0.7250 -0.0361 0.6878 +vn 0.7377 0.0295 0.6745 +vn 0.4541 0.3203 0.8314 +vn 0.3648 0.2343 0.9011 +vn 0.2126 -0.2579 0.9425 +vn 0.1993 -0.4208 0.8850 +vn 0.2433 -0.1612 0.9565 +vn 0.2567 0.0221 0.9662 +vn 0.2659 0.0074 0.9640 +vn 0.1038 0.2161 0.9708 +vn 0.0578 0.2967 0.9532 +vn 0.0493 0.2470 0.9678 +vn 0.0910 0.2188 0.9715 +vn 0.1546 0.1669 0.9738 +vn 0.3353 -0.1569 0.9290 +vn 0.3413 -0.2318 0.9109 +vn 0.3444 -0.0470 0.9376 +vn 0.1949 -0.0721 0.9782 +vn 0.1772 0.2669 0.9473 +vn 0.2240 0.0060 0.9746 +vn 0.2996 0.3469 0.8887 +vn 0.3693 0.1412 0.9185 +vn 0.5413 0.1771 0.8220 +vn 0.6039 -0.0037 0.7971 +vn 0.7478 -0.2384 0.6197 +vn 0.7795 -0.2324 0.5818 +vn 0.8218 -0.2918 0.4894 +vn 0.8821 -0.3039 0.3599 +vn 0.8873 -0.2985 0.3516 +vn 0.8848 -0.3786 0.2717 +vn 0.8327 -0.4116 0.3704 +vn 0.8209 -0.4865 0.2989 +vn 0.8225 -0.4595 0.3351 +vn 0.9306 -0.1440 0.3367 +vn 0.9360 -0.1288 0.3277 +vn 0.9033 -0.0158 0.4288 +vn 0.8661 0.2185 0.4495 +vn 0.8497 0.2920 0.4390 +vn 0.7798 0.3659 0.5079 +vn 0.6357 0.5744 0.5157 +vn 0.6606 0.5223 0.5393 +vn 0.5580 0.6579 0.5059 +vn 0.5629 0.6267 0.5389 +vn 0.4879 0.6644 0.5662 +vn 0.4376 0.6922 0.5740 +vn 0.6110 0.5485 0.5708 +vn 0.6496 0.4880 0.5829 +vn 0.7957 0.2483 0.5525 +vn 0.8582 0.0047 0.5133 +vn 0.8984 -0.0975 0.4282 +vn 0.8798 -0.2401 0.4102 +vn 0.8730 -0.1527 0.4633 +vn 0.8287 -0.2309 0.5099 +vn 0.7775 -0.5247 0.3465 +vn 0.8195 -0.4578 0.3449 +vn 0.8340 -0.2831 0.4736 +vn 0.8218 -0.2896 0.4906 +vn 0.6833 -0.2029 0.7014 +vn 0.4177 0.0413 0.9077 +vn 0.6021 -0.1223 0.7890 +vn 0.3649 0.1261 0.9225 +vn 0.2646 0.2185 0.9393 +vn 0.2664 0.2191 0.9386 +vn 0.3290 0.3414 0.8805 +vn 0.1655 0.3618 0.9174 +vn 0.3483 0.2157 0.9122 +vn 0.6583 0.1218 0.7428 +vn 0.5117 0.0445 0.8580 +vn 0.1259 0.1249 0.9841 +vn 0.3844 -0.1851 0.9044 +vn 0.0312 0.1725 0.9845 +vn 0.0511 0.1555 0.9865 +vn 0.5752 0.0575 0.8160 +vn 0.3843 0.3109 0.8693 +vn 0.5364 0.1763 0.8254 +vn 0.6107 0.3910 0.6887 +vn 0.7255 0.2606 0.6370 +vn 0.5422 0.4001 0.7389 +vn 0.7254 0.3218 0.6085 +vn 0.3011 0.1672 0.9388 +vn 0.3574 0.2103 0.9100 +vn 0.6286 -0.1079 0.7702 +vn 0.7456 -0.2342 0.6238 +vn 0.7930 -0.2646 0.5488 +vn 0.6613 -0.2889 0.6923 +vn 0.7553 -0.3219 0.5708 +vn 0.8112 -0.3659 0.4562 +vn 0.6883 -0.6605 0.2999 +vn 0.7218 -0.5793 0.3786 +vn 0.6107 -0.6444 0.4602 +vn 0.5106 -0.7842 0.3526 +vn 0.6422 -0.6950 0.3234 +vn 0.6211 -0.7371 0.2664 +vn 0.7109 -0.6424 0.2862 +vn 0.7296 -0.5882 0.3489 +vn 0.7463 -0.4793 0.4618 +vn 0.6821 -0.1906 0.7060 +vn 0.5247 -0.2459 0.8150 +vn 0.4588 0.0002 0.8886 +vn 0.3410 0.0734 0.9372 +vn 0.4385 -0.3238 0.8384 +vn 0.4606 -0.1663 0.8719 +vn 0.4422 -0.2237 0.8686 +vn 0.4836 -0.1255 0.8663 +vn 0.5901 -0.0604 0.8051 +vn 0.6666 -0.1921 0.7203 +vn 0.5382 -0.8159 0.2113 +vn 0.3123 -0.8554 0.4133 +vn 0.1277 -0.9616 0.2430 +vn 0.2514 -0.9581 0.1369 +vn 0.7203 -0.5352 0.4413 +vn 0.6676 -0.6433 0.3748 +vn 0.5073 -0.1326 0.8515 +vn 0.2735 -0.1237 0.9539 +vn 0.3298 -0.4255 0.8427 +vn 0.2818 -0.2522 0.9257 +vn -0.0032 0.1197 0.9928 +vn 0.1930 0.0058 0.9812 +vn 0.1860 -0.3309 0.9252 +vn 0.0601 0.0361 0.9975 +vn 0.5467 -0.0409 0.8364 +vn 0.3524 0.1284 0.9270 +vn 0.7197 0.0031 0.6943 +vn 0.8430 0.2759 0.4618 +vn 0.7094 0.3821 0.5923 +vn 0.6517 0.3591 0.6680 +vn 0.4638 0.5074 0.7262 +vn 0.5040 0.5378 0.6759 +vn 0.7853 0.3659 0.4994 +vn 0.7129 -0.0248 0.7008 +vn 0.7554 0.1686 0.6332 +vn 0.7043 -0.0883 0.7044 +vn 0.5969 -0.3556 0.7192 +vn 0.6036 0.3670 0.7078 +vn 0.9659 0.1420 0.2164 +vn 0.0025 0.3591 0.9333 +vn 0.5819 0.6728 -0.4569 +vn 0.5526 0.7647 -0.3315 +vn 0.0491 0.9521 -0.3018 +vn 0.0029 0.2800 -0.9600 +vn 0.9908 -0.0310 0.1316 +vn 0.9955 0.0259 0.0910 +vn 0.9788 -0.1262 0.1613 +vn 0.1429 0.4500 -0.8815 +vn 0.0763 -0.4269 0.9011 +vn 0.7483 -0.3212 0.5804 +vn 0.5293 -0.6198 0.5794 +vn 0.0670 -0.9756 0.2089 +vn 0.0277 -0.9219 0.3865 +vn 0.0955 -0.8086 0.5805 +vn 0.2606 -0.8011 0.5388 +vn 0.1037 -0.2454 0.9639 +vn 0.1132 -0.5873 0.8014 +vn 0.2753 -0.4421 0.8537 +vn 0.0728 -0.1708 0.9826 +vn 0.2424 -0.5003 0.8312 +vn 0.9048 -0.0150 0.4255 +vn 0.5124 -0.7308 0.4510 +usemtl BioID-Blue +s off +f 1265//1 1841//1 1821//1 +f 347//2 348//2 5//2 +f 4//3 347//3 894//3 +f 347//4 4//4 348//4 +f 1842//5 347//5 5//5 +f 9//6 11//6 10//6 +f 13//7 14//7 15//7 +f 16//8 17//8 13//8 +f 17//9 18//9 13//9 +f 18//10 19//10 13//10 +f 19//11 20//11 13//11 +f 21//12 22//12 23//12 +f 21//13 24//13 59//13 +f 25//14 26//14 27//14 +f 27//15 28//15 25//15 +f 29//16 26//16 25//16 +f 32//17 31//17 30//17 +f 571//18 570//18 34//18 +f 38//19 39//19 40//19 +f 41//20 42//20 43//20 +f 44//21 41//21 45//21 +f 41//22 46//22 45//22 +f 54//23 51//23 52//23 +f 53//24 54//24 52//24 +f 57//25 58//25 16//25 +f 63//26 64//26 62//26 +f 69//27 68//27 67//27 +f 68//28 69//28 70//28 +f 78//29 66//29 65//29 +f 140//30 141//30 79//30 +f 79//31 80//31 140//31 +f 138//32 80//32 81//32 +f 81//33 82//33 138//33 +f 83//34 84//34 134//34 +f 85//35 134//35 84//35 +f 84//36 86//36 85//36 +f 132//37 85//37 86//37 +f 86//38 87//38 132//38 +f 126//39 90//39 91//39 +f 91//40 92//40 126//40 +f 92//41 44//41 126//41 +f 93//42 95//42 43//42 +f 94//43 96//43 95//43 +f 95//44 46//44 43//44 +f 95//45 98//45 37//45 +f 37//46 46//46 95//46 +f 99//47 37//47 98//47 +f 99//48 100//48 121//48 +f 101//49 121//49 100//49 +f 100//50 407//50 102//50 +f 407//51 104//51 103//51 +f 104//52 36//52 106//52 +f 107//53 35//53 36//53 +f 110//54 35//54 109//54 +f 110//55 411//55 111//55 +f 111//56 106//56 110//56 +f 112//57 105//57 111//57 +f 116//58 118//58 117//58 +f 117//59 119//59 101//59 +f 118//60 120//60 119//60 +f 121//61 101//61 119//61 +f 122//62 123//62 120//62 +f 120//63 45//63 37//63 +f 123//64 44//64 45//64 +f 123//65 126//65 44//65 +f 90//66 126//66 127//66 +f 89//67 90//67 128//67 +f 88//68 89//68 128//68 +f 128//69 422//69 88//69 +f 129//70 130//70 395//70 +f 87//71 395//71 130//71 +f 132//72 87//72 130//72 +f 131//73 133//73 132//73 +f 133//74 134//74 85//74 +f 85//75 132//75 133//75 +f 135//76 137//76 83//76 +f 138//77 139//77 80//77 +f 140//78 80//78 139//78 +f 140//79 142//79 141//79 +f 66//80 141//80 142//80 +f 144//81 143//81 142//81 +f 1147//82 143//82 144//82 +f 146//83 1409//83 468//83 +f 501//84 149//84 500//84 +f 502//85 149//85 501//85 +f 151//86 152//86 150//86 +f 151//87 531//87 152//87 +f 157//88 158//88 156//88 +f 160//89 161//89 159//89 +f 162//90 163//90 161//90 +f 164//91 165//91 163//91 +f 177//92 178//92 176//92 +f 178//93 180//93 179//93 +f 181//94 182//94 180//94 +f 183//95 184//95 182//95 +f 183//96 185//96 184//96 +f 186//97 187//97 185//97 +f 187//98 188//98 189//98 +f 188//99 190//99 189//99 +f 190//100 188//100 10//100 +f 15//101 9//101 10//101 +f 192//102 9//102 191//102 +f 192//103 193//103 194//103 +f 195//104 196//104 197//104 +f 197//105 198//105 199//105 +f 204//106 21//106 60//106 +f 59//107 60//107 21//107 +f 21//108 204//108 1015//108 +f 205//109 22//109 21//109 +f 207//110 23//110 22//110 +f 210//111 211//111 207//111 +f 212//112 210//112 213//112 +f 214//113 212//113 213//113 +f 213//114 215//114 214//114 +f 215//115 217//115 216//115 +f 219//116 218//116 220//116 +f 219//117 62//117 64//117 +f 221//118 222//118 219//118 +f 221//119 223//119 224//119 +f 224//120 225//120 221//120 +f 29//121 226//121 224//121 +f 227//122 25//122 228//122 +f 228//123 28//123 229//123 +f 231//124 229//124 230//124 +f 232//125 233//125 229//125 +f 238//126 239//126 236//126 +f 241//127 242//127 239//127 +f 245//128 246//128 241//128 +f 245//129 957//129 247//129 +f 245//130 247//130 246//130 +f 250//131 248//131 249//131 +f 249//132 251//132 250//132 +f 251//133 254//133 252//133 +f 255//134 252//134 254//134 +f 253//135 256//135 254//135 +f 255//136 258//136 257//136 +f 258//137 259//137 257//137 +f 268//138 257//138 259//138 +f 266//139 268//139 259//139 +f 259//140 260//140 261//140 +f 798//141 263//141 262//141 +f 265//142 263//142 48//142 +f 266//143 262//143 306//143 +f 267//144 269//144 268//144 +f 268//145 270//145 257//145 +f 271//146 257//146 270//146 +f 270//147 272//147 271//147 +f 271//148 273//148 252//148 +f 273//149 274//149 250//149 +f 250//150 252//150 273//150 +f 272//151 275//151 274//151 +f 247//152 274//152 275//152 +f 276//153 246//153 275//153 +f 246//154 247//154 275//154 +f 244//155 241//155 246//155 +f 244//156 277//156 242//156 +f 242//157 279//157 278//157 +f 278//158 279//158 237//158 +f 278//159 239//159 242//159 +f 237//160 280//160 281//160 +f 281//161 234//161 237//161 +f 233//162 232//162 234//162 +f 233//163 282//163 229//163 +f 282//164 227//164 228//164 +f 228//165 229//165 282//165 +f 282//166 283//166 227//166 +f 283//167 284//167 226//167 +f 226//168 227//168 283//168 +f 285//169 225//169 284//169 +f 225//170 226//170 284//170 +f 286//171 222//171 285//171 +f 30//172 31//172 289//172 +f 289//173 290//173 30//173 +f 290//174 291//174 30//174 +f 292//175 30//175 291//175 +f 291//176 293//176 292//176 +f 295//177 296//177 294//177 +f 298//178 300//178 301//178 +f 304//179 303//180 333//181 +f 333//182 334//182 304//182 +f 306//183 262//183 265//183 +f 334//184 308//184 267//184 +f 267//185 308//185 269//185 +f 308//186 309//186 269//186 +f 272//187 310//187 311//187 +f 312//188 311//188 313//188 +f 313//189 314//189 312//189 +f 314//190 272//190 312//190 +f 314//191 313//191 315//191 +f 315//192 276//192 314//192 +f 317//193 318//193 277//193 +f 319//194 279//194 318//194 +f 318//195 242//195 277//195 +f 279//196 320//196 280//196 +f 280//197 320//197 321//197 +f 321//198 281//198 280//198 +f 282//199 281//199 322//199 +f 322//200 283//200 282//200 +f 323//201 324//201 283//201 +f 324//202 325//202 284//202 +f 324//203 287//203 325//203 +f 324//204 323//204 290//204 +f 323//205 320//205 326//205 +f 319//206 326//206 320//206 +f 297//207 327//207 318//207 +f 318//208 317//208 297//208 +f 328//209 315//209 313//209 +f 313//210 329//210 328//210 +f 329//211 313//211 311//211 +f 311//212 330//212 329//212 +f 331//213 330//213 311//213 +f 311//214 332//214 331//214 +f 302//215 331//215 332//215 +f 333//216 302//216 332//216 +f 306//217 1//217 334//217 +f 330//218 335//218 301//218 +f 301//219 329//219 330//219 +f 327//220 326//220 319//220 +f 291//221 290//221 326//221 +f 290//222 288//222 324//222 +f 307//223 305//223 265//223 +f 1//224 873//224 334//224 +f 871//225 301//225 335//225 +f 296//226 336//226 294//226 +f 30//227 292//227 32//227 +f 872//228 341//228 340//228 +f 350//229 351//229 55//229 +f 349//230 352//230 350//230 +f 351//231 353//231 55//231 +f 354//232 55//232 353//232 +f 356//233 901//233 357//233 +f 901//234 377//234 357//234 +f 901//235 54//235 53//235 +f 360//236 361//236 359//236 +f 50//237 361//237 360//237 +f 360//238 362//238 49//238 +f 362//239 364//239 49//239 +f 49//240 365//240 366//240 +f 366//241 50//241 49//241 +f 366//242 368//242 50//242 +f 368//243 369//243 370//243 +f 370//244 361//244 368//244 +f 372//245 359//245 361//245 +f 373//246 375//246 374//246 +f 376//247 377//247 52//247 +f 357//248 67//248 379//248 +f 379//249 356//249 357//249 +f 379//250 355//250 356//250 +f 1072//251 354//251 355//251 +f 380//252 67//252 378//252 +f 381//253 70//253 69//253 +f 382//254 71//254 381//254 +f 382//255 72//255 71//255 +f 383//256 73//256 72//256 +f 384//257 74//257 73//257 +f 385//258 77//258 76//258 +f 84//259 391//259 392//259 +f 392//260 393//260 84//260 +f 86//261 84//261 393//261 +f 393//262 394//262 86//262 +f 396//263 400//263 88//263 +f 400//264 398//264 89//264 +f 398//265 47//265 91//265 +f 399//266 94//266 93//266 +f 93//267 47//267 399//267 +f 399//268 400//268 401//268 +f 401//269 94//269 399//269 +f 403//270 94//270 402//270 +f 405//271 97//271 96//271 +f 406//272 408//272 407//272 +f 408//273 104//273 407//273 +f 40//274 108//274 36//274 +f 109//275 107//275 409//275 +f 411//276 110//276 410//276 +f 412//277 111//277 411//277 +f 34//278 116//278 413//278 +f 34//279 414//279 116//279 +f 118//280 116//280 414//280 +f 414//281 124//281 122//281 +f 122//282 118//282 414//282 +f 415//283 125//283 124//283 +f 416//284 127//284 126//284 +f 418//285 90//285 127//285 +f 128//286 419//286 420//286 +f 422//287 420//287 421//287 +f 421//288 423//288 422//288 +f 131//289 424//289 425//289 +f 425//290 426//290 131//290 +f 133//291 131//291 426//291 +f 426//292 427//292 133//292 +f 428//293 133//293 427//293 +f 428//294 429//294 430//294 +f 430//295 431//295 136//295 +f 432//296 136//296 431//296 +f 139//297 432//297 433//297 +f 433//298 434//298 139//298 +f 434//299 144//299 139//299 +f 144//300 142//300 140//300 +f 140//301 139//301 144//301 +f 144//302 434//302 145//302 +f 469//303 145//303 434//303 +f 434//304 433//304 469//304 +f 435//305 469//305 433//305 +f 433//306 436//306 435//306 +f 437//307 435//307 436//307 +f 436//308 438//308 437//308 +f 427//309 440//309 429//309 +f 440//310 427//310 441//310 +f 441//311 442//311 440//311 +f 441//312 426//312 443//312 +f 444//313 443//313 426//313 +f 426//314 425//314 444//314 +f 445//315 444//315 425//315 +f 445//316 423//316 446//316 +f 421//317 447//317 423//317 +f 421//318 420//318 448//318 +f 418//319 449//319 419//319 +f 127//320 417//320 418//320 +f 417//321 127//321 416//321 +f 416//322 454//322 417//322 +f 570//323 569//323 450//323 +f 569//324 452//324 450//324 +f 450//325 452//325 453//325 +f 416//326 453//326 454//326 +f 417//327 482//327 449//327 +f 449//328 418//328 417//328 +f 457//329 447//329 456//329 +f 447//330 458//330 459//330 +f 459//331 423//331 447//331 +f 446//332 460//332 461//332 +f 461//333 445//333 446//333 +f 461//334 463//334 444//334 +f 443//335 444//335 463//335 +f 463//336 442//336 443//336 +f 464//337 440//337 442//337 +f 465//338 438//338 439//338 +f 467//339 468//339 437//339 +f 437//340 468//340 435//340 +f 469//341 435//341 468//341 +f 467//342 466//342 471//342 +f 465//343 471//343 466//343 +f 473//344 439//344 464//344 +f 474//345 473//345 464//345 +f 464//346 475//346 474//346 +f 476//347 475//347 463//347 +f 476//348 463//348 462//348 +f 461//349 478//349 462//349 +f 459//350 479//350 460//350 +f 456//351 481//351 457//351 +f 449//352 482//352 455//352 +f 454//353 486//353 482//353 +f 453//354 483//354 454//354 +f 487//355 482//355 486//355 +f 488//356 455//356 487//356 +f 489//357 457//357 481//357 +f 480//358 490//358 491//358 +f 491//359 479//359 480//359 +f 479//360 492//360 493//360 +f 493//361 478//361 479//361 +f 477//362 462//362 478//362 +f 495//363 496//363 477//363 +f 496//364 498//364 474//364 +f 499//365 474//365 498//365 +f 500//366 499//366 498//366 +f 148//367 473//367 499//367 +f 472//368 471//368 473//368 +f 472//369 147//369 470//369 +f 471//370 472//370 470//370 +f 501//371 498//371 497//371 +f 497//372 502//372 501//372 +f 497//373 503//373 502//373 +f 495//374 504//374 503//374 +f 493//375 505//375 494//375 +f 492//376 506//376 505//376 +f 491//377 507//377 492//377 +f 490//378 480//378 489//378 +f 489//379 508//379 490//379 +f 510//380 511//380 509//380 +f 515//381 483//381 485//381 +f 514//382 483//382 515//382 +f 513//383 518//383 512//383 +f 512//384 510//384 513//384 +f 511//385 510//385 512//385 +f 520//386 509//386 511//386 +f 520//387 521//387 522//387 +f 522//388 508//388 520//388 +f 522//389 523//389 524//389 +f 524//390 507//390 522//390 +f 522//391 490//391 508//391 +f 526//392 505//392 506//392 +f 527//393 494//393 505//393 +f 504//394 528//394 529//394 +f 530//395 502//395 503//395 +f 151//396 503//396 529//396 +f 528//397 531//397 151//397 +f 528//398 527//398 526//398 +f 526//399 532//399 528//399 +f 523//400 535//400 534//400 +f 522//401 536//401 523//401 +f 520//402 537//402 521//402 +f 538//403 511//403 519//403 +f 539//404 512//404 518//404 +f 518//405 540//405 539//405 +f 518//406 513//406 517//406 +f 515//407 33//407 517//407 +f 33//408 516//408 541//408 +f 542//409 543//409 33//409 +f 543//410 517//410 33//410 +f 544//411 518//411 543//411 +f 546//412 539//412 540//412 +f 538//413 546//413 547//413 +f 537//414 548//414 549//414 +f 549//415 521//415 537//415 +f 535//416 523//416 536//416 +f 535//417 551//417 552//417 +f 552//418 534//418 535//418 +f 553//419 532//419 533//419 +f 532//420 553//420 153//420 +f 153//421 528//421 532//421 +f 553//422 155//422 154//422 +f 553//423 157//423 155//423 +f 552//424 554//424 157//424 +f 551//425 555//425 554//425 +f 556//426 535//426 550//426 +f 550//427 557//427 556//427 +f 548//428 559//428 558//428 +f 547//429 561//429 548//429 +f 561//430 547//430 546//430 +f 546//431 562//431 561//431 +f 544//432 563//432 545//432 +f 544//433 543//433 542//433 +f 542//434 564//434 563//434 +f 541//435 566//435 683//435 +f 568//436 567//436 484//436 +f 681//437 568//437 569//437 +f 571//438 681//438 570//438 +f 572//439 573//439 571//439 +f 572//440 114//440 574//440 +f 574//441 573//441 572//441 +f 574//442 575//442 573//442 +f 575//443 576//443 577//443 +f 577//444 711//444 575//444 +f 577//445 578//445 711//445 +f 579//446 580//446 581//446 +f 581//447 582//447 579//447 +f 583//448 585//448 586//448 +f 586//449 584//449 583//449 +f 586//450 587//450 584//450 +f 588//451 589//451 587//451 +f 586//452 591//452 590//452 +f 593//453 591//453 592//453 +f 593//454 595//454 594//454 +f 604//455 596//455 597//455 +f 597//456 99//456 604//456 +f 598//457 406//457 99//457 +f 598//458 408//458 406//458 +f 408//459 598//459 38//459 +f 38//460 598//460 39//460 +f 598//461 599//461 39//461 +f 597//462 600//462 599//462 +f 600//463 585//463 601//463 +f 601//464 599//464 600//464 +f 580//465 109//465 409//465 +f 410//466 579//466 577//466 +f 576//467 411//467 577//467 +f 577//468 411//468 410//468 +f 576//469 412//469 411//469 +f 114//470 113//470 574//470 +f 576//471 575//471 574//471 +f 581//472 601//472 602//472 +f 602//473 583//473 581//473 +f 97//474 605//474 604//474 +f 605//475 606//475 595//475 +f 595//476 604//476 605//476 +f 649//477 607//477 606//477 +f 594//478 606//478 607//478 +f 607//479 608//479 594//479 +f 610//480 611//480 588//480 +f 589//481 588//481 611//481 +f 611//482 612//482 589//482 +f 614//483 677//483 612//483 +f 612//484 613//484 614//484 +f 613//485 616//485 617//485 +f 617//486 619//486 620//486 +f 620//487 618//487 617//487 +f 621//488 623//488 624//488 +f 624//489 622//489 621//489 +f 625//490 624//490 626//490 +f 629//491 630//491 627//491 +f 631//492 633//492 630//492 +f 631//493 634//493 633//493 +f 380//494 633//494 634//494 +f 69//495 634//495 635//495 +f 382//496 381//496 635//496 +f 386//497 75//497 74//497 +f 386//498 662//498 637//498 +f 637//499 385//499 386//499 +f 639//500 388//500 638//500 +f 389//501 639//501 640//501 +f 640//502 641//502 389//502 +f 390//503 389//503 641//503 +f 641//504 642//504 390//504 +f 642//505 392//505 391//505 +f 393//506 643//506 394//506 +f 395//507 394//507 643//507 +f 396//508 395//508 643//508 +f 397//509 396//509 644//509 +f 645//510 401//510 400//510 +f 646//511 402//511 401//511 +f 649//512 405//512 403//512 +f 404//513 648//513 650//513 +f 650//514 651//514 607//514 +f 648//515 652//515 651//515 +f 652//516 653//516 609//516 +f 609//517 651//517 652//517 +f 653//518 654//518 610//518 +f 610//519 609//519 653//519 +f 611//520 610//520 654//520 +f 655//521 657//521 612//521 +f 613//522 612//522 657//522 +f 657//523 616//523 613//523 +f 619//524 617//524 616//524 +f 658//525 620//525 619//525 +f 623//526 659//526 660//526 +f 660//527 624//527 623//527 +f 660//528 626//528 624//528 +f 661//529 626//529 660//529 +f 661//530 662//530 629//530 +f 629//531 663//531 632//531 +f 632//532 631//532 629//532 +f 636//533 382//533 632//533 +f 632//534 384//534 636//534 +f 663//535 386//535 384//535 +f 659//536 638//536 637//536 +f 638//537 659//537 664//537 +f 664//538 639//538 638//538 +f 640//539 639//539 664//539 +f 664//540 641//540 640//540 +f 658//541 665//541 641//541 +f 666//542 655//542 667//542 +f 655//543 668//543 667//543 +f 670//544 643//544 669//544 +f 670//545 671//545 644//545 +f 653//546 671//546 670//546 +f 669//547 653//547 670//547 +f 668//548 654//548 643//548 +f 616//549 656//549 665//549 +f 659//550 623//550 664//550 +f 662//551 661//551 660//551 +f 663//552 629//552 662//552 +f 645//553 671//553 647//553 +f 375//554 630//554 633//554 +f 672//555 630//555 375//555 +f 673//556 625//556 628//556 +f 674//557 622//557 625//557 +f 622//558 674//558 675//558 +f 675//559 621//559 622//559 +f 615//560 617//560 618//560 +f 615//561 715//561 614//561 +f 582//562 584//562 587//562 +f 582//563 583//563 584//563 +f 582//564 2//564 578//564 +f 679//565 575//565 711//565 +f 575//566 710//566 573//566 +f 683//567 566//567 567//567 +f 683//568 684//568 541//568 +f 687//569 564//569 686//569 +f 564//570 687//570 688//570 +f 689//571 563//571 564//571 +f 690//572 545//572 563//572 +f 562//573 691//573 692//573 +f 692//574 561//574 562//574 +f 559//575 548//575 560//575 +f 695//576 556//576 557//576 +f 160//577 554//577 555//577 +f 554//578 160//578 158//578 +f 158//579 157//579 554//579 +f 555//580 162//580 160//580 +f 695//581 164//581 162//581 +f 694//582 697//582 696//582 +f 693//583 698//583 697//583 +f 692//584 698//584 693//584 +f 699//585 692//585 691//585 +f 700//586 699//586 691//586 +f 704//587 705//587 703//587 +f 704//588 688//588 687//588 +f 687//589 705//589 704//589 +f 686//590 707//590 706//590 +f 686//591 565//591 685//591 +f 685//592 707//592 686//592 +f 685//593 541//593 684//593 +f 683//594 709//594 684//594 +f 734//595 682//595 568//595 +f 573//596 710//596 680//596 +f 710//597 575//597 679//597 +f 712//598 582//598 587//598 +f 712//599 713//599 2//599 +f 716//600 615//600 676//600 +f 676//601 717//601 716//601 +f 675//602 719//602 718//602 +f 719//603 675//603 674//603 +f 674//604 720//604 719//604 +f 674//605 721//605 720//605 +f 673//606 722//606 721//606 +f 723//607 628//607 672//607 +f 672//608 724//608 723//608 +f 725//609 726//609 724//609 +f 726//610 723//610 724//610 +f 373//611 725//611 724//611 +f 726//612 727//612 722//612 +f 716//613 717//613 728//613 +f 728//614 715//614 716//614 +f 714//615 729//615 713//615 +f 713//616 730//616 731//616 +f 731//617 2//617 713//617 +f 679//618 768//618 766//618 +f 766//619 710//619 679//619 +f 733//620 680//620 710//620 +f 733//621 734//621 680//621 +f 734//622 735//622 682//622 +f 737//623 709//623 735//623 +f 684//624 738//624 708//624 +f 708//625 685//625 684//625 +f 707//626 740//626 741//626 +f 741//627 706//627 707//627 +f 705//628 742//628 743//628 +f 743//629 703//629 705//629 +f 745//630 702//630 703//630 +f 702//631 746//631 747//631 +f 747//632 700//632 702//632 +f 748//633 749//633 700//633 +f 749//634 698//634 699//634 +f 750//635 697//635 698//635 +f 750//636 751//636 168//636 +f 749//637 751//637 750//637 +f 748//638 752//638 751//638 +f 747//639 753//639 752//639 +f 746//640 754//640 753//640 +f 745//641 755//641 746//641 +f 744//642 743//642 742//642 +f 742//643 756//643 744//643 +f 741//644 758//644 742//644 +f 758//645 741//645 740//645 +f 740//646 759//646 758//646 +f 738//647 760//647 739//647 +f 737//648 762//648 738//648 +f 764//649 735//649 736//649 +f 765//650 736//650 735//650 +f 679//651 769//651 768//651 +f 769//652 679//652 732//652 +f 731//653 770//653 732//653 +f 730//654 818//654 731//654 +f 713//655 816//655 730//655 +f 729//656 714//656 728//656 +f 728//657 771//657 729//657 +f 771//658 728//658 717//658 +f 717//659 772//659 771//659 +f 776//660 777//660 809//660 +f 778//661 809//661 777//661 +f 777//662 779//662 778//662 +f 725//663 371//663 780//663 +f 725//664 372//664 371//664 +f 780//665 369//665 781//665 +f 780//666 371//666 369//666 +f 781//667 369//667 782//667 +f 783//668 782//668 369//668 +f 786//669 784//669 783//669 +f 783//670 366//670 786//670 +f 367//671 786//671 366//671 +f 367//672 785//672 786//672 +f 365//673 788//673 787//673 +f 364//674 789//674 365//674 +f 790//675 791//675 789//675 +f 793//676 796//676 790//676 +f 795//677 797//677 796//677 +f 796//678 798//678 799//678 +f 799//679 791//679 796//679 +f 791//680 799//680 800//680 +f 800//681 788//681 791//681 +f 788//682 800//682 801//682 +f 801//683 787//683 788//683 +f 802//684 803//684 785//684 +f 804//685 785//685 803//685 +f 804//686 805//686 784//686 +f 784//687 786//687 804//687 +f 806//688 784//688 805//688 +f 807//689 782//689 806//689 +f 778//690 781//690 807//690 +f 809//691 811//691 776//691 +f 775//692 776//692 811//692 +f 811//693 812//693 774//693 +f 774//694 775//694 811//694 +f 812//695 773//695 774//695 +f 814//696 772//696 773//696 +f 772//697 814//697 815//697 +f 815//698 771//698 772//698 +f 816//699 729//699 771//699 +f 819//700 770//700 818//700 +f 820//701 770//701 819//701 +f 821//702 732//702 770//702 +f 767//703 766//703 768//703 +f 822//704 733//704 766//704 +f 765//705 734//705 733//705 +f 764//706 736//706 765//706 +f 825//707 763//707 824//707 +f 761//708 826//708 827//708 +f 760//709 828//709 829//709 +f 760//710 738//710 761//710 +f 829//711 838//711 830//711 +f 830//712 759//712 829//712 +f 829//713 739//713 760//713 +f 759//714 830//714 831//714 +f 831//715 758//715 759//715 +f 833//716 756//716 757//716 +f 834//717 744//717 756//717 +f 754//718 746//718 755//718 +f 174//719 753//719 754//719 +f 172//720 752//720 753//720 +f 170//721 751//721 752//721 +f 754//722 175//722 174//722 +f 836//723 177//723 175//723 +f 835//724 834//724 833//724 +f 833//725 757//725 832//725 +f 832//726 831//726 830//726 +f 830//727 838//727 832//727 +f 828//728 840//728 839//728 +f 826//729 929//729 840//729 +f 825//730 842//730 826//730 +f 842//731 825//731 824//731 +f 824//732 843//732 842//732 +f 823//733 843//733 824//733 +f 823//734 765//734 822//734 +f 822//735 844//735 823//735 +f 767//736 845//736 822//736 +f 767//737 846//737 845//737 +f 768//738 921//738 846//738 +f 820//739 848//739 847//739 +f 851//740 816//740 815//740 +f 852//741 815//741 814//741 +f 812//742 853//742 814//742 +f 811//743 855//743 813//743 +f 811//744 810//744 855//744 +f 810//745 808//745 857//745 +f 808//746 807//746 858//746 +f 859//747 860//747 858//747 +f 861//748 859//748 858//748 +f 858//749 806//749 861//749 +f 861//750 862//750 859//750 +f 861//751 805//751 863//751 +f 805//752 804//752 863//752 +f 863//753 864//753 256//753 +f 865//754 256//754 864//754 +f 864//755 801//755 865//755 +f 866//756 801//756 800//756 +f 260//757 866//757 800//757 +f 260//758 800//758 799//758 +f 799//759 261//759 260//759 +f 263//760 798//760 797//760 +f 797//761 867//761 263//761 +f 867//762 876//762 868//762 +f 868//763 264//763 867//763 +f 868//764 874//764 264//764 +f 3//765 305//765 307//765 +f 870//766 871//766 335//766 +f 872//767 298//767 299//767 +f 336//768 872//768 339//768 +f 339//769 294//769 336//769 +f 338//770 337//770 294//770 +f 299//771 871//771 341//771 +f 870//772 304//772 891//772 +f 305//773 892//773 873//773 +f 1//774 305//774 873//774 +f 893//775 305//776 3//777 +f 307//778 869//778 894//778 +f 894//779 3//779 307//779 +f 869//780 874//780 4//780 +f 875//781 874//781 868//781 +f 879//782 877//782 878//782 +f 867//783 878//783 876//783 +f 878//784 867//784 795//784 +f 795//785 879//785 878//785 +f 879//786 793//786 880//786 +f 880//787 794//787 882//787 +f 883//788 884//788 880//788 +f 882//789 885//789 883//789 +f 363//790 358//790 882//790 +f 363//791 792//791 362//791 +f 882//792 358//792 885//792 +f 885//793 886//793 883//793 +f 886//794 884//794 883//794 +f 887//795 881//795 898//795 +f 887//796 888//796 876//796 +f 876//797 877//797 887//797 +f 888//798 889//798 868//798 +f 868//799 876//799 888//799 +f 875//800 868//800 889//800 +f 892//801 891//801 873//801 +f 873//802 891//802 304//802 +f 891//803 344//803 343//803 +f 892//804 893//804 346//804 +f 3//805 894//805 893//805 +f 889//806 888//806 903//806 +f 897//807 903//807 888//807 +f 888//808 887//808 897//808 +f 899//809 886//809 355//809 +f 900//810 355//810 886//810 +f 900//811 902//811 901//811 +f 902//812 358//812 54//812 +f 901//813 356//813 900//813 +f 903//814 349//814 889//814 +f 349//815 904//815 896//815 +f 896//816 889//816 349//816 +f 904//817 895//817 896//817 +f 895//818 348//818 890//818 +f 352//819 897//819 351//819 +f 353//820 351//820 897//820 +f 897//821 898//821 353//821 +f 353//822 899//822 354//822 +f 355//823 354//823 899//823 +f 342//824 341//824 871//824 +f 797//825 795//825 867//825 +f 253//826 862//826 861//826 +f 906//827 862//827 905//827 +f 907//828 860//828 906//828 +f 908//829 860//829 907//829 +f 907//830 909//830 908//830 +f 854//831 855//831 856//831 +f 912//832 813//832 854//832 +f 853//833 913//833 852//833 +f 914//834 851//834 852//834 +f 850//835 817//835 851//835 +f 916//836 819//836 850//836 +f 849//837 917//837 918//837 +f 919//838 848//838 918//838 +f 845//839 923//839 924//839 +f 844//840 925//840 926//840 +f 926//841 823//841 844//841 +f 926//842 927//842 843//842 +f 928//843 929//843 841//843 +f 929//844 930//844 840//844 +f 840//845 931//845 839//845 +f 931//846 932//846 838//846 +f 838//847 839//847 931//847 +f 932//848 832//848 838//848 +f 934//849 835//849 837//849 +f 935//850 837//850 933//850 +f 933//851 932//851 931//851 +f 931//852 936//852 933//852 +f 931//853 937//853 936//853 +f 938//854 928//854 939//854 +f 940//855 939//855 928//855 +f 940//856 942//856 941//856 +f 926//857 943//857 927//857 +f 945//858 946//858 925//858 +f 945//859 924//859 923//859 +f 948//860 921//860 947//860 +f 949//861 919//861 918//861 +f 918//862 27//862 949//862 +f 917//863 28//863 918//863 +f 916//864 951//864 917//864 +f 915//865 952//865 916//865 +f 914//866 952//866 915//866 +f 953//867 852//867 913//867 +f 913//868 853//868 912//868 +f 912//869 954//869 913//869 +f 909//870 240//870 955//870 +f 956//871 957//871 909//871 +f 909//872 907//872 956//872 +f 243//873 909//873 957//873 +f 956//874 906//874 248//874 +f 955//875 240//875 238//875 +f 238//876 911//876 955//876 +f 954//877 236//877 235//877 +f 235//878 913//878 954//878 +f 953//879 232//879 952//879 +f 952//880 231//880 951//880 +f 951//881 916//881 952//881 +f 230//882 229//882 28//882 +f 960//883 923//883 922//883 +f 945//884 923//884 960//884 +f 944//885 925//885 946//885 +f 963//886 943//886 962//886 +f 962//887 925//887 944//887 +f 964//888 966//888 965//888 +f 965//889 967//889 941//889 +f 967//890 969//890 968//890 +f 971//891 968//891 969//891 +f 971//892 972//892 973//892 +f 973//893 937//893 971//893 +f 937//894 973//894 974//894 +f 974//895 936//895 937//895 +f 181//896 935//896 974//896 +f 974//897 933//897 936//897 +f 181//898 178//898 935//898 +f 975//899 182//899 181//899 +f 972//900 976//900 975//900 +f 1019//901 976//901 970//901 +f 977//902 1019//902 970//902 +f 970//903 978//903 977//903 +f 969//904 967//904 978//904 +f 979//905 977//905 978//905 +f 980//906 979//906 978//906 +f 978//907 966//907 980//907 +f 981//908 962//908 944//908 +f 946//909 982//909 944//909 +f 983//910 985//910 982//910 +f 961//911 984//911 983//911 +f 960//912 984//912 961//912 +f 985//913 983//913 984//913 +f 958//914 988//914 959//914 +f 958//915 223//915 989//915 +f 220//916 988//916 989//916 +f 989//917 988//917 958//917 +f 989//918 223//918 220//918 +f 991//919 987//919 959//919 +f 987//920 991//920 986//920 +f 986//921 992//921 993//921 +f 993//922 948//922 986//922 +f 985//923 984//923 993//923 +f 997//924 998//924 996//924 +f 996//925 982//925 985//925 +f 999//926 963//926 981//926 +f 1000//927 19//927 999//927 +f 999//928 19//928 18//928 +f 1001//929 998//929 997//929 +f 997//930 1002//930 1001//930 +f 1003//931 1004//931 1002//931 +f 1004//932 1001//932 1002//932 +f 1003//933 997//933 996//933 +f 1005//934 1006//934 1003//934 +f 1009//935 988//935 990//935 +f 990//936 1010//936 1009//936 +f 218//937 1011//937 1010//937 +f 1011//938 217//938 215//938 +f 1011//939 213//939 1010//939 +f 210//940 1008//940 1009//940 +f 992//941 1008//941 210//941 +f 206//942 994//942 208//942 +f 206//943 22//943 1005//943 +f 205//944 1012//944 1006//944 +f 1007//945 1006//945 1013//945 +f 1007//946 200//946 1004//946 +f 1004//947 198//947 1001//947 +f 1001//948 196//948 1000//948 +f 196//949 195//949 1014//949 +f 1014//950 1000//950 196//950 +f 1012//951 201//951 1007//951 +f 1015//952 203//952 1012//952 +f 1012//953 205//953 1015//953 +f 18//954 980//954 966//954 +f 17//955 1016//955 979//955 +f 1016//956 1017//956 1018//956 +f 1018//957 979//957 1016//957 +f 1018//958 1017//958 1019//958 +f 1018//959 1019//959 977//959 +f 976//960 1019//960 1020//960 +f 1020//961 186//961 183//961 +f 1019//962 1021//962 1020//962 +f 1021//963 1019//963 1017//963 +f 1022//964 188//964 1023//964 +f 1023//965 1017//965 1022//965 +f 57//966 10//966 188//966 +f 1017//967 58//967 1022//967 +f 183//968 975//968 976//968 +f 367//969 366//969 365//969 +f 569//970 570//970 681//970 +f 567//971 566//971 485//971 +f 415//972 414//972 451//972 +f 451//973 416//973 415//973 +f 451//974 450//974 416//974 +f 145//975 1147//975 144//975 +f 414//976 34//976 570//976 +f 53//977 52//977 377//977 +f 51//978 54//978 359//978 +f 224//979 223//979 29//979 +f 1024//980 209//980 211//980 +f 211//981 214//981 1024//981 +f 1025//982 24//982 209//982 +f 24//983 207//983 209//983 +f 193//984 1014//984 195//984 +f 191//985 20//985 193//985 +f 46//986 37//986 45//986 +f 15//987 16//987 13//987 +f 1026//988 11//988 12//988 +f 1028//989 1029//989 1030//989 +f 1031//990 1032//990 1028//990 +f 1033//991 1031//991 1028//991 +f 1034//992 1033//992 1028//992 +f 1030//993 1034//993 1028//993 +f 1035//994 1036//994 1037//994 +f 1042//995 1898//995 1040//995 +f 1041//996 1043//996 1040//996 +f 1046//997 1519//997 1047//997 +f 1518//998 1519//998 1046//998 +f 1054//999 1055//999 1056//999 +f 1054//1000 1056//1000 1057//1000 +f 1054//1001 1058//1001 1059//1001 +f 1062//1002 1625//1002 1558//1002 +f 1067//1003 1069//1003 1066//1003 +f 1069//1004 1068//1004 1066//1004 +f 1071//1005 56//1005 1070//1005 +f 56//1006 1072//1006 1070//1006 +f 1073//1007 1074//1007 1032//1007 +f 62//1008 1075//1008 63//1008 +f 66//1009 1077//1009 65//1009 +f 1300//1010 67//1010 68//1010 +f 1078//1011 1322//1011 68//1011 +f 68//1012 1079//1012 1078//1012 +f 1080//1013 70//1013 71//1013 +f 72//1014 1080//1014 71//1014 +f 73//1015 1081//1015 72//1015 +f 75//1016 1082//1016 74//1016 +f 76//1017 1083//1017 75//1017 +f 65//1018 1083//1018 76//1018 +f 1085//1019 1086//1019 1077//1019 +f 1085//1020 1145//1020 1087//1020 +f 1088//1021 1089//1021 1331//1021 +f 1089//1022 1090//1022 1332//1022 +f 1139//1023 1092//1023 1091//1023 +f 1093//1024 1091//1024 1092//1024 +f 1092//1025 1137//1025 1093//1025 +f 1337//1026 1095//1026 1338//1026 +f 1096//1027 1097//1027 1340//1027 +f 1098//1028 1340//1028 1097//1028 +f 1097//1029 1131//1029 1098//1029 +f 1099//1030 1056//1030 1055//1030 +f 1055//1031 1060//1031 1099//1031 +f 1055//1032 1059//1032 1100//1032 +f 1100//1033 1102//1033 1101//1033 +f 1103//1034 1101//1034 1102//1034 +f 1102//1035 1104//1035 1103//1035 +f 1102//1036 1050//1036 1104//1036 +f 1058//1037 1050//1037 1102//1037 +f 1051//1038 1105//1038 1050//1038 +f 1051//1039 1106//1039 1105//1039 +f 1107//1040 1105//1040 1106//1040 +f 1106//1041 1108//1041 1107//1041 +f 1349//1042 1107//1042 1108//1042 +f 1110//1043 1349//1043 1109//1043 +f 1109//1044 1111//1044 1110//1044 +f 1111//1045 1049//1045 1110//1045 +f 1112//1046 1053//1046 1049//1046 +f 1049//1047 1048//1047 1112//1047 +f 1113//1048 1114//1048 1112//1048 +f 1048//1049 1115//1049 1113//1049 +f 1048//1050 1116//1050 1115//1050 +f 1049//1051 1117//1051 1116//1051 +f 1048//1052 1049//1052 1116//1052 +f 1118//1053 1119//1053 1117//1053 +f 1121//1054 1123//1054 1122//1054 +f 1124//1055 1122//1055 1123//1055 +f 1123//1056 1125//1056 1124//1056 +f 1126//1057 1124//1057 1125//1057 +f 1125//1058 1051//1058 1126//1058 +f 1106//1059 1051//1059 1125//1059 +f 1127//1060 1128//1060 1126//1060 +f 1126//1061 1057//1061 1127//1061 +f 1127//1062 1131//1062 1130//1062 +f 1131//1063 1097//1063 1132//1063 +f 1097//1064 1096//1064 1133//1064 +f 1133//1065 1134//1065 1364//1065 +f 1337//1066 1094//1066 1135//1066 +f 1135//1067 1137//1067 1136//1067 +f 1094//1068 1137//1068 1135//1068 +f 1138//1069 1136//1069 1137//1069 +f 1137//1070 1092//1070 1138//1070 +f 1140//1071 1142//1071 1141//1071 +f 1142//1072 1143//1072 1374//1072 +f 1145//1073 1085//1073 1146//1073 +f 1085//1074 1084//1074 1146//1074 +f 1077//1075 66//1075 1084//1075 +f 1146//1076 66//1076 143//1076 +f 1410//1077 145//1077 469//1077 +f 1409//1078 469//1078 468//1078 +f 1148//1079 1409//1079 146//1079 +f 470//1080 1148//1080 146//1080 +f 1442//1081 148//1081 149//1081 +f 1475//1082 149//1082 530//1082 +f 150//1083 1474//1083 530//1083 +f 153//1084 1476//1084 531//1084 +f 154//1085 1498//1085 153//1085 +f 169//1086 1150//1086 168//1086 +f 182//1087 1151//1087 180//1087 +f 184//1088 1152//1088 182//1088 +f 187//1089 1153//1089 185//1089 +f 187//1090 1154//1090 1155//1090 +f 190//1091 1154//1091 189//1091 +f 11//1092 1156//1092 190//1092 +f 1026//1093 192//1093 1158//1093 +f 1158//1094 1029//1094 1157//1094 +f 192//1095 1159//1095 1158//1095 +f 194//1096 1160//1096 1159//1096 +f 195//1097 1161//1097 1160//1097 +f 197//1098 1162//1098 1161//1098 +f 199//1099 1163//1099 1162//1099 +f 200//1100 1164//1100 1163//1100 +f 201//1101 1165//1101 1164//1101 +f 203//1102 1166//1102 1165//1102 +f 60//1103 59//1103 1166//1103 +f 59//1104 1035//1104 1039//1104 +f 1037//1105 1962//1105 1039//1105 +f 1168//1106 1169//1106 1167//1106 +f 1170//1107 1172//1107 1169//1107 +f 1173//1108 1174//1108 1175//1108 +f 64//1109 1177//1109 217//1109 +f 63//1110 1178//1110 1177//1110 +f 1180//1111 1181//1111 1075//1111 +f 1181//1112 1180//1112 1182//1112 +f 1182//1113 1184//1113 1181//1113 +f 1184//1114 1182//1114 1185//1114 +f 1185//1115 1043//1115 1184//1115 +f 1186//1116 1040//1116 1043//1116 +f 1188//1117 1190//1117 1189//1117 +f 1193//1118 1194//1118 1191//1118 +f 1195//1119 1193//1119 1196//1119 +f 1195//1120 1196//1120 1197//1120 +f 1197//1121 1198//1121 1195//1121 +f 1197//1122 1199//1122 1198//1122 +f 1200//1123 1202//1123 1199//1123 +f 1200//1124 1204//1124 1202//1124 +f 1207//1125 1209//1125 1208//1125 +f 1208//1126 1210//1126 1852//1126 +f 1210//1127 1213//1127 1212//1127 +f 1211//1128 1214//1128 1213//1128 +f 1215//1129 1212//1129 1213//1129 +f 1213//1130 1214//1130 1215//1130 +f 1216//1131 1229//1131 1218//1131 +f 1229//1132 1227//1132 1218//1132 +f 1220//1133 1218//1133 1227//1133 +f 1220//1134 1221//1134 1745//1134 +f 1222//1135 1745//1135 1221//1135 +f 1223//1136 1224//1136 1063//1136 +f 1063//1137 1225//1137 1223//1137 +f 1222//1138 1226//1138 1063//1138 +f 1221//1139 1227//1139 1266//1139 +f 1227//1140 1229//1140 1228//1140 +f 1230//1141 1228//1141 1229//1141 +f 1229//1142 1231//1142 1230//1142 +f 1216//1143 1232//1143 1231//1143 +f 1233//1144 1231//1144 1232//1144 +f 1232//1145 1234//1145 1233//1145 +f 1234//1146 1235//1146 1233//1146 +f 1211//1147 1209//1147 1234//1147 +f 1236//1148 1233//1148 1235//1148 +f 1235//1149 1206//1149 1236//1149 +f 1205//1150 1237//1150 1236//1150 +f 1206//1151 1205//1151 1236//1151 +f 1205//1152 1203//1152 1237//1152 +f 1203//1153 1238//1153 1237//1153 +f 1203//1154 1200//1154 1201//1154 +f 1239//1155 1197//1155 1196//1155 +f 1193//1156 1242//1156 1196//1156 +f 1188//1157 1187//1157 1243//1157 +f 1244//1158 1243//1158 1186//1158 +f 1186//1159 1185//1159 1244//1159 +f 1182//1160 1246//1160 1245//1160 +f 1185//1161 1182//1161 1245//1161 +f 1246//1162 1182//1162 1180//1162 +f 1075//1163 62//1163 1180//1163 +f 1247//1164 286//1164 287//1164 +f 288//1165 1248//1165 1247//1165 +f 288//1166 1249//1166 1248//1166 +f 289//1167 1044//1167 1249//1167 +f 31//1168 1044//1168 289//1168 +f 1249//1169 1044//1169 1250//1169 +f 1251//1170 1250//1170 1044//1170 +f 1044//1171 1252//1171 1251//1171 +f 1255//1172 1257//1172 1253//1172 +f 1258//1173 1259//1173 1260//1173 +f 1258//1174 1261//1174 1256//1174 +f 1294//1175 1262//1175 1291//1175 +f 1263//1176 1264//1177 7//1178 +f 8//1179 7//1179 1264//1179 +f 1265//1180 1226//1180 1266//1180 +f 1266//1181 1228//1181 8//1181 +f 1268//1182 8//1182 1228//1182 +f 1270//1183 1233//1183 1271//1183 +f 1273//1184 1274//1184 1272//1184 +f 1233//1185 1273//1185 1272//1185 +f 1237//1186 1275//1186 1273//1186 +f 1273//1187 1236//1187 1237//1187 +f 1238//1188 1277//1188 1275//1188 +f 1278//1189 1277//1189 1238//1189 +f 1240//1190 1279//1190 1278//1190 +f 1278//1191 1201//1191 1240//1191 +f 1240//1192 1280//1192 1279//1192 +f 1240//1193 1196//1193 1241//1193 +f 1242//1194 1281//1194 1241//1194 +f 1242//1195 1243//1195 1282//1195 +f 1282//1196 1244//1196 1283//1196 +f 1284//1197 1283//1197 1244//1197 +f 1244//1198 1245//1198 1284//1198 +f 1285//1199 1284//1199 1245//1199 +f 1247//1200 1248//1200 1285//1200 +f 1248//1201 1250//1201 1284//1201 +f 1283//1202 1280//1202 1281//1202 +f 1286//1203 1279//1203 1280//1203 +f 1287//1204 1257//1204 1278//1204 +f 1277//1205 1257//1205 1276//1205 +f 1276//1206 1255//1206 1288//1206 +f 1276//1207 1275//1207 1277//1207 +f 1288//1208 1261//1208 1289//1208 +f 1288//1209 1275//1209 1276//1209 +f 1289//1210 1274//1210 1288//1210 +f 1290//1211 1271//1211 1289//1211 +f 1290//1212 1291//1212 1271//1212 +f 1292//1213 1271//1213 1291//1213 +f 1291//1214 1262//1214 1292//1214 +f 1262//1215 7//1215 1292//1215 +f 1290//1216 1294//1216 1291//1216 +f 1289//1217 1260//1217 1290//1217 +f 1255//1218 1256//1218 1261//1218 +f 1257//1219 1287//1219 1251//1219 +f 1287//1220 1286//1220 1251//1220 +f 1250//1221 1251//1221 1286//1221 +f 1286//1222 1283//1222 1250//1222 +f 1820//1223 1061//1223 8//1223 +f 8//1224 1264//1224 1820//1224 +f 1260//1225 1818//1225 1294//1225 +f 1818//1226 1260//1226 1259//1226 +f 1256//1227 1295//1227 1258//1227 +f 1252//1228 1044//1228 31//1228 +f 31//1229 32//1229 1296//1229 +f 338//1230 1296//1230 337//1230 +f 1295//1231 338//1231 339//1231 +f 340//1232 1295//1232 339//1232 +f 341//1233 1819//1233 340//1233 +f 349//1234 350//1234 1297//1234 +f 1848//1235 1321//1235 1068//1235 +f 1069//1236 1848//1236 1068//1236 +f 1065//1237 1064//1237 1305//1237 +f 1306//1238 1305//1238 1064//1238 +f 1064//1239 1309//1239 1308//1239 +f 1312//1240 1313//1240 1730//1240 +f 1304//1241 1314//1241 1312//1241 +f 1314//1242 1315//1242 1313//1242 +f 1067//1243 1318//1243 1317//1243 +f 1319//1244 1317//1244 1318//1244 +f 1318//1245 1320//1245 1319//1245 +f 1318//1246 1066//1246 1320//1246 +f 1321//1247 1301//1247 1322//1247 +f 1301//1248 68//1248 1322//1248 +f 1072//1249 379//1249 1300//1249 +f 67//1250 1300//1250 379//1250 +f 1078//1251 1323//1251 1322//1251 +f 1078//1252 1079//1252 1324//1252 +f 1080//1253 1325//1253 1079//1253 +f 1082//1254 1326//1254 73//1254 +f 1327//1255 1328//1255 1083//1255 +f 1086//1256 1329//1256 1076//1256 +f 1086//1257 1085//1257 1087//1257 +f 1087//1258 1330//1258 1086//1258 +f 1332//1259 1333//1259 1587//1259 +f 1333//1260 1091//1260 1334//1260 +f 1091//1261 1093//1261 1335//1261 +f 1338//1262 1342//1262 1339//1262 +f 1341//1263 1342//1263 1340//1263 +f 1060//1264 1100//1264 1341//1264 +f 1101//1265 1343//1265 1341//1265 +f 1101//1266 1345//1266 1344//1266 +f 1345//1267 1346//1267 1344//1267 +f 1104//1268 1347//1268 1103//1268 +f 1050//1269 1105//1269 1348//1269 +f 1350//1270 1105//1270 1349//1270 +f 1110//1271 1350//1271 1349//1271 +f 1114//1272 1352//1272 1546//1272 +f 1113//1273 1115//1273 1352//1273 +f 1116//1274 1354//1274 1353//1274 +f 1119//1275 1521//1275 1355//1275 +f 1119//1276 1118//1276 1120//1276 +f 1120//1277 1118//1277 1356//1277 +f 1356//1278 1046//1278 1120//1278 +f 1122//1279 1046//1279 1356//1279 +f 1356//1280 1121//1280 1122//1280 +f 1357//1281 1046//1281 1122//1281 +f 1122//1282 1124//1282 1357//1282 +f 1357//1283 1129//1283 1358//1283 +f 1124//1284 1128//1284 1357//1284 +f 1130//1285 1131//1285 1391//1285 +f 1097//1286 1360//1286 1132//1286 +f 1133//1287 1361//1287 1360//1287 +f 1362//1288 1364//1288 1363//1288 +f 1365//1289 1363//1289 1364//1289 +f 1364//1290 1366//1290 1365//1290 +f 1366//1291 1136//1291 1367//1291 +f 1368//1292 1367//1292 1136//1292 +f 1136//1293 1138//1293 1368//1293 +f 1369//1294 1368//1294 1138//1294 +f 1138//1295 1370//1295 1369//1295 +f 1370//1296 1371//1296 1369//1296 +f 1141//1297 1374//1297 1373//1297 +f 1375//1298 1373//1298 1374//1298 +f 1374//1299 1376//1299 1375//1299 +f 1147//1300 1376//1300 1145//1300 +f 1145//1301 1146//1301 1147//1301 +f 1147//1302 1377//1302 1376//1302 +f 1377//1303 1378//1303 1375//1303 +f 1373//1304 1375//1304 1379//1304 +f 1379//1305 1380//1305 1373//1305 +f 1371//1306 1372//1306 1380//1306 +f 1371//1307 1381//1307 1382//1307 +f 1382//1308 1369//1308 1371//1308 +f 1383//1309 1384//1309 1385//1309 +f 1385//1310 1402//1310 1368//1310 +f 1367//1311 1368//1311 1402//1311 +f 1402//1312 1386//1312 1367//1312 +f 1388//1313 1363//1313 1365//1313 +f 1362//1314 1363//1314 1389//1314 +f 1390//1315 1360//1315 1361//1315 +f 1392//1316 1393//1316 1517//1316 +f 1393//1317 1392//1317 1391//1317 +f 1391//1318 1396//1318 1393//1318 +f 1361//1319 1397//1319 1390//1319 +f 1389//1320 1398//1320 1397//1320 +f 1398//1321 1389//1321 1388//1321 +f 1388//1322 1399//1322 1398//1322 +f 1400//1323 1365//1323 1387//1323 +f 1387//1324 1401//1324 1400//1324 +f 1386//1325 1401//1325 1387//1325 +f 1401//1326 1402//1326 1403//1326 +f 1386//1327 1402//1327 1401//1327 +f 1403//1328 1402//1328 1404//1328 +f 1402//1329 1385//1329 1404//1329 +f 1384//1330 1404//1330 1385//1330 +f 1382//1331 1405//1331 1384//1331 +f 1380//1332 1406//1332 1381//1332 +f 1381//1333 1371//1333 1380//1333 +f 1407//1334 1378//1334 1408//1334 +f 1409//1335 1408//1335 1378//1335 +f 1378//1336 1377//1336 1409//1336 +f 1409//1337 1148//1337 1408//1337 +f 1411//1338 1408//1338 1148//1338 +f 1407//1339 1408//1339 1411//1339 +f 1412//1340 1406//1340 1411//1340 +f 1412//1341 1413//1341 1406//1341 +f 1413//1342 1381//1342 1406//1342 +f 1413//1343 1414//1343 1405//1343 +f 1416//1344 1417//1344 1403//1344 +f 1418//1345 1401//1345 1403//1345 +f 1401//1346 1418//1346 1419//1346 +f 1419//1347 1400//1347 1401//1347 +f 1398//1348 1421//1348 1397//1348 +f 1422//1349 1390//1349 1397//1349 +f 1426//1350 1359//1350 1422//1350 +f 1423//1351 1393//1351 1396//1351 +f 1395//1352 1425//1352 1424//1352 +f 1424//1353 1517//1353 1395//1353 +f 1425//1354 1395//1354 1423//1354 +f 1396//1355 1426//1355 1423//1355 +f 1397//1356 1428//1356 1427//1356 +f 1427//1357 1422//1357 1397//1357 +f 1421//1358 1429//1358 1428//1358 +f 1428//1359 1397//1359 1421//1359 +f 1421//1360 1398//1360 1399//1360 +f 1399//1361 1430//1361 1421//1361 +f 1419//1362 1431//1362 1420//1362 +f 1432//1363 1419//1363 1418//1363 +f 1418//1364 1433//1364 1432//1364 +f 1417//1365 1434//1365 1433//1365 +f 1436//1366 1435//1366 1417//1366 +f 1417//1367 1416//1367 1436//1367 +f 1436//1368 1437//1368 1435//1368 +f 1436//1369 1438//1369 1437//1369 +f 1414//1370 1439//1370 1438//1370 +f 1442//1371 1439//1371 1441//1371 +f 1412//1372 1441//1372 1439//1372 +f 1440//1373 147//1373 1412//1373 +f 1411//1374 1440//1374 1412//1374 +f 1443//1375 1438//1375 1442//1375 +f 1444//1376 1437//1376 1443//1376 +f 1434//1377 1417//1377 1435//1377 +f 1446//1378 1433//1378 1434//1378 +f 1446//1379 1432//1379 1433//1379 +f 1449//1380 1420//1380 1431//1380 +f 1430//1381 1450//1381 1451//1381 +f 1451//1382 1421//1382 1430//1382 +f 1453//1383 1428//1383 1429//1383 +f 1454//1384 1427//1384 1428//1384 +f 1456//1385 1460//1385 1457//1385 +f 1453//1386 1461//1386 1455//1386 +f 1452//1387 1462//1387 1461//1387 +f 1463//1388 1464//1388 1462//1388 +f 1450//1389 1465//1389 1463//1389 +f 1450//1390 1466//1390 1465//1390 +f 1447//1391 1468//1391 1467//1391 +f 1446//1392 1469//1392 1447//1392 +f 1470//1393 1434//1393 1445//1393 +f 1445//1394 1471//1394 1470//1394 +f 1443//1395 1475//1395 1444//1395 +f 1477//1396 1469//1396 1471//1396 +f 1469//1397 1477//1397 1478//1397 +f 1468//1398 1478//1398 1479//1398 +f 1468//1399 1447//1399 1469//1399 +f 1479//1400 1467//1400 1468//1400 +f 1481//1401 1465//1401 1466//1401 +f 1464//1402 1463//1402 1465//1402 +f 1482//1403 1462//1403 1464//1403 +f 1483//1404 1455//1404 1461//1404 +f 1488//1405 1487//1405 1045//1405 +f 1488//1406 1457//1406 1460//1406 +f 1460//1407 1489//1407 1488//1407 +f 1491//1408 1461//1408 1482//1408 +f 1482//1409 1492//1409 1491//1409 +f 1493//1410 1464//1410 1481//1410 +f 1481//1411 1494//1411 1493//1411 +f 1479//1412 1496//1412 1480//1412 +f 1496//1413 1479//1413 1478//1413 +f 1478//1414 1497//1414 1496//1414 +f 1477//1415 1497//1415 1478//1415 +f 1498//1416 1471//1416 1476//1416 +f 1498//1417 154//1417 1497//1417 +f 1497//1418 156//1418 1499//1418 +f 1499//1419 1496//1419 1497//1419 +f 1495//1420 1500//1420 1501//1420 +f 1501//1421 1480//1421 1495//1421 +f 1494//1422 1502//1422 1503//1422 +f 1503//1423 1493//1423 1494//1423 +f 1492//1424 1505//1424 1506//1424 +f 1506//1425 1491//1425 1492//1425 +f 1490//1426 1507//1426 1508//1426 +f 1508//1427 1484//1427 1490//1427 +f 1489//1428 1510//1428 1487//1428 +f 1489//1429 1509//1429 1510//1429 +f 1513//1430 1486//1430 1487//1430 +f 1514//1431 1485//1431 1486//1431 +f 1516//1432 1424//1432 1515//1432 +f 1516//1433 1630//1433 1517//1433 +f 1630//1434 1519//1434 1518//1434 +f 1520//1435 1047//1435 1519//1435 +f 1520//1436 1521//1436 1047//1436 +f 1522//1437 1521//1437 1520//1437 +f 1657//1438 1524//1438 1522//1438 +f 1525//1439 1526//1439 1524//1439 +f 1527//1440 1528//1440 1526//1440 +f 1529//1441 1530//1441 1531//1441 +f 1532//1442 1534//1442 1531//1442 +f 1536//1443 1537//1443 1534//1443 +f 1538//1444 1534//1444 1537//1444 +f 1538//1445 1540//1445 1539//1445 +f 1543//1446 1550//1446 1544//1446 +f 1544//1447 1348//1447 1545//1447 +f 1105//1448 1545//1448 1348//1448 +f 1350//1449 1545//1449 1105//1449 +f 1545//1450 1546//1450 1544//1450 +f 1547//1451 1544//1451 1546//1451 +f 1546//1452 1352//1452 1547//1452 +f 1353//1453 1528//1453 1352//1453 +f 1354//1454 1523//1454 1524//1454 +f 1119//1455 1120//1455 1521//1455 +f 1047//1456 1120//1456 1046//1456 +f 1529//1457 1352//1457 1528//1457 +f 1548//1458 1531//1458 1533//1458 +f 1534//1459 1549//1459 1533//1459 +f 1549//1460 1539//1460 1533//1460 +f 1539//1461 1547//1461 1533//1461 +f 1551//1462 1104//1462 1550//1462 +f 1551//1463 1552//1463 1595//1463 +f 1550//1464 1542//1464 1551//1464 +f 1553//1465 1595//1465 1552//1465 +f 1552//1466 1541//1466 1553//1466 +f 1554//1467 1553//1467 1541//1467 +f 1555//1468 1536//1468 1556//1468 +f 1557//1469 1556//1469 1536//1469 +f 1536//1470 1062//1470 1557//1470 +f 1062//1471 1558//1471 1557//1471 +f 1558//1472 1602//1472 1557//1472 +f 1625//1473 1559//1473 1558//1473 +f 1560//1474 1558//1474 1559//1474 +f 1561//1475 1563//1475 1560//1475 +f 1564//1476 1566//1476 1561//1476 +f 1566//1477 1564//1477 1567//1477 +f 1567//1478 1569//1478 1566//1478 +f 1568//1479 1570//1479 1567//1479 +f 1572//1480 1571//1480 1573//1480 +f 1573//1481 1574//1481 1575//1481 +f 1573//1482 1576//1482 1572//1482 +f 1575//1483 1576//1483 1573//1483 +f 1575//1484 1577//1484 1576//1484 +f 1324//1485 1577//1485 1578//1485 +f 1578//1486 1323//1486 1324//1486 +f 1079//1487 1325//1487 1580//1487 +f 1325//1488 1081//1488 1581//1488 +f 1327//1489 1582//1489 1328//1489 +f 1327//1490 1076//1490 1329//1490 +f 1329//1491 1582//1491 1327//1491 +f 1583//1492 1086//1492 1330//1492 +f 1584//1493 1331//1493 1585//1493 +f 1586//1494 1585//1494 1331//1494 +f 1331//1495 1332//1495 1586//1495 +f 1586//1496 1587//1496 1612//1496 +f 1587//1497 1334//1497 1614//1497 +f 1334//1498 1335//1498 1615//1498 +f 1336//1499 1337//1499 1589//1499 +f 1337//1500 1338//1500 1589//1500 +f 1589//1501 1338//1501 1590//1501 +f 1338//1502 1339//1502 1590//1502 +f 1339//1503 1342//1503 1591//1503 +f 1592//1504 1593//1504 1591//1504 +f 1592//1505 1344//1505 1346//1505 +f 1346//1506 1594//1506 1592//1506 +f 1346//1507 1345//1507 1595//1507 +f 1595//1508 1596//1508 1346//1508 +f 1594//1509 1346//1509 1596//1509 +f 1596//1510 1597//1510 1594//1510 +f 1598//1511 1594//1511 1597//1511 +f 1597//1512 1555//1512 1598//1512 +f 1555//1513 1556//1513 1599//1513 +f 1556//1514 1601//1514 1600//1514 +f 1604//1515 1602//1515 1603//1515 +f 1558//1516 1560//1516 1603//1516 +f 1603//1517 1563//1517 1613//1517 +f 1566//1518 1605//1518 1565//1518 +f 1569//1519 1611//1519 1605//1519 +f 1570//1520 1607//1520 1569//1520 +f 1572//1521 1608//1521 1607//1521 +f 1608//1522 1572//1522 1576//1522 +f 1577//1523 1579//1523 1576//1523 +f 1325//1524 1581//1524 1579//1524 +f 1326//1525 1579//1525 1581//1525 +f 1581//1526 1081//1526 1326//1526 +f 1610//1527 1328//1527 1609//1527 +f 1582//1528 1606//1528 1607//1528 +f 1583//1529 1606//1529 1582//1529 +f 1582//1530 1329//1530 1583//1530 +f 1584//1531 1611//1531 1583//1531 +f 1584//1532 1585//1532 1611//1532 +f 1611//1533 1586//1533 1605//1533 +f 1612//1534 1605//1534 1586//1534 +f 1615//1535 1588//1535 1616//1535 +f 1589//1536 1618//1536 1617//1536 +f 1593//1537 1592//1537 1594//1537 +f 1619//1538 1599//1538 1618//1538 +f 1599//1539 1617//1539 1618//1539 +f 1600//1540 1616//1540 1588//1540 +f 1616//1541 1602//1541 1615//1541 +f 1604//1542 1613//1542 1614//1542 +f 1619//1543 1591//1543 1593//1543 +f 1578//1544 1319//1544 1320//1544 +f 1575//1545 1319//1545 1578//1545 +f 1575//1546 1620//1546 1319//1546 +f 1571//1547 1621//1547 1574//1547 +f 1574//1548 1573//1548 1571//1548 +f 1571//1549 1622//1549 1621//1549 +f 1568//1550 1622//1550 1571//1550 +f 1623//1551 1567//1551 1564//1551 +f 1564//1552 1624//1552 1623//1552 +f 1625//1553 1660//1553 1559//1553 +f 1626//1554 1062//1554 1535//1554 +f 1532//1555 1530//1555 1535//1555 +f 1530//1556 1527//1556 1525//1556 +f 1522//1557 1628//1557 1657//1557 +f 1519//1558 1629//1558 1520//1558 +f 1485//1559 1631//1559 1515//1559 +f 1515//1560 1424//1560 1485//1560 +f 1485//1561 1632//1561 1631//1561 +f 1511//1562 1635//1562 1512//1562 +f 1509//1563 1636//1563 1511//1563 +f 1508//1564 1637//1564 1509//1564 +f 1505//1565 1639//1565 1638//1565 +f 1504//1566 1640//1566 1505//1566 +f 1502//1567 1641//1567 1504//1567 +f 1501//1568 1642//1568 1502//1568 +f 1642//1569 1501//1569 1500//1569 +f 1500//1570 161//1570 1642//1570 +f 159//1571 1499//1571 156//1571 +f 1149//1572 1642//1572 161//1572 +f 1641//1573 1643//1573 1644//1573 +f 1644//1574 1504//1574 1641//1574 +f 1639//1575 1505//1575 1640//1575 +f 1647//1576 1636//1576 1646//1576 +f 1647//1577 1648//1577 1649//1577 +f 1649//1578 1635//1578 1647//1578 +f 1651//1579 1512//1579 1635//1579 +f 1634//1580 1652//1580 1653//1580 +f 1653//1581 1513//1581 1634//1581 +f 1633//1582 1685//1582 1655//1582 +f 1655//1583 1514//1583 1633//1583 +f 1515//1584 1631//1584 1681//1584 +f 1656//1585 1520//1585 1629//1585 +f 1677//1586 1525//1587 1657//1588 +f 1658//1589 1535//1589 1627//1589 +f 1659//1590 1658//1590 1627//1590 +f 1658//1591 1659//1591 1674//1591 +f 1658//1592 1674//1592 1626//1592 +f 1674//1593 1660//1593 1625//1593 +f 1661//1594 1562//1594 1660//1594 +f 1624//1595 1662//1595 1663//1595 +f 1663//1596 1623//1596 1624//1596 +f 1664//1597 1623//1597 1663//1597 +f 1665//1598 1622//1598 1664//1598 +f 1668//1599 1574//1599 1621//1599 +f 1620//1600 1669//1600 1317//1600 +f 1670//1601 1671//1601 1669//1601 +f 1668//1602 1670//1602 1669//1602 +f 1671//1603 1317//1603 1669//1603 +f 1666//1604 1672//1604 1667//1604 +f 1665//1605 1672//1605 1666//1605 +f 1665//1606 1664//1606 1663//1606 +f 1662//1607 1719//1607 1663//1607 +f 1673//1608 1660//1608 1674//1608 +f 1657//1609 1678//1609 1677//1609 +f 1678//1610 1657//1610 1628//1610 +f 1680//1611 1679//1611 1629//1611 +f 1629//1612 1630//1612 1680//1612 +f 1631//1613 1683//1613 1681//1613 +f 1632//1614 1684//1614 1683//1614 +f 1653//1615 1687//1615 1654//1615 +f 1651//1616 1688//1616 1652//1616 +f 1648//1617 1689//1617 1650//1617 +f 1650//1618 1649//1618 1648//1618 +f 1690//1619 1691//1619 1648//1619 +f 1648//1620 1647//1620 1690//1620 +f 1692//1621 1693//1621 1646//1621 +f 1643//1622 167//1622 1694//1622 +f 1149//1623 165//1623 1643//1623 +f 1150//1624 1694//1624 167//1624 +f 1691//1625 1690//1625 1696//1625 +f 1696//1626 1697//1626 1691//1626 +f 1699//1627 1689//1627 1691//1627 +f 1689//1628 1699//1628 1700//1628 +f 1700//1629 1650//1629 1689//1629 +f 1688//1630 1701//1630 1702//1630 +f 1702//1631 1652//1631 1688//1631 +f 1687//1632 1703//1632 1704//1632 +f 1704//1633 1654//1633 1687//1633 +f 1706//1634 1685//1634 1686//1634 +f 1684//1635 1655//1635 1685//1635 +f 1684//1636 1708//1636 1709//1636 +f 1709//1637 1683//1637 1684//1637 +f 1711//1638 1681//1638 1683//1638 +f 1682//1639 1712//1639 1681//1639 +f 1680//1640 1681//1640 1712//1640 +f 1713//1641 1656//1641 1679//1641 +f 1716//1642 1628//1642 1715//1642 +f 1717//1643 1677//1643 1678//1643 +f 1763//1644 1659//1644 1676//1644 +f 1719//1645 1662//1645 1718//1645 +f 1756//1646 1725//1646 1724//1646 +f 1726//1647 1727//1647 1671//1647 +f 1315//1648 1671//1648 1727//1648 +f 1316//1649 1671//1649 1315//1649 +f 1315//1650 1314//1650 1316//1650 +f 1315//1651 1727//1651 1313//1651 +f 1729//1652 1730//1652 1313//1652 +f 1731//1653 1732//1653 1730//1653 +f 1732//1654 1311//1654 1310//1654 +f 1733//1655 1311//1655 1732//1655 +f 1311//1656 1733//1656 1734//1656 +f 1734//1657 1309//1657 1311//1657 +f 1309//1658 1735//1658 1736//1658 +f 1736//1659 1308//1659 1309//1659 +f 1737//1660 1739//1660 1308//1660 +f 1740//1661 1741//1661 1739//1661 +f 1740//1662 1743//1662 1742//1662 +f 1744//1663 1742//1663 1743//1663 +f 1743//1664 1745//1664 1744//1664 +f 1738//1665 1746//1665 1743//1665 +f 1735//1666 1747//1666 1738//1666 +f 1738//1667 1736//1667 1735//1667 +f 1734//1668 1748//1668 1735//1668 +f 1749//1669 1734//1669 1733//1669 +f 1750//1670 1749//1670 1733//1670 +f 1733//1671 1751//1671 1750//1671 +f 1732//1672 1731//1672 1751//1672 +f 1731//1673 1753//1673 1752//1673 +f 1729//1674 1754//1674 1753//1674 +f 1754//1675 1725//1675 1755//1675 +f 1728//1676 1725//1676 1754//1676 +f 1725//1677 1756//1677 1755//1677 +f 1756//1678 1758//1678 1757//1678 +f 1758//1679 1760//1679 1759//1679 +f 1718//1680 1762//1680 1719//1680 +f 1676//1681 1765//1681 1764//1681 +f 1717//1682 1766//1682 1765//1682 +f 1766//1683 1717//1683 1768//1683 +f 1770//1684 1711//1684 1771//1684 +f 1710//1685 1772//1685 1771//1685 +f 1771//1686 1711//1686 1710//1686 +f 1772//1687 1710//1687 1709//1687 +f 1708//1688 1773//1688 1772//1688 +f 1707//1689 1775//1689 1774//1689 +f 1705//1690 1777//1690 1776//1690 +f 1776//1691 1706//1691 1705//1691 +f 1703//1692 1779//1692 1778//1692 +f 1702//1693 1780//1693 1703//1693 +f 1700//1694 1782//1694 1701//1694 +f 1782//1695 1700//1695 1699//1695 +f 1699//1696 1783//1696 1782//1696 +f 1697//1697 173//1697 1698//1697 +f 1695//1698 171//1698 1697//1698 +f 1150//1699 169//1699 1695//1699 +f 1698//1700 174//1700 1783//1700 +f 1783//1701 175//1701 1784//1701 +f 1784//1702 1782//1702 1783//1702 +f 1785//1703 1780//1703 1781//1703 +f 1780//1704 1785//1704 1779//1704 +f 1786//1705 1777//1705 1779//1705 +f 1775//1706 1776//1706 1786//1706 +f 1773//1707 1774//1707 1775//1707 +f 1773//1708 1789//1708 1790//1708 +f 1790//1709 1772//1709 1773//1709 +f 1791//1710 1770//1710 1771//1710 +f 1792//1711 1769//1711 1770//1711 +f 1769//1712 1792//1712 1793//1712 +f 1793//1713 1714//1713 1769//1713 +f 1794//1714 1715//1714 1714//1714 +f 1768//1715 1716//1715 1715//1715 +f 1767//1716 1796//1716 1797//1716 +f 1797//1717 1766//1717 1767//1717 +f 1797//1718 1765//1718 1766//1718 +f 1765//1719 1797//1719 1798//1719 +f 1799//1720 1763//1720 1764//1720 +f 1761//1721 1800//1721 1801//1721 +f 1801//1722 1760//1722 1761//1722 +f 1760//1723 1801//1723 1759//1723 +f 1803//1724 1804//1724 1805//1724 +f 1803//1725 1805//1725 1757//1725 +f 1805//1726 1806//1726 1757//1726 +f 1755//1727 1757//1727 1806//1727 +f 1806//1728 1807//1728 1755//1728 +f 1754//1729 1755//1729 1807//1729 +f 1809//1730 1810//1730 1807//1730 +f 1215//1731 1814//1731 1813//1731 +f 1748//1732 1813//1732 1814//1732 +f 1814//1733 1815//1733 1748//1733 +f 1220//1734 1746//1734 1219//1734 +f 1745//1735 1222//1735 1744//1735 +f 1816//1736 1223//1736 1225//1736 +f 1063//1737 1293//1737 1225//1737 +f 1265//1738 1821//1738 1267//1738 +f 1819//1739 1258//1739 1295//1739 +f 341//1740 342//1740 1818//1740 +f 1818//1741 1259//1741 341//1741 +f 1840//1742 1265//1742 1820//1742 +f 1265//1743 1061//1743 1820//1743 +f 1821//1744 1842//1744 1267//1744 +f 1822//1745 1293//1745 5//1745 +f 1823//1746 1816//1746 1822//1746 +f 1826//1747 1824//1747 1825//1747 +f 1825//1748 1827//1748 1826//1748 +f 1826//1749 1224//1749 1223//1749 +f 1827//1750 1742//1750 1826//1750 +f 1828//1751 1829//1751 1827//1751 +f 1740//1752 1827//1752 1829//1752 +f 1829//1753 1741//1753 1740//1753 +f 1307//1754 1739//1754 1741//1754 +f 1306//1755 1308//1755 1739//1755 +f 1302//1756 1830//1756 1833//1756 +f 1833//1757 1834//1757 1849//1757 +f 1832//1758 1834//1758 1831//1758 +f 1828//1759 1835//1759 1845//1759 +f 1825//1760 1824//1760 1835//1760 +f 1824//1761 1816//1761 1836//1761 +f 1816//1762 1823//1762 1837//1762 +f 5//1763 1838//1763 1822//1763 +f 1839//1764 1840//1764 1820//1764 +f 1842//1765 1821//1765 1841//1765 +f 5//1766 1293//1766 1842//1766 +f 1850//1767 1844//1767 1836//1767 +f 1835//1768 1836//1768 1844//1768 +f 1844//1769 1845//1769 1835//1769 +f 1846//1770 1834//1770 1832//1770 +f 1299//1771 1847//1771 1834//1771 +f 1849//1772 1302//1772 1833//1772 +f 1301//1773 1848//1773 1847//1773 +f 1847//1774 1299//1774 1301//1774 +f 1837//1775 1843//1775 1297//1775 +f 1838//1776 1851//1776 1843//1776 +f 1838//1777 5//1777 348//1777 +f 1071//1778 1070//1778 1844//1778 +f 1845//1779 1844//1779 1070//1779 +f 1070//1780 1846//1780 1845//1780 +f 1298//1781 1299//1781 1846//1781 +f 904//1782 1297//1782 1851//1782 +f 1851//1783 895//1783 904//1783 +f 1742//1784 1744//1784 1224//1784 +f 1852//1785 1853//1785 1208//1785 +f 1811//1786 1853//1786 1852//1786 +f 1808//1787 1854//1787 1853//1787 +f 1808//1788 1855//1788 1854//1788 +f 1856//1789 1854//1789 1855//1789 +f 1855//1790 1804//1790 1856//1790 +f 1804//1791 1857//1791 1856//1791 +f 1802//1792 1858//1792 1857//1792 +f 1859//1793 1759//1793 1801//1793 +f 1799//1794 1861//1794 1800//1794 +f 1799//1795 1862//1795 1861//1795 +f 1797//1796 1863//1796 1798//1796 +f 1797//1797 1864//1797 1863//1797 +f 1797//1798 1866//1798 1864//1798 +f 1866//1799 1797//1799 1796//1799 +f 1867//1800 1767//1800 1795//1800 +f 1795//1801 1768//1801 1868//1801 +f 1870//1802 1793//1802 1792//1802 +f 1792//1803 1871//1803 1870//1803 +f 1872//1804 1770//1804 1791//1804 +f 1873//1805 1872//1805 1791//1805 +f 1789//1806 1873//1806 1790//1806 +f 1790//1807 1873//1807 1791//1807 +f 1875//1808 1874//1808 1789//1808 +f 1789//1809 1773//1809 1875//1809 +f 1876//1810 1875//1810 1788//1810 +f 1788//1811 1877//1811 1876//1811 +f 1787//1812 1786//1812 1877//1812 +f 1879//1813 1779//1813 1785//1813 +f 1881//1814 1878//1814 1879//1814 +f 1882//1815 1877//1815 1881//1815 +f 1877//1816 1883//1816 1884//1816 +f 1877//1817 1884//1817 1876//1817 +f 1885//1818 1886//1818 1874//1818 +f 1886//1819 1888//1819 1789//1819 +f 1888//1820 1889//1820 1873//1820 +f 1868//1821 1893//1821 1894//1821 +f 1867//1822 1896//1822 1897//1822 +f 1897//1823 1796//1823 1867//1823 +f 1898//1824 1866//1824 1897//1824 +f 1866//1825 1898//1825 1042//1825 +f 1042//1826 1865//1826 1866//1826 +f 1864//1827 1865//1827 1899//1827 +f 1899//1828 1863//1828 1864//1828 +f 1901//1829 1800//1829 1861//1829 +f 1902//1830 1859//1830 1860//1830 +f 1903//1831 1857//1831 1858//1831 +f 1854//1832 1856//1832 1904//1832 +f 1904//1833 1853//1833 1854//1833 +f 1856//1834 1202//1834 1905//1834 +f 1905//1835 1207//1835 1904//1835 +f 1853//1836 1904//1836 1207//1836 +f 1207//1837 1208//1837 1853//1837 +f 1198//1838 1858//1838 1902//1838 +f 1860//1839 1194//1839 1902//1839 +f 1194//1840 1860//1840 1901//1840 +f 1901//1841 1861//1841 1900//1841 +f 1865//1842 1042//1842 1189//1842 +f 1899//1843 1189//1843 1190//1843 +f 1900//1844 1190//1844 1191//1844 +f 1901//1845 1191//1845 1194//1845 +f 1902//1846 1195//1846 1198//1846 +f 1897//1847 1933//1847 1906//1847 +f 1896//1848 1932//1848 1897//1848 +f 1889//1849 1909//1849 1908//1849 +f 1908//1850 1871//1850 1889//1850 +f 1912//1851 1910//1851 1911//1851 +f 1887//1852 1914//1852 1913//1852 +f 1915//1853 1913//1853 1914//1853 +f 1915//1854 1917//1854 1916//1854 +f 1914//1855 1917//1855 1915//1855 +f 1917//1856 1918//1856 1916//1856 +f 1883//1857 1919//1857 1917//1857 +f 1917//1858 1884//1858 1883//1858 +f 1882//1859 1920//1859 1883//1859 +f 1879//1860 1151//1860 1920//1860 +f 1920//1861 1881//1861 1879//1861 +f 1920//1862 1151//1862 1921//1862 +f 1922//1863 1966//1863 1916//1863 +f 1966//1864 1923//1864 1916//1864 +f 1916//1865 1924//1865 1915//1865 +f 1913//1866 1915//1866 1924//1866 +f 1923//1867 1925//1867 1924//1867 +f 1925//1868 1926//1868 1924//1868 +f 1912//1869 1909//1869 1910//1869 +f 1927//1870 1908//1870 1909//1870 +f 1890//1871 1927//1871 1928//1871 +f 1928//1872 1892//1872 1890//1872 +f 1929//1873 1891//1873 1892//1873 +f 1930//1874 1893//1874 1907//1874 +f 1929//1875 1931//1875 1930//1875 +f 1932//1876 1896//1876 1895//1876 +f 1933//1877 1897//1877 1932//1877 +f 1934//1878 1898//1878 1906//1878 +f 1934//1879 1936//1879 1041//1879 +f 1935//1880 1179//1880 1936//1880 +f 1183//1881 1936//1881 1179//1881 +f 1179//1882 1181//1882 1183//1882 +f 1895//1883 1940//1883 1932//1883 +f 1930//1884 1931//1884 1940//1884 +f 1940//1885 1895//1885 1930//1885 +f 1942//1886 1931//1886 1943//1886 +f 1944//1887 1945//1887 1943//1887 +f 1943//1888 1928//1888 1944//1888 +f 1944//1889 1928//1889 1927//1889 +f 1927//1890 1947//1890 1944//1890 +f 1033//1891 1946//1891 1912//1891 +f 1946//1892 1909//1892 1912//1892 +f 1034//1893 1947//1893 1946//1893 +f 1947//1894 1927//1894 1946//1894 +f 1948//1895 1944//1895 1947//1895 +f 1949//1896 1945//1896 1948//1896 +f 1950//1897 1951//1897 1949//1897 +f 1948//1898 1950//1898 1949//1898 +f 1951//1899 1945//1899 1949//1899 +f 1952//1900 1953//1900 1951//1900 +f 1951//1901 1954//1901 1952//1901 +f 1942//1902 1953//1902 1167//1902 +f 1931//1903 1941//1903 1940//1903 +f 1932//1904 1939//1904 1938//1904 +f 1955//1905 1935//1905 1938//1905 +f 1956//1906 1935//1906 1955//1906 +f 1957//1907 1937//1907 1956//1907 +f 1937//1908 1957//1908 1178//1908 +f 1958//1909 1178//1909 1957//1909 +f 1173//1910 1958//1910 1957//1910 +f 1957//1911 1956//1911 1173//1911 +f 1173//1912 1956//1912 1172//1912 +f 1955//1913 1172//1913 1956//1913 +f 1955//1914 1939//1914 1172//1914 +f 1169//1915 1939//1915 1940//1915 +f 1941//1916 1167//1916 1169//1916 +f 1169//1917 1940//1917 1941//1917 +f 1953//1918 1952//1918 1962//1918 +f 1959//1919 1962//1919 1952//1919 +f 1952//1920 1960//1920 1959//1920 +f 1960//1921 1954//1921 1959//1921 +f 1952//1922 1954//1922 1960//1922 +f 1950//1923 1162//1923 1163//1923 +f 1948//1924 1161//1924 1162//1924 +f 1947//1925 1961//1925 1161//1925 +f 1164//1926 1959//1926 1954//1926 +f 1165//1927 1962//1927 1959//1927 +f 1165//1928 1166//1928 1962//1928 +f 1962//1929 1036//1929 1953//1929 +f 1958//1930 1177//1930 1178//1930 +f 1926//1931 1925//1931 1031//1931 +f 1031//1932 1963//1932 1073//1932 +f 1963//1933 1964//1933 1073//1933 +f 1925//1934 1965//1934 1963//1934 +f 1966//1935 1922//1935 1967//1935 +f 1922//1936 184//1936 1967//1936 +f 1967//1937 1153//1937 1968//1937 +f 1968//1938 1966//1938 1967//1938 +f 1964//1939 1155//1939 1969//1939 +f 1074//1940 1156//1940 1157//1940 +f 1921//1941 1152//1941 1922//1941 +f 1104//1942 1050//1942 1550//1942 +f 1348//1943 1550//1943 1050//1943 +f 1518//1944 1517//1944 1630//1944 +f 1517//1945 1424//1945 1516//1945 +f 1424//1946 1458//1946 1485//1946 +f 1486//1947 1045//1947 1487//1947 +f 1357//1948 1358//1948 1394//1948 +f 1391//1949 1394//1949 1358//1949 +f 1110//1950 1052//1950 1350//1950 +f 1066//1951 1068//1951 1321//1951 +f 1069//1952 1067//1952 1303//1952 +f 1267//1953 1063//1953 1226//1953 +f 214//1954 1174//1954 1024//1954 +f 1171//1955 1024//1955 1174//1955 +f 1038//1956 1025//1956 1171//1956 +f 1168//1957 1038//1957 1171//1957 +f 1961//1958 1159//1958 1160//1958 +f 1030//1959 1158//1959 1159//1959 +f 1051//1960 1058//1960 1057//1960 +f 1032//1961 1157//1961 1028//1961 +f 9//1962 12//1962 11//1962 +f 13//1963 20//1963 14//1963 +f 16//1964 58//1964 17//1964 +f 17//1965 980//1965 18//1965 +f 19//1966 1014//1966 20//1966 +f 21//1967 23//1967 24//1967 +f 29//1968 223//1968 26//1968 +f 34//1969 572//1969 571//1969 +f 41//1970 44//1970 42//1970 +f 41//1971 43//1971 46//1971 +f 589//1972 612//1972 677//1972 +f 57//1973 1022//1973 58//1973 +f 59//1974 24//1974 61//1974 +f 76//1975 77//1975 65//1975 +f 77//1976 78//1976 65//1976 +f 78//1977 79//1977 66//1977 +f 79//1978 388//1978 80//1978 +f 81//1979 389//1979 82//1979 +f 83//1980 137//1980 82//1980 +f 82//1981 390//1981 83//1981 +f 390//1982 391//1982 83//1982 +f 134//1983 135//1983 83//1983 +f 83//1984 391//1984 84//1984 +f 86//1985 394//1985 87//1985 +f 395//1986 396//1986 88//1986 +f 88//1987 129//1987 395//1987 +f 88//1988 400//1988 89//1988 +f 89//1989 398//1989 90//1989 +f 91//1990 47//1990 92//1990 +f 92//1991 42//1991 44//1991 +f 42//1992 92//1992 47//1992 +f 42//1993 93//1993 43//1993 +f 93//1994 94//1994 95//1994 +f 94//1995 403//1995 96//1995 +f 95//1996 96//1996 97//1996 +f 95//1997 97//1997 98//1997 +f 99//1998 121//1998 37//1998 +f 101//1999 100//1999 102//1999 +f 102//2000 117//2000 101//2000 +f 407//2001 103//2001 102//2001 +f 103//2002 104//2002 105//2002 +f 105//2003 112//2003 103//2003 +f 105//2004 104//2004 106//2004 +f 106//2005 111//2005 105//2005 +f 104//2006 40//2006 36//2006 +f 36//2007 108//2007 107//2007 +f 107//2008 109//2008 35//2008 +f 110//2009 106//2009 35//2009 +f 106//2010 36//2010 35//2010 +f 112//2011 111//2011 113//2011 +f 112//2012 115//2012 103//2012 +f 112//2013 413//2013 115//2013 +f 115//2014 102//2014 103//2014 +f 115//2015 116//2015 117//2015 +f 117//2016 102//2016 115//2016 +f 117//2017 118//2017 119//2017 +f 118//2018 122//2018 120//2018 +f 119//2019 120//2019 121//2019 +f 121//2020 120//2020 37//2020 +f 122//2021 124//2021 123//2021 +f 120//2022 123//2022 45//2022 +f 124//2023 125//2023 123//2023 +f 123//2024 125//2024 126//2024 +f 90//2025 418//2025 128//2025 +f 88//2026 422//2026 129//2026 +f 129//2027 424//2027 130//2027 +f 130//2028 131//2028 132//2028 +f 134//2029 428//2029 135//2029 +f 135//2030 430//2030 136//2030 +f 135//2031 136//2031 137//2031 +f 137//2032 432//2032 138//2032 +f 138//2033 82//2033 137//2033 +f 66//2034 79//2034 141//2034 +f 142//2035 143//2035 66//2035 +f 500//2036 149//2036 148//2036 +f 502//2037 530//2037 149//2037 +f 530//2038 503//2038 150//2038 +f 503//2039 151//2039 150//2039 +f 155//2040 157//2040 156//2040 +f 158//2041 160//2041 159//2041 +f 160//2042 162//2042 161//2042 +f 162//2043 164//2043 163//2043 +f 164//2044 166//2044 165//2044 +f 166//2045 168//2045 167//2045 +f 175//2046 177//2046 176//2046 +f 178//2047 181//2047 180//2047 +f 183//2048 186//2048 185//2048 +f 186//2049 1023//2049 187//2049 +f 187//2050 1023//2050 188//2050 +f 190//2051 10//2051 11//2051 +f 15//2052 191//2052 9//2052 +f 192//2053 12//2053 9//2053 +f 191//2054 15//2054 14//2054 +f 192//2055 191//2055 193//2055 +f 194//2056 193//2056 195//2056 +f 197//2057 196//2057 198//2057 +f 199//2058 198//2058 200//2058 +f 1015//2059 205//2059 21//2059 +f 206//2060 207//2060 22//2060 +f 207//2061 206//2061 208//2061 +f 207//2062 211//2062 209//2062 +f 207//2063 208//2063 210//2063 +f 210//2064 212//2064 211//2064 +f 213//2065 1011//2065 215//2065 +f 214//2066 215//2066 216//2066 +f 64//2067 217//2067 218//2067 +f 218//2068 219//2068 64//2068 +f 220//2069 221//2069 219//2069 +f 219//2070 222//2070 62//2070 +f 221//2071 225//2071 222//2071 +f 224//2072 226//2072 225//2072 +f 29//2073 227//2073 226//2073 +f 227//2074 29//2074 25//2074 +f 231//2075 232//2075 229//2075 +f 234//2076 232//2076 235//2076 +f 235//2077 236//2077 234//2077 +f 236//2078 237//2078 234//2078 +f 236//2079 239//2079 237//2079 +f 239//2080 238//2080 240//2080 +f 240//2081 241//2081 239//2081 +f 241//2082 240//2082 243//2082 +f 241//2083 243//2083 245//2083 +f 250//2084 274//2084 248//2084 +f 249//2085 905//2085 251//2085 +f 905//2086 862//2086 251//2086 +f 251//2087 253//2087 254//2087 +f 255//2088 271//2088 252//2088 +f 253//2089 863//2089 256//2089 +f 254//2090 256//2090 255//2090 +f 255//2091 865//2091 258//2091 +f 258//2092 866//2092 259//2092 +f 261//2093 266//2093 259//2093 +f 261//2094 798//2094 262//2094 +f 263//2095 264//2095 48//2095 +f 264//2096 874//2096 48//2096 +f 265//2097 262//2097 263//2097 +f 266//2098 261//2098 262//2098 +f 266//2099 267//2099 268//2099 +f 268//2100 269//2100 270//2100 +f 271//2101 255//2101 257//2101 +f 270//2102 310//2102 272//2102 +f 271//2103 272//2103 273//2103 +f 273//2104 272//2104 274//2104 +f 250//2105 251//2105 252//2105 +f 247//2106 248//2106 274//2106 +f 246//2107 276//2107 244//2107 +f 244//2108 276//2108 277//2108 +f 244//2109 242//2109 241//2109 +f 278//2110 237//2110 239//2110 +f 281//2111 233//2111 234//2111 +f 284//2112 325//2112 285//2112 +f 285//2113 222//2113 225//2113 +f 286//2114 62//2114 222//2114 +f 285//2115 287//2115 286//2115 +f 293//2116 337//2116 292//2116 +f 293//2117 295//2117 294//2117 +f 295//2118 293//2118 297//2118 +f 298//2119 301//2119 299//2119 +f 298//2120 296//2120 300//2120 +f 335//2121 331//2121 302//2121 +f 305//2122 1//2122 306//2122 +f 305//2123 306//2123 265//2123 +f 306//2124 334//2124 267//2124 +f 267//2125 266//2125 306//2125 +f 308//2126 332//2126 309//2126 +f 309//2127 270//2127 269//2127 +f 309//2128 332//2128 310//2128 +f 310//2129 270//2129 309//2129 +f 311//2130 312//2130 272//2130 +f 314//2131 275//2131 272//2131 +f 314//2132 276//2132 275//2132 +f 315//2133 277//2133 276//2133 +f 277//2134 315//2134 317//2134 +f 318//2135 279//2135 242//2135 +f 279//2136 319//2136 320//2136 +f 279//2137 280//2137 237//2137 +f 321//2138 322//2138 281//2138 +f 282//2139 233//2139 281//2139 +f 322//2140 323//2140 283//2140 +f 283//2141 324//2141 284//2141 +f 325//2142 287//2142 285//2142 +f 324//2143 288//2143 287//2143 +f 323//2144 322//2144 321//2144 +f 323//2145 321//2145 320//2145 +f 319//2146 318//2146 327//2146 +f 317//2147 316//2147 297//2147 +f 316//2148 328//2148 295//2148 +f 316//2149 317//2149 315//2149 +f 328//2150 329//2150 300//2150 +f 328//2151 316//2151 315//2151 +f 311//2152 310//2152 332//2152 +f 332//2153 308//2153 333//2153 +f 333//2154 308//2154 334//2154 +f 333//2155 303//2155 302//2155 +f 330//2156 331//2156 335//2156 +f 301//2157 300//2157 329//2157 +f 300//2158 295//2158 328//2158 +f 295//2159 300//2159 296//2159 +f 295//2160 297//2160 316//2160 +f 297//2161 293//2161 291//2161 +f 297//2162 291//2162 327//2162 +f 327//2163 291//2163 326//2163 +f 326//2164 290//2164 323//2164 +f 290//2165 289//2165 288//2165 +f 48//2166 869//2166 307//2166 +f 334//2167 873//2167 304//2167 +f 303//2168 870//2168 335//2168 +f 335//2169 302//2169 303//2169 +f 871//2170 299//2170 301//2170 +f 296//2171 298//2171 336//2171 +f 337//2172 293//2172 294//2172 +f 337//2173 32//2173 292//2173 +f 339//2174 872//2174 340//2174 +f 346//2175 6//2176 345//2176 +f 350//2177 352//2177 351//2177 +f 901//2178 53//2178 377//2178 +f 901//2179 902//2179 54//2179 +f 358//2180 359//2180 54//2180 +f 359//2181 358//2181 360//2181 +f 50//2182 360//2182 49//2182 +f 360//2183 358//2183 362//2183 +f 362//2184 358//2184 363//2184 +f 49//2185 364//2185 365//2185 +f 368//2186 783//2186 369//2186 +f 368//2187 361//2187 50//2187 +f 370//2188 369//2188 371//2188 +f 370//2189 372//2189 361//2189 +f 372//2190 51//2190 359//2190 +f 372//2191 373//2191 51//2191 +f 51//2192 373//2192 374//2192 +f 373//2193 672//2193 375//2193 +f 374//2194 375//2194 376//2194 +f 374//2195 52//2195 51//2195 +f 374//2196 376//2196 52//2196 +f 376//2197 380//2197 377//2197 +f 377//2198 378//2198 357//2198 +f 357//2199 378//2199 67//2199 +f 379//2200 1072//2200 355//2200 +f 1072//2201 56//2201 354//2201 +f 380//2202 69//2202 67//2202 +f 381//2203 71//2203 70//2203 +f 382//2204 383//2204 72//2204 +f 383//2205 384//2205 73//2205 +f 75//2206 385//2206 76//2206 +f 385//2207 75//2207 386//2207 +f 78//2208 77//2208 387//2208 +f 79//2209 78//2209 388//2209 +f 388//2210 639//2210 81//2210 +f 81//2211 80//2211 388//2211 +f 390//2212 82//2212 389//2212 +f 390//2213 642//2213 391//2213 +f 395//2214 87//2214 394//2214 +f 396//2215 397//2215 400//2215 +f 91//2216 90//2216 398//2216 +f 398//2217 399//2217 47//2217 +f 399//2218 398//2218 400//2218 +f 93//2219 42//2219 47//2219 +f 401//2220 402//2220 94//2220 +f 403//2221 402//2221 404//2221 +f 403//2222 405//2222 96//2222 +f 406//2223 100//2223 99//2223 +f 407//2224 100//2224 406//2224 +f 38//2225 40//2225 104//2225 +f 40//2226 39//2226 108//2226 +f 39//2227 599//2227 108//2227 +f 108//2228 599//2228 409//2228 +f 409//2229 107//2229 108//2229 +f 410//2230 110//2230 109//2230 +f 412//2231 113//2231 111//2231 +f 113//2232 412//2232 574//2232 +f 113//2233 114//2233 112//2233 +f 114//2234 413//2234 112//2234 +f 413//2235 114//2235 34//2235 +f 413//2236 116//2236 115//2236 +f 414//2237 415//2237 124//2237 +f 415//2238 416//2238 125//2238 +f 125//2239 416//2239 126//2239 +f 128//2240 418//2240 419//2240 +f 422//2241 128//2241 420//2241 +f 422//2242 423//2242 424//2242 +f 424//2243 129//2243 422//2243 +f 424//2244 445//2244 425//2244 +f 131//2245 130//2245 424//2245 +f 426//2246 441//2246 427//2246 +f 428//2247 134//2247 133//2247 +f 428//2248 427//2248 429//2248 +f 430//2249 135//2249 428//2249 +f 430//2250 438//2250 431//2250 +f 432//2251 137//2251 136//2251 +f 431//2252 436//2252 432//2252 +f 139//2253 138//2253 432//2253 +f 433//2254 432//2254 436//2254 +f 436//2255 431//2255 438//2255 +f 438//2256 466//2256 437//2256 +f 429//2257 438//2257 430//2257 +f 429//2258 440//2258 439//2258 +f 441//2259 443//2259 442//2259 +f 445//2260 461//2260 444//2260 +f 445//2261 424//2261 423//2261 +f 421//2262 448//2262 447//2262 +f 420//2263 419//2263 448//2263 +f 451//2264 570//2264 450//2264 +f 453//2265 416//2265 450//2265 +f 417//2266 454//2266 482//2266 +f 419//2267 449//2267 455//2267 +f 455//2268 448//2268 419//2268 +f 448//2269 455//2269 456//2269 +f 456//2270 447//2270 448//2270 +f 447//2271 457//2271 458//2271 +f 459//2272 446//2272 423//2272 +f 446//2273 459//2273 460//2273 +f 462//2274 463//2274 461//2274 +f 463//2275 475//2275 442//2275 +f 464//2276 439//2276 440//2276 +f 465//2277 466//2277 438//2277 +f 439//2278 438//2278 429//2278 +f 466//2279 467//2279 437//2279 +f 468//2280 467//2280 146//2280 +f 470//2281 146//2281 467//2281 +f 471//2282 470//2282 467//2282 +f 465//2283 473//2283 471//2283 +f 473//2284 465//2284 439//2284 +f 474//2285 499//2285 473//2285 +f 464//2286 442//2286 475//2286 +f 476//2287 496//2287 475//2287 +f 476//2288 462//2288 477//2288 +f 461//2289 460//2289 478//2289 +f 459//2290 458//2290 479//2290 +f 458//2291 457//2291 480//2291 +f 456//2292 455//2292 481//2292 +f 453//2293 452//2293 483//2293 +f 452//2294 484//2294 485//2294 +f 484//2295 452//2295 569//2295 +f 484//2296 567//2296 485//2296 +f 485//2297 483//2297 452//2297 +f 483//2298 486//2298 454//2298 +f 487//2299 455//2299 482//2299 +f 488//2300 481//2300 455//2300 +f 489//2301 480//2301 457//2301 +f 480//2302 479//2302 458//2302 +f 479//2303 491//2303 492//2303 +f 479//2304 478//2304 460//2304 +f 493//2305 477//2305 478//2305 +f 477//2306 493//2306 494//2306 +f 477//2307 496//2307 476//2307 +f 496//2308 495//2308 497//2308 +f 496//2309 497//2309 498//2309 +f 474//2310 475//2310 496//2310 +f 500//2311 148//2311 499//2311 +f 148//2312 472//2312 473//2312 +f 147//2313 472//2313 148//2313 +f 501//2314 500//2314 498//2314 +f 497//2315 495//2315 503//2315 +f 495//2316 494//2316 504//2316 +f 494//2317 495//2317 477//2317 +f 493//2318 492//2318 505//2318 +f 492//2319 507//2319 506//2319 +f 491//2320 490//2320 507//2320 +f 489//2321 509//2321 508//2321 +f 509//2322 489//2322 481//2322 +f 481//2323 510//2323 509//2323 +f 510//2324 481//2324 488//2324 +f 510//2325 488//2325 487//2325 +f 487//2326 513//2326 510//2326 +f 513//2327 487//2327 486//2327 +f 486//2328 514//2328 513//2328 +f 486//2329 483//2329 514//2329 +f 485//2330 566//2330 516//2330 +f 516//2331 515//2331 485//2331 +f 514//2332 515//2332 517//2332 +f 517//2333 513//2333 514//2333 +f 519//2334 511//2334 512//2334 +f 520//2335 508//2335 509//2335 +f 522//2336 507//2336 490//2336 +f 524//2337 506//2337 507//2337 +f 506//2338 524//2338 525//2338 +f 526//2339 527//2339 505//2339 +f 527//2340 504//2340 494//2340 +f 504//2341 527//2341 528//2341 +f 529//2342 503//2342 504//2342 +f 529//2343 528//2343 151//2343 +f 526//2344 533//2344 532//2344 +f 526//2345 525//2345 533//2345 +f 525//2346 534//2346 533//2346 +f 525//2347 526//2347 506//2347 +f 534//2348 525//2348 524//2348 +f 524//2349 523//2349 534//2349 +f 522//2350 521//2350 536//2350 +f 520//2351 538//2351 537//2351 +f 538//2352 520//2352 511//2352 +f 519//2353 539//2353 538//2353 +f 539//2354 519//2354 512//2354 +f 515//2355 516//2355 33//2355 +f 544//2356 540//2356 518//2356 +f 543//2357 518//2357 517//2357 +f 540//2358 544//2358 545//2358 +f 546//2359 538//2359 539//2359 +f 547//2360 537//2360 538//2360 +f 537//2361 547//2361 548//2361 +f 549//2362 536//2362 521//2362 +f 536//2363 549//2363 550//2363 +f 550//2364 535//2364 536//2364 +f 552//2365 533//2365 534//2365 +f 533//2366 552//2366 553//2366 +f 153//2367 531//2367 528//2367 +f 153//2368 553//2368 154//2368 +f 157//2369 553//2369 552//2369 +f 552//2370 551//2370 554//2370 +f 551//2371 556//2371 555//2371 +f 556//2372 551//2372 535//2372 +f 550//2373 558//2373 557//2373 +f 558//2374 550//2374 549//2374 +f 549//2375 548//2375 558//2375 +f 548//2376 561//2376 560//2376 +f 546//2377 545//2377 562//2377 +f 545//2378 546//2378 540//2378 +f 544//2379 542//2379 563//2379 +f 542//2380 565//2380 564//2380 +f 565//2381 542//2381 33//2381 +f 33//2382 541//2382 565//2382 +f 578//2383 577//2383 579//2383 +f 581//2384 583//2384 582//2384 +f 586//2385 588//2385 587//2385 +f 588//2386 586//2386 590//2386 +f 591//2387 609//2387 590//2387 +f 593//2388 608//2388 591//2388 +f 591//2389 603//2389 592//2389 +f 593//2390 600//2390 595//2390 +f 595//2391 606//2391 594//2391 +f 597//2392 598//2392 99//2392 +f 598//2393 597//2393 599//2393 +f 597//2394 596//2394 600//2394 +f 601//2395 409//2395 599//2395 +f 409//2396 601//2396 580//2396 +f 580//2397 410//2397 109//2397 +f 410//2398 580//2398 579//2398 +f 576//2399 574//2399 412//2399 +f 572//2400 34//2400 114//2400 +f 581//2401 580//2401 601//2401 +f 602//2402 585//2402 583//2402 +f 603//2403 591//2403 586//2403 +f 586//2404 585//2404 603//2404 +f 603//2405 585//2405 592//2405 +f 592//2406 585//2406 600//2406 +f 602//2407 601//2407 585//2407 +f 600//2408 593//2408 592//2408 +f 596//2409 595//2409 600//2409 +f 97//2410 405//2410 605//2410 +f 605//2411 649//2411 606//2411 +f 595//2412 596//2412 604//2412 +f 649//2413 650//2413 607//2413 +f 607//2414 651//2414 608//2414 +f 608//2415 593//2415 594//2415 +f 609//2416 610//2416 588//2416 +f 609//2417 591//2417 608//2417 +f 588//2418 590//2418 609//2418 +f 613//2419 617//2419 615//2419 +f 620//2420 621//2420 618//2420 +f 621//2421 620//2421 623//2421 +f 624//2422 625//2422 622//2422 +f 626//2423 627//2423 625//2423 +f 627//2424 630//2424 628//2424 +f 627//2425 626//2425 629//2425 +f 630//2426 629//2426 631//2426 +f 631//2427 635//2427 634//2427 +f 380//2428 376//2428 633//2428 +f 634//2429 69//2429 380//2429 +f 635//2430 381//2430 69//2430 +f 382//2431 636//2431 383//2431 +f 384//2432 386//2432 74//2432 +f 385//2433 387//2433 77//2433 +f 387//2434 385//2434 637//2434 +f 387//2435 638//2435 78//2435 +f 638//2436 388//2436 78//2436 +f 389//2437 81//2437 639//2437 +f 641//2438 665//2438 642//2438 +f 642//2439 666//2439 392//2439 +f 392//2440 666//2440 667//2440 +f 392//2441 667//2441 393//2441 +f 393//2442 667//2442 643//2442 +f 643//2443 670//2443 644//2443 +f 643//2444 644//2444 396//2444 +f 644//2445 671//2445 397//2445 +f 397//2446 645//2446 400//2446 +f 645//2447 646//2447 401//2447 +f 646//2448 645//2448 647//2448 +f 646//2449 404//2449 402//2449 +f 404//2450 646//2450 648//2450 +f 404//2451 649//2451 403//2451 +f 649//2452 404//2452 650//2452 +f 649//2453 605//2453 405//2453 +f 650//2454 648//2454 651//2454 +f 648//2455 671//2455 652//2455 +f 609//2456 608//2456 651//2456 +f 653//2457 669//2457 654//2457 +f 654//2458 668//2458 611//2458 +f 611//2459 668//2459 655//2459 +f 655//2460 612//2460 611//2460 +f 655//2461 666//2461 656//2461 +f 655//2462 656//2462 657//2462 +f 657//2463 656//2463 616//2463 +f 616//2464 665//2464 619//2464 +f 658//2465 623//2465 620//2465 +f 623//2466 658//2466 664//2466 +f 661//2467 629//2467 626//2467 +f 635//2468 631//2468 632//2468 +f 632//2469 382//2469 635//2469 +f 632//2470 663//2470 384//2470 +f 636//2471 384//2471 383//2471 +f 663//2472 662//2472 386//2472 +f 662//2473 660//2473 637//2473 +f 637//2474 660//2474 659//2474 +f 637//2475 638//2475 387//2475 +f 664//2476 658//2476 641//2476 +f 665//2477 656//2477 666//2477 +f 666//2478 642//2478 665//2478 +f 667//2479 668//2479 643//2479 +f 643//2480 654//2480 669//2480 +f 671//2481 648//2481 647//2481 +f 647//2482 648//2482 646//2482 +f 653//2483 652//2483 671//2483 +f 658//2484 619//2484 665//2484 +f 645//2485 397//2485 671//2485 +f 633//2486 376//2486 375//2486 +f 672//2487 628//2487 630//2487 +f 628//2488 625//2488 627//2488 +f 625//2489 673//2489 674//2489 +f 675//2490 618//2490 621//2490 +f 618//2491 675//2491 676//2491 +f 676//2492 615//2492 618//2492 +f 615//2493 614//2493 613//2493 +f 614//2494 715//2494 677//2494 +f 677//2495 714//2495 678//2495 +f 678//2496 589//2496 677//2496 +f 678//2497 587//2497 589//2497 +f 582//2498 578//2498 579//2498 +f 571//2499 573//2499 680//2499 +f 680//2500 681//2500 571//2500 +f 682//2501 567//2501 568//2501 +f 682//2502 683//2502 567//2502 +f 685//2503 565//2503 541//2503 +f 686//2504 564//2504 565//2504 +f 688//2505 689//2505 564//2505 +f 689//2506 690//2506 563//2506 +f 690//2507 562//2507 545//2507 +f 562//2508 690//2508 691//2508 +f 692//2509 560//2509 561//2509 +f 560//2510 692//2510 693//2510 +f 559//2511 560//2511 693//2511 +f 559//2512 557//2512 558//2512 +f 557//2513 559//2513 694//2513 +f 695//2514 555//2514 556//2514 +f 555//2515 695//2515 162//2515 +f 695//2516 696//2516 164//2516 +f 695//2517 694//2517 696//2517 +f 694//2518 695//2518 557//2518 +f 694//2519 693//2519 697//2519 +f 693//2520 694//2520 559//2520 +f 692//2521 699//2521 698//2521 +f 690//2522 689//2522 691//2522 +f 691//2523 701//2523 700//2523 +f 701//2524 691//2524 689//2524 +f 689//2525 688//2525 701//2525 +f 701//2526 703//2526 702//2526 +f 702//2527 700//2527 701//2527 +f 703//2528 701//2528 688//2528 +f 688//2529 704//2529 703//2529 +f 687//2530 706//2530 705//2530 +f 706//2531 687//2531 686//2531 +f 685//2532 708//2532 707//2532 +f 683//2533 682//2533 709//2533 +f 682//2534 735//2534 709//2534 +f 568//2535 681//2535 734//2535 +f 2//2536 711//2537 578//2538 +f 712//2539 2//2539 582//2539 +f 587//2540 678//2540 712//2540 +f 712//2541 714//2541 713//2541 +f 712//2542 678//2542 714//2542 +f 714//2543 677//2543 715//2543 +f 716//2544 715//2544 615//2544 +f 676//2545 718//2545 717//2545 +f 718//2546 676//2546 675//2546 +f 674//2547 673//2547 721//2547 +f 673//2548 723//2548 722//2548 +f 723//2549 673//2549 628//2549 +f 672//2550 373//2550 724//2550 +f 726//2551 722//2551 723//2551 +f 725//2552 779//2552 726//2552 +f 726//2553 776//2553 727//2553 +f 721//2554 722//2554 727//2554 +f 720//2555 721//2555 727//2555 +f 727//2556 774//2556 720//2556 +f 720//2557 718//2557 719//2557 +f 720//2558 773//2558 718//2558 +f 717//2559 718//2559 772//2559 +f 728//2560 714//2560 715//2560 +f 731//2561 711//2561 2//2562 +f 711//2563 731//2563 732//2563 +f 732//2564 679//2564 711//2564 +f 680//2565 734//2565 681//2565 +f 737//2566 684//2566 709//2566 +f 684//2567 737//2567 738//2567 +f 739//2568 707//2568 708//2568 +f 707//2569 739//2569 740//2569 +f 741//2570 705//2570 706//2570 +f 705//2571 741//2571 742//2571 +f 703//2572 743//2572 745//2572 +f 702//2573 745//2573 746//2573 +f 747//2574 748//2574 700//2574 +f 700//2575 749//2575 699//2575 +f 749//2576 750//2576 698//2576 +f 750//2577 696//2577 697//2577 +f 696//2578 750//2578 166//2578 +f 164//2579 696//2579 166//2579 +f 166//2580 750//2580 168//2580 +f 748//2581 751//2581 749//2581 +f 748//2582 747//2582 752//2582 +f 747//2583 746//2583 753//2583 +f 745//2584 744//2584 755//2584 +f 744//2585 745//2585 743//2585 +f 742//2586 758//2586 757//2586 +f 740//2587 739//2587 759//2587 +f 738//2588 739//2588 708//2588 +f 738//2589 762//2589 761//2589 +f 737//2590 763//2590 762//2590 +f 737//2591 764//2591 763//2591 +f 764//2592 737//2592 735//2592 +f 734//2593 765//2593 735//2593 +f 766//2594 733//2594 710//2594 +f 713//2595 729//2595 816//2595 +f 772//2596 718//2596 773//2596 +f 773//2597 720//2597 774//2597 +f 774//2598 727//2598 775//2598 +f 775//2599 727//2599 776//2599 +f 776//2600 726//2600 777//2600 +f 777//2601 726//2601 779//2601 +f 780//2602 781//2602 779//2602 +f 779//2603 725//2603 780//2603 +f 371//2604 372//2604 370//2604 +f 725//2605 373//2605 372//2605 +f 783//2606 784//2606 782//2606 +f 367//2607 787//2607 785//2607 +f 787//2608 367//2608 365//2608 +f 365//2609 789//2609 788//2609 +f 364//2610 790//2610 789//2610 +f 790//2611 364//2611 792//2611 +f 792//2612 793//2612 790//2612 +f 793//2613 792//2613 794//2613 +f 793//2614 879//2614 795//2614 +f 793//2615 795//2615 796//2615 +f 796//2616 791//2616 790//2616 +f 796//2617 797//2617 798//2617 +f 791//2618 788//2618 789//2618 +f 801//2619 802//2619 787//2619 +f 802//2620 785//2620 787//2620 +f 802//2621 864//2621 803//2621 +f 803//2622 864//2622 804//2622 +f 804//2623 786//2623 785//2623 +f 806//2624 782//2624 784//2624 +f 807//2625 781//2625 782//2625 +f 807//2626 808//2626 778//2626 +f 778//2627 779//2627 781//2627 +f 778//2628 808//2628 809//2628 +f 809//2629 810//2629 811//2629 +f 812//2630 811//2630 813//2630 +f 812//2631 814//2631 773//2631 +f 815//2632 816//2632 771//2632 +f 817//2633 730//2633 816//2633 +f 730//2634 817//2634 818//2634 +f 818//2635 770//2635 731//2635 +f 820//2636 821//2636 770//2636 +f 821//2637 769//2637 732//2637 +f 767//2638 822//2638 766//2638 +f 822//2639 765//2639 733//2639 +f 823//2640 764//2640 765//2640 +f 823//2641 824//2641 764//2641 +f 824//2642 763//2642 764//2642 +f 825//2643 761//2643 762//2643 +f 825//2644 762//2644 763//2644 +f 761//2645 825//2645 826//2645 +f 827//2646 760//2646 761//2646 +f 760//2647 827//2647 828//2647 +f 829//2648 759//2648 739//2648 +f 831//2649 757//2649 758//2649 +f 757//2650 831//2650 832//2650 +f 833//2651 834//2651 756//2651 +f 757//2652 756//2652 742//2652 +f 834//2653 755//2653 744//2653 +f 755//2654 834//2654 836//2654 +f 836//2655 754//2655 755//2655 +f 174//2656 173//2656 753//2656 +f 173//2657 172//2657 753//2657 +f 172//2658 171//2658 752//2658 +f 171//2659 170//2659 752//2659 +f 170//2660 169//2660 751//2660 +f 169//2661 168//2661 751//2661 +f 754//2662 836//2662 175//2662 +f 836//2663 835//2663 177//2663 +f 835//2664 836//2664 834//2664 +f 833//2665 832//2665 837//2665 +f 838//2666 828//2666 839//2666 +f 828//2667 838//2667 829//2667 +f 828//2668 826//2668 840//2668 +f 826//2669 828//2669 827//2669 +f 826//2670 842//2670 841//2670 +f 822//2671 845//2671 844//2671 +f 846//2672 767//2672 768//2672 +f 768//2673 821//2673 921//2673 +f 821//2674 768//2674 769//2674 +f 821//2675 820//2675 847//2675 +f 820//2676 849//2676 848//2676 +f 820//2677 819//2677 849//2677 +f 818//2678 850//2678 819//2678 +f 818//2679 817//2679 850//2679 +f 851//2680 817//2680 816//2680 +f 815//2681 852//2681 851//2681 +f 814//2682 853//2682 852//2682 +f 812//2683 813//2683 853//2683 +f 813//2684 855//2684 854//2684 +f 857//2685 856//2685 855//2685 +f 855//2686 810//2686 857//2686 +f 810//2687 809//2687 808//2687 +f 858//2688 860//2688 857//2688 +f 857//2689 808//2689 858//2689 +f 859//2690 862//2690 860//2690 +f 858//2691 807//2691 806//2691 +f 863//2692 253//2692 861//2692 +f 861//2693 806//2693 805//2693 +f 863//2694 804//2694 864//2694 +f 865//2695 255//2695 256//2695 +f 866//2696 258//2696 865//2696 +f 864//2697 802//2697 801//2697 +f 865//2698 801//2698 866//2698 +f 260//2699 259//2699 866//2699 +f 799//2700 798//2700 261//2700 +f 263//2701 867//2701 264//2701 +f 869//2702 48//2702 874//2702 +f 870//2703 303//2703 304//2703 +f 872//2704 336//2704 298//2704 +f 339//2705 338//2705 294//2705 +f 872//2706 299//2706 341//2706 +f 870//2707 343//2707 871//2707 +f 878//2708 877//2708 876//2708 +f 879//2709 881//2709 877//2709 +f 880//2710 884//2710 881//2710 +f 881//2711 879//2711 880//2711 +f 882//2712 883//2712 880//2712 +f 880//2713 793//2713 794//2713 +f 794//2714 363//2714 882//2714 +f 363//2715 794//2715 792//2715 +f 362//2716 792//2716 364//2716 +f 885//2717 902//2717 886//2717 +f 898//2718 881//2718 884//2718 +f 887//2719 877//2719 881//2719 +f 875//2720 890//2720 874//2720 +f 893//2721 892//2721 305//2721 +f 343//2722 870//2722 891//2722 +f 342//2723 871//2723 343//2723 +f 891//2724 345//2724 344//2724 +f 892//2725 346//2725 891//2725 +f 4//2726 894//2726 869//2726 +f 890//2727 875//2727 895//2727 +f 890//2728 4//2728 874//2728 +f 896//2729 895//2729 875//2729 +f 896//2730 875//2730 889//2730 +f 897//2731 887//2731 898//2731 +f 884//2732 899//2732 898//2732 +f 899//2733 884//2733 886//2733 +f 900//2734 886//2734 902//2734 +f 902//2735 885//2735 358//2735 +f 900//2736 356//2736 355//2736 +f 903//2737 352//2737 349//2737 +f 894//2738 347//2738 893//2738 +f 345//2739 891//2739 346//2739 +f 893//2740 347//2740 346//2740 +f 890//2741 348//2741 4//2741 +f 352//2742 903//2742 897//2742 +f 353//2743 898//2743 899//2743 +f 253//2744 251//2744 862//2744 +f 905//2745 249//2745 906//2745 +f 906//2746 860//2746 862//2746 +f 908//2747 857//2747 860//2747 +f 908//2748 909//2748 856//2748 +f 856//2749 857//2749 908//2749 +f 856//2750 909//2750 910//2750 +f 910//2751 854//2751 856//2751 +f 854//2752 910//2752 911//2752 +f 911//2753 912//2753 854//2753 +f 912//2754 853//2754 813//2754 +f 851//2755 914//2755 915//2755 +f 915//2756 850//2756 851//2756 +f 819//2757 916//2757 917//2757 +f 917//2758 849//2758 819//2758 +f 918//2759 848//2759 849//2759 +f 919//2760 920//2760 848//2760 +f 920//2761 847//2761 848//2761 +f 920//2762 921//2762 847//2762 +f 847//2763 921//2763 821//2763 +f 846//2764 921//2764 922//2764 +f 923//2765 845//2765 846//2765 +f 924//2766 844//2766 845//2766 +f 844//2767 924//2767 925//2767 +f 926//2768 843//2768 823//2768 +f 927//2769 942//2769 841//2769 +f 841//2770 842//2770 927//2770 +f 842//2771 843//2771 927//2771 +f 841//2772 940//2772 928//2772 +f 841//2773 929//2773 826//2773 +f 929//2774 938//2774 930//2774 +f 840//2775 930//2775 931//2775 +f 932//2776 837//2776 832//2776 +f 837//2777 932//2777 933//2777 +f 837//2778 835//2778 833//2778 +f 934//2779 177//2779 835//2779 +f 177//2780 934//2780 178//2780 +f 934//2781 935//2781 178//2781 +f 935//2782 934//2782 837//2782 +f 931//2783 938//2783 937//2783 +f 931//2784 930//2784 938//2784 +f 939//2785 971//2785 938//2785 +f 940//2786 968//2786 939//2786 +f 938//2787 929//2787 928//2787 +f 940//2788 841//2788 942//2788 +f 943//2789 965//2789 942//2789 +f 942//2790 927//2790 943//2790 +f 926//2791 925//2791 943//2791 +f 924//2792 945//2792 925//2792 +f 846//2793 922//2793 923//2793 +f 922//2794 921//2794 960//2794 +f 921//2795 920//2795 947//2795 +f 948//2796 960//2796 921//2796 +f 920//2797 919//2797 947//2797 +f 918//2798 28//2798 27//2798 +f 917//2799 950//2799 28//2799 +f 917//2800 951//2800 950//2800 +f 850//2801 915//2801 916//2801 +f 953//2802 914//2802 852//2802 +f 912//2803 911//2803 954//2803 +f 955//2804 911//2804 910//2804 +f 910//2805 909//2805 955//2805 +f 956//2806 907//2806 906//2806 +f 243//2807 240//2807 909//2807 +f 245//2808 243//2808 957//2808 +f 957//2809 956//2809 248//2809 +f 248//2810 247//2810 957//2810 +f 248//2811 906//2811 249//2811 +f 238//2812 954//2812 911//2812 +f 235//2813 953//2813 913//2813 +f 953//2814 952//2814 914//2814 +f 230//2815 950//2815 951//2815 +f 950//2816 230//2816 28//2816 +f 951//2817 231//2817 230//2817 +f 952//2818 232//2818 231//2818 +f 953//2819 235//2819 232//2819 +f 954//2820 238//2820 236//2820 +f 27//2821 26//2821 958//2821 +f 949//2822 27//2822 959//2822 +f 949//2823 947//2823 919//2823 +f 947//2824 949//2824 986//2824 +f 961//2825 945//2825 960//2825 +f 963//2826 964//2826 943//2826 +f 962//2827 943//2827 925//2827 +f 965//2828 943//2828 964//2828 +f 941//2829 942//2829 965//2829 +f 965//2830 978//2830 967//2830 +f 941//2831 967//2831 968//2831 +f 968//2832 940//2832 941//2832 +f 969//2833 970//2833 971//2833 +f 971//2834 939//2834 968//2834 +f 971//2835 970//2835 972//2835 +f 971//2836 937//2836 938//2836 +f 974//2837 935//2837 933//2837 +f 974//2838 975//2838 181//2838 +f 974//2839 973//2839 975//2839 +f 973//2840 972//2840 975//2840 +f 972//2841 970//2841 976//2841 +f 970//2842 969//2842 978//2842 +f 979//2843 1018//2843 977//2843 +f 978//2844 965//2844 966//2844 +f 966//2845 964//2845 963//2845 +f 981//2846 963//2846 962//2846 +f 944//2847 982//2847 981//2847 +f 946//2848 983//2848 982//2848 +f 983//2849 946//2849 945//2849 +f 945//2850 961//2850 983//2850 +f 960//2851 948//2851 984//2851 +f 986//2852 948//2852 947//2852 +f 987//2853 986//2853 949//2853 +f 959//2854 987//2854 949//2854 +f 958//2855 959//2855 27//2855 +f 958//2856 26//2856 223//2856 +f 220//2857 990//2857 988//2857 +f 220//2858 223//2858 221//2858 +f 988//2859 991//2859 959//2859 +f 993//2860 984//2860 948//2860 +f 994//2861 995//2861 985//2861 +f 995//2862 996//2862 985//2862 +f 996//2863 998//2863 982//2863 +f 998//2864 981//2864 982//2864 +f 981//2865 998//2865 1000//2865 +f 18//2866 966//2866 999//2866 +f 999//2867 966//2867 963//2867 +f 1000//2868 999//2868 981//2868 +f 1001//2869 1000//2869 998//2869 +f 1003//2870 1007//2870 1004//2870 +f 1003//2871 1002//2871 997//2871 +f 996//2872 995//2872 1003//2872 +f 1003//2873 1006//2873 1007//2873 +f 1003//2874 995//2874 1005//2874 +f 995//2875 206//2875 1005//2875 +f 995//2876 994//2876 206//2876 +f 985//2877 993//2877 994//2877 +f 986//2878 991//2878 992//2878 +f 1008//2879 991//2879 988//2879 +f 1009//2880 1008//2880 988//2880 +f 990//2881 218//2881 1010//2881 +f 990//2882 220//2882 218//2882 +f 1010//2883 213//2883 1009//2883 +f 213//2884 210//2884 1009//2884 +f 210//2885 208//2885 992//2885 +f 992//2886 991//2886 1008//2886 +f 208//2887 993//2887 992//2887 +f 208//2888 994//2888 993//2888 +f 1005//2889 205//2889 1006//2889 +f 1006//2890 1012//2890 1013//2890 +f 1013//2891 1012//2891 1007//2891 +f 1004//2892 200//2892 198//2892 +f 1001//2893 198//2893 196//2893 +f 1014//2894 19//2894 1000//2894 +f 1007//2895 201//2895 200//2895 +f 1012//2896 202//2896 201//2896 +f 1012//2897 203//2897 202//2897 +f 1015//2898 204//2898 203//2898 +f 205//2899 1005//2899 22//2899 +f 1011//2900 218//2900 217//2900 +f 980//2901 17//2901 979//2901 +f 17//2902 58//2902 1016//2902 +f 1016//2903 58//2903 1017//2903 +f 976//2904 1020//2904 183//2904 +f 1020//2905 1021//2905 186//2905 +f 1023//2906 1021//2906 1017//2906 +f 1022//2907 57//2907 188//2907 +f 57//2908 15//2908 10//2908 +f 1023//2909 186//2909 1021//2909 +f 183//2910 182//2910 975//2910 +f 783//2911 368//2911 366//2911 +f 97//2912 604//2912 98//2912 +f 99//2913 98//2913 604//2913 +f 569//2914 568//2914 484//2914 +f 541//2915 516//2915 566//2915 +f 451//2916 414//2916 570//2916 +f 104//2917 408//2917 38//2917 +f 378//2918 377//2918 380//2918 +f 354//2919 56//2919 55//2919 +f 307//2920 265//2920 48//2920 +f 228//2921 25//2921 28//2921 +f 1024//2922 1025//2922 209//2922 +f 211//2923 212//2923 214//2923 +f 1025//2924 61//2924 24//2924 +f 24//2925 23//2925 207//2925 +f 193//2926 20//2926 1014//2926 +f 191//2927 14//2927 20//2927 +f 15//2928 57//2928 16//2928 +f 1026//2929 1027//2929 11//2929 +f 1028//2930 1157//2930 1029//2930 +f 1031//2931 1073//2931 1032//2931 +f 1033//2932 1926//2932 1031//2932 +f 1030//2933 1961//2933 1034//2933 +f 1035//2934 1037//2934 1039//2934 +f 1040//2935 1898//2935 1041//2935 +f 1041//2936 1183//2936 1043//2936 +f 1052//2937 1053//2937 1351//2937 +f 1054//2938 1059//2938 1055//2938 +f 1054//2939 1057//2939 1058//2939 +f 1071//2940 55//2940 56//2940 +f 1073//2941 1969//2941 1074//2941 +f 61//2942 1038//2942 59//2942 +f 65//2943 1077//2943 1076//2943 +f 68//2944 70//2944 1079//2944 +f 1080//2945 1079//2945 70//2945 +f 72//2946 1081//2946 1080//2946 +f 74//2947 1082//2947 73//2947 +f 75//2948 1083//2948 1082//2948 +f 65//2949 1076//2949 1083//2949 +f 1084//2950 1085//2950 1077//2950 +f 1144//2951 1330//2951 1087//2951 +f 1087//2952 1145//2952 1144//2952 +f 1088//2953 1330//2953 1144//2953 +f 1088//2954 1143//2954 1089//2954 +f 1089//2955 1142//2955 1090//2955 +f 1090//2956 1333//2956 1332//2956 +f 1090//2957 1140//2957 1139//2957 +f 1091//2958 1333//2958 1090//2958 +f 1090//2959 1139//2959 1091//2959 +f 1094//2960 1336//2960 1093//2960 +f 1093//2961 1137//2961 1094//2961 +f 1337//2962 1134//2962 1095//2962 +f 1095//2963 1133//2963 1096//2963 +f 1096//2964 1342//2964 1095//2964 +f 1099//2965 1060//2965 1098//2965 +f 1098//2966 1131//2966 1099//2966 +f 1099//2967 1131//2967 1056//2967 +f 1100//2968 1059//2968 1102//2968 +f 1103//2969 1345//2969 1101//2969 +f 1102//2970 1059//2970 1058//2970 +f 1106//2971 1123//2971 1108//2971 +f 1108//2972 1109//2972 1349//2972 +f 1109//2973 1118//2973 1111//2973 +f 1111//2974 1117//2974 1049//2974 +f 1049//2975 1053//2975 1110//2975 +f 1048//2976 1113//2976 1112//2976 +f 1116//2977 1117//2977 1354//2977 +f 1118//2978 1117//2978 1111//2978 +f 1109//2979 1121//2979 1118//2979 +f 1121//2980 1356//2980 1118//2980 +f 1109//2981 1108//2981 1121//2981 +f 1121//2982 1108//2982 1123//2982 +f 1123//2983 1106//2983 1125//2983 +f 1126//2984 1128//2984 1124//2984 +f 1127//2985 1129//2985 1128//2985 +f 1126//2986 1051//2986 1057//2986 +f 1127//2987 1130//2987 1129//2987 +f 1127//2988 1057//2988 1056//2988 +f 1127//2989 1056//2989 1131//2989 +f 1133//2990 1360//2990 1097//2990 +f 1133//2991 1095//2991 1134//2991 +f 1135//2992 1366//2992 1134//2992 +f 1134//2993 1337//2993 1135//2993 +f 1138//2994 1092//2994 1139//2994 +f 1140//2995 1370//2995 1139//2995 +f 1141//2996 1372//2996 1140//2996 +f 1140//2997 1090//2997 1142//2997 +f 1142//2998 1089//2998 1143//2998 +f 1144//2999 1376//2999 1143//2999 +f 1143//3000 1088//3000 1144//3000 +f 66//3001 1146//3001 1084//3001 +f 1147//3002 1146//3002 143//3002 +f 1409//3003 1410//3003 469//3003 +f 470//3004 1440//3004 1148//3004 +f 147//3005 1440//3005 470//3005 +f 1443//3006 1442//3006 149//3006 +f 1475//3007 1443//3007 149//3007 +f 530//3008 1474//3008 1475//3008 +f 150//3009 1473//3009 1474//3009 +f 150//3010 152//3010 1473//3010 +f 531//87 1476//87 152//87 +f 153//1084 1498//1084 1476//1084 +f 158//3011 159//3011 156//3011 +f 163//3012 1149//3012 161//3012 +f 166//3013 167//3013 165//3013 +f 168//3014 1150//3014 167//3014 +f 178//3015 179//3015 176//3015 +f 182//3016 1152//3016 1151//3016 +f 185//3017 1153//3017 184//3017 +f 187//3018 1155//3018 1153//3018 +f 187//3019 189//3019 1154//3019 +f 190//3020 1156//3020 1154//3020 +f 11//3021 1027//3021 1156//3021 +f 1027//3022 1157//3022 1156//3022 +f 1157//3023 1027//3023 1026//3023 +f 1026//3024 1158//3024 1157//3024 +f 1026//3025 12//3025 192//3025 +f 192//3026 194//3026 1159//3026 +f 194//3027 195//3027 1160//3027 +f 195//3028 197//3028 1161//3028 +f 197//3029 199//3029 1162//3029 +f 199//3030 200//3030 1163//3030 +f 200//3031 201//3031 1164//3031 +f 201//3032 202//3032 1165//3032 +f 202//3033 203//3033 1165//3033 +f 203//3034 204//3034 1166//3034 +f 204//3035 60//3035 1166//3035 +f 1166//3036 59//3036 1039//3036 +f 1039//3037 1962//3037 1166//3037 +f 1036//3038 1168//3038 1167//3038 +f 1168//3039 1036//3039 1035//3039 +f 1168//3040 1170//3040 1169//3040 +f 1170//3041 1168//3041 1171//3041 +f 1170//3042 1173//3042 1172//3042 +f 1173//3043 1170//3043 1174//3043 +f 1175//3044 1176//3044 1173//3044 +f 216//3045 1958//3045 1176//3045 +f 1176//3046 214//3046 216//3046 +f 217//3047 1177//3047 216//3047 +f 64//3048 63//3048 1177//3048 +f 63//3049 1075//3049 1178//3049 +f 1075//3050 1179//3050 1178//3050 +f 1075//3051 1181//3051 1179//3051 +f 1181//3052 1184//3052 1183//3052 +f 1185//3053 1186//3053 1043//3053 +f 1186//3054 1187//3054 1040//3054 +f 1187//3055 1188//3055 1042//3055 +f 1188//3056 1191//3056 1190//3056 +f 1191//3057 1188//3057 1192//3057 +f 1193//3058 1195//3058 1194//3058 +f 1197//3059 1200//3059 1199//3059 +f 1200//3060 1197//3060 1201//3060 +f 1204//3061 1200//3061 1205//3061 +f 1204//3062 1206//3062 1905//3062 +f 1204//3063 1205//3063 1206//3063 +f 1207//3064 1235//3064 1209//3064 +f 1208//3065 1209//3065 1210//3065 +f 1210//3066 1811//3066 1852//3066 +f 1210//3067 1211//3067 1213//3067 +f 1211//3068 1232//3068 1214//3068 +f 1215//3069 1812//3069 1212//3069 +f 1214//3070 1216//3070 1217//3070 +f 1217//3071 1814//3071 1214//3071 +f 1217//3072 1216//3072 1218//3072 +f 1218//3073 1815//3073 1217//3073 +f 1218//3074 1220//3074 1219//3074 +f 1063//3075 1224//3075 1222//3075 +f 1222//3076 1221//3076 1226//3076 +f 1221//3077 1220//3077 1227//3077 +f 1229//3078 1216//3078 1231//3078 +f 1216//3079 1214//3079 1232//3079 +f 1233//3080 1270//3080 1231//3080 +f 1232//3081 1211//3081 1234//3081 +f 1234//3082 1209//3082 1235//3082 +f 1211//3083 1210//3083 1209//3083 +f 1235//3084 1207//3084 1206//3084 +f 1203//3085 1205//3085 1200//3085 +f 1203//3086 1201//3086 1238//3086 +f 1201//3087 1239//3087 1240//3087 +f 1239//3088 1196//3088 1240//3088 +f 1239//3089 1201//3089 1197//3089 +f 1196//3090 1242//3090 1241//3090 +f 1193//3091 1192//3091 1242//3091 +f 1192//3092 1193//3092 1191//3092 +f 1192//3093 1188//3093 1243//3093 +f 1243//3094 1187//3094 1186//3094 +f 1244//3095 1185//3095 1245//3095 +f 1246//3096 1285//3096 1245//3096 +f 1246//3097 1180//3097 1247//3097 +f 1180//3098 62//3098 286//3098 +f 286//3099 1247//3099 1180//3099 +f 287//3100 288//3100 1247//3100 +f 288//3101 289//3101 1249//3101 +f 1251//3102 1252//3102 1253//3102 +f 1252//3103 1296//3103 1253//3103 +f 1254//3104 1255//3104 1253//3104 +f 1255//3105 1254//3105 1256//3105 +f 1258//3106 1260//3106 1261//3106 +f 1266//3107 1061//3107 1265//3107 +f 1266//3108 1226//3108 1221//3108 +f 1266//3109 1227//3109 1228//3109 +f 1228//3110 1230//3110 1268//3110 +f 1269//3111 1292//3111 1268//3111 +f 1268//3112 1230//3112 1269//3112 +f 1230//3113 1231//3113 1269//3113 +f 1270//3114 1292//3114 1269//3114 +f 1269//3115 1231//3115 1270//3115 +f 1233//3116 1272//3116 1271//3116 +f 1272//3117 1274//3117 1271//3117 +f 1233//3118 1236//3118 1273//3118 +f 1273//3119 1275//3119 1274//3119 +f 1237//3120 1238//3120 1275//3120 +f 1278//3121 1238//3121 1201//3121 +f 1240//3122 1241//3122 1280//3122 +f 1241//3123 1281//3123 1280//3123 +f 1242//3124 1282//3124 1281//3124 +f 1242//3125 1192//3125 1243//3125 +f 1282//3126 1243//3126 1244//3126 +f 1285//3127 1248//3127 1284//3127 +f 1248//3128 1249//3128 1250//3128 +f 1284//3129 1250//3129 1283//3129 +f 1281//3130 1282//3130 1283//3130 +f 1283//3131 1286//3131 1280//3131 +f 1287//3132 1278//3132 1279//3132 +f 1278//3133 1257//3133 1277//3133 +f 1288//3134 1274//3134 1275//3134 +f 1289//3135 1271//3135 1274//3135 +f 1292//3136 1270//3136 1271//3136 +f 7//3137 1268//3137 1292//3137 +f 8//3138 1268//3138 7//3138 +f 1266//3139 8//3139 1061//3139 +f 1262//3140 1263//3140 7//3140 +f 1290//3141 1260//3141 1294//3141 +f 1289//3142 1261//3142 1260//3142 +f 1288//3143 1255//3143 1261//3143 +f 1276//3144 1257//3144 1255//3144 +f 1251//3145 1253//3145 1257//3145 +f 1287//3146 1279//3146 1286//3146 +f 1267//3147 1293//3147 1063//3147 +f 1267//3148 1226//3148 1265//3148 +f 1294//3149 1817//3149 1263//3149 +f 1263//3150 1262//3150 1294//3150 +f 1256//3151 1254//3151 1295//3151 +f 1254//3152 1253//3152 1296//3152 +f 1252//3153 31//3153 1296//3153 +f 1296//3154 32//3154 337//3154 +f 340//3155 1819//3155 1295//3155 +f 343//3156 344//3156 342//3156 +f 347//3157 6//3157 346//3157 +f 904//3158 349//3158 1297//3158 +f 350//3159 55//3159 1071//3159 +f 1299//3160 1300//3160 1301//3160 +f 1848//3161 1301//3161 1321//3161 +f 1069//3162 1849//3162 1848//3162 +f 1069//3163 1303//3163 1302//3163 +f 1305//3164 1303//3164 1304//3164 +f 1305//3165 1302//3165 1303//3165 +f 1065//3166 1305//3166 1304//3166 +f 1306//3167 1302//3167 1305//3167 +f 1307//3168 1302//3168 1306//3168 +f 1306//3169 1064//3169 1308//3169 +f 1064//3170 1310//3170 1309//3170 +f 1310//3171 1064//3171 1065//3171 +f 1310//3172 1065//3172 1312//3172 +f 1065//3173 1304//3173 1312//3173 +f 1312//3174 1314//3174 1313//3174 +f 1304//3175 1316//3175 1314//3175 +f 1303//3176 1067//3176 1316//3176 +f 1316//3177 1304//3177 1303//3177 +f 1067//3178 1317//3178 1316//3178 +f 1319//3179 1620//3179 1317//3179 +f 1067//3180 1066//3180 1318//3180 +f 1320//3181 1066//3181 1321//3181 +f 1321//3182 1323//3182 1320//3182 +f 1301//3183 1300//3183 68//3183 +f 1299//3184 1072//3184 1300//3184 +f 1298//3185 1070//3185 1072//3185 +f 1072//3186 1299//3186 1298//3186 +f 1078//3187 1324//3187 1323//3187 +f 1325//3188 1080//3188 1081//3188 +f 73//3189 1326//3189 1081//3189 +f 1327//3190 1083//3190 1076//3190 +f 1077//3191 1086//3191 1076//3191 +f 1088//3192 1584//3192 1330//3192 +f 1331//3193 1089//3193 1332//3193 +f 1334//3194 1091//3194 1335//3194 +f 1335//3195 1093//3195 1336//3195 +f 1336//3196 1094//3196 1337//3196 +f 1338//3197 1095//3197 1342//3197 +f 1342//3198 1096//3198 1340//3198 +f 1060//3199 1341//3199 1340//3199 +f 1340//3200 1098//3200 1060//3200 +f 1341//3201 1100//3201 1101//3201 +f 1060//3202 1055//3202 1100//3202 +f 1341//3203 1343//3203 1342//3203 +f 1101//3204 1344//3204 1343//3204 +f 1103//3205 1347//3205 1345//3205 +f 1105//3206 1107//3206 1349//3206 +f 1110//3207 1053//3207 1052//3207 +f 1114//3208 1351//3208 1053//3208 +f 1053//3209 1112//3209 1114//3209 +f 1114//3210 1546//3210 1351//3210 +f 1114//3211 1113//3211 1352//3211 +f 1115//3212 1116//3212 1353//3212 +f 1353//3213 1354//3213 1526//3213 +f 1117//3214 1119//3214 1355//3214 +f 1355//3215 1354//3215 1117//3215 +f 1357//3216 1128//3216 1129//3216 +f 1358//3217 1129//3217 1130//3217 +f 1130//3218 1391//3218 1358//3218 +f 1391//3219 1131//3219 1132//3219 +f 1133//3220 1362//3220 1361//3220 +f 1362//3221 1133//3221 1364//3221 +f 1364//3222 1134//3222 1366//3222 +f 1367//3223 1386//3223 1366//3223 +f 1366//3224 1135//3224 1136//3224 +f 1369//3225 1383//3225 1368//3225 +f 1138//3226 1139//3226 1370//3226 +f 1370//3227 1372//3227 1371//3227 +f 1370//3228 1140//3228 1372//3228 +f 1373//3229 1380//3229 1372//3229 +f 1372//3230 1141//3230 1373//3230 +f 1141//3231 1142//3231 1374//3231 +f 1376//3232 1377//3232 1375//3232 +f 1374//3233 1143//3233 1376//3233 +f 1376//3234 1144//3234 1145//3234 +f 1147//3235 1410//3235 1377//3235 +f 1375//3236 1378//3236 1379//3236 +f 1379//3237 1407//3237 1380//3237 +f 1382//3238 1383//3238 1369//3238 +f 1383//3239 1382//3239 1384//3239 +f 1383//3240 1385//3240 1368//3240 +f 1365//3241 1366//3241 1386//3241 +f 1386//3242 1387//3242 1365//3242 +f 1388//3243 1389//3243 1363//3243 +f 1389//3244 1361//3244 1362//3244 +f 1132//3245 1360//3245 1359//3245 +f 1359//3246 1391//3246 1132//3246 +f 1392//3247 1518//3247 1394//3247 +f 1518//3248 1392//3248 1517//3248 +f 1517//3249 1393//3249 1395//3249 +f 1391//3250 1359//3250 1396//3250 +f 1359//3251 1390//3251 1422//3251 +f 1390//3252 1359//3252 1360//3252 +f 1361//3253 1389//3253 1397//3253 +f 1388//3254 1400//3254 1399//3254 +f 1400//3255 1388//3255 1365//3255 +f 1384//3256 1415//3256 1404//3256 +f 1382//3257 1381//3257 1405//3257 +f 1380//3258 1407//3258 1406//3258 +f 1407//3259 1379//3259 1378//3259 +f 1409//3260 1377//3260 1410//3260 +f 1410//3261 1147//3261 145//3261 +f 1406//3262 1407//3262 1411//3262 +f 1413//3263 1405//3263 1381//3263 +f 1413//3264 1439//3264 1414//3264 +f 1405//3265 1414//3265 1415//3265 +f 1415//3266 1384//3266 1405//3266 +f 1415//3267 1436//3267 1416//3267 +f 1416//3268 1404//3268 1415//3268 +f 1416//3269 1403//3269 1404//3269 +f 1419//3270 1399//3270 1400//3270 +f 1399//3271 1419//3271 1420//3271 +f 1426//3272 1396//3272 1359//3272 +f 1423//1351 1395//1351 1393//1351 +f 1427//3273 1426//3273 1422//3273 +f 1399//3274 1420//3274 1430//3274 +f 1419//3275 1432//3275 1431//3275 +f 1418//3276 1417//3276 1433//3276 +f 1417//3277 1418//3277 1403//3277 +f 1436//3278 1414//3278 1438//3278 +f 1436//3279 1415//3279 1414//3279 +f 1442//3280 1438//3280 1439//3280 +f 1439//3281 1413//3281 1412//3281 +f 1440//3282 1411//3282 1148//3282 +f 147//3283 148//3283 1441//3283 +f 1441//3284 1412//3284 147//3284 +f 1443//3285 1437//3285 1438//3285 +f 1444//3286 1435//3286 1437//3286 +f 1435//3287 1444//3287 1445//3287 +f 1445//3288 1434//3288 1435//3288 +f 1446//3289 1431//3289 1432//3289 +f 1431//3290 1446//3290 1448//3290 +f 1448//3291 1449//3291 1431//3291 +f 1449//3292 1448//3292 1450//3292 +f 1449//3293 1430//3293 1420//3293 +f 1430//3294 1449//3294 1450//3294 +f 1451//3295 1429//3295 1421//3295 +f 1429//3296 1451//3296 1452//3296 +f 1453//3297 1454//3297 1428//3297 +f 1454//3298 1453//3298 1455//3298 +f 1454//3299 1456//3299 1427//3299 +f 1456//3300 1426//3300 1427//3300 +f 1423//3301 1426//3301 1457//3301 +f 1424//3302 1425//3302 1458//3302 +f 1425//3303 1459//3303 1458//3303 +f 1423//3304 1457//3304 1459//3304 +f 1459//3305 1425//3305 1423//3305 +f 1456//3306 1454//3306 1460//3306 +f 1456//3307 1457//3307 1426//3307 +f 1454//3308 1455//3308 1460//3308 +f 1453//3309 1452//3309 1461//3309 +f 1452//3310 1463//3310 1462//3310 +f 1452//3311 1453//3311 1429//3311 +f 1451//3312 1450//3312 1463//3312 +f 1463//3313 1452//3313 1451//3313 +f 1450//3314 1467//3314 1466//3314 +f 1448//3315 1447//3315 1467//3315 +f 1467//3316 1450//3316 1448//3316 +f 1447//3317 1448//3317 1446//3317 +f 1446//3318 1470//3318 1469//3318 +f 1470//3319 1446//3319 1434//3319 +f 1445//3320 1472//3320 1471//3320 +f 1445//3321 1473//3321 1472//3321 +f 1444//3322 1474//3322 1473//3322 +f 1473//3323 1445//3323 1444//3323 +f 1444//3324 1475//3324 1474//3324 +f 152//3325 1472//3325 1473//3325 +f 152//3326 1471//3326 1472//3326 +f 1471//3327 152//3327 1476//3327 +f 1471//3328 1469//3328 1470//3328 +f 1478//3329 1468//3329 1469//3329 +f 1479//3330 1466//3330 1467//3330 +f 1466//3331 1479//3331 1480//3331 +f 1481//3332 1464//3332 1465//3332 +f 1482//3333 1461//3333 1462//3333 +f 1483//3334 1484//3334 1455//3334 +f 1484//3335 1460//3335 1455//3335 +f 1459//3336 1457//3336 1045//3336 +f 1458//3337 1459//3337 1045//3337 +f 1045//3338 1485//3338 1458//3338 +f 1488//3339 1045//3339 1457//3339 +f 1460//3340 1484//3340 1489//3340 +f 1484//3341 1483//3341 1490//3341 +f 1483//3342 1491//3342 1490//3342 +f 1491//3343 1483//3343 1461//3343 +f 1482//3344 1493//3344 1492//3344 +f 1493//3345 1482//3345 1464//3345 +f 1481//3346 1480//3346 1494//3346 +f 1480//3347 1481//3347 1466//3347 +f 1480//3348 1496//3348 1495//3348 +f 1477//3349 1498//3349 1497//3349 +f 1498//3350 1477//3350 1471//3350 +f 1497//3351 154//3351 155//3351 +f 1499//3352 1495//3352 1496//3352 +f 1495//3353 1499//3353 1500//3353 +f 1501//3354 1494//3354 1480//3354 +f 1494//3355 1501//3355 1502//3355 +f 1503//3356 1492//3356 1493//3356 +f 1492//3357 1503//3357 1504//3357 +f 1506//3358 1490//3358 1491//3358 +f 1490//3359 1506//3359 1507//3359 +f 1508//3360 1489//3360 1484//3360 +f 1489//3361 1508//3361 1509//3361 +f 1489//3362 1487//3362 1488//3362 +f 1510//3363 1509//3363 1511//3363 +f 1510//3364 1511//3364 1512//3364 +f 1487//3365 1510//3365 1513//3365 +f 1486//3366 1485//3366 1045//3366 +f 1047//3367 1521//3367 1120//3367 +f 1522//3368 1524//3368 1523//3368 +f 1524//3369 1657//3369 1525//3369 +f 1525//3370 1527//3370 1526//3370 +f 1527//3371 1529//3371 1528//3371 +f 1529//3372 1527//3372 1530//3372 +f 1531//3373 1534//3373 1533//3373 +f 1534//3374 1532//3374 1535//3374 +f 1535//3375 1536//3375 1534//3375 +f 1536//3376 1535//3376 1062//3376 +f 1537//3377 1555//3377 1538//3377 +f 1538//3378 1554//3378 1540//3378 +f 1539//3379 1549//3379 1538//3379 +f 1542//3380 1547//3380 1540//3380 +f 1540//3381 1541//3381 1542//3381 +f 1541//3382 1552//3382 1542//3382 +f 1544//3383 1550//3383 1348//3383 +f 1350//3384 1052//3384 1545//3384 +f 1052//3385 1351//3385 1545//3385 +f 1545//3386 1351//3386 1546//3386 +f 1547//3387 1543//3387 1544//3387 +f 1547//3388 1352//3388 1533//3388 +f 1352//3389 1115//3389 1353//3389 +f 1353//3390 1526//3390 1528//3390 +f 1524//3391 1526//3391 1354//3391 +f 1355//3392 1521//3392 1523//3392 +f 1523//3393 1354//3393 1355//3393 +f 1523//3394 1521//3394 1522//3394 +f 1529//3395 1548//3395 1352//3395 +f 1548//3396 1529//3396 1531//3396 +f 1534//3397 1538//3397 1549//3397 +f 1533//3398 1352//3398 1548//3398 +f 1539//3399 1540//3399 1547//3399 +f 1547//3400 1542//3400 1543//3400 +f 1551//3401 1347//3401 1104//3401 +f 1551//3402 1542//3402 1552//3402 +f 1550//3403 1543//3403 1542//3403 +f 1553//3404 1596//3404 1595//3404 +f 1554//3405 1597//3405 1553//3405 +f 1541//3406 1540//3406 1554//3406 +f 1554//3407 1538//3407 1555//3407 +f 1557//3408 1601//3408 1556//3408 +f 1555//3409 1537//3409 1536//3409 +f 1559//3410 1561//3410 1560//3410 +f 1561//3411 1559//3411 1562//3411 +f 1561//3412 1566//3412 1565//3412 +f 1567//3413 1570//3413 1569//3413 +f 1568//3414 1571//3414 1570//3414 +f 1571//3415 1572//3415 1570//3415 +f 1577//3416 1575//3416 1578//3416 +f 1324//3417 1580//3417 1577//3417 +f 1578//3418 1320//3418 1323//3418 +f 1580//3419 1324//3419 1079//3419 +f 1082//3420 1328//3420 1326//3420 +f 1328//3421 1082//3421 1083//3421 +f 1328//3422 1582//3422 1609//3422 +f 1086//3423 1583//3423 1329//3423 +f 1584//3424 1583//3424 1330//3424 +f 1584//3425 1088//3425 1331//3425 +f 1586//3426 1332//3426 1587//3426 +f 1587//3427 1613//3427 1612//3427 +f 1587//3428 1333//3428 1334//3428 +f 1334//3429 1604//3429 1614//3429 +f 1615//3430 1604//3430 1334//3430 +f 1335//3431 1336//3431 1588//3431 +f 1588//3432 1336//3432 1589//3432 +f 1590//3433 1618//3433 1589//3433 +f 1339//3434 1619//3434 1590//3434 +f 1343//3435 1592//3435 1591//3435 +f 1591//3436 1342//3436 1343//3436 +f 1592//3437 1343//3437 1344//3437 +f 1595//3438 1345//3438 1347//3438 +f 1347//3439 1551//3439 1595//3439 +f 1596//3440 1553//3440 1597//3440 +f 1598//3441 1619//3441 1594//3441 +f 1598//3442 1555//3442 1599//3442 +f 1597//3443 1554//3443 1555//3443 +f 1600//3444 1617//3444 1599//3444 +f 1599//3445 1556//3445 1600//3445 +f 1601//3446 1616//3446 1600//3446 +f 1602//3447 1616//3447 1601//3447 +f 1601//3448 1557//3448 1602//3448 +f 1602//3449 1558//3449 1603//3449 +f 1603//3450 1560//3450 1563//3450 +f 1565//3451 1563//3451 1561//3451 +f 1565//3452 1612//3452 1563//3452 +f 1566//3453 1569//3453 1605//3453 +f 1569//3454 1607//3454 1606//3454 +f 1607//3455 1570//3455 1572//3455 +f 1608//3456 1576//3456 1609//3456 +f 1576//3457 1579//3457 1610//3457 +f 1579//3458 1577//3458 1580//3458 +f 1580//3459 1325//3459 1579//3459 +f 1326//3460 1610//3460 1579//3460 +f 1610//3461 1326//3461 1328//3461 +f 1582//3462 1607//3462 1609//3462 +f 1583//3463 1611//3463 1606//3463 +f 1611//3464 1585//3464 1586//3464 +f 1613//3465 1587//3465 1614//3465 +f 1615//3466 1602//3466 1604//3466 +f 1615//3467 1335//3467 1588//3467 +f 1617//3468 1600//3468 1588//3468 +f 1588//3469 1589//3469 1617//3469 +f 1618//3470 1590//3470 1619//3470 +f 1593//3471 1594//3471 1619//3471 +f 1619//3472 1598//3472 1599//3472 +f 1604//3473 1603//3473 1613//3473 +f 1613//3474 1563//3474 1612//3474 +f 1612//3475 1565//3475 1605//3475 +f 1606//3476 1611//3476 1569//3476 +f 1609//3477 1607//3477 1608//3477 +f 1610//3478 1609//3478 1576//3478 +f 1619//3479 1339//3479 1591//3479 +f 1575//3480 1574//3480 1620//3480 +f 1568//3481 1623//3481 1622//3481 +f 1623//3482 1568//3482 1567//3482 +f 1564//3483 1562//3483 1624//3483 +f 1562//3484 1564//3484 1561//3484 +f 1559//3485 1660//3485 1562//3485 +f 1626//3486 1674//3486 1625//3486 +f 1625//3487 1062//3487 1626//3487 +f 1530//3488 1532//3488 1531//3488 +f 1522//3489 1520//3489 1656//3489 +f 1519//3490 1630//3490 1629//3490 +f 1485//3491 1514//3491 1632//3491 +f 1514//3492 1513//3492 1633//3492 +f 1513//3493 1514//3493 1486//3493 +f 1513//3494 1512//3494 1634//3494 +f 1512//3495 1513//3495 1510//3495 +f 1511//3496 1636//3496 1635//3496 +f 1509//3497 1637//3497 1636//3497 +f 1508//3498 1507//3498 1637//3498 +f 1507//3499 1638//3499 1637//3499 +f 1506//3500 1505//3500 1638//3500 +f 1638//3501 1507//3501 1506//3501 +f 1504//3502 1505//3502 1492//3502 +f 1503//3503 1502//3503 1504//3503 +f 1502//3504 1642//3504 1641//3504 +f 1499//3505 159//3505 1500//3505 +f 1500//3506 159//3506 161//3506 +f 1149//3507 1641//3507 1642//3507 +f 1641//3508 1149//3508 1643//3508 +f 1644//3509 1640//3509 1504//3509 +f 1640//3510 1644//3510 1645//3510 +f 1645//3511 1639//3511 1640//3511 +f 1639//3512 1637//3512 1638//3512 +f 1637//3513 1639//3513 1646//3513 +f 1646//3514 1636//3514 1637//3514 +f 1647//3515 1635//3515 1636//3515 +f 1649//3516 1651//3516 1635//3516 +f 1651//3517 1634//3517 1512//3517 +f 1634//3518 1651//3518 1652//3518 +f 1653//3519 1633//3519 1513//3519 +f 1633//3520 1653//3520 1654//3520 +f 1655//3521 1632//3521 1514//3521 +f 1515//3522 1681//3522 1516//3522 +f 1680//3523 1630//3523 1516//3523 +f 1656//3524 1628//3524 1522//3524 +f 1677//3525 1530//3526 1525//3527 +f 1627//3528 1535//3528 1530//3528 +f 1658//3529 1626//3529 1535//3529 +f 1661//3530 1624//3530 1562//3530 +f 1624//3531 1661//3531 1662//3531 +f 1664//3532 1622//3532 1623//3532 +f 1622//3533 1665//3533 1666//3533 +f 1666//3534 1621//3534 1622//3534 +f 1621//3535 1666//3535 1667//3535 +f 1667//3536 1668//3536 1621//3536 +f 1668//3537 1620//3537 1574//3537 +f 1620//3538 1668//3538 1669//3538 +f 1668//3539 1667//3539 1670//3539 +f 1670//3540 1726//3540 1671//3540 +f 1670//3541 1667//3541 1672//3541 +f 1672//3542 1723//3542 1670//3542 +f 1665//3543 1721//3543 1672//3543 +f 1663//3544 1720//3544 1665//3544 +f 1661//3545 1673//3545 1662//3545 +f 1673//3546 1661//3546 1660//3546 +f 1674//3547 1659//3547 1675//3547 +f 1659//3548 1677//3548 1676//3548 +f 1677//3549 1659//3549 1627//3549 +f 1677//3550 1627//3550 1530//3550 +f 1628//3551 1713//3551 1715//3551 +f 1713//3552 1628//3552 1656//3552 +f 1679//3553 1656//3553 1629//3553 +f 1680//3554 1516//3554 1681//3554 +f 1631//3555 1632//3555 1683//3555 +f 1632//3556 1655//3556 1684//3556 +f 1685//3557 1654//3557 1686//3557 +f 1654//3558 1685//3558 1633//3558 +f 1653//3559 1652//3559 1687//3559 +f 1651//3560 1650//3560 1688//3560 +f 1650//3561 1651//3561 1649//3561 +f 1648//3562 1691//3562 1689//3562 +f 1646//3563 1693//3563 1690//3563 +f 1690//3564 1647//3564 1646//3564 +f 1692//3565 1646//3565 1639//3565 +f 1645//3566 1694//3566 1692//3566 +f 1692//3567 1639//3567 1645//3567 +f 1644//3568 1643//3568 1694//3568 +f 1694//3569 1645//3569 1644//3569 +f 1149//3570 163//3570 165//3570 +f 1643//3571 165//3571 167//3571 +f 1150//3572 1693//3572 1694//3572 +f 1693//3573 1150//3573 1695//3573 +f 1692//3574 1694//3574 1693//3574 +f 1693//3575 1695//3575 1696//3575 +f 1696//3576 1690//3576 1693//3576 +f 1696//3577 1695//3577 1697//3577 +f 1691//3578 1697//3578 1698//3578 +f 1700//3579 1688//3579 1650//3579 +f 1688//3580 1700//3580 1701//3580 +f 1702//3581 1687//3581 1652//3581 +f 1687//3582 1702//3582 1703//3582 +f 1704//3583 1686//3583 1654//3583 +f 1686//3584 1704//3584 1705//3584 +f 1705//3585 1706//3585 1686//3585 +f 1706//3586 1684//3586 1685//3586 +f 1684//3587 1706//3587 1707//3587 +f 1709//3588 1710//3588 1683//3588 +f 1710//3589 1711//3589 1683//3589 +f 1711//3590 1682//3590 1681//3590 +f 1716//3591 1678//3591 1628//3591 +f 1717//3592 1765//3592 1677//3592 +f 1765//3593 1676//3593 1677//3593 +f 1763//3594 1675//3594 1659//3594 +f 1675//3595 1673//3595 1674//3595 +f 1673//3596 1675//3596 1718//3596 +f 1718//3597 1662//3597 1673//3597 +f 1720//3598 1663//3598 1719//3598 +f 1721//3599 1758//3599 1722//3599 +f 1721//3600 1665//3600 1720//3600 +f 1722//3601 1672//3601 1721//3601 +f 1723//3602 1672//3602 1722//3602 +f 1723//3603 1756//3603 1724//3603 +f 1724//3604 1670//3604 1723//3604 +f 1724//3605 1725//3605 1726//3605 +f 1726//3606 1670//3606 1724//3606 +f 1726//3607 1728//3607 1727//3607 +f 1316//3608 1317//3608 1671//3608 +f 1727//3609 1728//3609 1313//3609 +f 1728//3610 1729//3610 1313//3610 +f 1729//3611 1731//3611 1730//3611 +f 1730//3612 1732//3612 1310//3612 +f 1309//3613 1734//3613 1735//3613 +f 1736//3614 1737//3614 1308//3614 +f 1737//3615 1736//3615 1738//3615 +f 1737//3616 1740//3616 1739//3616 +f 1742//3617 1827//3617 1740//3617 +f 1740//3618 1737//3618 1743//3618 +f 1737//3619 1738//3619 1743//3619 +f 1743//3620 1746//3620 1745//3620 +f 1738//3621 1747//3621 1746//3621 +f 1735//3622 1748//3622 1747//3622 +f 1734//3623 1749//3623 1748//3623 +f 1750//3624 1813//3624 1749//3624 +f 1751//3625 1813//3625 1750//3625 +f 1733//3626 1732//3626 1751//3626 +f 1751//3627 1731//3627 1752//3627 +f 1731//3628 1729//3628 1753//3628 +f 1729//3629 1728//3629 1754//3629 +f 1728//3630 1726//3630 1725//3630 +f 1756//3631 1723//3631 1758//3631 +f 1723//3632 1722//3632 1758//3632 +f 1759//3633 1803//3633 1758//3633 +f 1758//3634 1721//3634 1760//3634 +f 1720//3635 1761//3635 1760//3635 +f 1760//3636 1721//3636 1720//3636 +f 1761//3637 1720//3637 1719//3637 +f 1719//3638 1762//3638 1761//3638 +f 1718//3639 1763//3639 1762//3639 +f 1763//3640 1718//3640 1675//3640 +f 1763//3641 1676//3641 1764//3641 +f 1766//3642 1768//3642 1767//3642 +f 1678//3643 1716//3643 1768//3643 +f 1768//3644 1717//3644 1678//3644 +f 1714//3645 1715//3645 1713//3645 +f 1713//3646 1769//3646 1714//3646 +f 1679//3647 1712//3647 1769//3647 +f 1769//3648 1713//3648 1679//3648 +f 1712//3649 1679//3649 1680//3649 +f 1712//3650 1711//3650 1770//3650 +f 1711//3651 1712//3651 1682//3651 +f 1709//3652 1708//3652 1772//3652 +f 1708//3653 1774//3653 1773//3653 +f 1708//3654 1707//3654 1774//3654 +f 1707//3655 1776//3655 1775//3655 +f 1707//3656 1708//3656 1684//3656 +f 1776//3657 1777//3657 1786//3657 +f 1776//3658 1707//3658 1706//3658 +f 1705//3659 1778//3659 1777//3659 +f 1704//3660 1703//3660 1778//3660 +f 1778//3661 1705//3661 1704//3661 +f 1702//3662 1701//3662 1780//3662 +f 1701//3663 1782//3663 1781//3663 +f 1699//3664 1698//3664 1783//3664 +f 1698//3665 1699//3665 1691//3665 +f 1698//3666 173//3666 174//3666 +f 1697//3667 172//3667 173//3667 +f 1697//3668 171//3668 172//3668 +f 1695//3669 170//3669 171//3669 +f 1695//3670 169//3670 170//3670 +f 1783//3671 174//3671 175//3671 +f 1784//3672 1781//3672 1782//3672 +f 1781//3673 1780//3673 1701//3673 +f 1780//3674 1779//3674 1703//3674 +f 1779//3675 1777//3675 1778//3675 +f 1787//3676 1775//3676 1786//3676 +f 1775//3677 1787//3677 1788//3677 +f 1788//3678 1773//3678 1775//3678 +f 1773//3679 1788//3679 1875//3679 +f 1790//3680 1771//3680 1772//3680 +f 1771//3681 1790//3681 1791//3681 +f 1770//3682 1769//3682 1712//3682 +f 1714//3683 1793//3683 1794//3683 +f 1715//3684 1794//3684 1868//3684 +f 1868//3685 1768//3685 1715//3685 +f 1768//3686 1795//3686 1767//3686 +f 1798//3687 1764//3687 1765//3687 +f 1799//3688 1762//3688 1763//3688 +f 1799//3689 1800//3689 1762//3689 +f 1800//3690 1761//3690 1762//3690 +f 1802//3691 1803//3691 1759//3691 +f 1803//3692 1802//3692 1804//3692 +f 1757//3693 1758//3693 1803//3693 +f 1805//3694 1804//3694 1806//3694 +f 1755//3695 1756//3695 1757//3695 +f 1806//3696 1808//3696 1807//3696 +f 1809//3697 1807//3697 1808//3697 +f 1808//3698 1811//3698 1809//3698 +f 1753//3699 1754//3699 1807//3699 +f 1807//3700 1810//3700 1753//3700 +f 1810//3701 1809//3701 1811//3701 +f 1810//3702 1212//3702 1812//3702 +f 1752//3703 1753//3703 1810//3703 +f 1810//3704 1812//3704 1752//3704 +f 1752//3705 1812//3705 1751//3705 +f 1813//3706 1751//3706 1812//3706 +f 1812//3707 1215//3707 1813//3707 +f 1215//3708 1214//3708 1814//3708 +f 1814//3709 1217//3709 1815//3709 +f 1748//3710 1749//3710 1813//3710 +f 1815//3711 1218//3711 1219//3711 +f 1815//3712 1747//3712 1748//3712 +f 1219//3713 1747//3713 1815//3713 +f 1219//3714 1746//3714 1747//3714 +f 1220//3715 1745//3715 1746//3715 +f 1744//3716 1222//3716 1224//3716 +f 1223//3717 1816//3717 1824//3717 +f 1225//3718 1822//3718 1816//3718 +f 1225//3719 1293//3719 1822//3719 +f 1264//3720 1263//3720 1817//3720 +f 1817//3721 1294//3721 1818//3721 +f 1819//3722 1259//3722 1258//3722 +f 1254//3723 338//3723 1295//3723 +f 338//3724 1254//3724 1296//3724 +f 341//3725 1259//3725 1819//3725 +f 1818//3726 342//3726 1817//3726 +f 1817//3727 1839//3727 1264//3727 +f 1267//3728 1842//3728 1293//3728 +f 1825//3729 1828//3729 1827//3729 +f 1223//3730 1824//3730 1826//3730 +f 1826//3731 1742//3731 1224//3731 +f 1828//3732 1832//3732 1829//3732 +f 1829//3733 1831//3733 1830//3733 +f 1829//3734 1830//3734 1741//3734 +f 1831//3735 1829//3735 1832//3735 +f 1830//3736 1831//3736 1833//3736 +f 1830//3737 1307//3737 1741//3737 +f 1307//3738 1830//3738 1302//3738 +f 1307//3739 1306//3739 1739//3739 +f 1833//3740 1831//3740 1834//3740 +f 1832//3741 1828//3741 1845//3741 +f 1828//3742 1825//3742 1835//3742 +f 1835//3743 1824//3743 1836//3743 +f 1836//3744 1816//3744 1837//3744 +f 1822//3745 1838//3745 1823//3745 +f 1265//3746 1840//3746 1841//3747 +f 1820//3748 1264//3748 1839//3748 +f 1839//3749 1817//3749 342//3749 +f 342//3750 344//3750 1839//3750 +f 344//3751 345//3751 1839//3751 +f 1839//3752 6//3752 1840//3752 +f 1840//3753 6//3754 1841//3755 +f 1823//3756 1838//3756 1843//3756 +f 1837//3757 1823//3757 1843//3757 +f 1837//3758 1850//3758 1836//3758 +f 1845//3759 1846//3759 1832//3759 +f 1846//3760 1299//3760 1834//3760 +f 1847//3761 1848//3761 1849//3761 +f 1849//3762 1834//3762 1847//3762 +f 1849//3763 1069//3763 1302//3763 +f 1850//3764 1837//3764 1297//3764 +f 1297//3765 350//3765 1850//3765 +f 1297//3766 1843//3766 1851//3766 +f 1841//3767 347//3767 1842//3767 +f 6//3768 1839//3768 345//3768 +f 6//3769 347//3769 1841//3769 +f 348//3770 895//3770 1838//3770 +f 350//3771 1071//3771 1844//3771 +f 1844//3772 1850//3772 350//3772 +f 1070//3773 1298//3773 1846//3773 +f 1851//3774 1838//3774 895//3774 +f 1811//3775 1210//3775 1212//3775 +f 1212//3776 1810//3776 1811//3776 +f 1811//3777 1808//3777 1853//3777 +f 1808//3778 1806//3778 1855//3778 +f 1855//3779 1806//3779 1804//3779 +f 1804//3780 1802//3780 1857//3780 +f 1802//3781 1859//3781 1858//3781 +f 1859//3782 1802//3782 1759//3782 +f 1801//3783 1800//3783 1860//3783 +f 1799//3784 1798//3784 1862//3784 +f 1798//3785 1799//3785 1764//3785 +f 1864//3786 1866//3786 1865//3786 +f 1867//3787 1796//3787 1767//3787 +f 1795//3788 1894//3788 1867//3788 +f 1794//3789 1793//3789 1869//3789 +f 1793//3790 1870//3790 1869//3790 +f 1792//3791 1872//3791 1871//3791 +f 1872//3792 1792//3792 1770//3792 +f 1789//3793 1888//3793 1873//3793 +f 1874//3794 1886//3794 1789//3794 +f 1876//3795 1884//3795 1875//3795 +f 1788//3796 1787//3796 1877//3796 +f 1877//3797 1786//3797 1878//3797 +f 1779//3798 1879//3798 1878//3798 +f 1878//3799 1786//3799 1779//3799 +f 1785//3800 1781//3800 1880//3800 +f 176//3801 1781//3801 1784//3801 +f 1781//3802 176//3802 179//3802 +f 179//3803 1880//3803 1781//3803 +f 1880//3804 1879//3804 1785//3804 +f 1881//3805 1877//3805 1878//3805 +f 1877//3806 1882//3806 1883//3806 +f 1884//3807 1917//3807 1885//3807 +f 1884//3808 1885//3808 1874//3808 +f 1885//3809 1914//3809 1886//3809 +f 1874//3810 1875//3810 1884//3810 +f 1886//3811 1887//3811 1888//3811 +f 1888//3812 1911//3812 1889//3812 +f 1872//3813 1873//3813 1889//3813 +f 1889//3814 1871//3814 1872//3814 +f 1871//3815 1891//3815 1870//3815 +f 1891//3816 1871//3816 1892//3816 +f 1891//3817 1869//3817 1870//3817 +f 1869//3818 1868//3818 1794//3818 +f 1868//3819 1869//3819 1893//3819 +f 1894//3820 1795//3820 1868//3820 +f 1894//3821 1893//3821 1895//3821 +f 1867//3822 1894//3822 1896//3822 +f 1897//3823 1866//3823 1796//3823 +f 1863//3824 1862//3824 1798//3824 +f 1862//3825 1863//3825 1900//3825 +f 1861//3826 1862//3826 1900//3826 +f 1901//3827 1860//3827 1800//3827 +f 1860//3828 1859//3828 1801//3828 +f 1902//3829 1858//3829 1859//3829 +f 1903//3830 1856//3830 1857//3830 +f 1856//3831 1903//3831 1199//3831 +f 1904//3832 1856//3832 1905//3832 +f 1856//3833 1199//3833 1202//3833 +f 1905//3834 1202//3834 1204//3834 +f 1905//3835 1206//3835 1207//3835 +f 1903//3836 1198//3836 1199//3836 +f 1198//3837 1903//3837 1858//3837 +f 1902//3838 1194//3838 1195//3838 +f 1901//3839 1900//3839 1191//3839 +f 1900//3840 1899//3840 1190//3840 +f 1899//3841 1865//3841 1189//3841 +f 1899//3842 1900//3842 1863//3842 +f 1189//3843 1042//3843 1188//3843 +f 1906//3844 1898//3844 1897//3844 +f 1894//3845 1895//3845 1896//3845 +f 1893//3846 1891//3846 1907//3846 +f 1891//3847 1893//3847 1869//3847 +f 1890//3848 1892//3848 1871//3848 +f 1889//3849 1910//3849 1909//3849 +f 1908//3850 1890//3850 1871//3850 +f 1910//3851 1889//3851 1911//3851 +f 1911//3852 1887//3852 1913//3852 +f 1911//3853 1888//3853 1887//3853 +f 1913//3854 1924//3854 1911//3854 +f 1887//3855 1886//3855 1914//3855 +f 1914//3856 1885//3856 1917//3856 +f 1917//3857 1919//3857 1918//3857 +f 1883//3858 1920//3858 1919//3858 +f 1879//3859 1880//3859 1151//3859 +f 1920//3860 1882//3860 1881//3860 +f 1880//3861 180//3861 1151//3861 +f 1880//3862 179//3862 180//3862 +f 1921//3863 1919//3863 1920//3863 +f 1921//3864 1151//3864 1152//3864 +f 1921//3865 1918//3865 1919//3865 +f 1918//3866 1921//3866 1922//3866 +f 1922//3867 1916//3867 1918//3867 +f 1916//3868 1923//3868 1924//3868 +f 1923//3869 1965//3869 1925//3869 +f 1912//3870 1911//3870 1924//3870 +f 1924//3871 1926//3871 1912//3871 +f 1927//3872 1890//3872 1908//3872 +f 1928//3873 1929//3873 1892//3873 +f 1929//3874 1907//3874 1891//3874 +f 1929//3875 1928//3875 1931//3875 +f 1907//3876 1929//3876 1930//3876 +f 1930//3877 1895//3877 1893//3877 +f 1906//3878 1933//3878 1934//3878 +f 1934//3879 1041//3879 1898//3879 +f 1041//3880 1936//3880 1183//3880 +f 1935//3881 1937//3881 1179//3881 +f 1936//3882 1934//3882 1935//3882 +f 1934//3883 1938//3883 1935//3883 +f 1938//3884 1934//3884 1933//3884 +f 1933//3885 1932//3885 1938//3885 +f 1932//3886 1940//3886 1939//3886 +f 1931//3887 1942//3887 1941//3887 +f 1943//3888 1931//3888 1928//3888 +f 1946//3889 1927//3889 1909//3889 +f 1946//3890 1033//3890 1034//3890 +f 1948//3891 1945//3891 1944//3891 +f 1950//3892 1954//3892 1951//3892 +f 1951//3893 1943//3893 1945//3893 +f 1951//3894 1942//3894 1943//3894 +f 1953//3895 1942//3895 1951//3895 +f 1167//3896 1941//3896 1942//3896 +f 1956//3897 1937//3897 1935//3897 +f 1178//3898 1179//3898 1937//3898 +f 1958//3899 216//3899 1177//3899 +f 1173//3900 1176//3900 1958//3900 +f 1939//3901 1169//3901 1172//3901 +f 1955//3902 1938//3902 1939//3902 +f 1167//3903 1953//3903 1036//3903 +f 1954//3904 1950//3904 1163//3904 +f 1950//3905 1948//3905 1162//3905 +f 1948//3906 1947//3906 1161//3906 +f 1161//3907 1961//3907 1160//3907 +f 1947//3908 1034//3908 1961//3908 +f 1163//3909 1164//3909 1954//3909 +f 1164//3910 1165//3910 1959//3910 +f 1962//3911 1037//3911 1036//3911 +f 1033//3912 1912//3912 1926//3912 +f 1031//3913 1925//3913 1963//3913 +f 1963//3914 1965//3914 1964//3914 +f 1965//3915 1966//3915 1964//3915 +f 1965//3916 1923//3916 1966//3916 +f 1922//3917 1152//3917 184//3917 +f 1967//3918 184//3918 1153//3918 +f 1968//3919 1964//3919 1966//3919 +f 1969//3920 1155//3920 1154//3920 +f 1964//3921 1968//3921 1155//3921 +f 1154//3922 1074//3922 1969//3922 +f 1074//3923 1154//3923 1156//3923 +f 1968//3924 1153//3924 1155//3924 +f 1964//3925 1969//3925 1073//3925 +f 1784//3926 175//3926 176//3926 +f 1311//3927 1309//3927 1310//3927 +f 1310//3928 1312//3928 1730//3928 +f 1497//3929 155//3929 156//3929 +f 1442//3930 1441//3930 148//3930 +f 1518//3931 1357//3931 1394//3931 +f 1394//3932 1391//3932 1392//3932 +f 1357//3933 1518//3933 1046//3933 +f 1323//3934 1321//3934 1322//3934 +f 1285//3935 1246//3935 1247//3935 +f 1042//3936 1040//3936 1187//3936 +f 1184//3937 1043//3937 1183//3937 +f 214//3938 1176//3938 1175//3938 +f 214//3939 1175//3939 1174//3939 +f 1171//3940 1025//3940 1024//3940 +f 1171//3941 1174//3941 1170//3941 +f 1035//3942 59//3942 1038//3942 +f 1038//3943 61//3943 1025//3943 +f 1168//3944 1035//3944 1038//3944 +f 1961//3945 1030//3945 1159//3945 +f 1030//3946 1029//3946 1158//3946 +f 1051//3947 1050//3947 1058//3947 +f 1032//3948 1074//3948 1157//3948 diff --git a/oxAuth/Server/src/main/webapp/auth/bioid/video/nodyourhead.mp4 b/oxAuth/Server/src/main/webapp/auth/bioid/video/nodyourhead.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..a76bf619cfab31f1e1c24e86b4422ec141f4cc2d GIT binary patch literal 2025294 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

9Ln z+Id`M@BdHjUH~H3`0rc}qtNq@vcnu8K~9%EqDX_^{pB1{3(1Y^cu3DwYL;**vjGq( z{qAjTUqetgp{nw0t+-AbXDPbvR zrP2@QZCJK0aA6ON7Rexc$#n?s4f|yoJ3WANc>Y{c#Z|G`;T2QNyeG(TM%v89oUv%e zdRuo~MP5JVo8x*q&VS7td8r%7IpOC6@*Z}5Z4S*XI6Z80dV7OL4y@yG+*OM5V83DQ zu;T>p#I&`8>`(2LQ;#01ddku6E%?I5hZg9XV18wDz~Do1AS{PW{m?ufm!q_t1jyDM zIiI-rfLLW*GC1c~dazoyEgXF7k}+Y<_yCjA&j~PPS}=yIM#bt~%ZpdQ(yn@kxWHBY zJlMwlh8j&a&nOk@3s9gYFoOEfroJRaSzde(oOW|Ew7QP$0&b%e*n%b8dx^y&v{rS3U$jXygf81ClV&3xnn`bM~v2YL61EBt*eMO*W;v zMnbsmQmj~PU<%y~!`ECHE&3cur=hs|=We1yOZZoA)}gc#i5~s_cjG6xjtm8tDEpbs zT`;pgj|!Vwc8`idf^he3-`3%$l(e!Id{c(!1_<~`I;*vn|+?SH6-8iEJjEw`IvO7557zf^aC-w&bb{@ zSUtE<5flMVyho!EYIO9ULk;}f_a@jMM3iU4Q9>HlDL4*+C`4P(FRV8bZJ(h8$#m_e zhTaSe$jEepuPe2)@4p?I-Dk|AWyzfC8=!*iCddH5>lW6<{7fXxKsvb6cgoy4Y}Q&@ z-TmBSLnkh6)y*?Pq9E45%(FEST>s6-d1Lv&oSMEmt$Do|zw7c|!8Zh|0vY9HJPmi4 z+%4PDyh#3=k{?a=P}oVN_uh(Ob}%|#M`?+?NJ@p25(<>B+DWOEZNz%R1>gA8()p<- z3r|*{&cK_Hk5u;tmX^x0i)P6{F<8KP$tZ!P5a`o-LF-i@EWNZiu|A_N=+`y-n?^00 zr03abtK;lCD3>_eZL71TI@T{v6~TYWmNXjBJ?#k*Sx4d}EO*7Ku72zl?@;z9zz^Dw zc%Ul&Dp+`9djP0aXR`%*6%|etT%j2CcP(iAI)dKjQ#NVlF{LccX`2YvUaH(3OxMuK zPV{wj|HdCj(i?z_`>G0pzDBH2=z%90Vh%y4LG(-xQf=&8MHanrPd3s1d%SG}F|Ig8 z5pq}>yxdVUN+5Hmcy^b^Kt`gF+g(0WEe%*&{>7#gEmCs`j>dpIWCk{In16LqdPy_epqkSix5 z%uh`v9B;AS&1tRI%1>h_K;P;T-)0ld=}Bml{}`nsHUMH5Bv$)kxC1%*Aq;hYY-J!r z0a>E?+^HAZ-~YVLnU+H=kk2RVRAd9XI8)N=VT}LC^%%}wZ1D--0G<~71u-bD-Hc)B zF8=-BDrMj0dtT!Goc%`~er+0)Raaz=q?1WSR=?4FYDC)d$#UyepYbBy3D6yDEFmEH z@rz(#f7Tx^Q?}VCmwDIhv|Zn$ zH3T-)i8#{=H1#D)1i6Z?0m*u)g%cLkKEJ`xD|wCb`}3cvM4X%)1JjSjh+#&>e z#DvCM=_gJT{hTt8C}(HWVDt`(UH zarI%4;bU%B5Fj$pD$}~h{&NcpU@#E`ZqlS&CEao+-}qI|I9ptUZjcol;Zo-J-att~ z1)`vRe++(o8x2%VO$JAlWpYV$ultU^`k-5s{DZDod3BvrX4@VjrSOFo$P@DL93M#l zb~Vb|#p^X$5amWS-1H;Zyb7(QE0Dy$yRq_KdB7~I9y~{u^I^Ng)-}BGOHG*S5h7+t z{U$D z!&|p7gAzDzp0QIO!RXhhPCs5OP4d0-Z1VEL=5qBG9%}`t>`a3KMnWV0ItVjzQWiIy zba{_Dx4IX)rfyQAcX4eyJU<8|kFe0KFf!J`hH+mdodj5!pp005Cw-?=HN1X(64h;!Ty0}AIO0!2?H3+Y(X=N!U*DA?5M=TzKu z6o#>KUO@ZXE@;=w9Zbhs2aJ{H+os32K+MyIyU8FjGpZ~)mNdLs%XRe2k}p2IpouaS zuzc?psO13N16}vjTEYN0rEcSio!DWku{Eq+*%WAsHtdW}9}N9%G{KYh+CZLGKy6oT z)!9IAI74ORdw(O3Cy_~$nT2uTBGE+CF%Bk^up0a-B%U=AhvV0B7I9Hd7B_V;!8gt6 zn~Tn$TyEqXtr2FU@)~YlV!y<|#CG>I_(@2@wMc;l_67ajFI;Au%i551dtg}TVHlns zQYUcw(Jju2`D{C_M^|+*852w((Rpo8d8FVZN9+i=tK!o*SOkMXX`*nECv}EIYdQxX zO~O&TRnD|lnB<~}uWmeJOCGUlM9XzD12W6kj;Br?|E@H?c*k3lTHzd5Q@y5`Z;Ok! znKr3FWNpvcAtrQK6hPyXawKmUJNSY6+KHMKwe@{72$RwTnpVU@1)~x!PI!?taz8NP zBu31w3Xw*Zj<~P5K(Qc?$d7-4Hz@U>6=Wv}13PPd*m9sWgRDHf)lHfxPY9D{1;!Mv zt^tGq-W79ghfGy*PCZKD%v{~bNT*oRp1tZVc7upY54P2kv9e^7K>%F1v1~31I!_6m z*PU|3`Y&-q^iKFUf^^)f{=WP~A=D%T6blobjT5@BNUP&8H&jmJP-V6>m&7wWwp?GO z3@EoVo`0(ImblTh5>@{^vrKy=?uWx>=DcHQU^p0 zA6{;m1w}IFXc5nVcJXROBF!Lr{@+mRf^|Eq;mZL(@hS`6acqZ@?URB9N$3@WdbDkC zQX{=K6HG_?eMee}*s|_^bv4tHA+5hl3`vOHT-g9EzrF*{q+%+wv2P-~5a5+N!dtX4 zKI43;ZSv^xQ6IxlQaEm$pQsj7yRU1?c}8Z1ka}Rwt12H{aOLAKPH3KeYcFJ0B*QkG zXA$}f9(EPSB9RjMVT52DOnu`Nkqjo7z0AJ)%4QrxAdudhbEPrbI{l=){8F-l>8!}n z(w&wUdMKLX5^^=h#;5t1TNGDy(3#uchSVuQE|+~aJKa^{IcOUS za-;D+Sg)|3a&vvKQ0o9Xs~pHHFRWYCq}w=!0b+CLMw?a!a{Ph?sf;desgmN)1?(NS zynqW(Ts2&B(+~$!ov{A#&>nh!jBli0#5dk$yYM~Dz-N+!u|;+wycG-#EcPuSb?7;k z#^MrSbsfQEoMQ>n<##z9i~PGt4IVnkj<%!yX=~%K1nTxGUzMy6+bynHgxYbcKwGEv z5JKBP!r$oy95QQ^$Nk-9r?9d|ALdvV5x*>FBU!4FY~osv^#+bR`%FsvmkfL4!%v|? zWC4NFVy*O9Nln_!7AHGa8&ova-u>c5Rhkq*&V${_td6HA`-#-wxJdpCW6aQS(Buu9 zjdiMymH(%*=3MXjup^JT>^y>`P)2?5!`SdGCQ^b6>CDV;096sxw#jpB7__!%N!l<~Ok^nY0{A>? zak=lfJ$6qh=Ajyq`!>RbH6+Q2R&PFft!j9_qww!xEa29kVB{{rEq}i_N=B{UFZOD# zD}c);6?KE=Ax}hu^Vz?LshelL`YAi^Lj#FsEuR$r-#k6v8y}EMak7rZvA*4Ie<1kV&Ej+Ga>`yc=kg%-#%y^|=liy2ket3@%=2_% z=+9UYevIDC&{L^Sz!Y4&13K4!e(V!A@YkQb;~sp zk9{eP2}g#1=M4QL`%ECb?TsHw+QJ8@H@BVHuObf9a=h`WN}$maI`4AHzLN{;%L%)B zc&z2FAb0FHzW3>1-O`jH`}a6i9SX*}RQUM7hGo%Yhq)Ie`ene)wYLOk$m|!@%Wima z)n2cAAs;K14WPK0hL6+(uIKdj1*9h66)!H~4mfNdfvfiY=KeJ3y0qWSFzq9*9PZvJYLFN$ihUI@ zgDzv_6~gE$uZStU&i7lfZ)7H`py7C(TiM--vGe-yK3M8?f!ijX&si+t^6V{(z{B^N zG0-W8_Pp58$f^Rao0w2wk5N@E5>&lulCCz7Re%u8wW3)e637yr5$6SdS%>?r$t(S*t=y`sw~;@t;*h?k=XaHA#B>CxB+m1QaVcbKwr3L3*OyODE;{As zjLCAv%!)*nty`M}VJN>2{eCAkN+D(KnjDUxPob#WvZ74b0+{7g9D-dTp;~u5sai01 zauPA+8;y|@=(IFh5fi-D`vE$gs7`MI%Pw&%Ig+iki|UI{fS#=QA7{*d^}=FT{!Y85 z@`PZXPm9%I7A!@c{Wufv>+(UuqVDBB8n~kV55ybl4hX-{#<$VxBMOJa1Phx}orbD+ zL!A=bjCV?l3&{{&zT*^qQ3U#7gZu75ZDV!>nkq6N>ORw}qET4Ud zWFXY0H1%#XN?0sZzC0N!6DUv7#tpZBtO^Oiu^t(DBU9l+6G|tW^ZSdBJ6-vpm4zPfJRiOS|NpJ0}0DgDWXIt`&=&1y01fwLJTMk@+?SC*vcJR+W9;)y?u0m%^wNU%B`0FlD8>$%-<>`rpwg;4w z*~VAu6LBX^$Clchs5Y=SdB)pY1JbHV&|Fd9Ze2KN=ukDX!VDpxxhS)}Y1hpym*!#0 z^&|W&42g5>G+**gUXgcV`q7jlVd3i#5B;m?4jw~~P?$d!%M@*{*dW{3Ok9APdm-ru zMBV9BESIETV+V0d7B;r67)wTF6DdMN)3KPw zXqrr9`mD&~aw_yNVa~Z)7RDuw`M2BZy zjiTo${QffDX}Q+ykKQY-0wT-T$e}~#!=jS~Y*HKN$eBzrwwa6vxPBb>o1QDgQ`3ts zuoM$$h61a&`a^PXr_~3}JrJHwtV!?ESRS9vOJLejL<6ivf;EiXF04(uuf}98UoM~B ze^}O3S<4*Ffo3a($Aq>Vozx6Rgk5kr|GSHdQg#jju~}Z z+TTa=sOtxQK0#n`mDdOl{)-MCZCQ9;3r#Ai73ONFWeEv~va%CwqYsF5<`-SJBxUQ_ z!STeJ61B}3e5|SV6yCu%9cn6W!yn)zazgG^9qBJcl@fKxL&Ha z;oR8-q8;s@KBzEbP#A0ZKVCQ)lT<-5KKTy_BkgF5^-Un@7{we}Z*8UiL>L8UhT2-2 zC~tzGjLz8EIQReVz$!%R2nG%ppqAY;$q&GbDgv?;PFhwdcs%f%ELyR?3<#mw&DX*hVF4b9m*%_xJln? z40H#CHw^~Ce(%u_<3y~Z%r8rkB>5Z0aTQ3RBHp;^>Jo^#g*4O1Z}mM!5eGuX!@{1; zhrlLrx@{ytBp7Uf>o;#pAwbTcY-GpM4>OS74ON?mg=M(8-zM9Ja(Ty_6joww1AD_% z^=TxSbC0LUJTYlNxG65U-+~cIxWz86nu~_6SPowQ(gqX_aaQWHSwCA1VnWT{53YGR zU*t6IK#?LAewCV%ToZ}z#ST;Bjg+B!6TREYwgNSns6*j8o&^M(HSbP4*c*Ko-v?@- z3@#q_)awohNB1fzY-7QX{q8w$e{G>G-G@DwN+;QkK`NZy!~?hiapaTS`Bs9kMzTh% zVmft0qyGR8j(nM|%~MxtEO5UY@~|v~zAu-oyd9Fv_*xnioCExf-q*?_)!e{G6x5ON z<1>a-=-Dq0%uSlbl$y|{y|N+@gne&RfQkstj$<5j!%cck^`=*bMt5g!7^EqPw)77( zUV%bcN{+v+lGt|P#O5TEoLPG)ct%xZdQ!Uwj8+Cm}DVaU`fJ3P0*emK^P^pKe4`O?or%df%68 zrrv^1Ps|R@X2O&52o5Gs0n)isiN}2}z@-U{XO=e4n-!d6HB{^y2TP#Ql!|$`zC$li zN`DEgC6Fs0VLOD5?>YK)|Cw>qngt1}&I-^3+oT6Cz6m7)roL3_?w(g(joC0WgC{uW zwUhq%)J$KC*0%v`Yy=(dh;5tGyi=K&7dpu-*6=Nm%ML36PR|ndYdUL_ZH<$kQ?gMU`;C#1ZKF#TMp* zSFiVxhWPEDx|Aa`~=AX(GL;usk49vn=7|ykZzgqD&tSsGGW-JK4Ie=f4_&5Pz-&>zJqva2t_lP zHtc|aw*swG#|*&&x<%_Bj{)`0Yl#Y*3$JhmekVcEDnTomemeB3FP_mzeg+6&wz9;j zbx}lD>GX!JZ^fVXO6J>23CJ;mkFd192066?va@PY&j3MZ8Oc3p2#PSEDNbEqGU$)9 zfWU%Mj=V&oVot9F^V;a@I6}$#wnRT={;gUijAK=n22LE#q(Wlb_idm^4TT!K6KWcu z+rYVoE;yR)Lt_#_LpSdoJP3#5`^Fd$(6SJ)!cl}iV>c$Abaa}JrRc0p{);ophbFT$ z<+*CciIxdLZ@e3uUFiPgYM9sKC8*`J48&vMe0!bu1U$!#%QjT*ADz-~`UY(18EWOp zIwc)8p{Tz&j0V#qS!awxN9WQVvq*5fYvpkb8l))iuWUQY4FoRw!oTX2aY3S^{h;b1 z3xH{LA`f0LWmU5={fR|fa1wZ)Ni43Zj-qIJjWAQSwKVJ|sz`BIMUZ?JAGf+~m4LDfEPj)lrJ zvGf&H{q&6F@@!LAl<(!|x&+h$(&|WVTekNb!4{ zor8O)am`lx5gRTNIKL9crm3XbzS*|nr%UKM08Ht_b(h|Ec8cRlDsbWb3yKO|QvkeWoc|N54`c zbpbL}bJkvMo+}s`;5chCK^AhIx8%v~ULHQpBmHTHiH7DmObY|`7>${b+Q4hVg!nz*iq3bS?y3m z!gQ5JkK^;g-el|A3W_grZ<4UGTlbXYsMvFvWGzN9&XjhJ8~_`(3Bo*_vB1vQD>##IK zVYQ}EZxuq>Y8WpCI69E>-dwbFUTIOGYt6$`Q+!G<2eEvD1CV81X4!{^lOI(8)dlNJ zMC&hnlb!bVzk@5%FQm(P0I_7l-6SC8#t=%y#2&(5Wo+h`QWf@qoZNJhKx-1pmp~C+ zk+vNON7s0WIO{4hoT9fq7LS==w*MLx9uNx1q7>iI+MeYILYmoVH}|gd0lND^H7QxR|oq%A3V?Aa9?02#Y zcJ;1ogiFNb1zURFvQ2Ke;~dmBzDmg1hjkj5h;_B}`z`e&uf-o*T9SygEnFZ!qbI_B zwocPMMl%N0ozNIhKmBSf_HRI%$KpqIc>7g;3N^=4+v+I(;66r#L_#?+jC5)UuMz;I zd%L@WGbbPdzF4C~Z(-jh)J|F2*#BP=l#a- z>hHc-b~q0yr&LP3Q>THB(WDl_JVHCfkd2LJsK#$}xHRV2sNM}APC%+@I#hs#-0NjC z{m;bxdr@^fUKO-?wB{4m35WwTjY~A;@(1c0Z>-8d9+OW&w}mNI|+OpiZ7^kiT_RqnZ|rV19BGu zstkn({U4&F&rn5L@k<3=8pKIvHYMHK1XzcBwE)p#XW2Ova7OYzsnsHMIWX(#M;2l( zttoLn8L1mr{qd`_Y_#rV+#H0Twen5IQ?KLFVceCI*01Akr~~rHw_+Z@qax-rW#ej- z$!`%vnfG14Y7Js%>4ne;``O;Mu;V6uFU3X+X4F|^sriy#iVLBE5PAmj3 zEIZGMV4n1X&%+XC+XTGG8K!$TP@j-SDU2I#ODfyia;-P}C?E1&-#0Ow_PVPOD?!K* zlo}n+<@jSIEmdx?p~UL;bTU`As@caTy;jN6st$R_t>;Ev1IilK4^Y|4)=&@^Z z%B<<4^1?|Nk-zgEuX?Pl=8?LOcBl&*^&r{ZT zby{!i&6>K-^8pBDdygU|Q6&Q{ro`cnn7gHfQl#<~%Wx|VB99+RO%Y6TH}Ugv1s_!@ zv6i8-pd+AH+K^$yndl|*w)%jkqiHra^>E5zKN7`lDSWw7qp}C!CPVJ5(5`h!!xY#@ zL$tRh6j%zqwAd<^JZh@s7BrvbhzcVta7DDHb}A{aK)3%CoCelA*1n(weg>z0Xs-}% zL1j0ji!|@sw{ykSlUX`v;0rm~8!MJ~qJ1pqZkiWa^*sF|x&ZLAJ6?ACwaRmsosUUG z8g@E;VyI#&$QkSf@8pSRXtd$~GG&_xf&}vs(QHkbeufp`Pg_4&+HL7oXP?~Gg4ii4 zf)&|0;_jb;V?BIRSR)8o7K0Q&0Pb=*tNYiYRW^|J!cpCf4af0{f+aqk1Lz|e$uh*| z>^>|2oMiEtx7@rN=0j>{cp!CJIY{;Wrj zNH!}NDv-G%7>eIZ+?}C)tVFswddZK%#4EPyblPtoTU>^O15iM=5hsy-M8I z-~mKKc)-ZjFenYcKE*kYIR@Mz0u@F(B*W(2Hd^C8E>}p7cqy5U%cU>M6lJDYBJsDh zL0&s3L%R}n+HWLZmP74_J=vc3g3?5q+g>>GnuK=)t5VwGwJ59F>Q4OF8n#^`U8T`z z@k~!GD7v_|P?nw%V{SQW1rI~aL-ZJa*f4g`Y|n9R*jyA4THk3u_R`5^w`!BkM`dtu zV`}vQA^c_o#29G{ZZjK7Yv9ebz3S(jAMVL1<}&=MwldphHfTj(T0GEeX{$ws1|E*^6X>ZW8Vaz znewYn_;YKU=n@>&sV?k>?Sxz#BQ*-O9A4Pis-4h~+R2Gm@8QkKQga>MWJIh6$FJlb zNI<#*4xAH}|K2>^Ep!2v!$RHyUU9B6&r+L50h-&3bS$CK2NE>QVBI#F3vA@}+`fg( z^|Fzk%`BdB{mtneHF%cZW8>aa3+y3v%mBB7imu|ISVDm)cq}o{MgyS7`xhk}JVx4VC+!IJ%eZEz zkiVsJADoAH(e535wERZ*D0DzdA5g9gCdlFB?^5K4qR1F$M7jD-SS1Im2^{^PnKSCF zN=X~0t~ItS(2^ZAT2Xt({Q3p|k^onkv7XqupM-s`(|*~}&LN$BIzpSG3jkXW7-Tsa zxphBn1N<_4R_+h+2rH_Xv_!Y7wS6qdyKy^bSEiLg-i6I{1sas{>e>SgtiP?`U;%*3 zrU<2kWcF#d>cHdiw1~?`<$e-UCJ4x|81E#3&zu3OxUz{-v-KFoev&#RjcnhS0@W)p zZ~$TuWGD3#M?m83!t@@Jw`66 zK$4Hn#L5$M$T5?pS%JfiIB#|Oqin2l3Lf5SK^$$`HFbF9&e%j)s z8{sGqHAbW4Px^=0?}9&ljipG{9`FAN*-7L8~j?MohH4S?u}VZ+zmWh;k?A2E{W|PHyyV|GNFkpcSq*mdDQUxEEcz zFPHMD`%l`M%1%99y4$pM#@-s(>56O{g8ImVrbr1iKxB9DU~$T$^a9V5 z%m806lcuF@&U-1M}^a5R3-y zR38D0liY_-fC#l^-fpQZ)@yvtsbVxMN5e-k5Zr<6vHjvH)jbfm22+VBoBhY_o`DtN zdKta>uw%5!y-!cM@;?#v9bDufXl=ILO5k{$cbD*f>uz!#c!lRDnWF1X)?=d0ZqogT ziL-FA^;^AVL`_|8bNDDE!$n7yc5qAcY67DnXSI7eEC*)V_xKt{N4lzR?UP zUvWF&Lu1ANtFHU9Z{Tai;otN4RPQ3ih(t0PU<s8>HY0B%fHc6=XRg>i~3+ zq_EZv*0zmpsNj!tE-|#nENLDr zWdQbuKP;W^gwuLCxb6`dsAWjzyB;rudNm7S8io}j&n0&K0Y#yU-Z`Y$jx!C@CJ2{+ zJ%^OiOFcLUKw@7e_!^=Zep2)czJel(Bm(g_3x>$fXUGq5$X0pKmqXal;(N-OOyy4t zTwSs3DIx%8qbL9`YW4zjgej<{@0F03RCqU)yT6`UzBQut6#b=p0K8nFqt6ovm@^!JI}X6bQ!6xiqB3K&jOL-(;Djv<92->0vUFhp!2 z5$2}vAtZiw=im9lndL2EZ%PsdVxWu}RTI1PuknzpMR(Zuj@GT^oN=*;~0vu=Cft;VvFV7rljs{L@CSusH z3nm~MU*Wd&BYOAXcB;tUDi>avUJcc|#cyv}#p9K}kqcfbuozvz{gwAiJQzf_wce38 zST*Kqta5?+7zW!*H4Z#L;5x#y)*$(Ss4bFtuG-q7J(h=v%V{o0sE_j8c=( z0Rmx=%eeR`#tDqd9-k7E~c(&kmSRYnekE3SbA-@A(r!4|$!$ zX?w=6#Tn7vD`*yNH9IOVwKqHnBd<&XgN^V9MH$9Id&RtnrJT_&p0C%1Dio**|13O$ z3HOT6?unZ@*N!JPC|`cIeVA0IM(!1LcF65X=HFNEzGrslBK=BREZf}L(1>^gl)p@|k0NH1QXZ8%Pyy7Q{?C%eF7z252(dL08 zI+x|%_ofHvzF<85E$g+0ToY?Cu)h33dP4XX{^?OH(k4~rdGeEWRkvSU(Jx>ATLEN} zj%q_>|5yR{ajUVSYhQz0fDKNxq}IQ{d+R7hEscIaFP%g)1{I~P$gDa8ZM4S?K%Uap`00}ONtNtApao6l6df(QqKUSR!wvA(fzU4_q z)QoY;;V}M#$d}K&o)0c96qWa-nX3FFez{C2Ix1{=_!(bGUT6Lnmrmu`f*3<*b1EN< zi65X95Au#2|2dYAwtGa4(7|?j;YhidLG8Bd&dKPxVavd9(I?LXRo-s4-Zb38&@a(N z`BVt%I4{5IQGNr|eb6xj9u|9p_vmVl`2;ZnEv};rgg@+MreR{E*nTjW~kvyh*tP=<)Wp(6FoM&o~0#Tu4M_lERHzo>RPg2#zsBkU^i9}v|d zD)EH!fv$-w=3wp#33?P#t*VtG3$8uJDgn#yDs@13dXn2SpZMwy;jqFQIsk6rba-K2 zj7la|vb@ojc@ODu@FArYU36zeh_Gat5&f7E*&o?w@?(Pz4TwK@%l|~;{CL=j)7K^G zJ3EwS?97_DK`uc8&7^r9{G5hGhcx7ZSJre;?{52)|N8^IlT z)^VYQLxA&o>#X(XzPj`R{$_j_3h0niu$57UmQh{hI_b(E^I9Nr*n;qq-n6nV3ubV^ z|DP6&Wv$XhV(eEtYTeCP9*t@q#I*#YlB;S)->wq{A_P_xFwOa3k>b7DOXO}s^`{~J z9%U>T=vCR~02dU7<)1{W;p)9!4|UOKisIJz4^2CU$n;iVB`t2WpmZiYrMfd>MNDN~ z1T{z+3`#*LfEhhcAHv=|JGF49W4RZ~RZkib8Knp3CU8WaD$GUI1NPTvzK2VkCd#zTxRlulO~n`k>nlfXdT~NijI{UjRkZL=b(nT! zbNU8iM@f$<8>wzQ1{HdJ<&VxsKgI^3F9}(3dZTBddYbVx#muJRez$+m2TR>4*9&9f zwEv*Kti#${nZeuYk*)elc>x=ePtKSsTL3KHcbp2vhm!O(W7V$WQ@LdnGe<3nZdlWo zrxQg}yeKY$Tb14n+`%0ZXRsjd9x@_Rg4@4(%<#4h>sUNOU;r0m?{>@RgQLA$^MvWmrt%ihT=P*|g`aX_G?M#DcT53adMcSb{s>CS zK#9)$mu$Ln(+e|zvZb7Ei^B3Xbx;lo=qh4K^`>GzEUNb8VvKy;{-!hMJ;8V%2|X1c z^%Dn?J7%)a`xu4lq&EDen$J3D7OnqmeTdM#yyKM9&I@eA!_aBGu$3Zg>_?}VImv-# zbUelGG?`G*U4-2UUDGRrPWf^jl`?m-eW7h^30odhO`G7iADGZqym#0x^n}_=>X>G@9hrS{}`pPs@ z!-rz>Y?0?WR9YCV-k z1giIJN5NBGraLqtH1W-aF0t1h`{wN8i7476)u9SppL1UgD~{J8gF0gnyr+NB>GrRz3x9za%pxzm zBS280!Q87>OnWvcJtyG|f+^3607OsMW;n9^wF8r1FP6+TcwHrLgImpEeNe+Do`YdsPAGty)sz{s*u7&cWBN}Se`G}^nQ$wA zKjxn!%Rd}7EP!vbWvG)ING58_6*T^E75f$*HE&81$-?6&Ni3_tF@FIA+Iljj%3kj= zqcS1>Dz7L#%^oDvMvU7j;!QWu76Zlgo@DF%5{t_wb4Y-w`^Y7zz4n6D57rP-p z3MD-$Xz}&S=&0r(P2{-o&ejmU;nX5)om{XSoUc1kLu5!9PxxU#)A_e4!g-g;-)lT!{t31 zPo+dB3bH=RA2a2j(6zr{c{0b1?TuYx+>3OCZZqldV+}%*x|=T9mXd;ztK+9I=(~4F z=0BgyxcHOZHj)TUdDaptrLF4QX*I#t4_ z=<980Hq^aKm>6zoB-_yd;x z{R_V(@VWLY^#eqbwhX>E&`zI6}_nPz)BybSAH^XtqP*VRB!etAQ*c=O$n}+{Ba@tg0nMI z#cCvy_gL-G^8~Odii?;_0dabJZ0F>E7cNpqe@)G9i@uj(1Abiu8%JUpz@hfL|UC zE&6=s%HBe3iE9>G(*yv+Ht`Eb;l*Qz`iDW?3BmW?A#UikJ1mdx>JLwwp~=@iw#(LE z-z6)F8yP!9*-u$>tQF@RJ)Xr(=KM)NjxA4ja!(b`sc9%m#vLA=%=6#K)pQ#1zd!$~ z?w%m7dw{PGbo~=vAKi+;l8L8$_^V!X(`|NYNl(k1VglB6dM%1C_x8nPoQA+@!sj`B zY!81w&)A53l!`FF;dJ%b0Wb@&F(hf&HtHF3mXN1*6;&^59RE6wJ|J^W+iHB*^K3`9 z)#Rm;bb0d6hDO5c@U1bpyWEm4#{|4|s5pm^?ryZ(fa!(1LeTUj#5_CceYi8-586+r zS=#UWfU})VaN^aq_s7ED^h-8qkFOM6V_xT~iKB$Mjf5ETE23OZZG{4G;)A%^QpG|( zqS!D?5|(QD6Izdw`bn@Z0*V-Jqw%mCT_Rl=1jbL6AWuK|1GNM!m-=5|cJT{1SE~9; zFHBoVGVP%jarW;ieo(Zyk4Tz#=4f_|#|hBj!Du}Zo0lmZq38nyF=gf`x-$EJ=_;5= zDo%ruqqJ^#`eefB8b_`k&@!eI$EH6 z2_Ba=?n~XqXu$@C;O-DG7r7gE^nEN!>qJDND6n@@HjKocoX2Mbv$znXAYsW)SEH?w zOF~~Z{i;7^$?y{wvhlF4UI>VRDgRy~U54b^aS5|j#!}L+;;!pbZg2@BQLu}7VCRi? z#?@|LAml-0tVxcoxT82mo4$x(8w?|~#R8=h_^=xb`QhCh3l|ZzhKSHH_lJ0?!jhSh z4Jo#3IP*yAamK7yYWJ|Glf%ygBe;<%_aIcCd7`ao!cu52vGJXnQRK-*--A2}Sf=Cu zvVL2eb#-0(b2xf}Zsv~WtK4H-TqH<-hpIP66#BHMXCiMKbz#AReNqs?Go6ap820etb?r)Uu7d(4J7!sFmuUm{gdKWq0nnFiWs6%@{^BTs%oEiiDL zQgWo*G;f(K5a}!RMmb60S%$){REpO$j+W>u6#DI7pI?Ng7Og=0$fAeEJJ0?7cDPl4 z*qY@q%h%J5xh)4$DyzNtl6la@D#@WbFY->2t*HBXAxMUSFHO2h9I$zogQxa!JU|{r z4CLGs98JXkXpT8?F?!gL?s=U09v4>aD%?Yo!`a3%~a|;f04r)p`K>n2}mngF=z5)%fN8gpH-&zI=+tee@^DhshqV zM+`z6z>jMzX(QI}7nGJ3hWL~X}D^qA(<`96*7a#4W2|6A5FNQ`F>CqfaAZ)0a|(mS@vcS<%f zzTT2n@ewfz-jd4`-IX8Zye3}OUk+We~N?X+;3xy*|L@^U)YUvUlutFfTk zOaL{{Tc-;Uesb3r0h#&I?WnxT3870D5u_^CvIZ`9y#L>+HoJ6@?vUvQsX{l%C38nh^zZnR>bg7sTefN%Dh0`)q_v$EFyB8 zl!|mXkcasnu+)-7hcZ!?6u(aKp{kIE@?-ih!pRa!xIXOD{>IoBM!_{Ua5$`cTvd6Zvc{SnbkU(N+u+2#+iBw_Rv-k~6e{2IJ9=y1VmsZ%M)T4c5 z{&)YQ|4+iLAuGuLkM;Pq9%S&^!MBcnVBvidBLB8s%XvR7P{Wi5#zq_sLL0nnF51{q z0xKiVfj?zwPjPsybC;@+gcV$4VbO{WkcA-GGtllcadR0JtEeL zkv-mNYVtD5hj#q7jGSlRNoEa=cyS{ASA^8zfbKv73NkZ){EV#%;`uOJlQqSWMxj{( z>}b6I&=7NITp9aqggRYAWvgX!NMD02c*;xBd|u_h>*XY{z37DGx=mdx{I89Vc26-_ z*h5R&r!{4$C~!8*W}qzBj#qn7H4|=!_fSxz0@VAHq2z9WSP!H4s8Q4*$M~|^X7__v zC0kwNj$ZODZwe}I`EXa=wM-OnB>bad(IDWH+NHWTfLzTg0nNZrL&+RSwRBQE(ANdi zdRNo*#AN7Ub~{eb9$Az6;Otm6&OkkY+=#% zFjqb}jic|*XthL%xaMO(f=I&Cc;^x8k;Em^+p6nNPas@xqNgRku*$W8Me>*Zl~_d z4zv4L?DUpgzoRU@M=iL%HwP@Fk$X#4_%^^yl1+MX1-GYLntB9~9|Ai^u|KQI;a07B z1aY9DCIA{^G`s$~W=}M#tfbhDyjfJ1^?NLUTX_pxNqn8hLt8eN^DfP?!!4(#QX2ksxprf&fgx(6S&iuA4XAO?AP$j&ftR7dn}hCUPf zkTN{U#C^rVW-(?9Z1XvEB?Jm~WIErxCHi98ykYa-w#P7NMX92he2p;1#RIh*+( z1SL5!?a!Dah_{!e&ZDy@9``g+m=_D9L$vWE?IMCCy8mibM2BvtX@xA5Ab={P5ktqJ z=a++}tIe<2a|e=rV8vw;kl_!C^sBV`6bJw=EsvNk2NJZ^fZ#p9S*O<94Fj!hY1I%rf8 z3%^>60vV}>-_;emguc9s8N{{m%e6Mh)L(K*}5QB4?khROHh z$RjEkPpDm!X|~|d7f{!lZhK)hah#OgU;$fZcqq9=CKp%*;5Zi%mcz+#?+r!8y4Q8U z+j(g)=_KF?%rMBvk;Y%!pY+wQfXz`iZH=IrKoF#01|x0y>bZk;nuNDolb!m}E>5JB z;UDbIy^oGQp2WHW$un&f+q5 zOCS~_pJ|qmxzQt%HQ!USLtU}@2Ebip_`yiK*KpIL zy7YxttJ0?w1*>jgNP=rd&_iC3qp`F;R5PNzfX&5k~bKds}Kn2Vh)oHYk zia@9>pcQM$vJl2smysbM5IneC+DERFDnA@@kk=05meV;7qocB$;jkp9@Hx@F=PW@c z9%*dxuVy5LP~q1lOMHWZ9Y|QSuOLx>Gz-vjIF5^eb6|Z?QdDWCVf3}c`wpxu^M);z z;*@P3unN3BcdGnUeo-^prD;)2e}s@dvpqRMNAv?BPtU|+K)!=Vb!@L-at#PIr_>sL zbLyUZw6ac+`ICG(agVgVy&UrgEN;gw7?`lMlV?1-FuV7NQ?|b4c z`5mHFEUd$jXbi*@g8Bt>ebck&`5YmCm?_Nug_`b!g@@4Kdo63VJL4u*ifG-c>-9T{($>e3(G_oeD-hUTuLJtJLJZq9i%+QDH3H=g0tD_ zRNtT;puC@MLpOhWyw!x!9o~UQn1&Us;7xEsjv!r(#`Z_D$h)vgBWrinxqd~MwH&OU z+qh@9TG4>20$9A`15d^G+L7(TIOQSR{ZJ@V2x1)^ITq-AeVzuYsdB?yn)PITBd9EY z-y5~tNp&DI-;>Od) zRv)B^710BB!W>P%)B9Sc8ay`2@7@*DI_A%9fT3JpyH4VAwwdjZ{C$QSk0!3#X!o91 zX>l2%I0HEfbZ_J7K)`xjgBtDR4TnWB^AaJ@^IKl{g-^v1XA8|P9N8f5F_CNseQmo8 zZH2*;#EbITimBh3w0;5t{PaPq&&Mo++-titWg4SLBMnyfoaBMVF82bL4a5g?TvWV- zMsa@O(ZQaL!~%ewc86}QF>`)H4oX84@xe0tr3o@9#d5^7+X8UzlJAm%^daHu)dCMU zQ7ogRg~(L$CadxFaH5hF3|yQQ>(ajWqjM2u3HWjwFYRPNFb7qm? zgj`#T`Pb=Wn@cBWP65_frPIk~BD#`*Dh--!K9y_>&ajG@H`|lTsHVNFpo&%Q98HhAT{^I(iXH?st3j4VNj8_&`r_^?JcSPWn7MYmS=oYd2dw@{U+K7}Vm3?3 z#CFm?fATMb#Rx;9Q2#3~&cus@)*U41`R4!6ig_YPbN9YSm&j$jo!K_}*FhdHhj41# zB1qLvI98$p3Z-08nX6coiQ`=Fa}B4I8%@JhIx^$#A6!mD8|=TI03y9cBQ{>7aV*Q- z39|vSw4uu>922%nhSz-Mg*OLwG*wQbUHoIyR;Hj1hf3&?RjI4CY744nrljG`u6@+k+X?dY(7yWk;q60*%mpr9Naebuz?DDBt>^EVv%TQ`x*8??{bou-5ZGY4toE0+qs%p&DBneAP`NEZ=)X{F zQG3H{RbUOyU_oXK3XBE%W8+40crXC<{T|zll4GV?u<2%J3LnB3h`}2?cogqcz=R%G1}uoa9xZ zh~d^&s9CofVW%CX2gp|Eq$bdR6vVq0z{Znnov9JWVW=qYS?~)TZM@$Xl&xKhLa+MS zOx>RM+~cvfPfV_#s;}aI`V41D$aq^UZ}FfTxQi5 z?q8JkP3<5yJ-ASC6h`)u%YD)#X~^*?eEhVQx-PI9tp1K9!;q&04x7NjWNN`PXx0tR z`qVc=o{YDPTU*3x;03Wssak2sXIlj7CO#{;9Et$*KkN~-jjGoZPeod~P6TM-81<0B z)*>3)Vgh0Lkx`fdV1!V{9Dy2`)qWbBo)?r5d|<2Nmo|*EPRs2qMFsW4-3MJuHGLWN z{sFbh!ulHaJ!>W|i#-4zh$;{z#x56jc>RbS8RwND1Ep(EIq0MS3LV&^MDQ=DbgOZ4 z8))X?1D_X!av!pHps<+B%ReO?oe4)nAzR~?$OE(|7Gn}}=fzI5{sK{$o`q&c_`_WX z7_+i)QpNe`!nRLjGitauQnmmG^D;rg>v?y7K*dG$)L{o+Z34w8JA;{XMgwN+!(KyH zAM*^tEJ5G1Fy3kt#yLpv-$jsu{%oCdcT3U4OwXN4P9*E@f@psy=-Gmg=1C2W(GXls z%py-4eL~$^4U0>UV^WPS-`|e^vk1h^=QUEdO8H9frTX-mww6EUEr)o>uJPcK!xJJ~ zamNjL@L==`g#;fr(W_vusXPb=ALzTDqfz_%+K=JlouJx%zKl z60PYD1NSRI)$Po{%sB0s=2vQ^jgR<|@@~n9Cb2->Cn--Sg^cR{Wss0AU-y=$(oklF zLapqD+s~;0kqR+D(@GbtI83MG$noOV7KUIcl#3+7MH}%00Gnu||8FG`C%&O#Q*Jnc^5MfTfsP=#}bMvH}>m*a?EU#{?c~~@#dPENj4Q2G;?N}9C zunE0Vh4tD9>!o#B-TVSKkHwcVErVN2#M&OcbcH-K=x`t*L~;+>t2HCfY?*(^9$`b8 z94Rp{EMF#Uufvak$ApG1O1H3QMA5|T8eh&A+<*BBPn)a&0iGJvbJnrg`MbthTY1dK zf>VA+u(di0kpVw90sspXYWk2A?^lubkLFc;_i4P|_)(^Q!~!Q#TJ?b8|FWbdB+Z23 z@xrc_mzJnvfm;<2rc7%lyX#WdPWUVah-@s33|v-l=kp=gyg@JWi0 zJ-UIIDi@*0HrB!0DjC~oPBUm!=)%2mCCG5TVMw%Z!3|Bj6C+#96XD#g!~yI@VYCOW z9{A|uT+9`GB+=k;8$|riRU0_OuBJ1BMF*GaS^>-SPYzWtND~F6q=l*6W2(^N74%bf z{%d|Ql3WK$x@-dGWY5|DB&_^L6V-Cx@^_5B7j7r+mZk4^r3<87;Dm3Ws|SU&+-4dx9K|)c5nl0NsdpE38|L1(JY#WU80micIwqN~V=!mYo?vBl z6N*|%@7xWRW&XClx+qC<7b$x`FQ-8h&8eZSL_5s@r-nI{?t*dJmsE@meJc|-0 z=~}~XcHBj(IAruBnyuoAO=us_NTic>p~zLliXPu&c;)ky(9DeIENRcWv?)`mX6bRD z7xVB5!z*y1N=?{F?YiPVD`#OC^JBFvSX+4xVqTEH;)ck(E-~R$`zIC|EiY~&ID!h{ znN@kV=4gytG35vd!q7D`G2APTW>$!M_}QBqv?}i;xlA^)(c*I8{opoCR+ipO-Ei$F zJf=kH^gz!~WOMMsFkna`q(kH9wMM-$l{%Qf=%(hS(p0yXeuojWKJEogf;iDH9?#f` zY!{gBJF8^`+(iC6`GRZD8C@tJ}A8?T>3b~e4&(KKw_So39XQhFj{a;0_`w$DHp zbS?*lko9L{OC-j~y-?k*Rud(d10{W`HqTa0zu2A4d0s|Q7Gzu1`uO)yt2uqKg79^N zIczBxKnjb>5J;8rAsFn@?@D_vaNKzL1AVnibL~>{o55rI2KapX$dOoEcB(3AUJ*ik z$?5}zBxZCil=f7Z3GQx1CZ`}$=!+4R8V{d3mPJeds$e4PqFqt*9q(}*B`k%p`EY{e zXlWt3tJeWiK2p-W2gwl~K~l#RpHXF|?X_3X_Ui1o!+Z5Mz`8(_M2*2lFtCVVs4MZE zZzJvbtHxk*n7@l0)a0NuR&XD$iBNa^%K^#Uc)xG>uE%3)hyYa(%?^QZeC~<#X9Akq zpek-}51)(9+r{PSU-%!NZ6rHVZcf=0*o>ZDEFP*bfvjS5o|sCUTn~r4?^^W$gyy=f z!wXG;A7Xv^VabK$6Eprx1vH!sM=EdLPdLX!C0R>Lu5EfA)|SWD)6_g6dvz!q4TqI9uAPd zy&oKH?tPcrburfN-jhOVRTZQ3#b>ye2u(EvlPl(`t-|J74~P~!Ov*RCDg$Je^%Y7_ zCb3VcS&F_kW}$&uiPJwB&cxIXzVA12_Swvv3Je3eMQJdjQGQgow*fn)~JHAnF zqxqMFTW63<#dP4JM_?+CtXkJvrw+%l2XH zW@ullNhqH2t{VSi@4t{8;69;nQNIJsSF(TrfOMGAu&y<<6DpfRc-XQQM;U>tR(N;- zWBUfUF<9|)DAJcX_aaz*BhSJ}8&bs;Se2zwOPdBTQ~Ho`p8|3fEmA5Oj+q(jf|C1S zCmz6%KPMHBOmR3q1Vj#SDNlFLWf{9(0E zH+7Hll+NHw^9|deIop|i+=wHB>9NN&0g9Z3D!aIDiYF&f?lM}@2ZyZQEKvDcL0uF$ zoJ|tT0ZMmAo>JY8=v8NYUSD^%lE?GV&+cFoCQGs|&~HuflPYB3*0^+CsPp>RoeEP} zqV>r_j)HUbMrMxcu1{q7`w3y0{4*&@gSvy)_RV1p6Q?01AN#|FB@`)D2rgZylTUE~ zvQ;Gf)0nX*!bVxE9Bit5WsrCc+V|7->XU&5KXdso z>fsfN3>{^sNyvb%zqG~ATv$B>C`Y_$c`!Sx@os88AfuD&AM7kp5r~}ol=!raEM^joKhl*aX0w~*7dWc@{G|S?DrZ$vQqWRKJ z`rz8#vvrPMnIdgd4d%1&UeBB&a(lIQzRG0w@ftSGj^~7IZB<-NzusOfChzAtg_Bby=EiWgyo}}^sN_xQd z&V2N2n~)h(gM%l2ips=!ATcE0!tH$ZX7nBt*L-QXU0)BOJ7SwPrS52Ph_TZ}K^Maw z^oyx|4LGAYtUQ@5nEDwnvg7qfhQ*CX-Vajv!rwe}x`b%ir04}R?IsA!xUJq0EOQXx zgmf3FmM?Nnlxx1xMU4OqEqWShI~$z8PHNZ;89$|;QNsRWpZ`sf`3;beILtq*vsUX+ zws0dum9@w)1TK~7QGog zC>-Ptuo>nP>5{C%v{K?I#uuAWki9*4VW+60vwKpZS!C`cfq_Z{lIGsu*8 zrSlt_{36g{^lkZdt$Pp->LksRsdj|Cp#Ec|!XeXWz_L=IWsoh{EvXs#uA>Mm zo77&+tfLGS#CDBGUHzE2)l?`(m%>Y>ef+Ez+$e^VlNl-`(&H%fSF?bKnl_-y1piAl zzP2^GG-{UBg|YweULjJ*~~|AU){T+9>rERrOc5v!*h`~$-}$1Z|SW7di+ zbv3XCc|rM+`#&Hz>((oZunPh6tGDpwXFrUc7zZ=aM7=)Z&#)DXPF88Ckfd&mSRCjLtDScdQWt`$U-Zj z0d*>Vy~$2kVl;*bZL5a6-r#q+TJNQN9n4xELj1XgIV7-){iAST^8@Q6w%NT~m9QnB zb)>4>0Z(vJRqyOAbg})$g?z5_aLS^@n|3|E@6U<{ULM6vjM2jwAve47+a-8r<_=F2 z!UCj{X4{ih5BQd3Z^wF0Q!8xA(vK1xLOi-r&J61J!lTDwGZCZHcSXIv>=A_TckYRm z!PFrH?9nkRMk6Kjn&flI1)1WN5 zXpKlAnzIo1IC$W@OGFNn7J$QZ?F*@o}*n?~aQv!M@9IqFnsj9pBu^J6hIQmW2 zmn@U;T0@^znXzDbtN2{EGj`4g1Yh*GY=(?bB9?~@w~scTPzCMu%!g2#BUrpd#wq?` ziNgQ7S6}juAGZB*{{Vl@&Y9gr^LpX8=e@)iJTh#)GYW57aa}#5FX-XVg#pK$dZTNl zXEtW9rL8Nsu70Ba0hrCb<|>Vfk%>%$j^~a!S|KC%RpKbPurptn*Z{FD2`H?GffkRS4Lz})~0<1t4)+5n}tK}}Y*0e7$dbwX{r8*Y`9MK_Cu z_0`&cJLehyD_f9M1|4?Kja;pM71kKX*?7|b(}pf;0?^KB0Zl9WPNE;|h*y93Z%F#l6+W!q>Jq4`+Hrnj|e&`LDb9_IY zFS?4OrkL0Zk2-Fr7xhn&wp54&r_(6Q6R7`$o`j|(o(cc1S2hz+P6480@_@5^RkY(w z0U*z3g^jgP%CM-^)9-t9w)h#QPb|g;N$TTRH%HnDG%yx*Yjs4`-|)se4WBbzmOe{s zS@6`q2cWh7$fptsXiCxwA9p!sd7c^TK!TLiNpPMMgJiY(W z{j3+B_n%HMkQq&M={rBM-_M2Jq*k|HW<|D3Sf)#5?Ur@h*qrV*s@{5&Yw0we|Zn(7kn^}qSlt? zldFEuwOs65A%2}4&zsY^xCL}aU!yK404bsVpid;e zn2z7;XbK$;iBI;<RRD#)Kb_>lW9(7ncZ4+6V3sRG-Jd;N7<>$tv9~%4}hUU&U^k#X5_+WS{ z*rCiNEJLL-d7&`2HT+LaX_(<^$1OcAB44SJu*(fB?wFR%>A@_$IFZAb#@5y$7e9(o zrQ5}tTs5)!ODhg|#*i0b4SiteG5*DMW)rCvl=Mk&MW-jhPk*O_)T(mhPN$i@%orb)m`@2vNN0ll&=plAIF7k^*vbVc z)M;2LN!F%E+Z&(s#;pVw^>;b&W7P=p>-%5ax?AMWUh9-CaSUQm3%6C|TIjGKA zW#@_8dls#uY7sa1i>c}62u7qd#1%ys*CsM(>zC-S0?3`-ruSs(|keA-k#m zwQ87Qr}cL}JwmoA`uFGV%XY2E8-5A>=IUi~(rBtbmYO(CW!TEUSrO@}qW@T<)34z+ zZ~vbq50FYf!p){kB>WI%aI_I`I6Jz7iR_`1+ll%OQEH|5_02@?`m5N0;x=yyH9 zENc4jMca-!r&;$YX2Z7>+u+^uKzo&Oh}_94s#wLLpbd%`1iDUC3I2fcvrH$3L-28J zWcg*$%yVxEsw?{$Qonv0Z*gA@3z|{IrlOn6NJHdN{vPxYL=i}TAsef_23v?-3mWKZ%`fYx`8DRLrM^)KEOY>9XsNTb(?XWua zvmHW^xJ2i#+7Sh`!!`|y>!Nte6vA~!p;!)SDDE4=Hc5=N zI$MzOOqv}}2P^7d)SSjW8khAg(OpCQ39hl($}ejVKm6hjV{g?~QL0wnoFO!ST|{=6 zP6oty$a&@$WAHC<7h&y8*#zUF*ZYh#pA7Taf5X?m)0=hJQ2dUtP?yCSgEnM9jS>dw zF!^YoGh?SAp4*)w+yWxJ5vkRMN=m}%=|sl%`V$K=XI4Lyn*5~m)qaaL6t{>v86vm> z&iy=(s3?e4sQu$Xg1c`p?fmFOU{4JpHII=9V~G+&$*9EuKe0{@e3 zxHCvH+AE=AO6HeVu*wWaQw^h?5DC-^6_G482qZ!R5HJb{{($VxZuF6) zyOt5mPz8EB#adMfcyxU*rdvhzuzSF*OZ|*?MOll)meZw8D=MRm0+lTWT-3%ILUdB( zX#^s%=M2~%5uhhYc(_mhBpWLWl*5Rm9Kp{v^4g`sM?W!JD88=P+-hiWk%5kIxdbR( z^y*v-bf__A$SzV(;#oKr6+a$9&pHHW?|yYB`q2R}j*ybjUGACP@$QE!Oo|?2Z2tXY z)MHD_;ojk8NWl#r3xm>0kRW+;98jD+HZM)#x613z=P0qv((E-q&!Ev;8YYtn_58sr5&!B52Zm@3<&15 zZpWXdMC<18n@D%YAAqeh=NAA+T!~LyLWkS3=ET$yPR`P>#MDPw#JBG*?IaX}8e^xG zT5x2$?~m=g_VL&#akHwmIx(=MYm!o$48YXAHjf#R4y=rORqc;1MbguzOU5J*_Qz?O zLH>9P`*r&(Bhl$)%0GO7)Y6EY#D?a_Cz!%3KKDaZvt7q zpFlEevZ*HKk2?S3(-)Jwp#7cgD{+9V+8Y_uuFcbrtxOb{h+)Mt@|3wLQp-As*gvb#@OB3{ky3$ry3(nW7G=5@AzBt^z`+jl*qAYl z(k;UOQ3sw8t*v2tK)F&5gdemXrey2(Bs1-4JauXIYTXm*joC6C6%(yqe&KrR9F0Le z2H{E?`%v3`w(cH~481N2zS30ki1R(aSl80H&h#ghBQS?|KcbZ83u{KJ-}P>(^rJ9> zl5X@PEGygWXWL6fkJ@NQGGTR<+buFsy1g;{fwi{LNu=NL zmyXHS^6GwGRT0SQyfjTKT8GE({^~~V-vC6KM1_l1iT0n&UbcX!qkuX=Iu`5r#G>>k z*cGqSkk8_)HD|u2O^S%sW5X{e{M=?*TZ_^-xZPWlJ9;0uhH$3DN7xpQe$t%}y~2FN zhH1hlL&a{iw=mh0>j>zSzl353NaKsDd~M2Q$F8uWEV`}6NbnfHjHugg%x3 z>Q2(n5`c$Zclc8H^!BjF)A2?(mHz6Shp-UU*a%3?hCX3vCu1}Pet{N;PE-fek9o3T zV#b$@dcRhaQwRXZF3-Nmi?$K0IbC2tH!=R3hkSFcDN%27*3=P95ENnNSa|Z^i{(Kg zL(Is2eG?VxcqOuFm73%2;%C3q$LQY({3RLomaqLVF!d0p3YN>#tw7Q@4kb%{KvmLT95DD0?TaL>A6t|yT#=H84zFLFA?5NPUJ(L-Mr2a^}p{oVB@^wbsn z_Wg>ju%9D?%VGYb1Sc)3YKUA21*e(XhckY=o zvHC(Q%lGbgsm815ci{qYEU9MYV~bL2xU>u1aR-hGh=*tkBPi5i=nof^(o0a!o}fwU zpb4+yd3GMfXvl4RQ3m=bH9qqo?w*T{e&z<$mv+zUkFvDE6a=TU0!QPCKDSpoc3Q0dg1g;9f-klb;~dj&<~fWUp8#oH*~Ot}hvY z$c=@eskdDE(afzCbPHRE_rP76t4IkmBF0pbsCm}9+1v;l1SODJ>LBssPo(l^)+netzlP#Av%T5 zxZLGrZO&bMRUUmNH~hsnsP3oOB~&RakukQ4^3vB+Jg$_=u>d zFz@2)!%iE?E}EWE-CuiAaDo7TQx+;aId2CDNdrmt1Z|98|PEoZoA_o^sYxrmrXJ#~lh+9IaE% z$xNnGUH8(Pckh;BqC1mO!&%QOWNvovN=SJRjH`532zO!X2L>o z*U@2Wx&_O+0|amB#WK7+oK%(Iv_<}*x3!CWSBeS z-teqlhwk%oR`9GCD|0y&4UptOIX>U{N-+Y>zeytE=K*Ev(*oMLM7tR(x-! zgpZe)adBT<)#Z<{9yliRaHSItry!pLg{>xc9^pU9)@VQGT9%w|-}E=^4&nbuW6xTQ zQ7s1tyf8$%{*O;BeLx{n`%G18XO?IQ0j`u(5&T@+L3!TbKlsonb$Sxj`G(z@Q zwF8K#pgI3;n36Z-eQ)mh@=u`>KL4*50yAOIPz+kLpY=RV;lSNA zgCL5E{LG)(2Zz&v2yAS?0>3UtTu>HCBMtKlB8c5)_C_< z(UqSkhUg?c{)$AaTy#I62}9ty@oMdTJV_2tM+?(9bBL*U6FQVg;$fXqsYR^GVls}> zU5i}l91~j8?*gQk>u%b)7 zi3!vAc;^o@u|oDGy|~tKvED%W7!lxC6W?H2W*wj&6$v~j%q`ILg3{0?dOHYf^24Wb8P42`7U!+TRy$Pnc{b!P_*Si`&p9(f=Y^^t&QPfHES~xrc_lYt#ORvK%qSHh&1p2 z1G<0!0G+@g;0OSKK|kokAt;-DvNE?mu!}XkcD9#$``>HJSH}uWVBLwe5>*LV>a3D)vCGK&~ z#skyzn=&RD8xlKKd6uE|Ymu7?;Y*-B%!a=A>8`pwRhw4p&%CvXCywR|sLy^%x)gRv zA}w;y2ItJfJ%7mRKj$bA0mr!M=hFsNxV)R~n2Twnn&dqD$lIvW8c}1Yg}jQcDV54l zpV_@DDEQ3uTN?$+FIR$?t9_iGh(>LYl`0uadgJTNlaT}mPQm#p$GHcHzX>!Rlg=MZ zlfj>A%H{-IE7uM!blT6l>JM3t>%Re0F12iWZFpr)hfio|AWqkUAa=EE z<~D`a{n0SyX?f#$BR10E8Du3d?ukbIk2OQwv5Lqz0?=_|t+*H32t8JN*v)XJJdr>A_?VZo89ZrAjrt;ZsG< zgX^Tv#a*G?c6DwVef5+tW44lWPsCz1UCTuuZknIAFOo`vi+-94X&-c*-d@RE06wKA zJCK1zNehUlwAAYpU0xC2_g!Y1kbSWpQk_i;Ow+%OK$j~gP?5($`c6FsH5iR8PKnZr zlQN(8Opl-gp%-g%)_N?TS@#N5h6=6#;$9ISL)1k$s_U2|6h6WTstnaJJu!{A`8O44 zAJ8}$VTJ7dh@^&=f%%=S@i9??4n?4(Q<2f3_QYKE z$F4%ro^VOz#H0z)r%Rh1iFN|g^zwSt;mBatUgXf4VV?I>xU}p@X!oEd9Yx7TiQGIa zDvn-w7tbhuoh#W!g~ADO_J%0a;vven1~7qV)BQ6>odf)Uf?9si|(Z^7nY@Osg2d<$u!y5RZ@W+ zpv<(w};{0t5GHH z?Y;pvNJ>oeFYYlP7sfYFrG8lOnzAl^(c}k6xL|p;w3@hukq;!t9lBSS#(6JQC1fzG zfm34nAL1bJ*yC+Tf9?3(I&F4}xKY|A?$q;=5lM08fsuzNCE6oeX<$5F^ov#BN*33G z04uqmdpIkk>5~cvz^UJ^KSs&B99V z{NbwKE6Xqbaw^t5()%x1180QleucDMk$1Tg8g9Xo-Qq-Z>g%5)8b&nh1jMY2@f_>z za~P%>=zx7EETsA9omwWzqlKq6bC4+Ty5P{AgPOC=0ehTrtuHGf=BrSb$K^OXp)%iR zDlFByfDJsVPcB+v<=uG~Vml5nVx5U#nv0`ip}NHS+G=w`MJ=EuKqz3`(=aM7PU}Gp zNX~OSdqM+ezbJffI-!yJ(cnK%k&?HHN#ks8_b8`V` zfZF;_bDVyyrLLo7M2Gw77+?vW*BK1>e`!W=_nQS)2xqSkT7I)0**9GHZs^&yNiaqE z7}VJ5WrbSgC~c#CARae$qVD)?$)_#MvRkjsG8Qd(ssl}jiWjl_)!@YH7^jY%7tWF?*xNEumU*rQAYshf<0rIRAVKf}R41tXFsqW;j#}oZ-?YbG;{Jxb zHL6@3r1>Burh9HQuu?3E`Yjny5ncL4tU~F&k9EQmzdh#(!w{PNHyU^{)iBSs%iPJL z#5tN)B+)Hf3YaEEO+a$jq;O-~@?+cjx_KLF-O(%w#9dp-RcEq(Lel6B5^*a(ir1Cw z5<29H3tY3;_H!IkdNUa^r1x^Na**Udgd8xk*wb9N^Xd8nj)MHt!x^#$>8 zVD89_-*Et`CsqFte!;BJt4K`~!m<>(%)1ymq(8|`JpIPr+C6amq_xf!hS^QVHhyv! zl#m{@FInR}D=aPpz?mokJLM#=Mq3p&kdIXGUrh=|1!q+bsY)E8H11@ifpE_)PwR4ZPBexS&%%q$-hYACH+o@@pja8B97$%T6W}I>|6AV_yE~_KSz%1=Ya! z^42C?arGwgYn+%3F)SK0E4*sQw9b1(a@(daAX^rIDKyD`vCebM;N_4j2!yQP6mwCo zYjXRtZ0-D6eEhn0og{BM(a)-yUD__->C@U5=?W#6gLGv1tUwz7uH+zHdbFDZTISF; z*;c09DrJ;ShC)(^An^ezLGlND)3h!>R}&9?q+@*vDU$G~hc&;}TD&*8(H)gQ_wwsq z&uj1{aN3pD;#bWifH=8%b$jh2c!x#QFvBHaJFgw#U?jJY$tSKvyHvj}VF+jQz=fO8 z`wD6XV9t&+ERYh1B!Rg=&qB%KLFv#*|LY|?6|Y+}fGHT@4DB>)9or}+F3X?kleXmm zoDiq(pHza6`xjo!P;DV=;quX*m=0aq)Q)MZbCaN^ZFk)vT`+)PYo}`7cx+6$lYpTv zm%=*ju!^P37wz+__MtLAm-Q#U8F`s(SfIg;0in4T=+a_6Mi_K?)k86)mcs;@>rgxo z&&-x_jfUB$!i#sJ>x%wV1he|EY~_Jo>*Vs~ydsH1EcSbJZD{kAH9%~vp+eWWo2}~# z{xz@39t6TBMD=u6 zutZAYGxrK_HNKO5Icw#8*aR4BOw~he`Jjv<+UPru5e(~-2U_Q9@RE?=O_=6u!$p?sHA;yvj zeKBK(@fx`XQxEvm(Z@<(-XpCQ$0-vi8ZgR{%Cl~8vz$AFrA8*~BI^xM4OAr7>)HB- zgoHm%;*%wr!^O~uFLE~oQyr6W*wxIO73jd4lN`PV?v$H@N7YsdxOM^doI2SZ+vWm- zSM$oNNnAT?IDIj!=HJ8N0ZGZsNrtLeJU^lWic2BE@P}`d7^h5TBqdV*F50$n!CBl> zqikwTW`+}D4q0{UuWqsE6;gwa0smnjL!WQ(OHBKjsa{{E>_x2 z$(g6Hkj{!#vNjYG@4N#OBV7A8-K|v2cQ8Es)?@3&bLhm=#e6Rdp$t@6$R3ZZZp~%5 zJh^$BN8To5Zu)A8bkV);Ry#K3lNQ$EWC&TaW`BdR^)n%JsusS%>n;6joy_OZygv&L z8Im1~M{v`fn=wNpCAe|5>ejdGmUs^a%7i;43P)YZ@Cfy=CFlxkj}qf3cqpi0kod?k z43b18PqkeGrVEiT-{z^Y~xxMO{;( zuWCncUF>F;A)!u^&TM4y1*5q-9}4;fa(s4p7ix7D3w*RkovjUG^=)=_>>_Mo$SDSv zb>G;JfGrT-jl*tNzXbycsHBFccz7$`6+}6^5Y7{t7Dm{Hlq2d2NTu}6!=gqeA3Z?4UA*}2G;AQC66 zT}n}C8}Fjh=$Ol^RPHJ|kp03N!IpXb17eG4ssN*;E0v(!*6vp_#TBtZt%o830zeez zV`LJ8P$&0ozS-PN_s8Abq!0^c$!LE}IRVuRO6v4P5AvAu^e1#z6}9(;h<5$@q2}-x zS1$Wio&KOznlqoA{hs4DEGV)m6w&WbUNdjXyPIrl3tA|uL+v}zVM760eH%XL&Wm*n zDmnQhEFpQy4v5aqlFuLG!86J3qXAyu&wS-G+uuNCH+iBLp6cd#{wL2Jx+L!B4BUi# zrXSp$o|M3PxmfiPJ)NB?-W{DocTxnA|8sGwJU!lC`inv**;rSP0UkfE>AlEg`mnf+ zBv|7n>mT53B1q3Zfa!5;4J-G;;4f7A;1}0?v2E}-QGvO=LV8?ux~(>9f0HHGy9e1& zLqeCTa;JspJEc^}7k9xC zcJq=zqzLtHi*67l>J`a^HxM0ShV1Un8}3Hh1Xkn>K(~whPonKTJJW?-nsh$4t{6jQ!c9|jTNjyKZ*m_qk z%Y`3<=`rhNah&#?iSZi;z^Sc9v8$pQ9zNkGXp*ieu)CAd*s-SKG*cjN>+q-atYZ20 zP~Mru(*_|_JkTK&vjR| zo6c3!XazYee#1NLX123yd{%^@1+_SrH)8OM0a5qZi;=7KlxG7*grVJerSl3w4p}9+ zegYOd&jS~6FB^F-c4F8sv^`ud_tSK*%izj!?72Z*bGWc7N^%1nO;h2gEMear#NwZJ zeMLnxBbSNEAoNQMdWr*@$Q;QqSU#@&U^hwpYmp4tdnGkP2itGO z@j9jG$AanMLon<{hm5`7#}~D#Xbaa1(*QnH>(d)wZd12kjI*4I)o>iz37q6PDxM=_oxh&nEd^>~K}UWzEgZl2CRB@9JxBikKdz<5 z+3-$aKEMHuNc`1Bv6Vj$J1%p2*N6=TX{Vcu-F3#rFL`Xf?h+f=k2sLyUf^5D=cdWg4=ga0Uj`wFNl=k0lpq-YkeA9+s;Y9z*kdT-@`;}X@$;;z@N;!_NfNb zeNj=NXraAOS{-&t(D;~2YTAhiU^>6I`qnd@3Z;qFKrmF{$JUzp?q3^Yq7r(x0$3lX z_m=$y8%Y?)GIL{^E*3oh>efVqjY%J#5#tCzYA&KoMaal_0{|0HSsud}PL%CjLpP+r zcr~TIVC5LtA$fWc|-~*ZT-h_+dwok`dOz|f0c%y6j9+n>v!;pJWn4_GMxdPZ^@9w|Tj^L>odmdE$KO#E;jh6!! zuP+D3BM#S~D*Wk2Nm8BOE_!+S&;;644)0zLH{HUu!5?SMA}Z^8$(9iEi*1B6Z8uX$k6^{wZrE^t7p)!Z z3+@Ad)I3$3*XJ2yzA)okW*r!E#~jP2gX55qSIYiwu0W0cO1}`$w|O)!g{?+a5PE36 zak=H^%3?!+vNhb=0T@0iDu&)Wo+Sq@6?c$dOJM-LM^sD@?dJ!QHB|c`2I;~<7sDIx zyAIMxGjt)7875yTAMzBiHbF~B@Yq9;KEhDOc2e>l@-0?a{HyZR z6Le-91y(qWtH6DYgoc(yYh4QA$i0qTX}9U&@XgoUtpzt9Wni9(&yh79o*!FH?3|;_ zimQ2K9ti^Si?^@}vZO^=C~sYNug@~}1Wj1~{dVIT?!o|BUnXgHadH_r8j4^wy9z{NONy zd~z1rmwHiSk>w=c?IsYo7psu)w1-X&}M>Zy9 zJ2@=?>TM~>AT)I?9Bq>JDb?y>e{cV3bC;`f9LAUwTb#4!R@}lhIua#sW}8j@zzlNS zOIgDC-XbK&BqX-U%95P^7)rc<{>1sc$ZY0`+Zxp0f?CR)s}3~uWj^DI(Qif~^&)vx z=4I>FMVML>1USWT-d$|)z#ZrwzZCj?w5)^id`ePyv+bE)?%EqxFcK;aN#LpN$bi<^ zbLWW=O)aI6?q6Ddm*r4DGdun$pN?Dc(oJk)O58gOo8AGF|d2#!XP z`dz7@T}C$Wnx)UlM1?B~+dMY_&HHJjxhW!{F41#;iAyr%2=~xJ>Dj>3#Z^D*F8M?W zrp`ZIBn8-uCUwkcw6L-XK)%Pa$rf^mk#sq)yjc&5$wk))hZ*BK(3|* zh*V-BuppUO()3P`-l_#l@L|Cnvc&6mmv56HC9;0RGO+xXj zr$I6jn6{HLPRUJpaEHQMV$S(wxDxyv#d5)|5#JxD)Rvdx?3UqL;1u|1hiaB|K!9u7ypI@kzXkDp&a^biO#4OJM=2gQ3t zaXf~?fhWLB!YL+S|gow1A3EH8sFO|kIK)4+5o zie9bCMn>Tz2mAQk_eCd*htej5Vfk^j?g(j^;x;x09ga?Px=z^Zo-$~eJ;^Ze6bjNl zp4pqbp^By*{ifCWp)6;O|9=TLZ|?P8G4a!_`*LTvNGU^~ z(~G&sd^+xZff3gL414g8Q$?5J%oef~BgC>>6QB^=y=!6T5X(O=Wi2@G>`8%53fk@? zeLN4iTO)Q;#SP00CuLk0WR{QQQ~(eyI}HgRSnnUV)bp$>^kqh4HV7=}FZ_-(i;B_M zRZk=bL94+Mabkx~F21IhyPcdHV)Mej zLltmk9sA6Hf2V!;yuoGY`&_X_R`CfzRM(~hzUEB+`w_$;V=7`b=_p$PZ(9KXS~W_f z`r9}IoUh*DR=`|;?hQ4{DIhO@H+ou@92=H(L6G&vg)jHtrlq;~;}#$v ziVqhOUr3c06%lCTe7_3VkXaz?=SC!PS_I2_+e|3v51@}g^E+cp%z{f>lUt@S_vGjh z**SSR4sy#N_?vfak63}wZFwo*;xxDhw`KDr%uFTVIGQE{4Gubg)?(8pK29%D7pff^NkE04{dpznD; z-p8eyY`JQ0A6Y&g-!8-=D^R&c{JMyGSGM{&*TVfbc;Qxi}~as0-|OSW-@Ym>_-&K^U9OTP(QrT*3FzK@QDx+J^A9ASlRt)N6FvBEMQ3;d6^xv%h15$I!BR%!$X%3VR~f-T^JR@QP* z?QSy>9CLmTcR^9OUnH1pd)M*^tSILZAahdQIa@v$JA{Dk*XRM>D@4PxFCjl$Eboy!)i>XE?CdH+}lhIzXs!nE~3tDZSE zOzi9AG(i*YPM`)uJWGgtDWX@4?AY6@=G`UTv^qw76+c0A zPd9t!T$Bqx*+=2TbFV66m9YF>ED(eStJM;D5AG*^$J#5cOVYGeu7)SX3G6jf`vb?X z)*dDd9voGXPl-?fwPZ%HCA;i`dmLO~?xgtubCTgpSGcu3Q!8w=R?++iDjd2>SS>_T z&+J*F{6s?x@~kC@nFE2WWNuPg<7UV%v6FgwBDOAQ?C z7s=MessM#W+lilh)TleiZ-%o5eaIfPImw`5=UnbCe>nUlvJCFdw`}&?aPh{%b{r#^ zg$d@AGoaS;~D@&EIJeE!CE~+|TNgRX(g9JvXW0 z_(UR%<6#7Xac7<$%8w#;8ZGG_5w_*lWKT!2#=2f4Xy~|Imi-p{4=9!x6$J-tM)lOt z948P)c-=R`86y81xpqXP*zYRF33!;Fq#w2i%bwN5GO|Q92(^gA_m<LBm_4SAi zjd0IKZ#1o4nl9cT4x(0{wAuQtz5tX91`Dewl3Ehcj$AE?b3(3a?9dI)VpYg&q77g2 zL*LyQ{IyQAnr0*yhl_6V8@4i+4itoW`3<2b_w4oBg?=vnqhaH#t2aN#97alYF>Dk7 z`s4QYGV6=W^V|q8$V2d&>ZvY~*2HG~ZA6k}wv_R&(P_X)Dc=rz=oL~WIZWH}_Z&q1 zC=i2bw7wkHOgEGNk0?WwGL-m2g>|(h`tIwi#J4mk?q&~dE{56g0ry>{Cv(<@=4hw1amAT{Nc@{E;7%m za9MGs*IaJ)xFA&k{RNe4zI0HmnL5I^qp+;ko~xPkAkl8_x>Lk0akYtuG1<=LrYQO2 zeYH{b#!7wt_(VK5B;mrxbjYEt4~BKJI`trC0PI>Zge3jGlPhiNmc~+Cs*9o0h4v(# zc)*N~5o>8mU7)#w2`r-u@+mu~x9p~R<&63XPm%i6&XA1dd4ycVjD-a}nz2+)AOWyp z#i%=lYo-VBk)F(2VJ%MSpyjrkGls+&cWmVfa~kgqloR)u9edjY{W3|(JuVVYwh2v| zBT4g~EpFb=b&e^*PSe(^_=6^qu~7aWBUAAuCo3?gi`=;et`!!}r?E~hU}YY{bf>CZuk8|RB# z_2tlPVe`fx?DJTOL=CcJ4*ovroe}pRz*Nhfmhahk_`N`=&h)y$?$-D9E3<-CJ`qlkY6j z%M}wO_UFwH&f|O%>Ai05fy$4*=Fz{L_B|%m0%UzpW}{iHXdtI}d=|x2yb{z92PYG! zXYaGMBF!TOqN$)p=@)`uUx^gqANz_*I36{Z+xTZjK-a}wUG4^9~ z6M5+!E{vVIrA~=jBa_3~Su4lEw8-3!`Q&OxNLmrdEyK zg*2G|wYoGV?FO0?HyA>yOfngrrFRjtUeLg&ZiF|7Qz{*oy8ZZs%` ze$*p4k=xoU-I1lAz*591It-~V4|KMKaE{dHk5hkWDZ zC(#h~TSBHHbQ@`)SwQ@P4tTC?*;MD=L}`kI&E3IZbNepJ1|oM_!7+xaM^!5o zDfzBEqQ0jMg-**UFV&*_$@kc2pfW7$b8othRq*wfBZ182gps~m7R=;%zi=ySQu{f> zz|H>T<$ku4K_Fj>p|f3zPMOW372SqVmp4t%UzcvmZS+|lbd#sHt(5*V5b+@9oEpS=2l2apso7f;Ok~Z-C%D60(?nQcX1Y880La%#!Cs zCv4*a>VeQ6>UG?KYiXAH>M4<=RoHh%O4WbTz2pY>;u{c*;$jD1Ib+DzTi*Vi4l5+= zJ=&}xX^nOQwqaG2>|i3}02DWrorawr3L;pYyiLb-nM6l?L{vf`gbeo;{R5i_IdfC4 zSWT_xBLpbly{YQQPfk_y6>ROMQf-baCd)~{?MJ~FcvZ}6c-V5RY-vHCbur@uEgBCx z?foCCdT=mdT5ZJ@-^tF5Aoe-9zhyM{bWCi*;2h$pdDgCY8)Y7VBV-H*+9-cSAx7c5 z0z4bZhY4d*>_g&1R#x=B5*F$n>G<+AGGy%$y z3vdbN@WIQZAlmDaNv#M|gZ+z(qz4NGmxQy{rk*WU^RrR7^pECs$ukV-=reLfR107> z^*>O#i;Zi?JY{Kw&+p?|9p226Bw3^q85XARdR$s z+K5Io|9}sdttYJmnsT3mr^Z{)U0lchUQiex@Y-%%@IVQfCXR!vQhMYI3$kvBK5mT2 zj5&HYWT~D6WWI~i9`!N69hGVsn6n_52Q7)}{1L>LRc9GR z*|#G#av-smhj3jK-OHqyr{`!O6`d`&pAt6mAR|0gt8c^>b`P;Bb2tV zzj_Xon1zI!2>deS-(Y&Ds>Iyhw}4SCpK*Uaa9H8BKwdU>IH~3OK5vB9o^2lRwYWdY z8T!u>-G{3cDhV?q5?B;jp*(JIOOG2u7CdI8j+0(c`kyg5&!}@pMU-Zq+Nouu9UQm* z(0PS7qUx}A15dxys}56Ir~W0@Y))pe+kXw66VYktIm^SLIn}rH{3v3;6$WZzU6MvZ ze0p14j6}p_+`xWf_XBw^yh{vr?I!p47`rmP%kzDwBLNaezn~9QNdeK+sI?k$n+UR|cZ z0EZWi^PBsU`Rd#=GaR-Wp0cL0N_N3?Dh=giDBQs4C(~F?jLfxh`V&AdDUN1%9bztQVWChubS@T0ESMK({%`ZvBYPTL zi&eT=5qmo=mp!wF>{Pp+qSB3-lWXAxgwZ>>&Y|Mb7#I+M55Ns>hiNV+f$14@IO&P& zsl)#N`h8}M!FbsLb^tQE$gq7$HnSY7fw{eu9vzOnzbgu@`FIx20 z@SsTMqmaO(G5QyVDV?u5oBA_7SE;EEudcSc6p%uzUAQ%Z5tt5(gTT>-kiof@&BFUAG*+v zFX>hZ2{{SwE&uvyI!ThvPo&8GtJTQzgIjX{&??UX#x&4mX@DJUC|Ru3_prp|i%Gl2 zp{;MUT5=9Sw$0f^H~wkmzcH6nkU|`kW%n*uV1Sxut7kn^l+wwQIPCRH z)r+I=4lzTWnu}Do>vl)f%;CrQ7d>NE+j2f&MmrzNl9&eROUIno0rb!8GO=oH19r1c z0g4Lyiq2eZgh(Q>bD)8oxzqLhHZYH)L;ehCuN;sbY-B69Foqa~b2HI}4O^%5OR8M`DHB^L#rGYwmm#L)B4pcHt=TU2E zy6SXR2ba!$d02surcQ~qXQ}YX9t;uUq+m6#a(~XL@&lW20T=PghTAyhQ|BmkY^`z@ zi@Rt>hii+Rr5~6^q>hZmbrIv*+<*wa3HZVw(%gh&bl#R$VI2yg;a_`c4tW)uzEy2B z)m3w7Pmt5SscxU%$adoz{jPNe2>EVy`=nEW{@tl;QY6%AV;&QW)+m2S4w}4&1P;Zx1ob`st1txg}U>RR6WUZf%yHswhBWw39W# z!gui$Rl8m@LL-xN#`uNx<&Nb#SJaiety!x20r+99&)4|8ha?vHi1 z8*E4re!DCsmeBO2ze|4r*g2nG0*b7!qAmeCnR+`hhUME z!JJo>ZuzB%o~Zse^rv{X4*d76@DPiuXU38aI@;~2k(vvmZv{T9DI!zs8EOzM>BC9q zrR92a=@q|%zz=GP6H$q4z%C~Hiy3hWz68vCm&B*4>W+IvDJ zRw86y;)2^Wy)}dx^4gn0oQ3v$F<&iL!Qyh(Y%JzSW5GSLz=0N72=mX5r9OW3zsNf4 zH>KYI--Awk(!t`+l*Bn0zdSU;NQ-Geup&w=cCQYahYke)r;6B-0r5asbx09Cmo#mTKaQ_Zo~4@%bO|!H*khX3)`70@6athZ>1@&RKmauR&4E2(3g2S-{<|X8i{LKVr60>0^PZeuvs95W2Rsk+ha9sIY9X zSjn9EE?KKgdtSG_1+b zE|T&+k;p`=@a`CVr1Eu(qI}aj2~`vkNBf9r&my<%ee?qJ;-!xNcmzzRwZ-vTc(5>H zi8e(sQ7onRCP+IV?bCwi8G)RQft;XY#1MWTf3~%L*UA}cf!!c`?2j4O%_zG5s`$IS zT)fs+!!Uh`bL;f<9Eb@&i8RgHLH6)VqHh{LRsR8yyLA@VRE63BNa?6-XVq+1ENR}E z4L^GA!Ef_f>?#dt(VHxZyVq-`=8U5+qMmY6xU&Uoq-oY;KNm*eeO4l#TiesmQl@ZP&A^+=d(*}Lv*E$ZVgdmPo4 zW~XLrheAmY{t~0&`-!UdZzsk6wA4g3(#rNoiZJPtlF=`z8|RYiNN4*1WOz*RN1oJP z-jFZk=tVBRYe1%_kBry(y@!A+`Lj~!hC@`9c<`8((>!5SY5?sv>9NzdDD6qSQ({NA zW(Y|pCs;}bZ@%{tC)Q3Ff}~B;37NtG^x3F&I+1{yGq-4mC?QAZdU$*qB@Rg<@ zp8wo+rh6nIp8>CPE=1Xo_ht8!Rb%gA;Pk5WXk{4aw9#bz|I`|Zf9TYLB)tM%e--^> zniLu$^h!m5`_-L;mN{Y_(HTYQn;EX@B?A3v8+6e`9M<)(@q_G|YfOjIuOwNN;q-Yu z!;6(T$We6XDB(GG)^o_NS(0xJ0c`d|!v7mv=SZ7OFS3-wt?@WtFe=gTk$APc3&k*{ za36@_b}zr(IS$SqIDSWCD>QTMS(D&^tm?Xh+8YD#uYagM>oaEl zWWLA%ht`jqu360#Qh>Xu>#~91Q@0vJx;u5FXWGpxs`2Bk0;?O*r_xB8*BNQ+4eynU zfF)R`<2>?iZ6C@$kAeKxeiWV++|*VW{mJTRBC4GtKa?#yxG7eK2YFv5ow8o8Yy(i~b*xOnf|A zg}SlKnu-?{pzBo?1b)s|D~}fe;~94Yr^u=BNU{hYEVxD-5v;*BJjL^l8&W!OAAHhw z`7&gXo;}+Q_NHu=?j}%XLw=Gyxx>2Mr*zTNg?NiL|LHe*0>JL@@GS4tRn}}%W9*pZ zu@~R6CABI#;L>KG@1Xry(3V=%l2Rpz&9ezR`~zbpq_cBWjm6k{76S&=&_)`mP%BRqR1=vKVlifZq^ zW;07jgE8Fl{-^vIVpb^*kPqfg75T)^-!yiHktl$y0hCR5dz#l-2k~5##cFj5)Obme zJrcmC6<(3}nf8F;s?1A1#>Z@&b46RkMC<$?CV1uaP_d`RjOA~o#*m?yn-dC9y)PV9 zj$SC>#WyaV%}Z0Z)GMlg(qz3udO5k)(h4C|*e}df%oM#Tia7ulL#vu3ABAKe#Rd}_v-|o_Jh}UUrSd~ljuGhvaH6gnOG&m}!?pA+S_D;0G$eu6nH#eac~eap7(y1)b!1A(<#dd-H20(%!$pleyDkP!}Vh;fGyM;|MYp6Pk)|sgveYe!FKWi`-{U)J*Mla zoe_qJ(N`7BDRW0+QmOL=v8N&E@)EltG-z6UNpfj@O|z3hocaE=Z%IBw`zZLo?zXhi$Dx4_HWC_p z0~$}W*Cx8Ch=Atsd=NP^ybi(wSB*PUM*UQP9O0YmK2G28cn$vA9c>eco8kK}Q{cC# zdhf?-P|azZjBeIrXB6WZ77HC%$ae|}FeR^oc#>hdaOQW&lC$nPkFIzyM zI7gF9%?6uYTg#AsMRa(xtX4G?N8+|sOskt!Q3T&CxXt}PDe1OsuknN=p#kR>_IkiB z9spR=9_i84h`W0B|EkzQGCngcf;ZOdQ9I;HFsy;ri&_5tSvigXjFg?(!d~GIauQet z1~0f&d*T5GI<}(?4|wgjE+o!{DjRGeLc{C&4VGIsSI6Nlx(6kGe<=$SIl_0HWyI%D z-*`|TPagI8;EWIEhIzV_41AlD8j7U1c8ho)0`?E+mXx)eWn<>F@6d!FmAw%q2#1M4wbt+EP(SI(t>Iu2$Hg=~ZGGISjF5JjB| zlS6N}Vj9tqnTgO^cuy2~M+?KL_OZnUv#5Sp@sqx7RRyOjw-~^QF&Eb|CT#AjUrFyg zKoa~tz|-DDpCaz1fMkb!FSvS9aqJLJT;%jYS!X~be{o=u30RTI;UKi6N#RA^5qkiM zMZTCi+#M9(>*z$s&uX7qi83PY|M)>h#FLB)Vqv^%+Yy?zPGekS{$EHfOfEH6s-?ZH zoRliXtq}z{qYrtANZIXZOZPh#8n>TnmXdnYID`r~W!m=T2S($++ry`20t#UA*#{4X zQPX&Bb~sJ`4=`!PThY@S2mK@%cE?|_{!LzmVxH`s^;i_Dd-=5QC9W-Yn9R1zLB1O) zGwu3hS}(fc0$2f;219Wht~T?m+x8(3Eg#OS%(Nem^E@iCuy)&5fFH}IW4_GQT5$}N znUwNDT+Nf?omJ)oQ=_{aKT&i3&vurIA5PS<6TfG5eOgkq4`Torg|CCiH`|d4ZWEs! zC0Xm?mo=|l)gSj{^Ex}1wDYbEcevt9__Z~E0$y2#3K)>v?hT)(e-UUow-xaoMC{!@`{Z5PGC;a_Axl#pot}lnm+TJKfCH(a6Y8i=Du5~7kU_@**r;vtQOwM) zOwM_{gAR4JJmEQPE@aH>~vfv9jz={WQZmt6F;@2I#} z8kAet2mW4~X$`Ay!Wvo$=gtsKLHR5_X~Y*}-q)q9`YMLoi1Lq*$SannE34G_4Dz3Z zUbL%hn`3rY8`G~jyTkFHJtIIcx&B0a1)`qWE57Jxp=nQbAs4XeIN-@n+`g25ONm=$ zTZfsxX1n)W0*WG9W|l!GOVz@E0I>|SsplxrJ+;UF@TypqU||pA9|wkfJF`t@C8zxF*QIcJA0}{;oe-rDKO9O8->7aW0gLx9Ge9MchILfq z1nE838>&Ku(lFR`4H|1c=o>&1;nEO0qo_t?N!Lk`?rjw2{Z|t^;;efM?t}StI`U`XjD%lmSQELywCC zYs+vE+!Q8IdOQ%Rq{LFjF;Gb?qf{M_t!KVOZ`pu9*g;X1y1WXV-E}Ws1K!8#FxHu% zUE&#X{o2_IgtNMfez+V~O$Uz1E*xyly~EeVnJ5v%ie2Z2@Kyn628L4+5A`Ziu4}R$ zb+Ti~n`XhGB9Nwv**0JBW;>sDx_vssy)dq$SmtO@5LKZH*@i}Sr@4~JzAqzTl~+KN zA+5?!Sj2*KOG&I*)K7uGjaS)JhgHo0Ln!w6r1*4y3ZmBv373lNt zBH0bI6fYQbUJ@oB+;L4b_2(?iuXsgdIdk_l%QX;ryFe+*+2KVka_7owHo-Mm%$pWA zOsDQFb&a*zb*lM!0^dWB=hafajQpPQ1TDQQr(3CPAx24^ICr{jE8^>B`T*Air}#iC zhGclBmsPUnNz{)``WvRD%d?GlkpIGyEzCEjW|cuuG7K#{*HdS{au}v$S7@tj7TP@yZJzg7CngN5=uLHB6(4uj($wp4j6Hvlp~&A+hz)1)z@UL07EhUflRWYU8Is}M0>k}}9KnfN$)LGbEkYD-p48QiBH4Z6h(C!i=^;(}g9<@qX058=%5Lf#nIUvU%=g9iMXl@3Ftct5%YO9cqn-4{M9H>4=MeD;n*;+`>Jfk7;VaUH zF_XsUbHp)D{P=3qLOnK)p3Y$;qwpSqhC+r`Tzw647-sYKRZ>G){$zaXxgQK3%H6e{ zm2|N_sS$Sq^%v6WNj;!H?5q&d9ZR^5(=H6r=@3yZ+$CahGB;s-*m%wZHk14|Epvnjo-X*rOvG++ z@nAw6CPCkjH{VQ=Bql55GfHplM79OCy0uUmYu0{mL`hxC<-%A*fTP)kPgycY1TknQ z?+Su6GFkp8q_WLbarv5p4gQR2p6=29}ol0Lu<=5@Q>M9~x(3cx^B0JA-10)GnPPEWmjw=0c z&a0`Nn2R47wxX#l-d+r|Rgj^a?C4~ExdjSrK(u|*+GNO8r?w#Uuv7W;!SM$!Y(Zz! z{I@6u%lpPc@n)WuGu{;LB6uKFs3mApyd0zL<453@F)^^@Qli!uOgtu`S_?l3)j^FS z#JO{2vl%!9j2}5rY*dgnI)Dt@qJHX0ET}0Zey!>T1yUM2f4`Tl z(_&bvjnk&?Bi{=%P|d^$I^3BpTpMm5-%#xE`fdI6JM+2-;9y&uyWrR9IT$SZ9Z5i} z?HxW+E+bZey$5SuQSJ~fJ2GKyB4hoD*sy$-DFYdE-st~eKdub9Q3qeh1#@&Sskkt37a#3pyjF5#eB`f&+L@3!>D!rSMb zP}aU3!w-gTNk#1R1!d3(XZomVSGyo5`1S9jS!od{T5nQ|>t11RlQkBnu^8?ui8C|_^x3~qRR+$LG`;B+Vh`_lLltf)r2 zC)LyN&pFTe;ga#8B24#p78_O9au?|5x@+N zrKqR#Jb_GDH4`-C0Gj-cZAr{6`}dAoQaGK^Pv{kz=9nwVoNHa?bo`_wn+E?dKCLkr ztncp4Mz$E))%QX|`_P*{e)%e;?q#kQYHv5}+9R7mS$tzUq`4rKMabL_k2$vsyuK0u zo-hB66gdJ}Y~NJKE(LZvzGf;Tu6dMxFLWA4Cg7^ft`Y+0lIeQd7O}l5OA*n*V`~HZ zrmMfye4yY=+s1TqqD1}AivSv-kut#Gdb{w&C%BcdO_?64Rec@HR%TRLwuV7c=?h1X zeE;NEZtPFDNcvg;6`ggt+XUvUVhP?pqu?5TA*71W=R6jTBnbEsWci@tr9nlCH>;9- zxf>G}PT|hiE0jwV$$mMrVyP)14y7i@+Q%Blrw|H6LnT@TT$I$+rDrHUGh9w|!xhX6 z%xFf*8sP8%URR}{k9PvEq`GIj>*S|jpWsmhNj2wpmDYqJr}PFRE*Md`#W4!JlL;y= z4@GwY(lbXtY9SPy)EK`HqW6&>49cm?MlP|=-_nz(j(oo~*770WPIKz2d+*LL)#`bK zL(S8)X?;IwP{uWZO|t#|x^OR&HAz}-(7p-gv|S5xMDa*s3b$0{wBe%k(r=*U{a(oP zzO`1k5!1DfxnZ@5l^B6No0oWRoj*m>+&n=e0to+&Y&=_XEjpZxb}w=uL?RB38GZeG zH-){S!5;&8h?DjG4Oaoi3sU6YYMfz>IrslA=!_M!V$*d$6o&Yo+7gQG{6+x9@ zyrp(gIc~gX$q$-iJORZ3hmr?ASiE6vH*-5Pe5vy)40>qFvpwrETJk0WO<7a!utole z&he}`Rt)!srb8kmuLz{C)-_^X_p-(nJi1uJJY}pTNSm*`7EBtBlt=y@?a%{&n?Bnt zQH|QPM-4u^t)olDFWWVgg41Ep-A-{@JW@z#kZ3!x}Q!E{?p| zFluGA%w=$X5O*V1svludY}Xo(GuG7p32|hsi_OBp;Tc^b@<&*DQ9Puwy*7bqsFH&5 zXNQVy{n0x+`FUP%oo{;K0D0FsMs>u;U#Us;y;OhA(w; zxM-P?RSwlftaIm6Umdz1G6%uaVN^Go+dNFJY;qwca41PnQu1Tlw%#sH`tWeVi~nc2 zR+@M;-?2J*jMygWrX8C@jEDtUL|0yRY^l*2Xf^6v=z!!YtpMU#Yhl46jX(}f^Gxcf zr#9LA`&fwAu8Q@8rvGcsEnmF-p2ey=sci*4PFJE(3CxvtW;?CjJcDx%y8Ez^WajJ) z^^5b4mO0@^H_XXMAZ-SAio{bQk%#xI*(ZVvwl2bFM6NVNd^sI;=ZhxYoKpnv*oUux zhvbr-uUwmzl#RKWtY&%Y0g_24lzx&q$S(D;v}CfM7W9D*>zh~z>96945*TaitxRP()Vup+jSnS_z|m zN&M_YnT629=BnzH>@egDw0v5}iwMf7p3jQ{vxmrvXI8EvwWUZtLU62&{CPNq6FT~x zyr`Ln{%vTX?U{BdimqC-lXFtZ%PUuO81!mPwGZe;DW$N@{sTe{S=un96r5HYBAy+g zsZTYkda2fADH8+O#$u_4r&GL%`I(;eSXtmE=$b9V2BKG-LOXAVqs6aVElfuQu zqI`|O(hOWlIz+7haT*a>dY*{lGGP&}G0bsfx)WgX*I!^Gf@1nG`n2cMC|KEQwWtZI zJzTKEu!li=m9bJlbIk~+hf!-S!#?;M;a1d1ahMcS+wzz|qraOl?T*FR83rp@3}|4` z;)L1PNIxDWGJ!J|;L#ULD`-0wc173clj0~xfCF}dK9*U(7y^iROeu6z5kAu6~&|`EKg(MV?q*UAmHSGZA&3! zzIKHohYU1wQ-jWBYzRU-`ZLjWSGmDgSsi|>y|o=I8DK-Pk&e~xgFg#T7uoT(HDtY4 ziNl5EN0nraWTqKQthNU5H zIBO20llZ7jNFX9vHzC~btsXL=tq9#v=B^*+$3oefnz zFnTlbm@wHNJ5nSq<+!OR@ zOjp>CwGtC#@0lJ$a>7X_Jq5VLRp`eru*)_PsmN} z)2YZ;0u&;+4*gDukcnY6!mErkqI3q@ooUP}wOodJlg?L*Mry}aIeLX!&FgZn)UTa; z2gu#E&Y?KI5E2A!>0vMZ{YMJ_jvC*r=X984GDCnSX>~u&??q8M)Xvj%ZU>Vz2 zQu^%J+^^Rfq1EQO4K{H-~UdTB$@8&RRy_b#>sa!)FMmxNjFZRGc#9r+Bp*o zzX7%Kkc?sXMe!YQ1Gf1iS_1`+eKHcH|4hd$Lg+p6R(Sf?Yw{Fs61Hv6WT zaTN0Z)kKUEVY`&^GCQ)?nr%xtP8Esm#Gh7I0na2Jw zOZ4^v^H0B6+a6V<-DcHNZMIZoa7dQ%kf6~=^dV=vmP?tZZQlDEhT~{x@qQ0&cl=jo ze&5Sw(xCZtw31}TKX#{88+wk!lv|xU6myInA{wj zFt7JHjY|mR_gFHyUEXWnSEuo_GD2NMn#Y_F`ZrvFaMKP>;wb9kkokp6@K0ynzKhVP z#38LwhA{2`k8RV0DGnMaI)1;~q1S#?%w|LS8n&6O_E+Iej~9Wwy-EUlHb;k-<%vI3 zzyJ|pS;W@VVZTD6kesutzv!8j5ITywr7(^(B+05^0L6R!6qykpp;2bUT71O-bz22E zrJH>rrqTF4YSIE797Pv(0Aw5*sz1|}F$dGr>C86gN8nJ6@JpAdt zs*L4aTxH`46WEKM!KoUWx6}2>@#7-S@OlzZ_0Oc(b}Bixfm8M+O_*qNv0*O)BK{Ao z2P0g+pD7^$r_@l+uPTv99a{g#4M4<;xqc24WZ4UyQkMjBOC3WU#uos#R^HEq;{W@< zao$(B8DHnM8MsW9kvDiJgk`GH0>IjL z7+Tq0b1KPY0>$AVDE@9DWPyeroIQLy?4Sf_s z9db6j?!af7w*+FwALA3~c@lt|L{H{!jwa0JXh2lQ1Nf6X^Sc}NtL5KZg%d_>3y)4r-u3wdV(ojvLqpzF?Jz)%&&fh6sWBxr)xzWb*j zmG3zGG;fPX)`RTG)aeXT$=8N}Yfd`gH@Lb{1Nf-DTGu7ExAIh_6Z^>%d@m57eav@_ zoQo|#Nk@R;+Ne0J(5I6dKNC8Xc>^OHl-O|yT;yUig3c}1g|-M?6)cKE!fvz{V-5Q= z32}yq`s&e-g0Ak}5G0RH?d3nqY+MHr$EFa1)U7dmi`_g(iIcn~ZS*enmW7^U#eB{n zjF-wResJ6Vpfns&nxJc7h1tMxSpS_mSogIIx=WlMc z!!e@`RHuKUO`OXAELwJ6EBOc2PcwOUe%P8QsB?=gA`(4KZUilGtI6v_1OY1LkJiDcFtHuH?qmc zyF}N0C8b?uvXEeL^q?WYybnL}FZ7&vyaT$(C9jbr*Kudmv19W^mwn^IW*Uu-r46j< zTZ?(QJk!w}*ki^9(J=P~kv(c?g@d1k>*h%4Bm^@H zc;;@AH}j4@Z^lowzVwfB!s_T>o!cogqfkTObC#G1k6o2p_QEOyDj(q#g7+3JLL$7aE_Qc5WA5B9 zTdhy=oFDYp+*M>W$F-PSJ!p{*pYELG7Y-BJMzN~Np4aEiGe#&>_Gc|TZoIHNR3~yf zcKhqp-gCh2_4ROUpqb@|*3C)&AJXDt8>+%voi>5>W)AS2bUSfdr2aCQlJ_f1T7iIg zQnNT{KAj|9!y`j;_B4Wh*(Xnx)N+4=%9?gr2?PZIPIe4am9bxm%)pQ~-~K2Ybx%e>Q+lQkU2iWwl0c}%Wd5{!hvA2?vi z0g2!=GJ+InRCzwgU!=5ReY!(wO{WO{o7$}Jo7n41ko?6$ngA0GKlY%%PHeNglWu8_ z2RGHcBMo#xmfo)p)C*!ZQaw0qFvgXXrVUFRO~^A1&G2bJ zcc)Y{w`-2CN}3>6mghLuN6nFOD!r@wO|5#|F+25O9Et;hp&F6eS}GAhfTN#@mk7yk|boTE9$_M-Md;5Bo-0N-}g z*AZ`u&@%o%n`D?b7Ei3K>RANxWeQ@mD4BFuEfAz%hrsO?J%7?u_ccIB`l&kMWMYlE zUxmc_w?P$20GkMFT2ZzBqI8#6%(Sc@$R1P%73#}CW8c#LyeT%XfYkm?52IOBMwQ98qHl;db@3NxY#ew8ONilQ`C$Li9D$97*)W8=A$beH}a z*sL$A9r>MqB8No7T*~uch)nYjWRGP>mZOy4 z+H`};ls&P^vEzK1qvrsOBE*O%Q`hnN(2X(TLGQj>zFjEaGjl`-gX$F})pb77R*v>@ zwp||cVNM}^YfYR4ucMAN;d45>`w*P|Z2RRpu1p5mnQsaSPj#2z68u;=Sa(B}Yt-!3 zt@4$|@)<_-%6ms@hCu_Ca6Xh7J8aB}aXHvXvmtEE#wOq63rt$fZe$w%$)Xy2qvIhe zK+78K;52lUc_p1FH1f^lCzmKf^lxVhHCB;d*A)e=VNlaN0;44uR<{q4!EA4k^>{v0 zU*BkE`pK#Y^Gk3@sI$y(S{X6duT~R>KMr8Vj6ee@#bZnO&bhEFFWt}Di`QH^^?lgB z3Ml=~)9@^X;zeN0-ZbL&m-USJ`^OHc=J(N{FRFl!h;&^HZoWd>8`e{zXB;ZQD(Juo zK}EBVikkDo1Ga*ai!1GAoOOAjc-S{GKd@Lk%_Dx&+CnoHsVSBfJDLo3F*b8g8}?2n z8-8>(0W_D193Z@(%(0-VO2FAw=t!RP%y*lF*`E0+Ui!>=U;ds_V#%bB!AioiU1Sc` zLe!lRzC%1LPJLL9wiNotkxs#5aF7C_X?e!zeEH!(RB-Di5>g$o*ORj{$VW@v*NrDD zQ_Y>|Q;Sqij=gS@U4O4`bc-S*~9^rC@7%FzPXjcy~hP ziRa1O9_g|wUq0N{1XnJ^uBRcxd1tn#u8NiPngEa)%w2Ta3BbkaiZi0G1~QO$PfS9J z^x<{Z^pie0^|@kWg%JU|()Y)adZ3QL-rIt8*qI65a%D-==V}BzAi=lcH_in+<%-BR z6Q%soy;98f86WPmYgh1z@Ax=|i6*46%-;NoHcJn^?S@x)}RT z3SN!PGu*5vrKVm2Hz~4h{?n20Mi2(+`FEQEhCl$w)6w`>$N0#L0+!clsMAUh3V#%u zyjZ}_+8P?=UO<$Xs0B>9=2Ak?kp_-T62A2zd0^`SOBmR*lEBd<@pCUUhl?(blJ*FS_yNb;g29!zHx7xat1J*$m*YatQO%)RF9(_r zB7as9Ne2iX#G9Z?Kmq@jF&iEmv&Onz9VFGI&d#&Z427S8iC??FY8E1*a3^!$_sp5r z=akiA4v_=Pi!`qV(L(mr3`I5>u2`zgM%BvMI1VSgnx2x)guwu&#N~Vb<^+mnmjJoZ z9!EJ(U}l9Oe47hAk^4`GlI(q}l5mG-O0zA=;SY+<F`LU<`o^{>{=IZfyXwEYq1_FcvlqNj_A#(9Z zi{s&zGqe~HZWH1$gBWI#_o9B-?dC9HMik#XQGTTK%xR(&9W6J*6UUBCb=P`i6eArs z>SZ3048kZ+l^D}GxT9)ZWWMAMSR^sPVNwWh5&smMI#BXuvrSYIKiOqbSIvqI4Jr?x z8i0vINUd-%Z|jx4eyYQ3px+UIY`~I9BU8BB<}PHUvM=+>`fSHej^%xbxUUhpx*_Km*Z7Py;=g-M7Tl^gH*M^XQi)U zPj!NR78A-1dO{{8nM7VfDevxP;d3wC06shL#)e)uhAmdxdKv`blS!?M+5r_S9+JoNH&3)TmWB za)DW9Nl`}f+|C+otL`Pcya`Gv+#&AJw5s}29@DOd)P^dw_*2;9`(^oSt5y7)j5`rw7|U|+@Kimj6>&ZXb?V04DtHT#VKC>u4Bgx$ zqBU*^xyk%qq%Yvk88d3Te_ zp#r=Xr3zf(vLWtsmU+vIqOPthFgAU#XZ{UTOi+y^X(-EveKkNG?Zkeo-Mszv#6TEa1td2@v#<=Q+jwdqA_-X_kF`K-%R8&o9*s{5~ zu040$m;^Nq1DpBUK20Vp5iPzk5(27<_m--=>yC|h7x^|!cM#ttW5`d%mo98K!KVR& znwB&YVuAm3tO1C3y8YZ}8}_b3Y` z#I-`LR-tx!96ZC7kMbPPIA-B5ESOw(gMx0T9W2aIpk& z`GLdIqmI9S)q_j(Xe`7X;ZeG_eDU9&vTxJQz%u8=lDMXwit(!pN~P9`|I;*aJmI-J zVi`u}a&9J?YVauMv2k|~2!coyDI%QVU3O#u>jELHS~dn|_7DB1>Viyf@NkMz%|m^A>GH0 z$+jwhGk^23I+x_C8zdY+(m!UbP>m~kiS49W z-^~3oJ~;;8mq*%WPMA9Srhq3ur9dUkpYOyZIEPlu4_A(!xs~V$p3D_FD23526YJ#X6(Cc zBMGDRO_<`dEU13=f0rv%!9FlqWDaDw2L*cu~KS5my zkC%~l)m{~MF|c+>RM1qmAV@3UULWm+p7KEL=@yAKVF_`yUkAty#}QD>TH(n{JC5A8 z=5Y!zaHU6&Bw{TeFbVZS8CnMrMIaGf>Hw0n4i4VEyXj!6?t>HIERsW|$x3UPCpgeM z`b)pJWtsburPcBNDt(@fvbZ(}Y}Te%5Gi7P_m{Le1?M=l*Bq1ao`b1a zQgkK1_$aU{IxDNT|CbPNN zYY%}M1M$^KUaqPHLo?s&PJW?^l!efp@$$V?9_D`0P42++4YRtzYX;BOtzl{2$3Kt> z`yl!v597+9V3^r`>3Cc!D_1tD2Y#B*8}ygidU_p!XTrl&hsA>a%Wb|8&3xCS+vX%n z7GsXIC8!n5`=y_4uszaQ)I@x`!q6}F(o%Ygn+>dX_wm0lMbT8QPnI3Ehrdbt@T z%<%U=m{QyQEWU?F+~~aAL}V?)>M2HgOtRx|%ddHq^I0yvn`ZVsd&@`go|nH7oCSRI z60Csj$h(0LOD|%9<;S*TBiS;8wl6%JT;cW9s=5N25JCN>z5A!~8PF6>?@?1cPPjy0 zi}r6CJvk9iPhbwRE^VUkCx~GF7#}e>6Ox4!XQ*kPv0v16AC+AMIYZ&C1cMKERV?xj z4;~HasbaUWh}9<8V35fHhkh+XM3gP}3HLhi zGV%k%^m{GfksM`TmnPi+RliFW9EO2i;%{q)lsqD74sd;!yWc!SnBv*t098S{Jt||6 z3C0}J3NH>n&Lu0)P`465>kxRe`{MNena1d0%mCf3(i_}ZZ8JR7KeDUn*U#xr=&(Dr zD7vA-Tbd}2`*SC@BbgE44F^aGeKdt~c+X@8P!7Lz7)fl4i`T>r(zV=Lm|!w&z#>6z zKXh|X-Ur`3jfehYjmGo1&a8a0&vo~XecgGZQ-L_&$5K;@=VvqKkNg8U)#8+|xI+0< zUnmTGsy$E;Wttb+bl(?*z5C(wG0l zoD)5oB;HN$)n(6M6VlQZuAI3@t?$;pi`gfN4257?UOSf z8RG*Y>tu1b;1~Q!vT}C0JAD3L|4#-4&gDwI%~{p%qkS2sR>?8r5pniCQKTdu4>r>< zDAvXRCrb8YuXZj=QEbpY|3(cAWVj(WK%)JW<%1_Sjh|$9k^0HeP5tW%c9)2R;Tj1) z>l{v?$31EWZM8mDNe66nAZ=~;_c>8mYSBmFhaXMJh%Tyu`eZ@8Q-3Xz(8AYWOY0+| zV3W~DHgXq@H2G3XfGW~TDTP=0%U0$U)W7>EPgemcU>J2>y-6seaz z)Z|aDBJvNA-|EW^3gagXv!Whz2YiDUOab5xf5~%_l;7}qLw+&*hB>}|jw7omV{o?% z{+R5ZL_S0Zp07Jln4=cQ@YnF1Bw9gt+&EQO)`U*8TAJzjmXIHo*II! zAaSsuBZ{FrWq>>vj_xx^SCjV&*BhLd9JjNiU+JFU9U(WhJ+7jrsJpU({~+;oBk4!U zhQ`=mB{@IYgNb-p<2I6~nyG(uW;y*lpRY-l)U0n@FpnKZCxE=UXBBk!f_I>=%sd3~ z)pKjIr__Zpa4p@1KxPg}vSSJ2l%q!5?G@t($_R_9m@*@gCO=avoCI4K1VxaOb&%w4 zM7qa}<8`q}gtfJKi`+2M1n640r1QHQXj0E-blYTIsF`wR)*v`pSZ*y4QrDVx-E6D8 zwy8-Ex#Qgbo+s1!#y_<+RnL~E`Wl&x!UJZvKcHZIc-he)IVP=birYwx$o&Y*Z$PrY zqg6^P@TLNgDlUJr?3CL+`oR2xtiMOf!5@;Nh~2(Q`%71gZ35Xp;>g-#=G3Qh#|NI2 zs6#`=3H60lH>pzt!@J+Nb1+odq5NKc`9}E*`AzqjvUQ5bQ4Uw2UhT?ZtP$lXdOm17 zBlEUoHAzx~&W}h>ASIqo5J!j9hN9)*UgadhTX7VWyv1Im9>Bt~Ite{vQW!%?fE_@e zaX_|PM9hMXNR?Iwsl@^|g5)mC=6Rc)fOd_83-1OXKisr50MK%!!!M;(3p3A}*`we) z-Szh|aR$h;`i;WULEa*bCcHk6(|5E3tsU)+9`N7KdJ^uB-kda_mdp%pjyn{SO~W5* z4NP14I;FzmM?_V%BQ25OeS*5{o@v;hf9#Fo?n|(7jg(FDJx9tSDXx@+$=4pdTUNlU znQ~1!eT4HV>ghJi-0ZrsiaeN5=8OwZVyx4O46m?v=;V7Z3;3khL)^#I7T1bT`|c5# z&N8@i=VLgYLKz!xe4%L6`mGJpD{U2{ggfCsX@VP)P>xo}^?NLAPXoXGsqvGFlRVa*gF94Xi5wXw@{w zWH@-Q+N!Da2W_^lDEHX6=ltj`^T&0md)xHb9_331T9KeHTn^*R?J)ktgGJ9bq%VPZrN86oay8WdFwr=tQDcfC7CZdtbE@|U@T}jUG4~qX@_;8wY zlJRgZ@FG);lj>XUEavK6CL<%r1z9 zqo>L5v}(*;m-S^nDfWd{5jaH?E|%r|4)BN;`sd+8DIHS7X?!^J2Ig)_8RJUN zh`iqzHWbjU)MM(KBK9+Mi5dfGXxLP6$||$O4lA7snAl%hSE+IeVTq4T!^Y4q@sG^a zEb3BfwfNsawhEou3bMV`KR8T5q4I#&oPQ`n#-#?yknO zmcrh&5pZ?^xvk~u(>UkDs|Vm>a99lvf6`R!=-Alk4%+ zdolp>pBH?ps^6JWXS(Wwdx4Ttt*lUE$a&+Wl^;516XPU?LMVrqY*3rbRqSs?gNd5%P^W`7<=@#Va4 z%`Hs6MU5_Jtq;ZOYH@-dWU{W@vCzoel1BP_#N4_474>PVmUq$-8Y!*jpb^5vDhf_k z6c~C92F?OdL+BR=xmGIyS^`Q^erezR0O*a7%~Y+8pqAU(g4Jlv!~1Tdjdg~!;jUS0 z>78ECC++K*CX>XuORoY*-K5pL$qyTdOjq@2L|%%(z<2JfuJFt4?D}v`#Lwr*GO^Uo zjgjAbAphvMMLYjLKI_}$PCyjV1R)jO?6M9|Pz)xY(t(3*VXs(3E8L*-P`h&TCa zuF);nce9ZyMN&y1k%e?}@Mh$=BQL~sp>Ht^Q%@P>chRD73yJM|j5i9ru;`9V(Vjb> zuZ9}v0w`#$+s0f0{=gp$=pz9#@LShLK%Tp%T*H}z;^2>tf6tQ*!H6Vvj+N(Iu?ib4 zrN{|+b)6r!I$nO@fC~FjMDlZVrnR%<7;ok~qxOl%d)n>c(VGJ~=k!6^o-h527rO;c z>*-J|-w-{Avowzncm*iVz~vWJE_4Pj_$l|fLKo_YE=Tl@(WV01)0bOVyrE}3fjzrd zYVzArk%ZnH9no1!@1sP;$8y`ljZ{pjkkICbsdN#2)lm@iy(LU6ZEJOEinO(6ynC2zZYcsI> z%&Vt}*aH@l9|lRjNo=l1K!Hek(&tkyw1T^zm86=ek^nq#2trC$%NYPso01TcO>2YI zV*yg8DmA=J|Hl+yx7}GwUZBQR=2dM$Z#0?l#DWZ;X{RWk$@N%<8XuWNT&BFw{Q~^C zEXj`JSj0*CQ0j!Nh89na$@NtFD+wUmz}#~NEp?Hc zC|GbYc_*#!0lZK@je5f}f>BycA|Qazvs_*9q~h>lYXBP&T9_pzsP%Q6tC(-hXoNio9s_*Pa)M;eX8BSmWf$Mv*`;KV9$8|6*?&qf@3gq6UA}*7Y;?X7k zBkU%C7;>0U71QkjrWz@)!mR^z<6c&_HzRFr8+$&Z*&8&;y=;xP&7g|ZI(4SgsXbaH zD*D4-hYiyMI*z?&4FqP7YR_k_?u3Y3CFqp%Ir1(B_GW|2JzBr}G_dKit=3PomT2G9 zFTLldGLYKf+4R$0H%->;fnz0nejb9lFW%Gfn9H<@htL(7EvnW2hM`Uz%^_X4) zQ7GOgqk{SwvJkKoy&YA=gCZ-Ek0rx5~$fh>_=f? z=~rurC8C@^OoMQnV6l5FdrcAB886?bi4@06<3E7s(4ILHLyxNCc;K(y^Ktr_G`*)B zexP^x35Wj>1H{76#+r9a7d+h)@lOl*MI|mIKS0hf?m{xXu@VTj*&Sodp2QNgeEm+P z95b5>J$4v5IpcH9ay5AjBVb}iHp>;==vJ)+*Zo+sljfW1wg<^pVO*@mMc^9}ei#xX z9J4moUA5JV#QV_P)I-5FG^nGQHl5@C-*{I>`s#8zYVO`rU5NK9n#SZ7FdaisMK?<> zoM;&-*Jro_P)%a7JSPeC1n|ZRZS>UrCn~W)khb zuzzW^N6{^(!~5{~FiLZO`E806IjK#R^PKPuCKhOU9^Y6Y0|U|WB0n!D6%=h9>Ev3j zT7N<>Cnv)~>RniOZJuxh>)O+WL-6)+C-GEVzPoTt<8#)rgq#e^_uO?Ud}T~r#P|0+ z0}YC;+nhSurj);>9KN_0QWfN6rRsoqs}w5wnAqqgD4YdSvNGK_|Fu@J8i``@HPP6tBwfA7_jOU;NVftk**Wi~}&%_wyFR z5$HKR@38+IUvlg9*7k2e^ENYYM?Y*;ZDa^w)*GZlMG|A>{?M(b6;H69`_@Ub&Q~-q zoke8x8A?d!Lq)Aa5iyH0XHp<9(n#xq}Eu^#5u)hU`&dUE}E5 zu;oeg?R-azU9OwDqPyjtma+BVv&59JF&xIiBbL_g#!x__(wHwibK+Ip#;VBE@QG==IVyI|NdUJTyhxICeoV7Fvd)8 z3oLP}PdiL#6`h-^ZlQ=Sfwpo6$Qe7G7BS>E$>D%uNw!;162mCVMx4pZxi^}oA^pyj2b8M=U z{vA%f%rpoL<=^vcJ=$l+<38-lz$jrf7fymL+F~1o{Cbn)icd4y6Qrx)t{zvA<&1J30UqhKZNj}~w$q!}#c+nNC*@zkIP z%X?j(pNU>*C5r|VCHrJg=fF|W83K-8E55G;m<3go0@{QL*H36jXX(t>5mf5;OLu1uo)l?| zJ5shTwbhfqk3kzHsYL?p{1SA3MVmGRj8rNN7v%cZEx}+DBCApV8Yu34Rn@pW=kRfm zIH1i%!(WV!kfe13(~4JV|0y_ZkV@CeYY$!Y!`zC8O$m`D)8W+(sa<#K3-LBE$0Crc zXrN4Tl1m8Sz!rpSezNNvsNt91OvcD{FOt?-VLb%$^u}Q~v0E0S9Sn7R3P8SdYp0bs z$on85k2pi#(HDGW;q)qlrHaFXlG*|zF=lCOc!2zV-z{t1o=G{m1EfP4t+&D>{DjXh zkq=&Lf7{AtV*+`|rRofW))+|NmT8gWN;OZm9q^|*l;f~(*THj-qtdl|-blvL*-rd3 zQ+YD!Z#-)X|ApBQ3%=sy!c)rZ0-w*fh@xkM9^J-*RwgcMh!wr6-ij#~1RXxPJ`MO+ zzSmC!vdKn(mvMSmZH4}k0dr@J2lxY~dIOdEWaHaL)a1Wt3b=b%f>gfPy0EMZ;PV6r z8{J(4YO+`^Lh&^qm;AP^ja^Eo9z~B-zrWRPY984lA!kf+xbh*?cJ;bmF$%b6Fd!-XB=$_HKj{p{8wt9=jgAc@?ZrTBY)rPx^ z)8#-Yhv#ym&yn9?@mczuU^c!p3rzf9H}mfb3+J50rDQvIU%~_m_+5Q?Pkj1*H!=Hz^oCU>d+M6?prh@;C}cFUb~)^x*+MYi$5 zfeMDd)+?meRx_}VHP3(l=n^dE`yV2jeqvyxHj*DFJ1GB;u6N2D<~b65#C#jtFsJ1ccF^g~XYFP-S%G9!Z6)sNRO^wwVF2^Q# zfhn^wPKu9G8B*G$P$=Ppw_^qf49})6cINfm>%s^o+&MGUIwVx1GJo)8yqU%9y>M)1 zxpu1rc$@444y&NPqjx-`v!6Hu^8N?9=#mhAHsJTBzbU2l%I2CZ1G7O&d_FZBY2*`x zuohdxQzrGoM`ij`+#W}|E8zDgci}lf$L65MC zUfnCyaWPsg;V2YSjwBnXi(p0ET@|r4?u^yp-4rPXCkYmmuRZiM2<+qnVv(1&05L$$ zzf>fU85|UCd=sb7t+aypB_}2c3io#hFg88-v7jYB&Mf$)dcs6HvuesA>0q#PwzACD|Lh|e(4d zmIecqgGqe*l5PzsgwJylz5_wf*0Y$@z&%qX=>{jxVvc#CQlG&&#{CeEIeWDfQ>)o-juG-o|%m6`D}<& zXZtwI*VmfTRq&O&<1CkFb!+ge5sgmWq21{^i2I=K)IiI#9N5A|Z{Wg4-t|qOC}Oxi z1&9yjw}k?s;pweXK=$YcK)~Ljy&y;?si6LVC}f;az6z4Vu zAY(gql(ubM9{mmamud-~Y|s;J${y@oY?PzUmK~9w)mj9;Yalgs_fl2EUdLxoUovM#3jO>xN{S*yef$@(H=@ADirJ&M{e`V-L8fU$!(d8HK_!NP>h-f-2 zsPujK-YRZKjXtoAK_bMS`(w$)kRU^{Y-x$@cAVh)>($hWm4fl-ValQFXrVJgHu3t- zu4Rz9UIS;NEP374P>*s0F6CV*sD5M1FzQGsI|<>EKS~gPL7qHBjAH$Rl$eC1v=4CB zRZWF|^EKOPf7B=WeTV>Bg_OrhVi{*J-aVvJLOrCIeYFx z`ZYw{0(28QLFO_5dNvf#D^SrXE|FCs|9gIG*?t=h@j2Hj6ChvH>A4Qikq3xtQ0D)p zDv1Q_8`FqIQAutj2L^jW zc^KGEj*+pcpoP>%a_76}1AD%+B`2ZIkbNKH`U>6?+9eaJTFd=d1ORXTPa7(9Lj=tVlV$60jd~(8yY(P$}2p3v^hN81^C!c z=i01dlt6a%R?L7ghi4FjOgtE0?Aqy1bhWH?PN}b#f|1nG`H(0nF?N;6vB-Zycx3s+R%UI6z+&OP-ICG!^ z6!v6(y)r^A&3;w9hhmE0XXS0nDAq2HL>TeX;@IRFk_taVoMkOXZp{dWl3|$9lzMj( z1MJ~i;x=Ydw52-}3KrkAifj#=ls1ci?4Y+A*QBxWm4JaB8MpqA(v9$?3b{N7f4_ztybYI3p_P^_X1xMc3NQ%HEl-;*FA~objUI+<2?2^8BP8*!ju- z+#`IEcVl8~~a#;~_Ldx3PU)!?LKySUdrOnd=m+B&coYBEcy9W3)*%cOI zv5ZKvx9vliEcAK|Ly5LNV|+qzxA?Gb4OCM(f(EPO;wUqn2iOQ5kfJz{L+t3~>FmQKfxY=n#rv`_XGW3HAY=0=FLNE_ zsoCrDEu{30=N(kkU42b_9M{tKZ2M<#>ukkYT`ACMhE~}9!AJIZt<~u-tjsT&_!(5| zi0p~xqkFjQGx+a{aB@Na?v@D=UwVPfvTB1^B}rvIuH`Z|$fy7$8?Ckq3s(CuyXWF) zp^{yusi%rO_*$A;OeP>mSn#->t?+nz#y|mS2xsl8PBtOCs1Gl3-0iWZ#~UWzVu;o@ z2NsQy!^0WML7U-R6J-D#WG{$jfv@DWO(sM_;7hJtQr&8)u$ZFFNOX+CDan95MiSMPID2Q#BXrE1LZpQ(zsE zX|f}H9(OTUq9D6at`gw|6^J5Vob=YRfNMx5aw(0rv{CD+EIo}l&yIUn2V;KP@gmk` z^^M?4ig`3l$t?1Zojylb+D-}l^LIGzksGo(tQc#voY)n4MiYKB?1w&fIH=P#tlN~@?x4;=T z?B`8bUNEKxVG1HFio3w4+S|D3$Gy;4BqP#tD{~yAE*j}_OhF1=rKGnf_aY?{0GlC) zjUt2%D`)nZ7X^t<3zH0?c>J{}Y03_JFV6I*%8)AU)xPE4C7NgVg0qy>5{d=^ayu}T z$}TQqtD^M@wvJ|*RxT*UyrITJ76nw?KEW-A;Rt;d@O3=i?Z-jM+r^chIJ{OH( zTmyalNug4iHILnkUzLkoP7#_pW<~->LG>@F;7RRLA=SZW;#M6jlxY56oLYNZR%iy- z%tXqHzSOH%ehk0Q-P0G3`T^_-o_Ne$rRO&iJt_Q%r}g=<-rfh`1IL+@Dj^#K21$NZ zjiJGN+wM6+S%rwU5maHe&pV)7@%o8%0OX!Tu#WbatWmu*s|?#ujeTXjLgH{B-aCi^ z$->4gjHCU1vx)jbdF9rLGQvm?*cYccKd>YP%Fg>|0JiGoJ@M9Tr3ElXpDV?H2j=6t zn&2D2$VOuadT#nJ6`QnpH(t>ETvZLNai4{#(?)mx=0vxz1@R3jFJpY2fV+0lWd!{h zC?a>`tR+-xAq1;4(yP(#mXP0B{h}45q)`{lQD0EYOYK}_c*9ZuE@t3=Q@PEA@O_$= z%v?>=pM?7P)(T#58SSTC(i$a}oR2gf=oJ0~9e)}*jTGC7LN-iMd!+6_2`_OljDara znMG;XXHf!hx~9)euvbBvo2QQ=4vY^W z>f^VPtZ_gO;R87;uZ7BsIr`A&r2KhzCrt@m37nsb02^>>O9;7~LGrn04=JwSp!J#* z0_#LE3TMegu!his=QRr7N6nC<7}Ti-c4=ka^2%5V>P=lBKqW5p2*clghUH_|;fLk$(s*N+d#g?nW#DZ% zT;kR|w`YX#72S71GfWimkk>c%LhCx^syqoXGy(0-OEmD&uC1y1Z@sBk+ivqTC9a8A1U+6){=000EJY$-g?bZJ zlJAxw>K;r6>qM`xF@@q+7zhO3tvqMoUl>SlM4`HYbcy)QEnKR|X3%txS_?^8^|J3+ zWK7w8D%#)tD{#jEYaLTM?A`2Gb*a|AgI%SEI61idJecr?Vpo5w;vS*-|VU{gPKOIg+j zDW6SzGv5pGLv-SYK7${p9bt)nqf9dzvEnQ(u1iKlP7#nkk1+E>^-pQ&B$2!+7a8HC z{~b_7Q#2eNs)T1I;09+%S&&(}nw~j#KfWb*l%c+_nfLVbJw*pXtv(J- zB{`(R*u|0q&k4ZYUDat(xGwOaYDR%Pv4l@X+=|vva=$q3tp>4g`hD5aR%AX_($`O9Iske)__6W5Dhh1LOayz*Zu z_)+4191iA)NBaRjJw3MNB|Mk5D6ig^`qIr;evdOH5RgNmWKxFQI9D;)wG|FF-67N{ zW;>h8F)68!5BXI*zc zt?-e%T|@1f7Ms; zYCt2&nUgI)F8{K{HCuifbApAqujsub^2KnxT3{rZ2SuDu9{$eBuhmyaKI^KH{nE{f7DA*a4!n#f^C3> zOL!h12eWpD1+JQ8N*hOC6tC~$YrJWTxf9li=l1fE<)-YJZ;~y`ecNlY1lD%rWe5B^fq^Wjn4#F8>4qv0y$-X0`85b9;=)F?4DV0^TegdvpcgXC+346 zzvzp?Z(T{vNSDFEK(&c-4DP*SPmK7_U`qXs z7&n;i#&~AiV4C6D(~sPUk_!y{)ebz57eb!#Hi#fyld5eyZ70@IkF}CR;;9jIa=9H6 zbgDZ5zRa}@Wmts8EQU_UGi<2kXa%%hqDu<%S8F}-F!e43Q?sUG$}9Qp;_hj^L&82f zmBQ>=?0rX9nlBAlQD)y=w&K6%=Fd|=-8mt?`r?%ZIL#mq$77tgpK?QuWG zJ3~n^guT?YWO}vhEk(g+n|ybOUFT>)TMy@aY9ll6xgUOjJy1uv!ja=zKF1RY^4&y* zhrAEtzFMh_2yIa@9)>i7yeF~lWZ>prvd~M!=rjAz)X8{ih(7%255sT3a%%ln?0?!~ z9($WiVRLj@%BuGUJ-8X#oj?ytdy`uR)j^Ym1k`)ws{oacEuBLV$`MrORNYkofPN#MEUeTO4me9Pn}Nk!nSgoAgdL73#1o-0A!$ zL#}BYRm1=7uX>n`0j zljnDgl-UTT)N;U35Jy*Xx{bKheABPC%vR%o8z@or3qOQ-jMQKF?vDiM>5!w0+H?gU zA@o2^LJ)%N91m|mk~fsM#-<-YC0dV28B9;m4Ij1KhK4m8}-2|KU|@93Zl1&i5_ zKVu!I^e5M8T6|x8On(k(p07e@re26ls$femVG7oOrKaBml#t>PC~p6m#Nb+d;~}w3cV(-psI#L!Xxz(vXa^k)uxPe-H!) zxrf3*K9e-40DK#zx4luk-{i6EcixR|eCeM6Eg1J50!4J?7D(95u$RiQ2yt$uZE6nZ zRXQw2>SKg0v|Pj{PGeVMi9RNEdkmufrERyeLZG?GE1oo3cuxurN3R)=3^q;`^Q|f* z*br#_!5@zmrqcI=u(V!}LD+j$WUHr&%hDInk*tk+m({dLF*Fc;rJ=^=UF(H+M9H;j z3@tgrM&-u#s#>9to{0(6$zI(Vn!fJNlm@`lG=s${LO$o+DX&dFr$SOs8-diK)pj>Qf^9|KKGd^}B7`~=3+<43eJw!QI%Et6M z?HO`^t5<1!VYr$+kneETTGJ1UW?;WI3}uHauGJ+V1(11m;^8(-06}bWHP326C6J%# z>&f{QIJh)Hi6FB&h5v^B|B1z&a-p~WPFXX6QTw!f5!eXGGU%ca_s70A8F7E6VpdAC zZ;w1>{nV3VA#YWITh64N;*g8*-7GJ@D+#^1%_uAYDE?`t_}s1!NbEL|H6?|{WSvFJ z>-nTBU7xBZiyQBh*++|c9T-5k{09cuhf{>qIFd2#`~b~mI2#50J5~OMiEL?8-g6m} zr_kD!`C8u)b5N3zRuM)?b%h1D3z~R^beuMz_1eghR8#CBEWtAmT+ zO*S{xKJq=e3z3eV{MFq|A`)-FG0?|4K=9eWLsC;j84(bsaSjz6Ux8bHnUuHsqRI+D z6$4|`^y(iS^Y=z0DzW|x`DwZ6Yu8{x?KQ-J_yT~q$x}5&{LP*qf@DZtFR{jayvlzQ zbmE6AIz(NRKQe!cX!k;eK9hcrbB*D_($M#~9h_SWy?-P6#gT#H3=eo*3|5|9%x0wg zKNV4kfT>54`tFmS-r+4%4}R1y3Q{Vf6HCZO^lpm7W=YqLsK6Vum?F_S-smHNn#|hW^EfdvJCD^ zWXvxfbpO^}_*04L5&R4v>JjFyrXuS1W0fPA2Vo_3tZ%Xdd(t4?) zAAkS=Di8sv0)QO|fBdO%WM_YIolhuA0t9}W_~l`++fQ+CbN9-wTHrUx2c^tZ=`@~C zC!MkF2;d~9g!xQ%0y||-Tr))Z0-dn*ZpAB5R)nqvwn$xpWG$`@AG%&GQm94I1 z`tQ!T**kwYNIxLysloC9D9`#ZU%gBWHPCREnA(;S=}wyjd{Jh|-tdp_SYoMJ17ikZ zdqv81i+KASgs!&oXw3d+NF}sJ(!0b@iWmS6(Wji_nBklP1J~i`Ux?F~ELv5uUlC1U zVfB|p_Z+;-oCM~$KJ&BT&!h|El3EPlaq`grm;AlGgrKlq{|ns0k@Q9Mt3g+nROt!` zqwApcK4Bc{!y!p${;_Zl^~%(yln}AWPpW?UyVFb2_Jo=fFw7N#WJK;Yy?>Pp21YdY zFtZ*J@U@?AeP>mcEn{#gZDNa1YwygTOyUyUky1}|!rPOa$gV@}>LO1Rk;)h2ziIOF zjk7A3Zf%}iDd?g8Qcbw9s3jwxp3Bfvry?NEEqCsIclRMo{Iu6WU_L)Oe(YB-l)c@G zm@uGcVOaf#qeI&3a&Jj=o`!4qpDdTP>z8=!$(1cRU`ZZ-j5@H?i;6-9i8M3~$+=m* ztO%jiii%P^u^RQ}5x`l^t$qLXzYlxgiH6;W$v^Z2S~f?f_m4DgR0**Nwp((6YyIVd zRM|+PXaNTOKHd<$L$6-G3-!6mzPK+HjV=Xn?-i^Grwi9Pr%i`@u)5uBBzQNzW7)pR zm-~^@@ks;&UKKVt%7v!nyuGum7)(bQ4<143D8W7}3bE4weQMh=c5-%$*}}i=DCZ** z!F?Z(=D`2kR`UjBZpiTY1N8_CEKkU-L}-7Jf9EzE@IObwt*?cGs#n|q*aMic>bfrm z@={J?;Pc?f6cvlXH$>$xsj(D-dforBPCe0r>&$0VM_pgW*o?REp4ONA?ono4;KW5* zQ55;ko4zNpm`{5!L! z3H|=Q^|p*FX`%x}(Bw1!kk^zrmI4`z`P4ALvT;hq{fz0_6XnGO`S!k%5;>+VkD5Iy zMVi`95I$0eKp!*EICInlzHWb=MpF>9(J6tj2~Vg?S%!SV$lF*VmKVvWPs`v3G|jyG z@(w=G%;EGP7S(;;MTTUKk13uFsKs`X+SN56Kg-8aNM%6rRNg?Dq=T%mue@r;`pv;ShMKUaR=&ZMZ*Z(X5}mk)xz&CtCjv8VWGD~oDUJ9t_JZ$5BmrGf0hH0Wq6<4* zzLNk&y8V|ai!%>jfB$b-g2!cJm92;9zZ&XPiHM>%5}er8gclVw?s=)wrF@|X5L_|K zs(-LADP0i}Q;VKrxIeKo+Fr{(`aanuYRP8{iVVE-<%B`8=iokF&PLEfy|;`&CTy-9 zfC4~`312IRM1ed{?7MCNQYyWXu;?oz2Wz%D>coCxOEH2a3CzMj!4k0bP^_yU;t+v{&BPcjytQFfh-hI$7d4ce2G1T5m z+;M&NmkeJSV|^asTY&Upo5#&*OE;^{vj$^PwvLr3Z!d@07!xOvmCyHt1{55gjioea_I zK7+R&YG>tEU`+z*%?tlH=A&UN$BWWs z*%_J$FrSVn5w+lXz1?pcxM#9pZ3-V?vF9WjZ3vu73o|2SFIPk_fh24HhP2826y3PU zTheKwN85e~6Fj5FxC2jY5AnDCp9xU#P_^vBx7atb4&(3i;X7OayB${Gf%|&vn06q8 zkm)croN~aM`;gjzb{V#+Cu5-$TB~HRi9q2PWgiw2foqq#@B}~D-WREloDslbE6;JJ zp>j0H*a^#sH3my_caX31*|D{(?L!n9^?K!viXhKOZS=sD6assxMk?ZwO&j2*VPvsG zrN+kwYT*5^TOJJerdd<&r}dx1tXDX^>s%!@(6T7voK|j8QELCmv}4gNocKJ4uZ2NI zt#g?J5a(n8sZp&y>CZQ2b?0#gljF|th`0R@ZpogSu!*h_s##F6b#L6{z68TOv^7b} z$3c8=6OL;!7e6Lew;shfKl4#newRXYl-7`gSJWHp=REI{58|)gXZ_9!XNh&O9eK4M ziA)S=Nu+o-X}U&#SQQ6iu%&O1I)TkcK`2qYT$!1VOR@R>?TM8$;L$D&`vAhjF3b2? z`-$t`K~l$i#Sw-1iNJu`6a#T)-!HYxFojlAujayTX@b~_S*$~DHIqT4G3tQC4N0$h z??pH~KzoRXJja*Et@uWdE*jE@i<@q_R2)WJmC0~%y_oR30=BegY&f-~=&)KUhE6h35L3^Q?iy;>Zs%w`PF$5DBLW zzo;U7_GND_{EJ*1d2!B4kNEXA`GdKjrG$iGEY2aPxfh!b>fMwAgl-+;T~D8F8=g9Q zvEx}9-96m$u$8%tS?UevNA!M9c7>Jt`k%%%-1(o;x&mR(XWg9K;+28qttDM$EZdlIVPPSULC8(W!)BgH`l>RL?{FWZWe zscpzoVqR!z2{mw4j3dQnAwKrZs#y8o9OtuIWZ(8FwMzP8zHZ9Nso zGJ%UGqkn8P$lT#T3>i~_Dc~|jDvmxeRI?CroY^}APuSwpB9QZ8h_?hlU|K_q^WXC$ zgM>Nqo6SJ=1d)D{ZnBJbh|>V^A)R!_zZBi)7o1VI`4rXy2b`BMtU1!wk|B+N7#2R; z{p-H(-37lvflhHYHUv0%>Ci8UArNtk<8tG5-pyTCUF%+o{D8yZp68tS(i9~EP-BLx zDmzrjVt*6tsTHB~`<=hIQaMcxW&%!?f05Q`#I!xD_29?VbCOVSj`EL@$~aTedRH2b zH-8r}vYg~LR&@*ZF_yefNF0WDi(URaMIz(zruK-xXQgK7B1crpf{$yXOxO@}I)}l( zfA;mTQl_Z*P6uLCQ!g-CTGLt`58Mq(=@=029(u2sxz7-dg9yku+ zaJ3CMkL&gjcJxCr{@HAsmP!*u`WdJTgOy;|I7Lnm8%8dbg|=7%%`e1c@|cSp{9wuQ zob8KlY;3Zqyu}Uo##}=sXwbNaQ@y;rqEG_sNW*tXM9z~psS1B2LieevX5{}{`R;W93Ru^2UzGARP;0J8jg(Lo+l)Jj+BM^8&ZkMvyfG{O#QpF&X97TDA znZ(z-fOSR6%`LSH9VA=*m+Fub;DSv62pdcaFd?p1@MtElG?6dUFm&@44`$I`(|;%`a*Ho(Qdnf`Cpj ze?`8B6D!$e96BKBJH3Ydg%c!QG8zNcOm1V+@;>kUtKylQ>6?f-g%5zsk)zSG6)#$2 zPFZLy{2j(paoRc6{rrDo_b&|QxywSl~ct8#}u$^8nS~U03A^Lr?a$+vvKbeHZROLNohY)H<>cVWs z@0znz^QQ`JxyeJY6+&*fta38DU*jr1$A|K4Q` zoUW|&iN_^#ptqGx(%n;VJG|A2Q2kuVBOf-7-SHCn>KXs+f_wyXLy!fsmsG9mJ?^d+ z-Vw^JGJJqAgOgd1KyDD%W`QAUvk(X8gTgp-Y3S!srY!&E=$D-AxQKA z->c)?PiAL7CI7-=JhOpLRa%frqtmYNn%iclgbTAZXJbcz<)UJlW{Sz`DG3-;9!S>T zg#IcQ&wz0j`(Q(&5N zM=eV+#AqLT6xZ4DIxUf}=3EG3jJi> z;hz*DYVB`Jz>uMC(f%m@;YZ|LgFXmt?XIo?^f~IdZ775CQ7;DS# z?PvTqj!>K3)I8DaZ-WGylf3r=<1RNR@P)(1z^SJq%|^2#co~QkgsohC8pu{<&|tAJ zmO~6Lq)2IokO1Nmc(*1I=MEHg2QS!BYis?4V=XNw*L)%5P_6_DcLByKmCR3mATAa! zsS+qq3(9j@ZNH}Raj!|s8CQvkXCL}TiSADk^q||7Ilsv*+J|uvjzIYqdCNl7;spN| zcqAcg;y>;O$0SIR+YEi!@1ewAj(3j<=9i0=VQOTAb}TAFG$8yFh|y8-W>31`rLRJ* zdq#}*-$nB5U-bn9-}gk==31x40X@Q|PLTZ32vP}~2N(}lZN@*7F2)c0HO3y{I>U^@ za@BGbMxmmUDo4`~=ClFACq0k(n(KsF zHJt6hf94~4N*xe(vr*u$e^^?id@TKrAS9PzzI&wC{&hU$o|bGRrv4Td90-d9IQd`* zgR+>&`*m4@K^NJuZw=~iN>LwY!Yzlh{U|@J>cR8@R#yIggiD;rlxD12eRhsm3d@lK zmxRJ&De}+lM?>M@)t`p%i~=`9ja`&-{bUBwZKQX5;cU%M5^-~e}55YB~k5w zA?fG%Hz)RzW1*oeY>qcP<*-987?aSUnF3< zBL|XMxG|W9i%7(KblonZdS4iGⓈ+lO;$(%s?BTPI+K+yBv5+DJur7epB5PJI7YK z*!Lp1p_`pZ89-g5kaQI=0OLqV=9LT-&?skIEONx98Z=?#jby8E=%-fj%285=K}gkx zZiSfJIoBjdx>*?XEpUhSg+WE6F|U9V-)E7S&IW1@9dBqwqgL;W>!qCce4I)=XORwh z<9Ux^Kc6D}XuxM$;20hDD8vbg?kru_heq|rTJr_6FXLx>4}@@}nw2UI0RPpI&T09T zY>trGzZ1xUo2I57gD?eF?XMNypB>ww|K5rn3pwSGx7uf{$ zOCInF%VS< z?g8sAG%q2`l(%0{N(;kDfAy<4%t6k@4OWUnZt?m1=1{8)`+@zTR>`zi*Z_Gk+_53| zD>Q!O0kYCgIVF_s-rBuy^!ZT9BWamnLowreoG^qcZCBo&XE=sx6q7@EfgP5SJ%(C` z#(K#_N?%&LWG?ZaSr=gO^k*2XQk%n$Sn_aZI?f%9{e&?3VIgwOt<-^35ofV32}JyD zI3ZL;%&fOIka>fR$P8B2h!#I+*4}G&gTS|4Skj!B2y&Ze;pY-gL3(tj`5ik^nD74I zP1<%Xf<@Q7g=#hg$`q@6au0IO7zR?{6C-Z>#}}Yh4t zw31`a5IWQb1C0ARBX4Gl*FT$9thT17He?K~NZh-XeIk9-$C zQ~KUs5Aj{;h{=2S-Dqp5`(iW`D3o9gSC3dtzX4dDjZSKp;sfZ7PguV$eTg(Zex^Jl z9u5AJt5L@4qXNGM6O0jAf-HLkkT32JbR7i>(qp;k_Zhw%+9{pRvi`Cd3qj7LS*;Xw zDQR2q!xW4ySWxGy*U}+{dm)zl+*8KY{9p z+MZSl6!9M7RdqyPbB}QDTUbfF%zTk*M?sRD#`3n?dn`&7;nBLtuOpB)IhCzNVq@^lgnaGTgOD=vEiSV5?~^F&6e3-jG>rPSc^ zv_?VQy`xcsXh&Npad*?37xK=1VRlk(&!x~nVNJu7b{vX*Z=?^^#P`$m89ucx6c}HZ zJsL}qt9NTw^}!b9i1qPg5Ba8fPOEF-!UqO?WVsm^IBe8~QYx+rAXI~cX(Hz>T2CB0 z#1ZX>x!8(#2c)~{nqWrzMk8$P_DJ@JZ>|5*HO_^@1*o?nnUiAyb3f-)eBO~E6$hBN z?x3)?w4khw%|eFumGWyKT+|^r!bpd=)qTY_*4>L0)kSKLlobzbdr5;7zlf3iaU;VM z3GmRx=8&z;|A>?-<}$`E@1yh>-d{s|5-bv`5aDqR0OZm-X{U~GS{;p>B7 zcUJ|nea9OsD)Eiqq36>Mc6WIxJ82qW$P~|qry@Y9U*c{~bo{&Hs$l`;uG0awzKc`uPD*ZE)moMyV`w zUYdpK-+RV2(N3Vub;Hz3y%R&N@I%FHt|s!eUDxWQU7% zxDkgSc~rnVv-NtwB#(p2Ak=YKnC0laSJqYIU6fd|Xmx5|y{b37Zu+&vHkFGe9;$Xu zvF2^i#3o0|Hc+pa0JLbWxtosbV2rRLl_$_9j%)cB%69u`!sT9cayXIA*!kRxB>*To zrc=Udg@lgWY)C~E73w|H7o0Ac&*L9D|HQqTu&vk34_(&?xtO>8_iIOiR>W?Au45nf zPVg*NCjDQV+5V-d8;aVsB!NY?*&=9fQa;3Xy19J`>Dc11yW2XID;p;r1Bc2_6Z#Lb z#6KGM`3Prixv{`{?O9FM#84HkbJEwDJrKm?3CN^DM_lY$Ab{M3rU45ZNZ@JdsMiY5 zw1SZjTqs%^!zB{Akko=MGV9`UXr2^jFJF4x&W3heUAXw2s0POMAfEW^#;Wvg2;0)r z4|S^8U_mr|Mw=2q^Cy_L+7qv7&zk*bUXtD67ABaw2Hds?W=(p`3FGQY zrv{g(fY6&^wX*do7LuxBy0LPWb1DfA1kt>B2`7ytXOqD4KF0mtz}2$+(3G2Wwj6=B z*!BIF#1_yYJn_o?bG(QFz$|kZrCM`g>$z9Fe>||%tGjHpFH`D$F|tU;DbzgD)f$@2 z{SpxzUP>#r=?*$?{h=%!52nGinTAphP)4a?#a!310`4hGw{m$5M7)!Q+?kk2ry7FJ zXMsZM<%F0;#|cli^%xilh!QXjje4_tsbpF?oZS6MxtJ@|$st#XzPwY|{8VOEW7=i< zRAyjdJd;+RCFuJRBA>@*>d<7By-11O>$K7^;T}I&Zs6ATomWj@#2R7ARxOu>u~@wr z-nVaMB0VgZ?4IeACsl6LD_&!T+?L`NE%aSJCz(4yI|2 z2yjSYsY6~YH@0N>miysLrNm?Luk9P};ZQ>py6{IEgjGcR@M=W24g6dUlbfn)0F78S zSL2&L$G4d(zBF4w1FeS6CS|;0m9yuDIEfF$CPDt-nr+kY-;%#DKZ>TX#P_zsz=<30 zVh%EGfcDEY8fO)qJEDJE9S}TMtq0}KyrI9cEs1_Q9nFs=CDk*ju?hGZO^g-Q!7~0d zPKSxAcW?!L%g5_I_(Ozkut86mZkPIzSK`ioSze3SChz6-^8qMwOW>BCdmrffyfIW0 zF3^tD;SB3&MTM=jwmoo5Qf=FO^$5ck3FyloCG1Pmfhw^W5XS7|a|FW{es7Xh*G^5@ ze$rxa{x(^L*ly7gnRW$(wtoBW1oT#BdfVhG^DD>a25Nx3b%zx}&B%UcwD9WQdvsj` z0a*9iA6`vcM+dx;pWutoWTG1osh}O)7oKjvnbcMz(t|MK(MeKlg6Sk{;9fUjo< zJE+`LD9fR(_g4S%d^+qm?76F}SocOI_(1;x#g&Ykb@>_IL120W`W6R}XBd1R6HvIC&$?1qo|SVN))g|z~#7=O4g33ug$ zd78BwT~;lISh#Cf)xJ9EfmPmReF-9_^>auyj$PUrL2L<1w|X>d%mPYkbQcN5qY!R z9ti%0_4(6iJ_`HAfvQCh5-0z(CAdGhx=z~6etv3=7jAR#zu z!-$>4IP+4$AXz!4UEGiJ4AT(cAL!AGqB6~IO$N)2^@gSFpE1khC&GP#|J78CkZv2m zR-c%CpQ~>qkJL&A2O1GU)rQd=th_pBuc`Q#$J6nZ;kdQ}JC9}^4cr?RZ95JuNFdI) zfhV-H!?R=R6{54OSj1z$@+bIj>-H9mh1M!d_S&H{ z4geieX{5AGbMsI>{_`h2kKV(Vqw+Rj*qc?AR@bn3MOM6Ua4XZIEF*$3JmZjx$cw&N zj1=-zBjow?%b~uFirj8BYP8a7IA3^gX5(JYI1o`SAVWDL`}j_~&eV-H^2Pw__dNT# z5mA;OI{}dCD~Cvg+>|S}Bj`P@3S2}9IfzS7R!mHdK;hZgKp6X(3Hmq%tJ*xWqH39_o`=74^*O zQv7)OWK0E??db{nqXgYtSNWr`$*O|=4nDK5v2(7z zeBrnijbVf$zLibMWbC&`Lk;Uw8Ho9-I?{PaSDHgg;6=r=2p3&WXRqhWc0y-j-*_p; zR1ddfe`rA$r3_UyEHYa_$gfj&-QUS|j+aA*7Q>r~lhO}_+K)$3gim(WoMY-t(EkLS z_gwsA9`t)}fXw8Xi{SX;!1Q!BU6#&*t?^F+XEY2;gk!gyJA_B zW!L_=iAD8=(FzQ>=0oHQ5U8eQSD-Bd=l|ZF{IQ10A7P|j6^|;pWv+uELyRlDu?FCS z9&)!T%#)VhI{mHP58!feXcqB%Cy@l90lxm#r;SA^V(yDsUF zmF^!4!0|`7vexSF1=?Y`f)ar+7{d{vv!onUa~PY5=F3Gt0k^;YuJ~WK#*I$#ART=T5S#RVAw#kSuDJbw@e2mNQvyuc1a;(uF7R!53y7M6 z!I8X64*V2Lgy@fW9Z1`$laq;JUX(+_uD`PO3u@W#0vQJKCQ+9LS%OB)N%r3j%h6O^ zhO9L$5>D$B)9ln>(?*WAl2CEiGaV@4E{}3Wcww!nZ)9PV+$ZCtvV^a_oGEwBA{VWD zC_#T+2zp8m&}rA6<&4s*???xs%J*#*80Zq&T2r~@kJT1$w55ws8+&N}!VotVTI@+F z_HQeXMT1y2ui50x01N3P*P-p>4nPAEbJXjEyAb%ISHXhKL~FTmEW*6&^VsG} zN+x#GuZ75A$XKQAD&nvv=gX7FoMq+HgLCGaJ}Xx&&Frbt5TJ0W1-OHpJ#w{|1z zhcuJS=;Yl_XGWKI6~G|OQB)=w(;-6bL-*|E{JgJd{wu)RtvbDWqpO2<1v(M?oPP0wF^|YoXCS+?=EZ(UywU#sumcm4SeR zI{<@VG^t6nA z(D#yL-c23<5Ye`~Mmq6ww0TfsQLIkY!B5${0Y=>a6ljDiL4K`eS=-XTsn@y=ufhh z^s-?25Y%T|84yOhx5CX8W7phW;g(t`A=va0=sJdd0bixb?*r<(}=q4S}%AXg4Ql%|9Q- zgM<$jV$MA2P=9cRX!JpJ>!DKdFjfpL9A_COX+nof_-J`bfQl$xq2LZ!2{N|^X+*ih6DPbW7Z{{XZSU9(@HJKMo?JMi5pzp| zc)cXSf6m#X zeHolU*@SnaZ1v6FD8`iSeL_H>_}J9dRn&2wI36r0R@{s07oaR%$$=0IaXyUQ6N8@i zu*>b*O)Z)|cE6^uN0q;jJjw%h5J4wr5|`f1OW|)HVZ(`+-iK@!B)$9LO`x>zOR zsK#-?_510-bS@Hme5^140G9Y6;0gesK|koMBs=2wQw$KC43fK`=K`|s8jS>lk^ z%xM3xMuQT)@_wDvXTf@kPKCqMUA(Id)#3{4_|B;+0 z0zY;8+Z#RB;WyBQ*y=W~MWm>jT`lM+wpSlYQP2|)QvV2UQjtfB=5B+!m%tR>5BgmVxef13&c9;k4|%{j0XH-Jt#m@SN(7Ko8tF8;ji^rlW!&2$Ct_R6L(`@( zt+kiyeR8RSliz8AeA;QDLyNr{cQofT+`EwOi9Gq8j7mOAhTZEfcv@y4# zRxiCb1qf|!vANM$@s^f_HmEX+dkvaTiLm^=&l1AguV6l@@OCcs2z zIi5pp(#Bj1KYc3!Sr)$vZEdmV@ed9`YARu=naB*Dql;j%ZUw`xiQ})W?qrKeR6byd zNcPx`77z}z&6LdS%CTP6(UCGP8sWnGyaiqPRv6`s4+xwn6GTFFIPk*i{ZTg0x@KEM z0Iwb?^Mu*eq;uvS^jUD4QGMTLV8Ww^Y%SG#1rwOe$2%fTLV`Uh0(h_$xf&yj+?zd#e6Yh@xL3dNb5uva)Jy%}L0G z#Vc!N^8|(vz5V(GBZJ;EWHxCIj1xeM9+Div zE~S;9hq=~VeGHOK@4e&a4Das9(H7K#zO|N-9|WE2nu21S(Hq#nQDU^6Yb&1BbLDpJ z>ErQ|(b5L!4DGFWIAsh+B&RGczD1ny%9A}V2`*!8?5NL%*d}afl0F&3M4Efk@Kv{( zP0tX^4D4t)%tCQTmhTYjHY-92>qLz3CF?8B!(5}tPv}8SVQ>!&v~nq-3JLQ4o@1gM zq@u;Z*G#;LCf-UB2Q<{y!An~;qrj{g3D|=xyME%|b-^>itGJWYr~=hQ5+uZz3V@bv zTgLMp0A@&u1;RV?^~<;aQ7S1Tz-ISXmTyjdZa zQ?R_c)P+7T&If|&PK(rEOKYaUzvuuX6J&&6k&r-1U>aaEt7c~F&HUy{J~yW3LA``m zVuYmnr;MpNOTPUsJ0 zrgSt@j7=N#*xJ42D1}^6UXacdp1$9DG!}u#+e?smYXlAs?LQ&7hBLY^4sPA*a|tim zjf%lER)P8UI&YRocMw z1RI=2k2J4q(q&TSr=)_(A@YsR#X{*RBci!$9o52o(*O=a8F@_}UeZJztrgB0TEr;u zaiv;g;CaGs|JuPSe;pFA0l#i*jeeY?htbl?>qj;iK-QZ6ND+iN9M{H-zc+qX(D57u z2S*%#UGY%T*J?>eWw(mi#WuG9P4ETM1i_~u2%3<~xn;m7(FU$nF5-Xq82F!H$w_Vn zq_RXJ&v^|05~higVI9Tjx|8?^i9BPWS6n&1id&!zRoO>%`Q0(lis0A;*+ol_dr*jy zwY|#1@UeOp+t+X<_4FXwO+VnUcm? zRUHXQ_j^bI(|F!e=vMAm-dm*g^DfBJM1Kc-8wBd@`5;PMosPu z!&WAHSnH%9N9(s78iodWArORWdCN!hzWA5tWs>bfun#q`>=};JhmnsT zPN=6)bT~S?ZHA!?GK*TRPA>UIn8Yu5%I>kQcLpFw&w+Mx919gyWQ3=3K~Jr3r@gw8 z$2(&CR->SW=GA=v;bZ2xWy)$weZ>vW8J~5MZEsM3EzS@`A!H2HX5wh^1ELF%3&Wui ztSA27Xte?jb!+w>n$X-W&V#Ryqyt_`G|?|tv?OT@!-Nbm@Wk1Mn52~LC(p<1UD%Gs~yi|1-`(0^TKKDOiko|EyvU{L3uO+|uJ%thW zx6=meQO59O$@?#B0?wuS{ELP$cVdnM2{&l})+ZQ|cwtAPTLq!d*JFqiUic+ut7sXl2v}xf;{$WMtJQ(P zmj*`!1aVl#bMpryv~9q-iw5`iD}bTUOCG3;=&AKHUmDGvqCQPedUVZ~rK6Hb6T4qw znTFm*N($F>3;4TFkh5-FP96dN58zb%PBv{aS~V70N6wr&;zSh2bMN{us= z0mF3k^8GOjO=h}1EDn$^q`Rk<1E&iH+62b=wq$E#e(*^qZW~kVuTvY6UoJ>@YmziO zt&U@7i^{N3m#Gx6PaZj47-^@OXfzPtlb-|)X3hor^|YAD@fSw9m;k=U7L?20aR?LPxl3sng@BN8X;m!O+C)*3MF#RVpYHruz zV&Za6O+Y*JZtXK15qrIRf#jIq8s7{g#xnbLok=WiV-h(!ackek@MOn? z6XH^`cH?Ophm5Jm?~2=X^IFZ6M((B7fRdY)Y>XYw=e=z0G~ty)_)db_RGN_@M)#aO z;K~X^#6GeYqJWu00DOJw$V5As$WE_WsBim=+G&JOPa=F zi;$Biwod<%l3EFIAhDDz!|_W+UqLQxiw#zBHUyJ5>GtwrbNXGqTnYIlfCH=!x+Mj3 zG|XINZlO_2XK^Lf%?2iQUSIQpwR$_39V&1d^xGQp?0HhMN?oYq2Onv#fMk4S#lm zPhi{EmheLiJ1I|T8Y7-&Kew||$4KFYe3GqNB0!0dp>XTXy-G~-z3V%+&BBtbLU3)_ z>0%l1Jbao9f(IEO(xWhog%=}9w6z9^u{68_{!(Tu@-OR7L~_{zQ;M+d`w)GT)UcQ@ zI?*d&3H`WSSyMzU0t`p;G;{4ccjW7pY8Ab4Qfy}$INbl}2Z_8&N>OKT%)pVu0@P5@ zVv$We(LV&lrGI2814!)&2sLhqR}$1EFtOI}iD7Y0E-1}E|I8JgtaC>oDm0AVN-_az zP);M!E(w$N9x@Qv9Qa<3=F)Ra^_fBz zoswKg-+QJ;)4knMZyA5^dXr9d(Y;1!kuq`moXr?i$yAAwqYBl#EAWW)_%A>)uVIMe zk6L7h8$4@w;^-JqvVjTa6^&+}TCg#HjDaI!+(?4T)1GuxN z1?+@}oB}8Hokm2##2kSU49SIi52$lOMmP@}Q#8vWW6h!1y>*!8TXd<;sUFSmW!9{Jo`9Ng$orc_ILX7etZ_h;s+azU2;TRVkbiZbev%N7E9_k&0hZTE z7zR_OX<3U!7-^TNju1q?PE!?u;i?c z*pYrc<=<*lV)ZO+@t!jvq&}G;`084;TiB)}ii6rus`PuD<$aw=Wx+F@0vJoDJA?k^ zEw!pe{4GhOxd#+xJ&kEE_)Iv*ym#I}ru@^{!4aTv(j0IVc@?`>bD>{v>_tTa zjg_m3Z98X)9dEQl&zg9s3gGEUXZFT?^WYHD*pv%<6wUvueoS~&62Z=37jqwck+C!! zU(}Srkz}cqSDqs8>7mBOn{uRv`U*F7wl!6r@itQZ&!DfhZT{=9 zFO*{JHZ~$+Cl@-yP_!>9xu?jqS#~kTu!Wvyai8EI=m7urM}dK?sq_kDcctBSUC1*H z()EM3HX78Opy@o}5~5$1J|UFF_UuQT3UnbfR;GaMTY{BBD5NP(@?O^@M-$_EB|^Z( z8e@t5i8R%EuH+vHd(1GWwi~=W%qDKOCcl23Z)ktX-wU+j7id8Rl(er#dZuBxB8FON z7C1MI#-UYZ5dSxO_aLDMWtG%ZYnO?ZCXA)Wvt4Y?&RWA232Rk@oe?@pw|n<)U|Xb{ zi}j$LSdL(3W;5`W!?Kn$a8Fp+)-+{UEUTB%Ys}2a*RO*GoQuSx|$~s~H`?rNCp3pxaR4Rl`jU5j}}I z&ZA*e?Y=huF2n-vtJo6@+Hsu2JJsts#aa_>0{-4V^DsBGqk7`LI7LW}v%gT04##ii ziVt1KS$<^l{N=|4GEGxCw1z;8egn+MSLN_>x>x%Nxd)T?-;jFl50m`$17}+*f0?ls zDnYX82&ouq8ebaj*DS5$8i!o@Wh!FDVQDz>vh#V6*orsH0$&1KUjWB+;!w+m9>ddn z34HwDMlaDQ{GFly7$c$Ys)U}}%*_W<=-Z3NTy>G6I6dK={tM{W-uN3qO#5tH9ZOh8 z)oqg-%T*mc^JSZnhU8{rjbrjT(UE^(*#gNMbNyJ@XJ~#S;dJOd}K*7 z2o)hpYU-)?Tz@d1Ax%n|9HP$7Fd>@v7?Gk`$wJAW!>dHFH{lVDb7r)9k;U%P2ao5C zV*l}f7v*8FG+?O2fS9)GTcWnhre75~^gXG67sSL~BmkFpncLF9v0Iy)*_e>T!Jy$sR1T5wq z*O!opNFtv2pkND4ITo7bTY=%xUKAJxzdt^3f})@}68DPe zKfb}cQPC76FpR4N(m`y8;w7u;T;j%5|2xvR^m;W1ai|)U?e#856g|jpvC|FPASh2A zzW3)z{wZk5ZA0*pCcBZV!>U^lD!^a&|7xtCJooj{VXN=CcpX*9 ziC2>YLLN6zi8zf&^K`y(e{IW0Sy55jKAYIR=oXqw(W=(SoY7EMg)*U9`>8g0|`pmz;jaG%9SQjmolz!>qs_Ms{^dN~aFI(E-l zeU0bMf^h1fplUy<GU8WM< zKg-Kp@@IIyt~+9BSU$NS>Cd|VfBU5|r~h zBuan!@frg7R@0zW-Y-F!#I?_hfFfNQJ=k{HVh&rxDhNHQ?Af9AkJ!C^q{;a0>@o)r z3bG8GaXS-+)NH8sbis1caEItbuFuR~I4lahoYL%Z7QFX|=Xt z6-J2aV?*JNSW;aMw!fc{+-PdJe%EyF6pq|g2EwNKNMeuQydXIdOA76~@8r4P+W$8y zxX9AA4x@@tDCSJ=hxB>aQAHyo{y8*Kyil{_MpJ2%MVm)>dVNRWXnr02Ck+)5Hqo1B zAXc!l--Fq8KZ4x{C>#=)xTcEHU#LCNu5u{DDo@*vIJAJM>A|E`1pJqx*F(*NA(oLA zSoM1R^)>1Q*cY(AP_9_x;&ycb<{NkOA{$OAnM2ufl9#yFd3Ipg6+jd2llhiqGZIa8lmn*Hyp+Q;O0brlO0m37r19`Lp?_ za?Fl-N-)X4(m);yN%hs>@lyf(feP-0vb8r2=h-x&5I8!ej${JbQit+(OE~d2rsQ+u ze(X@!V5*puX{VO#RkgLKT%)GN1ltMC#c?zWd7XKN*M~kN0Bj@UmB#^j4bTG%=(|6R zHk#o}32p*P!jrYkhx{!Fc&G@u`gP8s+6_9%l{ps&nXrBcSe84wjeYgmxMo9Enaa=X z{)#_p6D2K)j^`3Y8Y^Zsr`~K`nFVjAsJzVW>mI(14e?O2X4T2MY0ZSFYg$F115ZD^ z5nSi+YL_VM%|D>uAM82bJIV~*P9tJlvlGo>N1Q|MMIHb61>s{WD9M@$2``*FZ3}0m zU=YQ;oMQqsPhx+Weqt3r%W}y-PK)wL?%s9lcga#)7kw~+Nv{ja73(H&idiZ#2i5+@ z5=9)!(9*1ard*plGac+nzFsBI-}HMkVD=;+Kye(nAFpc=8`fPL=GR7~EjQ>7)Lu^g zZ=H+n0y~6RbZO;LOi<~p*U)2tekF-&J~UGmb`SlJ5~>=Jh$ojfL?oR5=GK##LA#l0 z!I32cwsU=HfLhK}R;1X5m+SV~uW4Gw=7leJZxmbcqks*yk5K1Jgd9=jq+~3Md#7!5a`EX@*(h$D?Vc)mUh5{J2 z%^fbsj!VscraD%cDh8CT6kO3EzsP=IUO`VNeOL;1An_BhKQ8GOJM7GuF*yA+jBkOO zBGJVA7ciyQ66dRhOnuuKCi(m@NSrOfNjd#rjw0tf|COn$%l76MRw{LYj0d&b2qRB} z?6Y7pK|8YE5Da>=8SfIwEsu~q_s0=M)u)JccS-cfyd}UUWG%#**Feb}=3UUMV?Qc0 zj|k4$GgKmT+OB}G-rw&NlP`*b5XPGmF8-XzB0!)6@mSfbs_kaeTy@7n0_M=~+0FED z`Wm!z9F;0jzbM@&_}g4quHP&1HT876G^fWf$nVYk(VLNv?!6RID9DfIgfCA7aWEoz z)G&|s{tC4-!$B04ZdmAlS=+38Ei++T}eP$U$44+2VMC z-5o_Pzs-dEF&MkUv>i7jUFWx#2fh@5ifjhTKB!l}>Ky_(%ruCC1Iyk1$5 zz^kJ=du1-%O@8pkK0a2O>e3Vw&8-X=0F%KQsTb#Mjm;iF?n?iaRgLpIdXa)j4kk)z zeoUAc_9M7u;8=qv*;zz%b{N|P3mpd`1P%&jH@F5ImXoedyGRD{?l9KHPwfP^kWgCu z^f+x{l5FJ|@bQFU4{Wlc)rH25g0E!164M@ryje5VPF4I+f~G{~OZcrwmD^{U#voN9 z|1_u9BA_u9BjP--_Q@3z^QmgT&U(!FS25Ob&9BkrLAiRzQQTTEf2ny}_;>HhR)#lS zIDK@KYn<)5$@jH?8HZ+S%DG4}1+Qu{7Qe;c);gbki}wGS$WUXw%v*qU1;lI4{L~XA z(zU}e1G=r2HW9t6#OJPGbpYym-#n5@_hk6=z|GKCpvUiwu^T+FaqDE%A{=q0-N6Cr z#(@KtIGf58=rZ{(T%`ovu)w5|mOK>RU#aKYX%e?LjyDHGIXaX!enbxSpKu;;2PkL) z!VRZL=zWD96tx7dY1j|9{2-L&HT;nS_Y@hzK1cP9tUf*Gz{!PPDbHS@FB3?O}I)GpEs@h@nFT15d_MxaV>pDXgh1%FSPFO_vJ;rZS33pGX2#C#2DTD3jwe6dF@!{fcK2 zn!AnQLR+oa_2Y7mf~;PVN`XCpo#V4o-R5%ii%G}yHNvzKiH&2nE)zJNcc}UJa!A8# zIH>|)-j)OnspjR66U?+RYO%@QDBLOVrDMN|wT+61@_u|gmwLU zUU{UAE(iskQK=2DiFUJ<(E)7f>!UMS;cEr#3H9_R5=@5F5TVYGn1tQnk$hLY^)v@d zSzTMzqBM`$4kW}N2KF&TQ|j$bI&`i?apBOU|2!Fp2uP{&3U%I4F{*kfS+$1Y?U?Gu z=rJrSZ9DWUs9XI5U>KgLsptkikkY#Ao~#5ClD&$bL^l;@-1Vl$#oz2FQQI332B>gl zZ5fbpk}nXCx#LoxiJpxel_YZ)FUO^b8RT%D z3UJGSX!Qc~WZ*$YY~qF1nL##P-cfQ9yivE^xIp5lp8{Q-POG zAUF49+a0nVGRGQ01m3Q@DZUv`LA11(+FG01KsVODwsLR*e-H)IZNr}24dq^h;m2z< zzdK;ogJvxBZ`b%|LttDX-qBYgXv=GMmTpK{JC4+zZn~Z;Uu}QerpEV^IMFkqsOD7U z>Nk;m2XC|~Cw)5m#)puH2y(qYq;jK3M*PuJ3k^`_QYRHY2`pUf@vNe8B1-{-%%j&f zgd^(POvae5{)V@e0`!&asBq~9r>fF#m%_Y6G0D6`#qQ|^l}i=xlMFaT`HN9FP{Zo0 zT*{gd+;xAO7%0~Yo9( zA=TCA5)pUK`b9jcA>99^y@WW>RWzO^J?!fi{4l-)h@EC|5NGJ!a*@H*(#8L^; zLb&M)ZnxDaLpJI=f_T&|Z(8CaUJ^ZP`);)Z;AG_kye^KJssRacJBPmQXiyo`PlieU zk|#KNT}crdtz-JR(ptq%7%@qr36)`2y_^hAe%g zt7qOhE_pl4xp-ZHmCJzK3uBSarEra7ZR_XgYlA|AvN1$*>DNh+y<+66=jrYEWjAwF{U9B_zpPrc4PO3Trm4*op9#* zHZ$u2{A1kBKxK(23#Qf;b_&5jx-2I6YYq*}7OJ^A9qU;{$~?*J=X93yLPV=EGI!AN=_o*!}-$@3@VKnRjsrXfKIq3VK zLd|2LJQtXe?3B49+~0X1lbj^UVU#p@en16b^eeH3J&)jC^mh&+53GNzjbmQ{l?2QfBeeE1k4fSnlB&Me|SEGxHKG%&{I$v1qEhF!SK>@w*OGGK++~6L~3PGVh6Ps3oG{#o7L#YnLD)U<5ekJ;T)M3){E1wF`_p)NP+g3XK zWT@Ys@^$YpXBa6CF2ME;q^qvox(S+Dd${;hXgbFk|9dOP0~S+$XK-HGY^&IGjG=Urj=O>=$Xc%Uz>eqa%{`QHdg zgReI9cqOE&VwsX(_$>=x20qp@gEa@O0Vz;`GKtE_?MTXDE^aoB&{D6-gzsp;aePedxV zQMpATwhXdGIY{J_;T_KnSRU> zl$(%Qn>GRSk_z_iZk9|WB})p9EO$&?=bm-ZfmjZzCW7xroz#LP>Rg5UWC;I?1vE;K zjWRXB_S=ji&+D;1^cARmu}p`JoKWi*xDo6Hr<6FPTXeP&L={oZALYII?OkV*@fVe# zNU#Y4x9#6)9phHuUe$KCw*G6ShMPVDIkWyfdBB;Aq)qRFQSXyrp#*q#j7aL%4hB_w zBNP*}#+TTR7I#KZMIIkw*Qv>!Eq^=h@*bFz)B}o-yM0i45~|pKEvR&@V_Q2M?mT=7 zq&CS@Z-FEI+R)z}1M*72vzr-^a~XX-bRGAH8f=<5V%%90^6^?mIN~4D6<8kZ_nsG1efL2iKVn; zvQWd$Z{WqlXW@zD`=L9}xm!vESE7Qr-Jn(dr(O@x%TZ7c;t0H?=CG@mO(05_G1Uj` zIXvj&tF{6^h=}0r+GdH3+mnjbsWx#a0)s_|{dZ2CRyGt=G-^r#9!rG*PRO+^$S3;T zWL+H;;Ch$_7`CTLmjP5;=33=bhefUIheczKuK?zroSi0Uunr2~%gfmN%W8sr(6+%b zM5>-wkM$8F(<%GS0EX4A&Kob_4C@ZI?(@0R*(cTa=mO@v7+rA>PWO{~Y;N{~wo54=U>C`oKo_B%U z9XOr7AA9#wBQZ|8!S^fI46?br3D8;C2knm2(L0J2#s3@BbKWQ{YGJ)uazV;rqCRvC zQ5i6=NB7g%oM|-X(%J~CvY%{+k6F73X(P%c&@H(T&P1@lSjB&zvqU5fU*XKx?;b6@mChEn1p52iejDot>=w0c=tKe>2&7>YXRBS_dxJx!p!lx3-=FfHG5454Ni!c z5C509SiKn`RqrEpWELafnC)xsR|}PzZ+&n1N;ddetxQI!0*NB49cjK;ICsvIs0ZI%^`Yx zWyL7xq7YxKug!!6%UYn4MrXbC8u18RtR)l;cCg8cUKB;Aoz8`Jnr4G{4dZ!!?+wdj z_NlV`;$|n{UR)2;f2fE^1(SN-Lj@=g*K)1w+3BCIqwF|K$YHm~jyamF;=e{Dje;Nm z?Oj{?C6|PIe87KLT8oA&z*e&``xbdb-!fM}Ug!9t^i23(T2pfIh-VaUo;c4Nu)x`6 z_?8o8XKS?S?7XQ9U=HamA&t?n@UB^_n1uFrxGrBlR1{ zKWz6u0yzB>>@>fC-}#uVnEVLw4#ZKX0!)Xm1TwWl0UtW>7+#B6ep#CoHweL0-An<_ z=@Q1E!~1gi7H559mEd^$(V(zy#3YCf-!X{S^U&=7OW4wfq2;S2m7q|qx2sDPl>RPt zC%o}`e7GGYnIr-;rgWej@@oTyd|FGeg*4I#L6)D!)xmbhH}-!u)H-Bs%gYHQ5Sys( z6H)%R^xaXjV(@E;`{*10E50P<8kmYjLjo2s_7F%KQXALP=_{pO_rRb6>?D{r)C#tsL#Kb0s1F~NK>JI=@TsYfHb1F zgE6jdGIoD0V>-yJ@lLN6azTK$b2>_5S?WxXv4Qz1cj2j1R(APcx@RiIJp*VM*+1uC zl{pleI1dZS35`xh^(W3ng_wzLgRLxB5TEhuHU-*g;H^m0&gb*q-AbR8GT3BU<)lsi z9Akg-uvY5qno15@yW0v_oNBuJE@Uwvf)+uLxNGs)x81Lef7Gk% zot|x&+VtF0Su{%@HF&Ct8hz?dC}?IvQwj`Tf2c%ZQhz9DZ^ z8lhAbHlWUviz{PEKR8{9p`krIZ_FLz#O@&_Pd+#$`WA{yAmvByZY|K1WvMC>->ar& zqQ}_S(GAdq6WmVJ94U0#v$>pp}9;u8@8G0F&^7B-j0f()fW0cR{UI?-IqH zW95cIkHwZ-Kcm)zh#AC3pl=)Hm2YY({P`nn=SrCTdeTZ>D!DE$YRy5IAA2XdS(tDu zk?njto`La_k1^|-T4^tIktg+(K;!hjI4H6|J^|CKMk1A9*;AC@AFN=Co-!cYpm0y*Gq-Eu zKA2AOaOT8*RDL@g(PR0#bT#C%8}ShZ61i3y)-%qGC%f#(87UC`p)OsWEBb zU};PdqZv!$tHDTpQXL~H_6+@92t`k?-H9HSDtyLJ^2Gg!`rb2ZB3QA@y5z(h29L^J zj%hcd~IEjM>~{biE0Fp6+;O#OB|*%sG7V*6w( zbe$jW70n@=iJbqush21SO_!PV!W`B{)HBhmFxtrer0{%cW_tCK_z6)aj?$;q6?;9O zl(6fcrUARyn81V_IeoHAf6mh|UJnuaHA#MvwaSf9E4R~(suG>KjU z*)kw~WJ?PXe8F!?x137KR+HN_^WLUbV5Eq?)3qn<@nLX!=zT6R?^12}NXNs$wNBk zH_+qGdpB~2awa0jU?ZH+Nt~z!SB%RC$m1}IjaswvUD0svY>{&k|O_R-OetWr^q}7AFHlFVR-QyPj7r;i*-Qd67{A#)pq>Y_U64)*2vbq>?Kic{|hhOCf;poB{5m9pO>#_wQ2xH z4g#H&*n#{Ycj$J{L#g798;!B8c_C%FakHKfSC0njy}yxSh2O}O>S7ZNB%tT(vtN*+ zB5&KQgh_u}SrP^3-wK1P6K=TA)Mu;#*UKhWIx(~SXv5Z=UBd6S4mdSMep~#)0ZrC+tNdB&4 z%66ndQpnNN-(-rQ?!R5n#D5p`i};LJCwAKraHJ3vtDwx9AK6@RS+xzak6V4YSa*zL zN%+Tu%99Z~4)m9kedB_L0)ZqeB<=I-5XdGZ#n~repE{n&h&i`z_2>e!58TldF?H=E zT&&8VCf}NFz#f~ux2Dp20UV?ul^y+aq^3a?9n=Om{$GhOGEdxZQQF8lP{n~`!^I|; zL2BJ6x%=dORIzb+;gE*sz=1s79Y`QK{>snm?m=)-m4Pvr;yJwr_BN;?H8dAC%IR8G zeWnvIr*H7-dMbDzW+V!8@b)?*WpQG-hPqZCsb>g`sKB8)UoMJ~fu)u3r#9xjxbvH2 zroPTT`;{Ho2PT~-7f%_ROT2>-2B*J|+AX;|8ng4*jQABp~1l!_0vYSiJ#hiL^V z&v!P-yECL05>uG4Ho@63mJabT14&NR@>vTnh=PX=Gql3@$VWg){k8K&$E7ohegl%c zQlElubMzMRk{{h1-d+oSMlGp)ur%Wc_yCOBG>jOt0@VZsI0oQF-@`ZdF2JHjJ(Wa=ief$l63GMu;iM-$;#nEYu^ z)}t|fF(v)sE$5VkPS{s25wL`{AqPv4Us(4QQAwRw^++D*Yn@WF*6-?;ruc=vTL*_Fv-H=Bax8y-PAty7fE%{k!nFrHInc~v-R z+;PlqXFe##%ya8pwAE8F_xZ6LH4jI$OwzWI1!SJJwGzV|`E3$j*D3q!jm`zy0>J)Uu~EVBp^FsU@bOyGZqx#gdTl^|uJ%&noT!w65jt@~(V-EWtdke@B_i$5CnEAkOSPQ2F zau$RPjF+V3h|@}|AZv)eC`3mUZdgKDA5z?D>@s%R($nc~HZLj_BYYwhgd+`D$J1qn zTSJ==U8uQyF^DF+yUIK?piFZ#VcB`8I5b!9YO$mku4BjidNf`-;>ToCVEAPpbZHrF zPErj4?zSWtbCz|B-18P})>6=U^~vCUjGgM==rP4~1lM+0K-4T64aHorwRY;!+M+hA z253D0F~TYC(kUOLW_4xJh6@WFjvFl9NTfYEO|M0I*zfqkXPJBSjz}Xf)w85hA3c!| zA?L46RsfzMHHIZ|$3F3b`KD4wyjIQq<&?2w9QGJe zk*3p6wNKhhw0L!os?EwSR}$Vi2#LwMVbhcGh(7~2c{w{IbWYR;nn~RsqCW&^NY^ zR%$)5xSp+MhJ(O3J_kUHOH*F|i6gX(G?Kvp(*dwr(m&M%<6s@3yj*Ne@B~w0)Swtj zR&9+nRQk8;G^edihZ%qd7%x8RZGoBb{y_q&1z_Hjk5F)Ic6EIxBEJ^y29LmyJd&bT zm2V#h9~l8^&{lL*U_BstFN z2RH*QMCH)&NHLk@K}CW;Y5DlcgRYY^0qWDjDcxqqtm#oideM&n0{Cib7N?}=G-nlA z(eWXr^Pj~;8CyU;80eQtlE$G{*2l0K1qnht>2&ORTlHm+JmWqs$ zUu4XuV6Bpe(Q?wnQX5Tx5ynn$lE_`7uVGBko3_8<0CO}SUk!n)LUeV%Omrr%UAqrj z{k4e1@WZn;-zkJZ&WD9$i}3_^ZgLm@##}*c%|Cs)X5t%5HwoL~#xjSj%=rp7Y9n9#?|kE@C(We7Hc~=YMGMT0uBp;Ha6=G>mi6 zC>)@ej+BL+LeI`4-l8v&6io_AlqK8GA7L~tR-{_HE3%CUux=05#?9%i=aG_udtpF_ z<1A2DVlPZ8Q5XsACy@O0caicsx9rc?d3^@M=2GVcGl<5dsz_-h3eofdzggcjAj6t_j0oa6X^AFnB40B>=qIt@Tnw zFOL;UELF{`dM5m)z1ZilWdw?2T`e>Mdhq&e9jna~MRideNZL3a3WZie#eAf@+pPIAz?dG?4aE z@)#TcfayqjHO!`%Nu#rN2{gVyWP>}Qs3qcO{6(=8Pe)3<*(OuIUCo&s9@Duk1f)lF zv?My~GcKm>yGI-t%F0+-H}@!Pd^J~qz{wRzo9YxrYg9)b)OouE*uY^JD+&c@M(!LM zcXiWKQH}mrZh5YT2fbE6Zcp`HQ(FoIKZUjvd*S&6z!zR}iXwZq$+^uh4%f6bm*X;_ zu$22Wudd2Frou4~rYe>dfoXf+|7<#ep&?OTvqHEL-L&i8q)t@y{U5vF^+>n>$9S9zekYR`jR$GpzJ&X%MXJ&;VIK&QB`668)e<016myy+fUaKhM zKB=3DO;it@kOHl;*TNu1PK1hPc@?`oh8LK6N&A@@7}w;N#bo9p=6#BQ33l6m8Y%yJ zPI^qbfx_0Qj=M|)4hg^1~IQo4YApuUA!HME88)2Fp)PK5&hv4g%r zWOw}-#efv~tn)u5Vp%!B*f$859aFTs&ax?p8byti0@#IV@hb9hqf@YgRZboH9H-7D z=_5ZlGhUZMAZy}ytf(+({%#csw{Xr6XM~RFB=e3abXR-wW(s?YC>nj6 z7Nkv(ljY7smkypL#oM%$=L9A*dPZb0V$Au|x=J$0=uzub&!`Jc*SfXqnf%otN0&?J zfkm*BnR*3J&ZV=Dl8Sfv?G>M+`50j&n-n-z3*HVgSSl^>Z@OC;(IbvG>kRpEQTgfW zE8F4AuLj{oaYy%bJ?~*hO_n8}x2?62iqA3O0P(A7IJzTTyAzFM(ka2snR zbRR78^S)HeRp%4!QXs7YQ`Hcltn|dbphF8|95D9}J4rgn^2cIEyd3Cl)_3+}H zoG}(VC_s^cww~H)+a;;S8@T==M#!IK$)V`esa4m1LTwU;PLC8L3AaWr=d4KBwK_NL zM)kUfvM@MvH;_fY_aZVj17IpHZM;e=X~x~*HK;extzwtYg5zg(#Q0!N2o(V#+HA_xhnhXKv8p%Ww=xUzh~iQWVuuhn{aaUn+YV&@~&V!94jxyTZjVIs?qFk=q3^8Mj%)O zSmBbIf%_7Zu70veMlI|z=4aI1Oqrs!BV3R9X9@O!?##jrc+BfI*^p2meYf*Qk>5aH z#ifU)waOv!N=*%<@jODt(}gO=6CB23@1%aootNOnR{#BQ(c|Tbxr0#Nht7r=G#qm) z@zsG=%^k}xb)xQWqRmVoitR}g;7y^_cQUu-Y;CYU;S^V0lsgN3`UZIXu1KF|!CzCz z#{&OuL`>ep$GtWYb}`gtst>`(wD&#Spny|b_KXAz^exkidlaDN2?~=-<73}h8*qgT zWiNw8BAtn6PGODmDBXzV{$F#$L)@4J?rDaPzxTN+JWu0}*q)h1|t+ahr zuISDTSEgdAWS6tVNNZ$QI+1vpX)0s@%I#~?gbb}|xi$pyxT>tU_`rMDo(&0K!FYi? z^kL$--W~Pka>Y9h;#j*m+chY&ZY&7%eP+2hzB_0@6LPT*Fg09lGJk?&K+vd$^r>XR zONm?Nury0UOR6tr1!mcz0aMtKItN4nK3N=7eVbJUa zquQ!sch~B}>{RI~El==cFs5!($+j5G$OsiOH%5^}+R77fJGOD|dDyT3{Tru~k=Vw( zxQHEvw(@DQIYArd9F>sX1gttejUv6{k%|ZX2VVtK#a#K^L3KAZxyJHpz<;NrLsFdP zs}+QH1->;xZ)L3+kKu&08kj&<@PRwqFy5Xz7bFno=VFUqmul`!uqLCNWhg7x5K4OB zcaxb~P#KR#t-EVl2=#xerLw}9`6CzKVhAfTA+>9uy`_&4^2^~Sc#zdG&bVoON-G&k z{PmDSGy`e1f!{E#wxv82Sf>Yo0ZOwhA0DLVF>R9zRnl{T{HCuOo6^yf+zB?4nIeJ+ zGJh!ErtEU!Q_g*~(~)_5S>gIzm1E~Th|WpYY!Kmb`sZO;HYA%{=_|ON`+5_$F5qX# zg_=49!-#l0guj@<4KGU!mP>Gb<;Fk|g=N9*A`?;st~p`$NVplVs=0!6ZLwdiO-aJM zY|6E4hI=bEQvD_I3_L+g>Me1psO72Za`bzwM<{`efG)pJ=0UyE$cgr8r@~yc9GvW{ zq}L`m(wS1UIny;YYiqBnAgV{~nWC5=WtAzcOR|?mmYWf&OW|0;9pz;wvp}Kyi0i#K z@(78LV4D9;2|(kyAycs63*+NAN=|^efx@uW#6aPKy4se7jnQ#KWx!fO%jd-*#t(f5jcq*)a3VxE+T~# zN_!2t_)b}5|Kldn$D|5@qSqa=YQK$*XeG6sQ5-4{^nC(qF&?ur;|Ty~IVd!~=-yT>-xj>cw#`!+FNMG6xrEv(sM+plee~AmNy#7M#qG$W z;c?fCL<42DeSFStJnSz+^L_UKL+PN=U!r>IckU_OEzr5;XMb5mq zWf}<^?$qqv&Rm$CjJ=)d&w%8nU3lE&*Z_t7ApuXH+CnaN{h}X`ed-XR1UMw4D^RJd zld`34r#TMzKKDZ98|p9I`b|gzcd}=7PDG8hmY|X;N$cyMF`dqLA4&s4C6OIj=d4@q z{wNiTPsd!xol<8Ia3c$s(&FlD`)Yec10s$c9m$O9GwJd16C9EpNX%Ywpy&0cj;f5s zB=B6bUh|z}57W1R^bg+_p!TZh$SfrhkH4hNJn?1VQL3o-y-Ls*0hYi)v5MZ=B|$2 z9{$nZjYOHcKD~58^#IFp2^MJHpM`bmVlBzjIz7=ef1v@e5_z6n@8oD*}5SLdmA+0o=VxKxnXC#>xFgR z7R6yDD5PG0GnBQS0Y5_1kl01uhE@GH6jY7Icn(P*BK}oKt9Ts%hNL;XyEY+*e`4PZ zvW-A{SLpX%x(VxO3^?prw0U}MtMX(Ye0cW;9l(}3+LcNV7)Zu+nsbz2XKBT{;evZqos*?dgRWB zzo6%v7RDg~oUuFj%m|MljNj17@u16&Bl>!R1yYZrq2EL-$D%9pzrU$uTMArS)&ro^ z3Jvi87|2P^Q9Hl-Y6Wv}aF-{=Jk$L#0`I#(5M7SD;ZTr3nrvDPl6u;8tl$Sc&DJ2_ zk)eZ6Qt3$JtymB@fjT?gHhBAR!~p=PMQw*HPb%pE9Nm6po_f!{;r zCjagU!bK(1BKP4Zth#)^5bXW6mfXpPxjs}={VwW41MTe!tFyIsiK_FM_;5=b_nSEM zuyb|dFzUuzbw(w{jByF_RzR1<5webEUW72yaD79yh#xM21F}uuSWJw;yP1|f7!h84 zL*Y4BOS9n~v8>GwjTG%6y{L0m|GVA3WQDJEq436KMsL0MvovGTLJX?x6M~TE2jchG%a1}K zBJ+tcZ&G372ybR=f6Weo&WFy)Y~aFPlptATX^vRCc9#=rtEWdUOThH68gV3K`M`)_ zPPN=$GTr)7W>kE0qjZUTvP^Jp&G6~3L=Kd_7$TgBnbEWYednZ;Bj_~Oik^L}V<||;UgjnYO@>Z}sM<2&3 z_^a^Wv-p_@qs9^c%D%^zn_7Uhek@34>B2w)f*Efeq^-QFIy}Hb%ps|TcS8goFK;-K zr9;q(oE6io)c6~#dg@kI1lQ)ZG7TqGq20?ne=%Q6-+6-KxFx^%+jZZWTm^~N+|KMM zOX;kT+!AHH^|cpQjL5)bFQ3AIF**q(PZ<(vT=>Q?NqRO$NQB!rBNz9T2oxzG`8w!+ zm?$I1hGH^_avUGUm@4GYI2?@KG-GCc94HbrE?SG=c$*B>Z5$j^uL7LMB|>xHSPa`_ z(`;*}>-a3$IULtTq5c?p0jS!hk993V&&gieHyRb$`Iz`}B387&wuD^M&Y?DRHNQyaX^~KHpJ!(S)=YZNd zl**2i^i*$Z{_<{8I+i}Jh@uwohqP1rNEK=V6~u*=F)9is_c#`LBMzI0NhmdJ!&{trkB!Hg+qe# z+;-*}8spV$*bU2~8`&Xt2-v)ccx+jkIl`+CTzzOQPyE`#|3zvjKL_$Jl!O_g39!`2 zv9K2dYY)h~q#7=YZRDaU#)(u#8uUr7A=M4`Tm=c8;zIdkG8+Lx%x1mrHO=Isf!v{Q z_3Z#B@<tsu-HLp6l&pxamBkE8iZ&D0%Q6V0nlm75@5elLmLy`^(NfJDK$t_F;w|lvzRr{exd#ne$@p(D+g|0$JEeya?0NScY zQhv2NfRbXd(%R7l3Nc@hu3T0C{uuPu14t!4AC0fKxqn~F-5{+7Q#d!D+$|OL+{S*~ zzsg+z(EQFNjP9bO-!{=_QFswZgCi!FUurg81n3Y{H66uw4XSMpC;fZd3a9T*TF7l} z;-{iA3+YX*k-Ay~TvkrA^O7h+cW?ywvqYXE0TXbgaEDBr=mYnV*B7(7c38NA%H0Fh z9G}th+iJU$=j>TPPfS8`>u@eO*Om2-u2i@kdUL*OuW}2S)L7X8IGKe43F!Ej2XX%) z4MoW@zC#SXR0+(BVB6ugiBgISkRuslLUOQp%z957l8yHqrH0+=T1vy8rtzZr(v>XK z^S#byAIW085JV{I6(=L|ec6?AYs0y-c0pS) zucLB4i#dh=bMu>8Tu(Wz$a7WxKm^R! zE>aUQ3t=n4Kd>6B0j8t%1XDG*7k_Hye_fNUcADJfee^wn7?N0CK)Iz_&sYpp!1NKh zzU0@h!=ut>6f@~4+Z%#MFtRw7t<@XA#d(n3U|vg))*x>A(G}c#v5CISC~q>2Sk=Dx zR3EqP08jwDgNvXwQZZfqpTW~3cUg9*Kd$QcZ5zIO{=3I;_UREST(q!Tj$(&m1#nt_ zluLm1a7;S9xkN^jt`v-U*9;Yo4<1PMM1QqG!bgjn{ivIy!k4k}X?J}PA7DfJbR<=#0UD@_lqNi1Zld9E zas>6|<3(hsf!oZlcWG1jCnq{5)l(6m_57f;dj~Dcy)7@wX2y1d4Qi2TB9qf5#C@IkVLKP5r(zDwzB8jxhv|WSDlJkKT~XrMT8Y+rY%1kpZ^+|MFH5Gl4o} zIGXbra>SI0!ar!}M&PT%2~!7?5G@{k+BOl=M^k~#Hw}WKb4Ta8?Y4!oyCw&t+XCfZ z>>qI0h@MvlDHaG~8Qt55v5ZFlV1g*2nnl%1(Ah0^yg2K!X%`yiiSsQ(p$J3%`FEYd z%vg1(PI%e8ze>3Y97ERyGfaDwBw3Ah=-2hoQeGlB;F0NB^ zMia#^-aJPuiSF3(VH6@17Eh}e;uE!yEZ#{*B_|Ml9@WL)uu#*F8Q(WkyfnY8Dpc`} zpY!AprMw-I^Lf!Db7mRLf#jy-ZPwXAWv;4hxwu|K-W@^HH+o%kmII9)enDUs!pnol z-ra7`aeGxRd(EWc8|LSc7}6R|VD8;eWR>#EY-r2(}f1l1Z|!hrDq){ z$CfP>wreZ<6b0dHbcYMW$3T?<3fOz&#vXyTi0A?(Z$pNa<+X+NOAc>99zZJ|W%%i% zt2{k>o@{0Nw^rj$NhM@u+GKZD>?yv~inBat~Ok z*~y*3t&5k!-ecsqIKq^U`%i=IiZKSV!v&?L%?hfjvioYLp?{M^=E}6ldTrYGL8~y5 zc#K;|rJ;tO*mx8f??-L_6zj@r1zAtQ<#XU z;pp3CX09>g_!=HEN>@gLtu zZ|ZIhhMAQkVeAj>lB&9UtaZH; zXGB6BTF0GQAN>4~<;X%(Ys3sc2zSuomIl%Cd;W>Vr4111XCwYI=_N3uD8LSe72Ipy zmw0l2$67`GR~;7DCOtX3vY9&`y2~}6P<&1#W|o_mK`Sf$@y?As{_*JX9ebUj;PUS+ z>222}SAb9=l+{8?!s5@+%c6F)?3|BTID+U2z2IYlr?s-Hs)LqT_5+pt4{$3H4uS4F z!`|WM+Mn(Mfm5x^muFG*DT1JxecN0*%X5z8TUC`e9?pJprd5?LQUL&ZNG1gXsF)?s zuBcr4d0$K%T@=FA`_4at4vG%skKQaimX1hW2N02Nn5|YVR+d_R-tt|g04x+;^ zjaT0FvyH9hm;{JBX9jmUM*Nn&CL7l3+=`Jt+=lLKHqA8%Y=W1DJ#+ljo6pCBrGTe} ze?VBF)SDsqR0F`SmI3kVxe!hVx&A`J=`jAFS$HbL`@oofJXGEJ`W{XF13<1FEvI;< zibDI7XafYw>Dhf%}Z@)hiU*93!^il18))`@Ob%Z9J}W3idBw1sQeq8c9f3<&o9Jj=^hUdBw0!EnUuG@C-09aK>72>Hdz0}ChA}CLyqhT{>C`;jG5=+^lS1KHCAh8K)dq5#3gN_*p9+BETyC6V zb9!d~<$lAS?SrqNyTkBq+v5bI(mptIP@`bwnHdfHjV0yN$cwg=uJHYKwVt~GTpoDc zR8-TRn|6$vT4fxh^CIq_M4&I6&loRvj(C!B6xNm9V$ZXh|F|}g&<>r(RHbM$TY?5A z$Ei^<^&5{w5OS2@f4p6!-{)knjw3C5SA)W^wdZ@8=*#Uc#mC|}`Y4rk#$e#t0xCMM z&92<#;f?j0>WL8XB8ZGt&+F`lC9(@JtcjUvG)C7eJ+V6XJet}5B27qo37h9U{z0IH zN0SwWy8{f9nbsX>V)$1BSh!mwd}pOvQ5!c~nk9}rk;hnUSOzdw1$HvedHdLs&0i&# zLAeBQYb;z3?nFpt%yyE_v->b{^c{&n99;vC8H|vbQ<$ovo4iH1i7$sil>*O(@;Ib+ zu=JA!9qgJ)N32@g;vCJjP(Q2RPS2s#Ky?W9KYNs{pG(9m-aaTuF%v&3mf

GDm!k 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%0De`UL-W4P^^XQ(o@&qc#8*E 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�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 z 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

^!rrB{7&X5+ zr*Ry9vezF=V*1B`SH3ds(f^>kZX7##`c6+|H$CuHmm?+GevyrLUg)m0mtgXFt=NbD z-y81ScS4YYA~}U&a22-68;l?`x?(M}$4k!=YV{De8$UbW9>g;KR0iQ~GCldCrR(I< z)>bG;r)41*itA5vNv8gNHD{g-k1##pKnlr31xbqHO^z*-$>+ar`!iey5WVzLX+4pd zLztU$1%M-}a$irXS4kzEMx>Uuy_~DCge|sWRlaYlb(zV|L!APnI+dU$G)H0Qn|aq+ z*t}8^8fF$=y5|_J#e^Ed;pG0UD)8JUp1=AJMA;2Yob0$+^kI&&9RCZJai$LtVlHGu zeTCK#(yYLn4dZ)qq?xke)-&4KGyHF!tS8oh$&I8X;T+-)r4Bn$##?2{?S2ogVE_OO zasj9Z0A&bo0CulEcZ%Y%q7nUy;^4o^G)Jt3OQ~$NE!0ZoY+0h&{At?HBg?nSVB13QWJu1x`xT&Qa zP4b;@CSs^w3c}|_>=g~ zxaF80Rb5nVGd-6T<9EDEH13Z7{`Wqu<)=NDR6fUX7IjY8nUJAFnZ z@R&%3YT=n@hQGZlliB=BJ_BHpCy)7AW;IT`KquI@w=WG@fo}dc2J~ec>sEAe`1X2S;etS zuN^4qSzCef5~2uqs*!Q~PN|thB0E3 zt0sT@d02X}*TL>jhvZs?yd{t5DBxTG?u}eOZk0_uzsZ zOE*$CpAB}ll|e&@jC2qiqwQQJb@(frzu?tL$`{?LHzU`O&V+w}Q;U{wsPv0;fg~$p z>-aF@qXn?4-U5)4c6az{dRYG83K3;YKKu^06uWgv-48RPs` z<@kQg`3ZyOC&9R-j_=w@ddP#?WI;<~@`^08;u9ZzI}8TkSMS}bHl9@Sx5qQx9epPgjxqZv zwxlj1fYbq|dvvzu`L{O99Im}SDNeV{7uiaPI2?;MxVy&$w9@pfEw?Y3gZNcr6!ODY zksuts{S|e|#?Yc4taV8xcF&+&4~h#P$P3k#k1b~_ymQ^N-qk)wAZ6kUd!G$p#}ut0 z2}Zvs&fHa1xasXzQpuY_>Du8Rhbvv)y&VAUHGQvI`$jbuY+Hdc0)w5ZHy&i&R<(A0 z`e0!W001y~*+06{aD7*HXY?1bxeZ@M%5`n(ToC_r<+55|F0Osx5NUtNn10=w{!a9O zono2&^9Q<1XG{a^x#2JQ1ipw__McNgW&mo@^j#c2eyV`CZT-!ryT80iDSSVV&+Z?= z(E%o-TAydehWHhSO-s9}1%=KO4znXz@5C)~e5RDcDJ)aFrgB>-FN9f#u9*~52FvBy z`rR%mq{glBPsFxXDun|IZQ^Rhmk7UU4Vez#|LsnEZERX|O}wu2{>4u-FOlpmy@oaC zjtUQeK|VI7Tq|g33dY2LL~*)MV+EuO{3ouj>P&uV8U;;1z0m)2+uFymi6eIuAvt$b zG7?Vgx^|1jN}o#ik#oiABZe@W?Q#GyK+eCJ@^{=HeH~T?c4OS)i#pYFgAo3G#`72L#N%k-QuYs=bL1NDmH1DFBJya_&9(TGUi?XN z_i(;KAO`ZOM;={ow*P{6*H^&JDLx#lhUra*)wW$#>#bq=f;aRoV)lA#scIUM4 zQrJ!@%=`H}B`hR+#+mt%z!{_rIbH5pW!^Vz+BDmxzx-(u@<2uk5$B195C<0uIzS3m z980JeGuvl3E>7J5(4GJ5L)w^d0CfM)HNJwlxe;D*RnO8NF*Eswy>{&|PUTOOD#xzD zqFS-Cbpq{pLEazmwJAeGDy|OhzR{2DhREX&nZ{<=PeZsaTp37q00A)D>K2%5`}jka z!zz>`g70oZ`ib>~Zjvo$l+2kIqo#%URwPzMtl1ESSWcSHlSZ;hPLOi2X_D`FDRiZJ zdP2F1t` z0IiLsxo&dFmy1gxL6DtlwI3#N{GO~?0g5ywE9lN&@W#W2ymHn6HO~6C;^7O>6{j;- z3l1;!$3g23SPaDUvZPv%^hBJ%U%l9$ZT;iZkdd{3LqS1I)S|vcUm&n2t-`^hn>=ZiFy%`^aga+ zNQJIlMm$__2yk$Gj8TuB+i)U{;dOmEG|g}6o};f8-4@6S&Igx30?L>HtU1Y$ZIr~$ z8?)YUHk>!GEWv;Dt~)B3MC2_=(V(Lm-&ThEu%t?gAiq>PAf&V!VYv9=V45X;2U%q06v^`AptERo0vvL=7x zX)C*o1$6#gxB9Wjg{Wa`l0JisslebMDM@hVF?3!#IV0QXtF;q&ATE*j$>lb5-12zTd0 zB+^R=l0N}Oc^(wT3S6KbH|}F8Pbo|-pqSmhxUy>7b}I>)nGDR?4&6eS69>(O1FV% z2{OgelcuvtxP%5^3@V1Mg=8E%)CXa zO%$T5nNk_CK_e6qUaqhx!1}=_006IIA>bSVz(GIg-+KOn{IXKqMnMD$`9tUK?D|eG zO-!-me-9#4k+Wb6F-pHdB(>YkWGdQ z8;mM|n4c*j;c6DZwD6_S9_B+|j-kpjYyX+d-FQ#X%%QWM?~^U*em}v;J@~cKLI8-# z#+gv0fH<~SHCN(R?Ck8Q_`NZrvD)~3cg$mfgr@|K-^@ENL19(+Abb2NOQRcsd(>gR zeKOBKB6v7_ma6gFwq}PsAy@ z{`2bNk6*0_v`oJ7aMGzO|FtTli!0ABdxNo&DHUoX#5-G0o4nTlBm{h6)pfh|OmH&B z1CrZTk!$dAyu}0Pg6%?ck59(mcCqcZJSwzFlC}-hmFJXs?K3I7kE~O6xQMmtmT{+R zF^;W*m~VPypsDC_7LHPx&bCdJB^uPH+T zt~K#5z?Gf7-0c17PK^u6&86CINMF&u;pv^6Tn}kk5J0GlZYjwPLC@G^APs+jO(M}> zp=&T=Y$K-T8wM?Sw&x<<*GX2UN*r5~j?zzw?6SNx1L)(F=$3xE ztK!$8eT$2sp}(P$1u7n52HS-Xm)uc0PBS;`!nEqIGTb}kD!H30DUAO2v~KFv42nW7 zS8uEv_5 z*hl&j__KZTq^CZBb90H#_Qm@-|J=Jc(;3>o#k_AXZdQ$hxo97k0%r3RQl`~h7nf2w z;R7$ZI$hBE23Fx1fRq_ue97SDW`rX5EKd`1z8@KdR{`a@rh+;^P{6(&II3J1RwD09cCM~>M~lrZVc z{<2=)L^;CFtfpNYRiV^%p95Sj#}J<;HG9T7!AqOgpN9B5&){p@ zq>3WlrVgWJ<%%e%E_olpv_(=7^R8vJE25>`G3g38f+lk>IMqJu z)Y!}v(NiTE`L>o8QYUPJ%m}TUZDN#epO6@u2NjLUnw~dLnbtJ^7k9-%hkOe2|1*~MN>LHpJBCfg5j)}`Sj`!6qbszC}UEs_1cjY zH4qjuRrnt#$|TPgk~$?#&GFm-r_-Xv{%Sy}^DT11e=I{zL}Eqy=`x3QkNc**u;Uu! zR>q*y2tS(Kpk`Qj+#`%zv)+NDRUC4_Iu6W>Ux3TiK&JeI!CWm1j`%>Al4m9STzwj&<7(+tv)ry z^Eo;HmYHE0zrVQ!!^CrC`XLCX_#!;UY*GP7$JS`F>!Job9-aE{e{>G{EDaN-Qq>}p z>F`#OCcFwht(iMJe{;pczroK7_vsK!jl{Ud$f(dtls5Iqc9kojzTNco6_%C-_yYHH zHP_4~=J#-BYxDkpzI8XXcn_Y4U3pRg&lT60?2uUyj$h!JA7GU%1PLl8hg*b|a?!zmmK}q_ot)2On zQoHLWBTRU67-0>TL(s`kDdl6p00LSL^`%YTk)E_3{J!)cSrj*tDfAqfB6a)Q=KvUI z!)*quBr*hrVrLP;N&&H@mjb=9T2 zsAB@k8sXZh+EGAGC#JTyvnw9k2uRTo@}QQonA-zut_-G^vG~#VKye#5HE(MI`OZyf`-#1re{`H5x*62EzcH+7l zt?LJBLy?w!!_ysW<>Z4hb2(>QqdL=-Cxcf;Zblh&Ay&+B-zPQvZ;c{GZXsKesD5Lu z2AEE!G}fUfO*9T!<`>W&p&5MQW%vfc`#bqi!qBr{5CTKx`MGp$OVU+v?3k=x=!Rle z0IvRpV)ilaab-)l;yUE93sdr-k-yG*moTC#YD$T_`#mtNAZt9_qajv$0h&GGN$!` zfq-J5kQ8fSxH2E;a3=Tehu%z?~bt-IUxe}s8 zFIM0dJeBj+lmVDcVAyQvRmg~lAFb{v-XTQR9j9JP|G?^kvzn8Kkw`IQSsjMEDAr*Y zTN@jrQ*yR|LiX_<%i_Jqd@cH8;{rj%qQRp2NBc}_F9xadD=NRsVirqxrpxcZl5H9( zjGho%IQxZ$%%9|NR>#CFEB?+iIFWWJUJFI@e8eOxv7k@8m#M_(VxawtNgPKqM`PQ( z$J}VeDSyD7YT0}*Kx=V~(cxb{lSX}WGA{urqqa0k!>W>)Mh-iud~I_~Un=XpN~K$f zMkPG_SL>V}_bhgL5p{`86_`~w#9VVy3=E_-=0mHO{fE|5QUi9wv9apzIIz7d=vXsX zj8s|wwcXI62B2yB^pYbyif!&GP>J((qWKWtND?GYU?0!H?GU3o`O4~I0VLUxsFnDtWrRk4HRdz7 z+j%}2{VC{;6W%+bjHiJo5B^MFfhk!<9Nwm`{lqh8oeo9#bM;+}7>9863gF{wM z|E!r(N12utd2QRF^YFQTuir%%G2^nJ3-E%KCVW5JJdfgIl~nQj;n5vXs-vC~pB7$` z#u)Coy3{u&hp%D}0$urHv{yMNKdBJE+Jj4LbJk%5q3j#;I>%yG>MsA_@jwYNSvbqK z=(rkgvK~EZmFy)*@1ZS(m#1N(5Q{O`7>Ok5MC6+I5a9wJX0)@P?C%jca0UsA8|j+Y z%)uVrRs}xHl-J}XQF#p8ZRbaFX;ElE2W{Es#s@W?{iym}6`#nd682vr2ExL#VXEZ! zs!RbSu^vO}o0?|rcRuKEqpXN1QnNOfTzQ-Leoed%W8DYtZkUyz z2Kpi7k@p6+!Ksd{51!obfE?-d7d|OZC?yiiC*F(u;dz3rg9wH{L)~hK3wuyp%r^1l zPs_gkm-D7+a8ubFPiIaozaNRtqAI*E%(Zx{AJ)J>d{oie8bZs!ZbFr$Ks{FG5vS{V z@<#d&*@SEk$yTVa#F&Van`ye0#@)G;JxS8s0Ko!?$ikq)YJ;4Gc80r~_O5Q!U z1mtK|OL_!0FykP+F3^v~&hAcu6$Z7SQ=8ypabjB#rtv{(UsP{8DP_a%w98gX?bJlN z-IunwwUP!kWxk0xb)CQNj_ZZiKk+kqgqtx`KNr-uzejYNjGvgTjcQB+BD6V{uwH5a z&L2`7h37gf{!V3{0^Zm4iC)0p0zE_2OLPox397mt+3t1EI9Cls>?%y8`?#jhZg zyTD{*yx1~U7&hK%-R>G<#%jiu81k3D#5_4<^y?D#Hy%RM8NY;%+p-YHh~w^cu6x^~ zMHO1?Xn?j+A0~{gL^<>yw)a5QT~mlxp6(U0ic|O7Ws9 zu3$WmQ&0o43!AYAz_w#v8*mfSgkEps$|qiWpsGBL2RI2+HED&rG+z4t;7&DWhN}5xDLiL1Choe~U%YbVbM4EG9&$r{ zUnd4Zy~920HIRmv<^VbK{S&yorP@C@p;V_G`h(<{1wnmrfz9SHslND*VoI*rU<5;V))@HhInN2gKB_jTB$Ag%wj zBQ1yI)i6Pa{C(H9QA#As-Bq`vi=n`*V>vjB3$~vLRt={8nA$9upA(KbwUk}qr7G}Y zvkfDRPyI4i!_}?bjcz$UfI%FHwJPiAS+51NDJG*YXYS%Kxzlv9jEw*=WI8V2%6t`s z{H^$0{Ky%tGtitPEOTO}m@nx+{F8ZAhgxD(OH>S5D4utEoznx!@Tp(``k}e(f^-3X z^1?<|+ZMbGNkF(d=(hjMuxlJaf&fNR~N?nbDw zEzLS8(z;SY@>?}PbZfgIkroa*lSOTdgv3&N!ju@93K0p94G7 z4)uCPA?BMaGb6rvP>v{CQ8r_sv<U0{E?+uDvTFIym7Fw!&^>#C8 zL0^&hIW6LBb27URB@`_%K@BOKqkKSP0}bdX1n1}?x!;ODds{r5?gI4S)P6V1h5Vo; zzdL$@r6v;M)C9}ga>QAVPKf52RxyO^Gb~5)+1CLq1haSHe)3l^U`{BS6)<=6{sY3A z@iLP8P1Jr3a;y~46339!Y^ayh0T)5S=zZB^%C-9&v<$7<>~eNv-2wZsGZFX1@Io5) zhgB!!8lCXHO0%>MHjV+&@dWTCjJi{Zf54npT~Y?QM^~93J_9iEX(1}(AS1SFKkg4` zCu=R2Spk%6Aa)?Y^enG_)olM=Q~t5}rHu_?>9W4|xDFa?#9QrW^mkDf2<90{qbens z3HSv?alT{-=761BV^7$(cKOY7b4r^&BtDk7R&%9OJtg`)dtJkiO^Z@l?yN$- zIRQVysTNi5cRWq-+(qV1jRgH+JJm}_ARyNlH;(~T-jN%CkFDLK03oWUd|i!#|{VNV7@X?mL%{$=ssb*EtQ9Z>~| zw;1=m#}|M!e)VUqy@*t$D|{)bCJHF{SoQNYIC(bMvYwL*t0&FRlaG|?wTAsf~5A@EcH@KR9*C0U!KqWX>~SFk7e>v=!Wp1 z=W$~p4AxF(8hu8Z$IFA7NJA1flIP*dZxC3lAnqvaHTqlz&cU%Cq|AMCk71RM%7F!( zu5vi6-j~ex&0%@|ob6*gQeV-%`_QPY!^k}ZwFIB6|7!G10w6YNv`djCXsaN2 znGk}RS)e|SVl{ZUj>tMTpjKXC!mBgNqm3(5KNkBGf!yyQ)lF&EC<^d= z(^BfGvEu}i<+!Xhcl|hZ(=DN)qskWBcV+G|Sb+7#%?zas3tXO!m)~OTDxY#!JL-&d=mlB&}CSNH5 z2;VZ!LTCa#HTHU{)m^XLt5DYU3U~vhsA?kO4ttdbqw7P`L$*9Y5ZpuY*tsZ@VHtbm ze=CZRP(hq|jl%C?4-z=!P#*2vQiv(5nD(pWx@W2o7bJ=>I}k6E@q!ta$nAu%`n_iY zj&I0ypd&^o#$^KDwdL<>UA>b{?~8|P;Z`_Ou|`i0q`DMdQA1cu)UNrEe3fMs38434 zUHJy73f(J=rPX;?)6GmQs8RYP^{{Z8IW&qcr9Ma=R0{7a`X&GmXwq(vIaXxPlSuyq zDoSNGx)`<}pMiTv-)FC=Np0sJSDsWvxTd-8~#$ih-b#(xoZ);l7< zS8QBtu`U@#qDxNHWGE=Q>EaGCV2j~7=vjkpQb&ZfDjZiHzdHs_L765xpF`d;8Qzbk zETe$}2u$tnx8>Q0<42|aLj}Z|ppr1esA0~0T)<<2!z|$08=CIi7U!^>79HEeuOhT;R-C+ zQyCOpHm7fD@4_0N_~UtL+&C*b4;Uf=%tY@Q2qlU!YuySw)4S6=H1=xut#>~~aO_mN zRyP4GSD#(2?jTBw0>3&5wK|?Q2>=>2@gG*oJc;2B76QV39vCXvsM^;UAeTkrVs+COlLbMZ!ryKy_xnS{UPs?ufcBf4VB zN9sc=8uSt+S-R-6UNNA3|7-zV#-1Fz`m1L%TDq{1f5ZFzW_xhx(y6f@7WK8AWag{4Q z4j`^+T3nDscZV^jo)SN#$P<;IZiNNK$;0t5+N21+;G7!huDwikOSUl%S_n$InShhi#!}H z%{NNypLFJ5hJjwKrd1HS9Jz=D8CyfJF?cp}z%rVe1LTvJvl$ZkRpws~MmgSJ?We~? z+AkY(B{oLv%-;yPYlLFMQ%1O&Y(cc!sa*;3T?AGsI!Gj=ET8)JX4lN}&6t9Pc@*JI z36qh_73V;b_WKceYFLiZ2S|M;EZxYD5@nIPftOHXNBgY=$m~vKU1bFdo-VJ%#HA~qVME+H)z0GUL;-9 z1D*hodAv8(<`A6+y;bO9RMU}eCoOzcrMU5;L%LmG?TKzH%l5yu$>iB*)w{XO1e&Cl zIlf-WX!gT=CBatqhVsb`GRL)HIHZULKgk5k6-kCq@r3mz=sO2?M^^Xo`V;@XRUjU` z;D%V31#AHRw7@-W5ppOc-1BUseU5P-4I#i*BQit*y%By z@w6Hz3cRe-l!4?x&v=o^mtFaEV}$FHT) za<~~}Fh9ynHNsfgBiCN&XO7<7+3RrjU#l9;R+ON}4T2Y(+ld8SPa>cZStJ*g{KreU zJwk^GoE}%uo*@X;WiDEs@_YP{NR@|p#DbGeWu3?);20z$C^VLY?(Q3aUgzJPSAO1} zA5;Rdg+aKCdr~qda7{I(+Srtyg@hnMzhgg@L{mmKBA0o|;#|_Jo=+u;KapKf6-mlR z6F0T}N9%nP|A9rjfTP^|KTj!>Y@>0}HGRvNx@5A9Y09GiF25siph#3DmOzc`_;c&- z2lk$u2VPAp!2M%B`w3xsegvgHupj%#VGom05bG&)9}EBi^rKz{zL($U}=97_O}S0Le06~eo4N&&o{GIkx8u--Q85;?-{qE z4!{~$Y3dzG4`$|5r3+#&HHwY#Z7+Y=*MMz2ZiRWq z7J1trg|h{Xn%vJMT_ZKo0z@L(J*L>i$qq4&5q;fH-#QOylp1wl#J(Fm@%$Q~|1*}4 zr>wvN$Hd57X{wi{5O&Dxcay<`G3?~7m{xl$+|n43+D3v+LK8Lm(h|*ffH3~`#(R_Z zlxjDx0Tv4Q488h)&It!EJ^;;a0vYO`lwp@=Ysf7+Ka3i-hM~1#p?!hWjlZH!OQTA| z%3f^8f4Bcc(mES4M3xD9qXbP<%k~<7Y`1z>_3J05^k(i;UC6Fr|0{J)Pvy)}{(U%v z6dq|Nhu|(n2XUaBhTgmPAKJFH0mVTl?(j__{mx-wWz>)lU6XOs-_fq?BRF5Ls~1|V z$8--Tj~6w;ua+V7js&KW8tny_Iub4`PF|t{SY>NvBNiEXv4A1Z7SU-s&A8fATCyGQ zhJf-Rg2x}~_RO~+h-DO8AmapEh?T0^@+%6R?zK|%ie|AR!r4xhch6i&t%I6ldgZBcPxTD^gNGRnI)=_zrYF0Zyi~ z$sEt*YFel~DKt~u7w5yg_ugfa7caB9d&of(zQ0*vS+9eVo zqAJ}A0@TYnuru=1x|2!EYVt1iHg#Fp#Cd_NUYg`JlM9-t?DtE>lbK#e<{Z=#?EG*b zYMRxN*#$l?4(eT}!HvH=`(EJVx#J&nPf*u$W&W2spr!tO!$}VLCWc=UMZ%vkeIrq% zHhh`7qK&irp%U?@21DNb%Pd4ss(^h)ub&Tzj&A1?o5vCpCp5aX@?Q3$B}i%i+eRDX z;EmgNlofmH(d@x|Nkj_Ax&Y(Gk92O#PmsjD)Sx$# zErZrpF)I5*FUHp^my4hu5*4TAuk{&u?6k0MXOms63St@0Gy z!Q|WDNTHyu6cG~ZoH-lgldcJ?T1(58JdX`3*E@v!Z|pi@AMRgXUeKPW42Y`>z#ocl zL^UekUwFnxfUPfO3|jnE0i<=|(X{cA6SAkyx4_4r27$!1ho%o9Yh*WeTJoBM8?m!d zlagvWIn=8}n)4(%70HyPcu0La^$}WCEeGK_~|7Mp#U&Fc1DJ~ z`Ck22%uKoC-KXD+sUvS&$BOS)eU^QZj{<3z+!ht^k6}-?li7~LCV;&!)JfUx&(@gO z#rW}O=7!74;BF5q)o&LSla-Q4K;W9+CDqyM`f|LwA|E%nb4BRl`w&(bYdrj&s5m9eDbaWd)>VRvB6C$#Y&l51uO(U zZ^W6-MqIB98;~NtfO!SK74gxmHMxz@5x2K z9C2fdn8&-J$iP_ak~_znBloTXeKcczKWwzJTFoY{0aw(NO=5pso@$-gx)hG37p%7P}%{eb2K-UeGp_@e79I^&;XUz~b6*27v`!u4C8AKgEUPrsXq2su_@;d1^d_5sTBr~Z!h zAg&N{RGBGBHL(r7yot5QNV!akVfic)Ehhd$u?m#Sz^*wbZYyu7qQ^AHe3^(82n-oC z`d+Q-g;ceVuWaucGZdgv7Rb|+caK4NO(Bxfo7^ow2ucrO23T`siDy)I6Al!gAL z=fZh%^fq(;D4G8`lfNibI&jSr#8o6;=~-(W^L}cs36WL8nrM^58Pe_HWj7jLGn@U! z-_#q;D`pFBG8V3J(6bNt7BQB~)_7C^oD~vLw*6^y*wczua|I+lUR@$#;R0G>sQ=o` z<1fpEO$r5#89zbmAIDQvb-toUhd1PjpT+4Bc_)GoESzJ6so&k?EflXG;Ytk7oFP4R zbh8PezMRzF0+;xAD@d<&(sAly0U_L*VySPhf6u@ur5Kc36te3QIoH1Gwb+Atu+Iiy z_0$09Y+!%gT!S_Oi^}DUQk@L&o_$%h*-3b7Arpv5`+v6m~cc)kqXZ1 zl1bKkeW8(s$H?uuv+~n-kR|ig)LF>vo(fo9LM%bNqp;RFd> zD4+R8`U@L~#HiWlPCNg1>6?oC@4Hg&(C;@y}369R*Ct%Zf&OCbEp28m2YjaS_trH+I;8&3wPo z4b)rtwp?Qw$F{9Jae(%43Z5()riF`>%W;PtJ@d{9|NNsmOqMHXu364N*GqV)nutb= zqL5E`OTLpfz|=$eJv#{@1=?dW*Gk~r!aHDOYi%PJML*PQ)IUWN>t1I&BA+h60$RD8 zJ{K4x{q@|4oJcrKvpwa4vSVaxvc08eecu`RvJW?%wK7m@;y5|WI* zx8MiD&Z_e*n=*oI(z*&`uA!EWY8Afy8v^{uoR4P&Ho@1%>yn4cUcznMg2o=D40`dl zi&?*gOP6}K=;s$)>gV({zukV2R`CKKRf@#UD9$QX+x2mo&rorN$in_FW#~9*pLT=T z*%uMrzym1YJ;BNV-ZO1l3o!+C0)0iNa&q^6DZK2I9K;{LRHd`lKA3{^;NE^3k>C%%JR(;&2KqMT( zL>@SE-F!p;k?WaLweA0uIIVLxj|tJb)|rl`Stw+QXGyWD(?y0uND}9} zqN|f6bJ@R!a6JxzFT!RF<)9y{kv07&^Ol+1f076+5;8x>fO76+STOVqC381H6iNlYKtY{4X1nxeEfxgA?>OTP+YTDG}1$l3=ys#$AwO*m0 zvUm9$ptXJw>8Ef6GYBR=1WQR8-NS<^^9GMyX2@bR=+Hz$!8dm)FCztl#O)DA3I2aT zktk7yN6wpNni`5bZfO;0Y(rF4)~}(ETI+tJ1~oI2qXGgtUPIMaQR{TC2P_$!fX}n` z8&3J=gu#Qy&Q~*xXQ=oFPI)wCQ{{nk&7FU}pt@U`(_gC%0)(cf>k{8`rrb_|i}p`V z(jf=UAUXM$Uo?X@lZ!N0#-AW}T}bzHiQ! z#Yx9(qEjM_=PcGrLw}!LiEzz}d7wPIHnMOdRME*K0h#eXhJS}F_J0}Yqcl#C-;LFK zcL0%LD92LNFkyf znQ|{iIezkF{6pNya+)g+fRi8+hVDakkjnqrE4`@$pbeHtK*w2jDJR5dV>3>KU?vgI zly)1Ok^nkA0J&&y-9nlMFOAFrT|^6!)EBX&VHY+ZE_*9mFeW~{MOYw^_j+c1ktcZ~ z*KE%?sw0eOxc_0`XFS0*3?i8stsPo_!$JdYo<^-w_P?nT-GcP(XF;f!w?ZwqY0)Y=<}xv z&HQyBW9E8}gEp9bCm;>BR`iOQl~Vys<;rPy6@>%y#WXJk2=em-zLpliB5|<^#>Kil zmO`|n-*-1d-A`3bm6n|%0U_t^y=(%mP%I$Gnawd6ZUA(T1e#|MJbT|uLnO}@nvz=S zWBfChUuE&lFB6AOsuG)hPgav#}azSuue5^OWfft zGS8FM3QlmYf8jEAfjJ&Ueg2}IMP}!Q33waAkt}R2@XFTmB|H&2QY80_CQl)Erd!&9 zhWe8p$fXy4$g#EFDVebTOM4q3y&3f5wsT^bkF3e~pTP8{wM=s|lKCJS1iX&*x%mCY zkX)yjS>%Ag_`9!Rc3ktO*=gpJIQ2GFw9!_?1;B}=cC%6=ORctM@t*x{=|N%3ZsQaN z_zc|xq(EO@nCQ6}19uv%Jh+xnJFsD6CQ&NWwdW23IId0Sa;Gz^_r=d|xUuxzVE2zH zA-+q*I)XPqBxvyOSTmeG>FXxLgQ`v2!Dbw(OqWC!9~GxoO3*pb%js6~Oa>L$pkTDPlXA(kbsh!wo;q~CqLKY!L} z-)Ob^iD$)qN;<(80c#uZb>>i755~}XE{k(xUrTiP8wLtT8_BeD)zBb|taPFeH${uF zf8yM+6{^|Mk>dQ@3l8JrOYTWeBP=)OqZMhJ&H^kpOHmpMoAAzDsl?V0&4irr7a#S- zA)GhFFX??0fDDyB0`%~kDG;~y^ zH`CjUNjVg}cZa;RR0Sb8hJ#D$9oB}d^E}s5!Mq=Bng;m$w7`@?+$)RxiC1e}WPa+o zWEiShPYj3XBPwR>Vb63KQ4*I!^HD&&QFP#Hk82NJVAnmIG_QN3C9GBI_H1fzp=0yg z7F3=pd|R(Z<*m8O$6JTHuEXz)gU(7}^zmIAW&>z5Ep@oeFt@*Hd-A*WUE=RN^6j`d z>hG$|Nl#O;Is>u*-~?N=MtiOyLex4+U9cMuM8CD|OBbhLw<1=z;yZYwj7-h>itS+D z`V^BhJZ8Ug)vHc>;DN4f-Po?paD26BAUs z1+=VB6u68$K_!QhB3M;TAxHg2ig7=aTzB&F$d7%-&Iz=_f%HJZVoy#z(Mi3k-SzO2 zf`wCaYkJxtecPb8b1Rx^$5+xFQhUhWA5bJxWf393^AbC>V2)_1rY*z9Z=VSonD*y# z&f*v3u4@P8b|bWnFk}z{TzNKQKU&ZT!8nE6d z#Cns)W_KY$!jyP)V$P<5MoIu(mXZi@mBPQcoxs_^icR1P7d4NlLtP@bl@m-ik8Gfp zcqh~41<|2nkI)!uGNAcL#U2NyiK}<@U$o(e04hM$zm+DXr`wcixs0)Qu- zR@#e!J2H{Qq$ki$8lIr_@U&X^P@F(@m>Q4H{`jv zh{5QzW7_o~ze16ULiz#Wu;;o1pDP>{P7`l*RPvB|A*Zu;KaQVWSSG7@^GNustC%Vq ztbZ$4X<>O_;#v7GjGLE1Kz0o%FstLR5j`Oe*f{233lp6C2%{(r{)>+fAf(eq`2Xr} zJk-^d)bl6vrW53}GcaV1-84=`Micp-c8}(DMDK6BG zMzDmpEoEH74JcFI>sqI8L5$#w=!=~lMtE(QTG> zF!8?5Duc4XY?A>`lRLWa0DBC5>oXUhuTt_UExYrtx7|BPouAM)bi*y`nQ3XYIHZ$G z%{f%2&VOdLRaH+LP({L4jql9vRO)+p?v*8Vl)YP!D^DO8)sY4L`Qwg;U0vsX%6hIp zm2L}nPp^nChJa+NY@`D>PjA#0UM~(5&5d=!v~b;94z<;x$(941fLFFx-v$aeZuqd9 zL%c!##!U6l15s+^UMpYv6&1^|e!@O_p{fQgU2?HB|Nz1jbqBlzWF-Ydkc8yYku!}sGyZNusqKAYM! zXh3TDT{zH~a}HQfpslDiggOL{-?TWBrW3%Mq?oEF>4Pit&_tq(&_F0@O*|it%h@e8sA^unM2gv z=$0ChDLj-JaB)1Zg;6)M$iJ_}nOhufJI}mVE?bUP0pF!mWJ#@e@IvFA5c1%Qb+Pd9 z%Z5@7LZS}KbX{WUnMdHJ9VJx3DwuIMP~=&4IBTABx?m1N5!?3IwJ_Dosyb!ZIl(Ii z9-G%m&`aSCFAh@qwwtK-9B=ynd2&F|Fcg#R<>2>KalNRdJq{5;?5V|`5 zaLd%J81@EL_FRZvv0jS@*j#{OVv?Pqt_tt62u+Hz%jeH^w_Cs^u5`wjj#0T%eNSIP zGnHdci~~+u4Y;4o2~&;Idb0Z` z^NLp|3?iT@i+DgOpT#q4bGAD@Za&rfciL!vP}j)TspfV)*!MzW>TkRi)~nhd!g^P;3P4c8DWZI*dL@Ji(bt40y{Q|BX%KPeh=S1j3MOmqM9m+ojSXm#I zF!E6DFjw^Yx*yz~U5~ySKgLau0R1@gi3muwV_;b;QGmP7Oez397pY^x1e9}v_{4~tmIoz~*=^MD{Le$hFzy42=Zwr3G zj#md%BI$=t0O&+z6QQa7VM|s2TTV~c^agsX3hc+`-#dEuMDYB!sH8t_4ZKDp$I}Tg zpNX@DfGZXh=%zcf!nxT{_G0DnMBfkb+ejY=QK$-RNp0KbFq%25Ql5l1A($99m!?(i z3{X($Yg#OV*N$D3N3w>PdhcDqouM*&arURLOd|bkksM*aAC9LS^*>&mSt|C9AAXWK z$27Txea!RhT4H@04dsg_E{jp8?vDjCRbv%bZ+Po zCG}X_mA)*FV1X3i)SF|OmPQ|Zw(CM?ixX?XV|BA2bcMo%t6L)dnH)sn*ve%?)rWmK z`MJ1WJgbDg{F+I0a&ehVR?XY+>|FY!=%xpDE4*sZH{(=~BjKzpZ572gqV#_#+k8Uf zCudIS6oS8csI3(m1k9A#yyiF!b1OWC7~+cIoXA1CMhi%xtHl(^csGX|M{;pl;F9dV zmd!6p#RV8T&{@^WAu30E$7=8tySu!?0&~{*paoyymwmeTYU{)=*6x3uSAFDO_{@BKcY+x> zCuc}S9iH!~cKwZ$#Xv5k|H%v;)nqF_Lmu3|cdvLzs}Kg?fKzL2-tD%uLyl?Qp%|OEXleYq}(%tqf-!VfiX4E;e2C5BD#0V@%eq2CdUu=90h>wH3ml z0ebw)mWoY4n*F0mNGiv2{qkW}Wg{viAH<~Vh&{df+zr>#QullvM9Er-D29bC;U|V2 z7=x%ELq(fA^fQprivX>T8%VgFErwPiM8akr%I8Umn{HP_owvA@a@u+Xwtw zIeJrsPIM3Vt4-^G;a}1;{$|-HYP2N5d;U(@mkbv`DEi7T#gu~t8|c1Ptcd8y#^t;8 zX(Z>ISEp@v_YNl!C1htrGPY7B*4YeOnfoKfflB)f&b33Q>vuLriohc%RLdYHVF&U5 z*jqVz)Hb-_B|nqA42DtSH4vNsY9&VDS|mLMk9C7`;qmo6ked*p>b^Ur{XhToj3U44 z@)SIJj~1dYb7)PU%@Ux&C|0mnTCx~;Qu@yY@M@D530@e71FLaTL50;i!c@K2axxVA zEo#0f0_6E{K0*;=mjkikUCy`~r%m-?eJhbQjfnHxef7Z93W4_LeC6m~;guv!1{KGw z%85^9=2~!6Z#6vQ89L;z)y`|zlaf_IKRcqFkeYl_sp-=C4KO#|C;#2z^H+-8W+=Sv zUpwm4sB}4wawoDR2g>PIn+roVwF-d?XmbLWiG1M_+gG!4_IKMv=w!qh7rgQo{H6B; zjFgrC{Ramxc5$ip=F3lNl;oLbdoct0e@Gn!?4QwipvnL*6IFF;b^o&+Rm1Jw;3K7oOh}2cQi;tgj zCz)}$0;y7`G>k`rVJM9Z@xPNpF43Ma>i2s}qr+1wng2SSi$&9#Sswl9+Ny*SKzmCD zCETb9*ISEGC@w{$kryAB-%hJ*aEeP(XtnrdEc+zEjCo;#I()?vwjNv|8U8K) zGjxqo_U?d(cG2jyPmC)7zj^&Yc=6I ziD*u4SyrTMcvd|vnf}?sWr`Fo;_u6WKVwY<-2>}nefhz*kpbCJX%nGAZJE~Z5{P95 zWadH{DUE>>TUB^_vKazc^@%sN`rD%gz2J!o-%mlqa-37x+E}Q{9M2)2vlid!`SyGl zuizvBytp1vEnakM)74G}9d$+>yPd5kuvYa1Jng=D@qOh$R$BcWt;nG> z%x$S4=8%#>ePKIhDMy@7qMF`3Qr}@oSD60HW2WQwNWNn(W*n`$vAShlsEGFVJ;xBk>r+DQcXwba_VDp>?=*UV2K>{6$jr} z%yE5z0)_0W#zTOKP zi_>ZEinb~fC#{KJtMQ6&!5KMT4?c&6RqUC0dY!wf%4PI8utJQ%)5EUlV&A*c;j#m^ zSF*N%+<%utzh()gupn+lM}HDTybVmU`xwSbUAr{ep_Tn(g-XXkW{^#FtN(*)`-%TF z^oCDR7hGf6)Y62QC2{FF+CQS4j*bGdi);->rnTMIq0rTv=iiQNfI9B&^D9^|gt+HG zd)YIpE=D!hYXEW-_If&Rl&&WG4?=;94hohXcxM+ zI`U!YhQaWg=}({lpnrXl;{%p5>^o_Tg zy1uOBQI-FyDACIJ{Q^X{IQURC+h-Q)=PoTw-}3IPrtYacz5j*#z87UxEfyfOAvdqg zlpikjV}7^#&P55TTMOew8)_$(jzV-x>J^&PC*B0?Te2fqe9mX0k(sjtcuk2w{vF9T zaInq)JjNA%L}wCMvkTTdDGRCvLfs)K$~3bXer{0uAtAzCT78&l#uPicY{hFM>=awk z%nsV{KZUh-c-T;baZ@+6{ziI2A&cHIT?(^5n#*`AiDgWw`uq^s()jIPvVWT`js==0 zJ(=?AkU>JYP5$${lQ}lS_W5HNyaASMk*U3yaMUypsWCG!P&DbfRH}^+k5-BX^C;$q2Gp5Q|B1Z&d@!;qE)&^btyP^Y0i!oNLRsiV| zLyrNCDC==K`6h$P7g@31O-B zBlfmzU^tCgS}#49=kVL-n;&Kp6vwWdjRVyVS2ZVG89zkYS5fqLp~QF@KuAFVT#P*R zuMMhjPabW2SMtf;=4~qeM?f44bs_BF+ikeeyH=@M@FT_Ku0u>xb^(C8#uh3fDCYpA zeLDXKrtmTlKDkq*f?|?C_EamJJ_mhy=yco5^mqFIpzQkxx_vP2c8DNOa>rhbn15lS zdvG#lGhiGJoz@`zKQK`BzX?aioQp9&O0XfG)l@Y>R|~_$RV^Nb5e8NfPScD^1|$vR zaC!118|*BT6|J$1i6Gqm1zRv;;_id~^*j%cfU+%rDWcRlXnsZFC^TDgSWMm#muKle z`o9*OE94TsJd<)o3O@!kw5iIeL5!m^mfU!ASy8xE5}*7BbMrI`)^ll&^2w={YwJIw z(~9=Wc31e8R}g@gQ)nX-1fqpfYXLtYqb$W9{T7|nRweeQTWDEW&@$Qg0 zf9$L+l0^CIVp<VYf;c@IWc=ZdGT#OafW?V36^^FP}c z#Dbt_MTG{o^V%2mA;vj8mq1Otj9;{qOQXyJpui}dIR~Yh;W3O$pWBA5F}}>5{}Q(; zaK9Q8%(;dq@iu0p1~&61hhCk@?xo^nNo^yIrdMp4P_~RMztcJD zp6Q+*i!!jc{e`t_d>RE*pC0ef03;oSBoHLT!`;zfDi;tkFRiBe9IzZh9rwg+xxqOB zRbCQ}p+9zZy-^%=v8K={X)H8w47(B;PsHUOObbey&SXttVUgQeDQ7RDmKoBB$Gho% zb`2U|r*0%H@3%|LpKzol5UKTEk!21~mmdG?0QuAfUdjhmt|LahiraGnFR&>PN{N?r z>cbc>h6MIuylno?P}L8>h+ruQ2-niArvdO8-q4AOME!Vmu>v}-Wj$C<>BdKB9;-GB zu1u<-v;5A-+_1W2PXxwS7(W6jMPK-<01%()s&X$n3l}dpw^IXVd9&|el$LJTU?>_$ zllhH!b_(+AbzSLO*vrFTP4C1a*0M?JJc1}6Nw6e#{Vw$)H;3SzZ3kIKqXnw}!4T-$=~o2pn&fOUoQ}sw zmmItp)5uZTtUo^Lwgq9VJ*`(ueyC_#4D+lFsTYd$k#WTybZU2X8L-B9E2DD%yjS?W z)(Ys;J`M~0?s2cS0Cogr!m`yL>VTpr*L$GYN7&Oyms*;C^EK#fg?IK=^Rf!GW}_xt zi4m;j`%wrw?jOmOq}-iI$YjwmKY3A^q`iVVD;C$rYYF_Hnz*SH04z2gxfoYDJFnc6 zIA~!d-Q|$HMH?t0uzBxqWa|)^iMwuiv$v{Fyh)55US$lM|HIt5=6(=_tF6>S5(=Rz z<9`c+x}06ubb?2HKr^sDk{WDHEZuZ}G^KQXlgn6CBT(OHc_5bArnBLLP)dBAnc6v@w0Uoxk?Y^W{$RbLwOyajcGAy<0Bl?nh+)W%UDQrAF&z z`a*+A$;E-nzD@;1*W!0BlvE6JEQwT2v_0p?i-XjiHU{$Qtx*I%40q#{Js_6%=T9yD zOvRWGnYwH$!rxOhv`)|$1&_1GCqo*Ee7Bj-!;|NP8-c>(Y^PLDa=7%;BXWXAoUcI5 zsx@R_c1N7z{OIVt$0sgaf^~@bYjU~l9z#Aw*|m;AON&d}?X-eS(vgy2LnFFCKCu3> z>lhl@oXEBT^XX%-S+l~ssD@%X5yh`S_G?(w`Hui~alyN%HvEu(3Kji^9r^e5E!&%&3$1KQJvYy|6!C|0nYYFyHLkVxOwG6n)Tn=!$ zPwNG9!&&=!f<2cMVdmk6fSEO7DBC52iW*RU(6qs$iV*zl9wC2XCH2nNuaY7{B`2z# zP7zmK@ylsN@4YU~sjk|fZFbZr+r6Ix~^bux__ zE-YFhjO}?z1L6@*h8Z%q*urm`yunKj@kf;X`JxgaJ@P(uJ>^5b6Gik6dBiAV`!>Ln zi*)$BFBKn!w!ut@ax}U!{_S|j_m|Ok;vk_eY62^qbwP~W z=h`_mtz+wBrOumYP-ZCFit8ZjLWyw5CJN;f>ojGuRinM#WOW&q5D^LO>ty9P-+km3 zhrc$ewe6IE=x1p={Qe<^9Ln}Y++L4sA(dVCdIVq~OQL99$k`O#fPC~lOU*?^G(Flb zCG3aDSJn(U+4*pee>m28g6?f3jql}_+%;OaamwGXCB4{-9vkUO1E*_j>za|tr*u|c z^(FMUi4C9_PbKNVcNG)1=IEg~f&Q)skCN6}Gj>4~!Y65*GDNhU_BSNZ-nP{2-!lQZbml)biy z0}%S@+=fV!D5Vt|a(lbWBX5gL(kR0Ikvw@~O!F7&rQ{WWhIB%_c9fKvSh|D5`Wl@2 zeD3leiukr|u_)w$nYt^WG3J7t4g%U8LAs&TloDuWcwJbk|1Tdn4B6+1ah4o$ndp{; z20eR_-4}Z@2yhiW{mzg#R%64V^T))a`I7xcrVy>iPNLl!)$rQ$4x^WK^pMIP@VS() z%sW|rEUV`{g&g6h5=)HC&|*9AfJaW?HWPf!`6EIkH(7`Ve50Tjnotzi6wzVj$pujw zq;n7T3L7})vQ|hDdp{$kGP~F-%^+4%2mU>cHoe8`&}j!n;)17AqE8U+)E$b@c-Q%z zWNLu~i9K0K#YzmpaxZ?C<1fbT1-RMcohtjKU~OFW7Ejzz`jw;? z`k0lLH~oBkZO1U_MA5c%d5TB676u0ji2fBRAvxhuF|l^!mEcl0(MdvFu98%O1mL%R z8xifi#7o`AiR%KLn3yidbz!UAOWP!3p>lalmT&~*hB>BhI9vj?>$g4QKh(Or)eSS> z8e(iL)Mi#P)gFP={BS&xJMW1m=unm{k>W3KaT?!_P}Ls*(S<4z#%8+t??X!8oeOvB z1(nrh*Dt&>`z6a}D@MLEO1bW%_32qWZ6L~}&|e_nX`VmF8}uP}uLF~kq?K=wWDvHF z17~A@x+)JTEire(8#*FA#w;wtlUyk(#1*C#{OSCBL?e!*(_J_ETcbGj8qQe|p;I3V z*T6<7Nr#C9l{bKvjWzs6YX*L59ybaEUz~A>CvU95VQw#Xl^6+^9=?TG)NE{qh5A{! zYt*F|#t{+sY-XHL?bEdO(O^w8+NLIs6blaJf-^S#t>S$3NFCFk!v1N}*8J&*bYi+y zQ!|HRp-8l%-i6kpT)b0PR9(vGu+Z|G$JsDTtSeKVJJ+YGy!VY6Va5)~mAu@hU0$4( z2w0h3(pzPe2$tV2sNB|wc-kuP7m-bvQagjcjTaq4;ADY_lFi*LE*D`TaY| zWm-OA_Y5V_82H0gfa1`aneu+z*4{QR6FUBDYoW9k(>I>i zcs;@7Xjwx8Yj0*2*)Vw&_Ob6 ziP!hCaL#shlrpqe#$Et(Hp#zrsiuC?@QHSwnW{GkE{&q|oP6v&HP;3J+``TKN{aan zvOiDg0%&O%*F<`$aL%+9q%Rgwe;P^Og3<;a+1=4*-mMUja=YwGi3R@$tc4k;4oy2u zdFfZyI1sM?lz`TF6tM(?cL2B> zY}cM2(KB*@6Q-}4mIpSkI-gFoF*UK`0De2YCs+rfr6Wl*J;21U3F2Qsj)tqi4zvL< zZd_1s0z0Ix8qK6{@;EL*1?|ZT-Awn7ryRg4CR|gs^bq?S&du{sT3NqG4tNo3<2G?#OC#p-N zM;zO;fgNMAg|t9D`y#Pyyr&_}&=B%Q@=F84W}~`_RU_#sCL6}^*_D>`hWW*R|Gi!@ z7)!7HHJ)p|Jo-4TD=2;A#t%u@J=*e;i$#upps4wVcr{Dg)ups?*{zc!AU%-Grcz1J zH&)`=ESA{u*rdTRdU;d1MS1!!D`2$>es6?Duog`3xW1^Wd#<}hW*~k7UtFFSNWDQh zF469rOG^&LcDK>DI@riBc-9L8$YSnvz{q5~Ls8%Z1J@^JUEjnO>hIOal5_tPuJ_ZUVW4>wDx+d1?B6ird`qziwk@+PELszD&*s zU$StY=HM_Dt4dlYgiBD~XQ?96AWoO0|6M@>>+^U9c<8u169MAeb}8((8xlN9#W2QZ z(F|*D=NCAn5lAgPC6%jO*N-VD`2p`RmL!!#j!wcz6&fh_;K%C3SGC?7c5qLLlAGHk zO6w}3Ebv|0I6zj&fBw`pc<=&(zsjlO-8utnAX?c&l?R(1A*Yth$3Q}_+QWa@o=sR@ zc(MtWW9ackV!nL&JV<}BIn($VQAQ%KX)3Vv<~wx=jLaelS7p;LFGlWk_!+xG1eh># zNOq{+e)G=gwvEZv6`M!xZ=rzdxZED+?m}17Cbr^Ee7&Y~jqNw)4Rzi7sn1~NkZjnb2a&-@Oet;kE{|4?~9>lDb&)*<7PQJ+ zl}~c@{<2zeMQ-RQ4TQh2`xKP=3fKifuse$YS0K9S!|gv=Y4FS(Rr~P)gGfBLrjC>-pFfc1Q>YqA35(@gMUtdaZPP@Hnqvzq@b1? zW6y{PbgxqwU#I4NRH$Y42tPO>`~8v;e5|p|@G)EB1*UnvPeEV%rypuGOC7FP%RsyW z*h-dis<(`5R;?TpB5?wb)?}yqaGGpue(OdRM8YO@Ib_#r-xnxsZ_1KBxv+W zldHvDAe{0OSs?`SCQ`sc*hbB-MTnorT8ThlRSJ}6EqR+ zaS(*jt2O?$b@uosUb+)2dpFHs!P!g;2|^H1u_W!w+E7dX&kx|>A;M{wull(pyT-eU zZvVLx8>0Nb#UQ?Iia|w|IkN8?XYG2wY;vg&+t1!iE@;H0t5p6#1q-1OU#BwC@KF|x z?6{yUFxwnePo0WR`CGWoI$;-P;EZ&eD1kmd1|MEy7TPmm4P~s)$BpD6DhWVxk&x!> z@Ew9q-dG;8-7TzCCQZ>f=@@X}uwOPhx4xfKD>r2P4f`(kaHnDby{4~9oW9T+Pw7MS zB|({jb#!gk)?CQJ-XBPbM!zS{pu(EyogzMzws6Sc+z^drnuFKZqWv@m2V0lNR~d5| zfJ!9i<-3%WSAvlbIg)X|aST$zwLW&ECsgms)Oa;m#A<~2+<$-ahmlpLNvD0|F9VA< zT>he%=K0)<4a0Jzqxm}eua%;e5QuiXnFD+YybqK?<6Z04@P*GJkBhr^!5mo%m!wew zw$Xb7{5d@x|wli=ZShiPEImFY=_Kw}J2kFM#SF*@0ckKJ>}FRKtgJyruJ2kG>{Xj9JWXgAY)k0# zj>z6xGCz(E1B~hHDfnA^b;E|i3AvloNWL6`#896R3NM$CNebefJ1IO8>{dm3olG(K zqx|=?3N(!fLJ@1K-#RvAan(71U)7wq=7!Gw(U`IJk3~u1Vgq1${{k1vMMJ`1iZd#4 zy4v8@xsb%uIM1 z$}N4$q-We%95AG?+vF@AUsa1G=UWIQGsuxk=fxD{4Ac`j_KQ4eq~9@GJ4pWXuR&R)wz$Uyy6F5!QkND2mRLKER1C zqoB<5M4yK_&=nU)lNd1g>!M~HI0~MW{*lvO5LTu;@?#Ex6*gSn{|4lb=>8c0d)R#T=fBX7!%`0m7x$7etNWPP6G zl7CYff!}yXuykWSSp4)sH=;$?RQuVlD~s#L!O-}l=j3mzE`Ss|$){A5oYt-{0b%f4 z1Od{v6GcP4Um!XV`AH>faLkGtp+q*Krp;cC|KBjV+4vy{-gN!B#&)l9ewkppTGEy- z(W>KYenrU0)l|s@bfg(G@}qQaPG&cQtZrgDRzD z&8@`l9a6cQye8gZw{^SLV!w*)4ht@A1AG^ypa(+p8wlc`1WX=NyJRqUC0`<0;X#tx2)XF4!g<(tRr$(GMFH5Hhl6;1G+||j zFS#1Q0dB@^crMMDU+xPc^NpYSOjVSH@OTMDZN``8so1+$Wiv<D#P+*oj|~v(FpAm58Al3#AhCH$vD#&CeZH?T8h%R5rdD7 znO&GlvaK+Ix^s?>IUUZzo0Q^7!!cyQ;hXv(aAT8tT5iVS%=FKiR4B-M$M~tPxWHKFgtPk2UJILDoQloEn39eV64Z2maVw!Hm;=+bD(nd^c$mawb;)B z!RMh%Gx;r%oeZo;cL9IyK2kqC01&`usi~vH;`#_xg`Kn zLjyKD{M>S!W{nhY1#02DFl_o^MufLtL-WYo_mMjC_n)L}SeKn*(-IAN#Z`Og6PHg^ zI+L*4M{$DIV~G(T5OVg5uKiV;IqrB&;Q(cXubHNY`4cj9J0Xo*1qyHlq}v9CnyL=r zl6FG!`OIUtf81I<-9uzaKdt3dSY^QGGkk;0>msCI<_@Z;Z+%nl zfTI#qFRnjpnY&2I&)#wt0Se2)gM*o{#h{gBRbay@FgvF{KbyMA(CcIi&*ud(->>G5 zpy}aWg~$LL_$`uobva2Cj=gaS!GyO1sLuAhE3G>PenUzCS+t}v@mFyaS6Q)l1BOdC zV_HvoH2TJUkxmuH0j-E~BFiPTf`S`Oz6{Cj<*4QpV{!QKJOs7i!l}>r=nB_36Y3G3 zCde$1ko$pn@ewbFYTav|IRq7aS z8^qq88?3c?gXPK7D|RwWRO@@b{HMqSswt_Dn-r3=^LP&RXJ=gM{F>yW^Mj0}OgupR zBp*Tu+VZ3TO-?!pLlW+zeT$Uu&geeToAi1Mq0DSJ&r(+A0VfeArryHfm1X!%DFUBe zd%oc%O^?x;4}x_X1r?s6gIs)Y*!p@N;kKlTik;ZUq`+n$aEtUK0B;bvy@umJqPn(3fONC1+wVu9`h9j^ltAJ%q;(9$=!X zM$vZ4DmVo2_m6O)*vl)q+B;4*CN5t5NPbyHCr-{0$CCRmn&~n#8k%dOc98|5A(%E2 z%Pl*!e%E$~RJ*OLiK;M=e!9*#O!O@Y`g_9D>-<%z)^L_x(hEw}$QArKs3rLVcd6NCF)>tjAq&unv;9{{`%={<)YeyVJL? zk5-7Nu#yTPV-?~l7<#9$fwo?~pPRC^dHl2HGAV+%EF98jJ!nV$ogj3Iiwk?}GQjwC zFs45Mf$}ToAZFD@icd4@iP%P^s>QiR8+1Lm~xw!)bY zMHdO1?BX$uBis^b)fV@T%Bbx?Vj+t0%%-Qv9jvA_%6Yb^P?%6630)!IY(znO;m~7^IlK^7bE?z z@E<~4dezJ(p}6PxRy6Pl>4aws2+n{-o&YN&l)Bfjl)uAD3ucwjwyZZ(K(Q6lswJVm zYggUF5hp_D#(L&i7`&mr@uO*kokX}TP~dsWJO!~9atzm5kIa6CeVJ-%l0izITf}lv zs3D8?syom>#O$J>Fq17~q8pXRl6V-?yFk#!s({S`cxGYHXJ>YB`RHT zqi=T_jo$&#mot|p`QlZO$3>9{!ZCOV5)DT%NUQ*j6!Kthp!wWbgr;KaZf8KnrpTxS z&vMNx#x}@YydjuyIZQh#I5jwe1!9?AzrdH*>>6MT!MJF@i$Y(=C#$``nwZDthPB`= zxpj$09E{zrzZ`r==ro}2MbHdiyo}j@@6uNua&UHiJK4Q-06wo&wnh6BQW3=ml$<{&Z>>3S*Fdu_iu zt192WO}fX0p|xq!U{2Lo_<{L27FR0cibM=V(N~1VUSptapb6vytYX?8thB%5Zu>`o zwu>Mx?Ip$Y=YMlb*g&jP4=jcm;XgnlCntu7Vt8a%>7Lk%xe}=e=$#bXqsgp184tWO zE9a6p#$J8MQ{V}KB51FidEjeTwreISg|K!(2UrRjq9YfLX>9W_SGc`GI8DG=t^u|ZazOWfSh*=1FU2X zhY`A;2%BDVCpU!;XYtAHl8v`u&E+0NNa$9tnB+7fX!jlLKmi$bf(Sl#G-GBa6QHjT zn(oIs$^oWwUaA9qo;S+DOd*+jg^q>vrlJVO((bb>P<$B7XDplP^9qd|``_N}yx#)D=NGDejOvi{`5_t_qxE}J5fc6<5K1^ucegP@|JR-j&R~YZfSt!L-_)uO7HUUhUbz z#FPIN#bfY#^KDTJsA6LqC?H_9YA8d1WFlvcQFi`?M;?hjff|j zu=Y&)TAGSynP*`q^V5r7mH`Gu?1=`27AM-6iUZLJ0^mZVjq-sLLEcF_nlO4>*KAMo3q&IcM5e$weIH^L|8UsA8@#AjxM zWdSG8kY0ICs?rWYFtEG7@CWFAg?_SjAcgUgJ#Y`)r5?wT>UmoAtGGBy z$8F2Devn-jN-q=D1v0jNopD5j0e^$+TXz+ZRxr3<+>xW`3eJl3onKH6%MX*0nRdi+ z9Gh+-<`dqZP@Adv3l^!n^5FEMK0?mZqESVul7*(5Gmr-1Mfv(N0~w`a-UtTA41*$c z%N&$8aT_)K}C&XuYoYEMK5TT#L4VY2l z-8wv}PzDUSEHpW)SnPXQQQgk2wxm-zX+VfF_E@;F>2e%X%G3R}k4|l@k!nO=``C-y zAJVH3x$|ymrDFTYftQycPlZ&$#)3>1P&5np{7ar zV0~w{R0;&Y2{|isz$=mXF?Rv#3xkT|u1z@kKqkrlXW=+!I`R<+ux;U*R{SLac-W6; zIW>Y+HE` z8t!mWD~5>KmQnzp;0J>fw)k;cn5m3^P2F~`0#TnN-=ynX2{>PKGROhnXn01_{>SgLYXs8m znOfpF_Tr?Cw&^5IEwvA=3}W^#dXnLbkm&oj4) z+e{J!&_M)`9xwo>QnF#f_D}YWoeP_b;BIUAXSY(vxkGyyqzxzZqri#s!IuLzW->VX zlJe>9>4R#IsPeB(uXg2}X9KjcYBms_zgtLGo29FO4gAtwszR$UYH99yJm(DlhAdn5L%l5M(d0*En za%gXVWwX)A z?wLf{FxnS@MWR6Xt=W{Z>D13^P0YDtnS25_=_=oEhV;L@h)95UlpFWZun+F6#K}YE z@Kjr9s)M`XP1hM%UO|dp@=4C)-tFF2IK;jdsPaWiKPv9m%{Wn%qEXPzsQ(@k;$?A9 z?xI5Tyo!V}XJSxtgJjYHW*afO;5{kLB@YJ_P5jAz9~r+r4(8#TW1p(hn=?11%G75{ z=O#I^-7_wO5UC&!$OzmNuQ-52Cf+dPLzjEBZ+C{id$)WdRpar z%urHqc8lp(P$Q*McVCs18xN#R)|}%3z5Z!hK0;a%Dc4Ck=~fIO%O_G&`>zshY(x~Y zz~h2-bhx6Tv`AT+@#IgmVNKU53T#RJ%fMhq$1jdbEj8wR0RokMdX}!6JYb2l-30IU zL)WC_z)7Kk+D4bT5kNTK(SL1F-nwX~Hy}ArflBZYxBgx@T>GXCBH?QTAEc}Ur^>qF zYI6L5jF(`3Uhh4mviSbi(<-(eftpfpLBH%S;Tg1|JW&@IoZmkP+K#z}{}MW)J{yhz zYSBO$yO`s!cT<2dy8wj+l=i{@kp+YjFZfu%QF{q#dYD9&FA3$MPyf(zx|%Bfc9*)9=iI#nXG}rdh7tNS14nrOZZ*4Vd zmfs8|&P%v2Cg-BR*Q-HsAe5p$FI$qYoWDk3%lL!|oisY41lZjqwhF8Nll;69Np0kp zA&bQGDI{hiwczn@Dw(Ro_wK#KVMix0_&#nzCQ+R-8pH2lT#a>*#Q|CBj7)%sd-+y`0iO^2i5i*L)=(! zTJ?riPR+$!tLX?`9*&rw;e!e52)mVwMVu9>8Yn*x%0%9&!7$p7?4s%B7Uwjjl+qI+ zwKgBPt7*Wh_TpxT^-20Y)gfVLCSB0m#|TenlCIRELtri~7S^D{q?!WQ`T@Y~CiJ`M zH4+jEy^i?G&&#wp#>yhoR^Q=eM)hj??2B@>u;1Blg0g}&Q~FP!jO{_IBzO5frUG_@ z0xT+cF_j^Z;kRXm$KMdb0S!B#nV|81wuG%SVoa+LqM&w%93EO_Hb)MurT!Y1^Ts%7 z;`7BC?Ay9^1W*bzE-Zj-8lMFPwEOXs;^=N!J24W9b`=o2m~e}wCAS3|n5^e;cDiii z``vH|8=Oj7R-;j@;s6`??0ovmaP@NSK{R3-XZE^%D}Vu^q9n>K@yqh#%a(Aqa;Q)5 zjd;`lqw%07#~~Vj7}iSeE8q6N-8Xhq__UzRJGCr+Te64#Ro#Ega@l~=QI!)ET&I&u zWh1UVsl0B*vp)uNc|WD=_WTbE^(bt*PeAy(3Y{)8-X#LMLJ7uL5|Or(0(=?yeVqb6 z0MqVX3S;*32!J)LyQ%R(bkr9%vdYCf``5>AX#wvKR_z{L!+xp0_&x^dTZ`h=&PRZ0 zJ00v#+~1|CTa+(8Gfr3Sjrt_5Icd-s7(HE8{(A^?sQ(og?kx%qp^?O}Nx36G+WQ%p zAMsf2itxiunS#bKClUleT=Nnm2&5p_s!*s#&aovaxX6T7#-uOAhy&^ioqumEbB{S% z9>NXEX zE%94ohCCNY(Sa@(1b89 zCEOEbUet$65@(L17<_2(;MUUcdqZ=4)_yQp7~fn>$=CJ&m0wSj#GoqD-*j-sPrC3Z zZ~Iy|RLM{V5{`bg(&Q(;lm*;oT_Lk0wp<1!OFKI?ATs!`YbSrEU7*9l{-~_F4ktW2 zT4+?t5IQLw-$wHhMwWdn;KHI%Y09ICM=D~k3YC5Luy=f6ie@Y;I+pE_RC%5JSp(h1 zM24U?H2(f?+!@2v<}%L5p;+)|wL(#Tpsn;Ow^gI?peB{ETE0SDuQ%!3iKcI5Ih~75 zd^zAI#~Jz5Fw1EGm^+ONIj83S_~cBqcoc>?-MOg$j;9PY8hnHXqDRbUdG*W-`u1cM z5FV`t?cKJ{|#6AJl{m--H-L0~MoBg`p3E*wzWu^4G^5 zO`~W$ssHaZkw%-Xu1QMIii-|F|6V@CZ0=yjQW-!MMpaVs(jz9W>4;xQY&oqOKuR$) zcM_JUMD3_13D}Ig@D@hO{+*rjaTNXDMo*2Z(cM$)vIZNQCVGP72R@noP}{iN3!=OC z;8t6MabVk!AKk>%cA_?hSfMuilF&2tA6Fl|q8(|(r3lE1!$tVlF@2Y;%OeK-j z%)PM_pzzQGz@b{I0R^1oY?2 zuC*0p3%wr)&NDAQYD=_^8wNh5`JkgI_P_=u9txNQYN&X=L{ri=)q_#5#hB+w(pj(* z{cVH~zI*~1u`)j?K!How4b~T(pQbp7VD8l-A`(}Jr7nEh!1X`MZL1CZ&<@rAkY#Id zW##AOfCH(naQ8&}Q-1UCFiC1s84Yzov^q)jOXjWws_eN@eCV~v)_%x@59{b>E^T8h z_dji3*gb<>L5pG#N0s`>lgTWrmY(?8N*Gfc;dq6gxqHaU;`ty z-rp|s9NV``d_*8+zq*M2p=yCq4+Hj#ZSf&IoyYB zX>rL`-{KnWZ-Ifv{hvts$G{K805JO2-1}}`gvA6dtk}$}4983iLJhN3h8-{dv}&pU zr4zw87|1s0YjFZ{r3572(S+z9_D)P!U2@rx!$C|C2NT8iZJ;h>p_%*;p)SIxRZ2KY z{mx|^t*kloKVpPXF!sBOoQ2MzFl&KDw4c_a2}P!M%+LI$!}|5W6$)M-FS7AW-4c%EhKJE#WLbejVv;Y_@!nKfZa z3A!7Jd8r%zu}7lO7Av5p24Ao?tp%$d;2^k@Zs3qqYI}9F8uBr&xwzG4=hC5V0r^VZ#K~4=JMcoj+Mdq9sL}4uRq1d z_x9``lm|)g_M`~9#FjAG=W=ZE5KhlliPb(btfaN5s9pxJmy?0R5{X_6ykP1L(Y@tS znQH9s^CF&S?0>RI^DaszA|dg!qi8sy{7Oux({{5uR1{F9}c)s6XCmQz$y_`IZh#5go3lNF~45G@=+3!B5c}CcR=N+{TCA& zXRs!n;p{w_1N3IU-ZG!IfuXy=oz=C|esxE_YE?Gx(#d;}cG9*+v)%qk>e}kD#XhuQ zqCGTt{cPG7EC>NI&&5ICU9MV?pV*o@;2@C zT{CR1l!sp?x`$-lp+l`QMcfzvcZouA1&4q zZx@Fmd>8)+^TPuJIoT$U_D@qcqyB@1uQze_ev*4uJpyfMUu0Kk~7mnhQG zrt8_=9Ww%=%VthPj4-kMq#7+%80fCc+wUGgCAEpuXvE-hebgfwtE5m-E;fl*OKI6+ z5St`eP&ClkPBau#;LDi z!j>koZc~uB$2%YXkkJMdDe<6ZJ%ell2c&_ii~@A>XmuBe@iXu1*;78ojAl&YR+=;Y z4}z(S%;qwJ6;wtyHO~1bQDt0xz(cT+Y2$c{zG$2$`)J$Ja8jzbz9oMA$RQRv79YN1 zxzEz_f%XNBZX366+Ee*)cAJHeU=Dncf7EzC^KxSsul6tU-%UVH_3(Yl#8H`~S7D?A zxEZOQKf-1)(^nPikZxjyg40rn{WKd-JS3VMbtD)XEdIFhi09x}u26;T^kvq>2;>aD zt1wgLo0)o|gutTA1P!*93Jd$(5@DMhL~8`;XlkIK2D*)0#QRukq-<|Mea^gh7?P%- zYo>L~y@-xE5~uQvq3n!#$A@;1&_jCIY?7Mnpuc5wn{i&Upy546lTDh5`_;PZM8z3w+m+5hHtGJS$uvJyY1IDWU+*oAPoTuX8Hx-O2cE_4#7idGD$bz3mQJd zUQNY&rfnxA5l|;eus7*PIsvsqR%MEO%ven$ryHuE-0+puOQI2tVK|o4z`U$w^7n?j zpWZyCQ519jPCe*F>>C&>&y{QM4a7-{sc6LEjj$q-I;mLc8nyP5&vIHb8=@~Fm=p#L zvW0`c6B#W4LP|gdx021lTML~DVwkAiFre_DR{)W#hhPr%m2dz!sal@F&!v#|v~OTG z+aZNX+lizzKUl6_FUa)`JRAjy`)7OV*^(PR#)KF$;XAq`H*%EZ1sgd{nmvImmmO7+lW{01T%w~fo5*#6a6JZrGH?uF}V~$j%ie#bV3#dAH1=qh3 z5ys=rhMah$T$kT8@UiVNq|x|{p_2niq8g!H}d&RH3{ke z0zp4GUv-0#!1}*H1RDnFhj|e4*FH%s)!-Hf1or3fW>tzhZ25di~dbt zs5AOv6?jTUpn%aYOJ774)aI^&CY0un5io?jRtdvDRr}#G^PR9kb)U8*O`!eRvUh>j z(-Utg0L0MA_glT+)N(YR+3{L3kKRKdu9g@3DXTX48jN zrc0a0zgiVeN*=HVSz#JGHs2kUp|uW+FD>6BXgBq^k=Fc$e z>}FgkrpL-1!gy>OAhi0~K%;UpDn$#JvLB#j_UfE{fsf@%s%8BvkBz9H!PZh3`_25A zb{fRwVSFhBR{d|O*7B{8PG?vRn5$JIWLMtxZ(vGk&0$Lt5@TC(G%e|-2;?FDG0mgP zq6HAc%Oe9xL#o1ckj5=UWPkeAbzK;ox^kni;lVQ+C7OK{>a^^X+EtAp`@RWU8@BEM zd0b$L6dHQOudBVlpZQiFH)=zhhwg!6O8;*r#;qNTPLO9GA&0E8Z#)oKXfsN<5Sj&_4Nwq`%jQ}@feLg#?tjh|t^!q+@aqEw!WrB5e3 zCr93Re@#&z@X9PiSPC!KvOGfy{LkH=!AFSLnrF>STldm^_UWIQt%s$2Gz3q4moF03=<$1dQq zxtr@)jTu!p+`|7PXvpYG4GSk5W6&&bl{^P)w)e!s51DcQJ#w{lTD2g={}N55T`e}a zRG^rw)qpi*gPqe_?L1Gq%`deyLE_R=_t3xe2FLC`_xftyJWU~Ge)Oqkl>t=uNn^}=2T`H`0R@9 z&g1HJmF!%DBfrzd()l*IQY`u5@MqrN&Q^7F7PqGf0Q~;8)tAWxNumS2KcUi#mGHj1 z1QcjhJNZ?$g3akjm0$K^*lzA(Cp^`JWJXn^?rI%Z(S9YA{*_T?-RaMrBiWe6Qj{FU z)kmg9@Z_>-Xj{1a7^sSg?1riCX$kB953m40afn&oXEo%fT9=qtwD(qgYr-{nT0xWf z_;!>RDCDt#ku=1|rCD3F5Our*ZbRCSc|ngntI#v<-Hrqg?8{zEsK}A7hek#EYu;a9 zQ$-|S#kq@c7QHi`QmPMegQ9~?Tb8cHOVI{s3nD8DqvZyy%TQxw9VchKMi0AX7B za36aGz^^A3BKA}#NnNJ1|DF|51v@i*3_+at6T=`8Jt4=3U0fh6JEo1JttUvF0CuW% zo79Paiy!Jre^o&9gF17oZugOXIdT!1p%@%G*Qifw=F?10Qd;FA?;FzlQy8M7m_2V8 z)N+i10BR7tOnxc6LN6Nk5Gmq;4T;jzs(^SoW9Tb%v}}r_Hq2(nM?;obyGXuVDfOFd)v&&~ zUgg7=uHpzm2iWbQf#GNR(D@>4ewo>yf^58W>@`_orb4BD?fa`Iu7fsf(A;?ezr0qZ zFF`7V8!z7Q@u>xa+43Rsh4h(peNYjRHO^P%QlGjlIVliiwUALkj{vFT_(UBNMyD_G z66=WW_>`q`VA;i22J8YiaBt>oays9qXEcap(T77=jc~qSr_4Qkj}6iS_Pj+(WwD{# zpdgsbV6IkA12t{YJ^zt?loX3zYB}K6ha;2o>h;}{j~_fU7`tr1w$(S$Zc{gjfBYg+ zEnQn^gsXk(bLIonXloXl(YO4k`PrG_o~18O<#8uc{Tmx*2x%p31iaC(CASbDeDD}I za4X^`7PscyA>HaBU4!6v1jDS;TH`Ib4ut(9%x7iI(x=%fU&yu8#s9rFoNiCwY5qZq z=K`ax4_CAdxDK&+mSCA8g@<$|+Ca%)aiUd(Iz(8fo*@DA@+R81IhD;AEV9rBeN2K!Qz8wk_ z>)~_c8_{+cOP26Er1bo|Vk8NpN6;~DENn7f_4s9>8NRakg*NY7erN@ ziRH2ZBHxEBq32V^eT#=wmwnk3!dl4fy{||U4X&1lU$bpARvb&~2t3cgxn5_{eTnk# zf-yrSn*H_C(FFcj$Oq&+iOE-g$UEktnxkul)0G*{*Xd0kbfaW>S;aO~8G93&iKE^;Mu|Uf(FJ0DOIZX-cQ@ z<6au1o2cz$52&nNEAA{3DZFLG+$@PX1YHz!065ZbLD=ek4(1RsG5kj*Ef1*!eO9lo z3D28M6%Bm>S5}OffWhmtU}9Dl;tI%RFKEX$!c##pWge8!5~1!y{ehtwWs51}MDI(` zP9Ycb)t&)QMaUWyIcx19MTO(!rY_kMO~PdM@PO(qPjBW>EvL)ZxXI#t-HTJVM^G#M zIZ*$HH5`5@@V^RPH|P`;g;u4<9P_BtFPXC_ZQNe)+eKxm3t%AN9kJ8Jt+)ESTQDu@ znV3PIY;4c4b9qP$@(>FfWnhg@bL`rkQUx%vD?lCzIi7%?MUz9S2!tAZ?0p*4W&vhs z>05ij(fRFlDSkNc(^?eAVWEcxqGT-NwNCnZ{01kVf^5p|Kl*VlOzivGWszUpVxF2N z|C4^YH?&%>2Eq*qN*%pV7C(PyB2DTprvG}FF#Rh3T}Z;}3h70-Mh<~3wNi{OSGaZF zuKhkg`(4KTqS^qgzJ23Ln_FJi`>%_bga?-aF|0if&(05#uZ>j;)4{e?@wyZ$tYhJ~ z{|_JBO3Iw2{FM|i_Q>m~@gIHG(iSN)M{Fz6<6xS%Mj*Lcqqf*$7&32fNQJ1cu30W(wJehQ0awskUY=e&!6HX>XU4Px=6CKfSQk%`0L`%u)C5+f*P5pKr*- z$d!jI0%_|{lA2ad{bXxTW&f`lWWr1vLigA2|8SP55Zv82HU3DfVW)4|{GqqL?9G{j zETL|6DqTE?tjteK(P6vQQU44{tnxB$U@D_0tciAOaA?H#SGdhBUI17XSS}6_!?GQ^ zm}NjaQcmZr*5yXb@+!{x`xF9m$jRnge2E1rLmi81CWpQTQ_&> zYoHA8)?1Jegpqs;`<0^~=X=q%?0zA?`L#9c~T z^L^kRq@oYv82*c3{yl4AgEQ{H7{1{2@koAp>SYgRq2Nayw@1?hCb24c(T=H=mVWt4 za2G1GmzA9FaXejZqE5xoIdFsX^bleqCGq-UK{r^fy_yYj^DJFZG;spk=+nPluV$}P z$N#9CCeDtuNMOR9Q*MYta&!}3&5j@=#2J;cS4lWMM4}haouAP|nPD&BhD!`T4V z4XK|C5lIso(LIgSM}M6RKC+TT+^y&_VHPt}ydNzW=nB7VJ}s5{sf|%u%@q)Oxi?T& z)u4AEuLM&$2!3y9Phs!&2PPUB-a!+1W`lN)JQzP`H>e_CElAMv$T}!#GjPM9Ssna@ zI%Z;O79=o??9FB*>}Z-@;r23`pvDFv_R=1D46v9G^@bBxS}P=FA$vu7#W?^3zgHrH z%I}Q6fT^boJ5t(2uxOjGpP!zQhWttC^#j)Qh_$#RCN!lKZSc>K#)Gyfb!b^037ZC% zpmL+w^{fP3GNqjno%lTry*T@L1?!}VdT=8Un5#MiRSxiX%}D~$`qSk`={9hbUdKkP z=0Wd%D_2Bxl&XS(nHq*qUuKCFXqc{Yf~Op-(t9yK5u62{o!jFy%1cmM1Cs(=_38H> zV=H6aynR%WJ+O{FjHcZ5py=8~lxb8k*GE9HG+$Nz{{JFj9g@fpv%9AAFP`V~Hxua0S!{)b1n<<^#bvRxDq+BGrU|#__ zU);oyI#ngsBM%0*w+oa_11uI>9`hZ64Qp&NqgZAZ-I%$LCNUz>rgTPt;c-#WhE^+F z7WD#sW3;gPg z6|?l#xb2r*(W>#yYyXWi3RxFYL~byr0f_^(8WI=$mUB9Fxo4#|m5GDH*^Q0O>gXV| z$@A=NyHgEhQ*!|f*~n?@Dd-q3`j4(bBzOZn5v+vTo^~xHI0!jhR6Anb5qPKTQD;Lp zV0~+4E(I#z2G-9i3D~b+ETKHU>y-y5Y>H=yo>A$*a0G@ zZjcrz#hXS>sz#7KMPiI$zlNCQ^29kjJ04CGZ>jub%pEJY2bdL8gK~BNG?H593&iDc z4;ST101)xE+jflZ-yIdU?R;~et0a4+A^^;#dHb_Hk^E=Gtc|v^)YrwI2KN$W@C8L* zJ|4o@Vp4~q?&=@P{N=%udl2L*JF?pXti{ts6shS=J2GxTSIMdu(bU3n9v@ z#RgzxhSue#;q1P74cQ;}tQs8>QE+5(jQJwt?(%qU57&|5NWJsr63(lg@FV=~B%6{h z9HqHEQmu$M+CDx~JFS}AfLqUa$6Yz^BDgxH{=o!a=i@RDYv&pu(-o-$AWQ2?#6sc5 zfYtM~?kjO^5$_ZW%UQvV5yQ?6AeySw6{;Ed>~-Nk9MGy$==Y`Rh)PV3cSOs88w;dv z?I4TE{XC4nNP*~dtNS~wj0F0?=ujdo;Ndr1-lbeg6Rk zpUpSe>B*WfQhn27>S@QroFlF{RoLzOGr1=nq0;x9J6f)`FtY98tU2r_Q8w*I{3!1l z6Hx#hL&v1}w||ml_^#9u0|OxuXDCP6tW$xqBJYmY-$){8anSVh`}YejF+j#QUw}y{ zV?xQa*o8c>sayD4vO+c|EHOP_>?|YqS*cA~o?`f$?bp5^ZtNxMi=k3|p+uMBm)aAl zq_Nr_G#!@bXQ>J?UH|&@?OHI(c0-2qDU}^q2B%42XAWG_bD0`Axel>}6@~|{Bqk65F2Z!p8F8Nczbi=qz_@%Jw} zw|BnrIYSUu$rcPQNm+&|p3q<@^Mji7-pr9CY_Na;7!7_{ATz&RBg-3dMFgMD%*F&_ zEb+g_wYXW|6%kgclHYV6CNSzLV;jL)c*yQn{h?y#2^cS>CSJsxp-kD_GQ-y3?Pi0Q zO=<2(>dc*~D#gdaco*5pJ8gECbRsLZ1z*ns^y2cqc#SXI`@wcgQH$B6?65!l|HDbH7uK|SCVcG@}aJkLy+&EypwFG z)<02tmLN>m9)4=O@M*X7`nxgdgRH~auK(k`Srp2%#k<%MCEB4V4xp>}4*w*@i0`6FIn3Ll|y156N2 zuQ^^H0!*{6)WNMTNXmNI(TZEewn>>lZI2zCEt%wrhJROlmE-5(E0ZG@vUk9RRj@AX zoqc*8J{?GM7EJ{v`qXGtJC};S*cSL-#}u<`qW93D^_(?&Fbo+tmr65m#n$ zXjgH>;6){rXBZorRr7&UfTvvWPU>8!Cg;ggjqMkafONz}@dU1= zCe}vlqVFW}GIDm-&tmjqKA8TeI`Ol#P*tkbQt=UH>b~Zuuu-GzzkkU9tT{0KB4{Uf zcZDKLm+M;nPEFZs{fL=ph%^7|?Ei3Vy9dxGwSen~ELP%HMh~6Yx%?ebirG=5;mQbC z4a#L{V;5oB;nM&G@E$BbyuqYaW_WeqKGrVfIn%@o=MQl!LdmljV(6{F>W>_&Eu3?C zenjtziG9py!+)sHboa*&!PVNOqP=Xbar#>gZY}dvu>q~+_nto@H$Kp1?t&n-nbHxF zIynnCJ5Cfz4<_c~BuHyK?{~K{5$n8m(!hwvX`IdzB5VxT?HsrGAteWTjouOs$^5u= zek3ynx7SQM-{15neTlFF@Lk)H?~Mdz-}NPoJ=v}=U=)SIZ1;4p{|$MI{_700b`1pc zkK^|fSJ(kCe(RGezhN%i>Mu}qs6l885Pk_`v}y~}=^@I%nk7Y1e!FjJL9Vek{ULDU z6CZIMeI6px1ES?f-JWO}&P>h5l75Z{Y>VV^`{Qe7 zGQ?NL`=~-`($$O;ry#hGdh`COpI<}CPCKWviBX4tm3zXH_{2H(-$9IfdqO$_k;)Uc zTFJ(t?yUBOuVIXdW+nF73im<_7Ppshm69Ud7Fn!diQU5Ate93`(I*I@)pbnP9xR#h zem)BzS&9qfYoXOI26jxjlLE9aVP~(iK5nJ^#DD}fpvv3lGDm~f`m~faY>P4^+2RT} z2ccn;PHz-uu6*fzXfy+#Ehf}Lc@nV_S;C9ea>|vi5KL*RX6u(%<#fD?BA<8L-M0y~ zVVH&MU%g(&I1A$oFF31HYZagjcF9)Z0wU4pwQvyS1V4;_>m?Mv?wja2fF0Aezjk;%pywD}i*j@&Gje{-Hvm&F|FP{x1IB*i9 zr;anaFEKRJ=%W_L3m3_hvK{%=gRlvuZ8Xnx-eic6p!Fs1wkQ9%9$B~-@q>4^$et35 z#qLx7#^>~q*Vxl{;_aN}&j}0X6#?PmhP4*HR&c(aFMB%?2k9s5+$|MFhVIj23tl|FV9eodT!HmkU$GLzJb=$oz;J zaEYSJ@MQ>9u>|jTel1I%kHJcZDo%aV(T^9KC3q86PnsV894B}UGw)*XFq6(zx!CS2 zySC1j8i(3LJ#|L+Cw1L_^s0S zbp3c0l*Krnw!9B=?AdBPuE&;57gHKnnzF*@UCSl>#gasOO+m;9IDgL6>?)OJO-}B~ z-Kz}Y@y^TV-3DL91nWQD)OxPGj%+Yjx@>`)n#l}z@V*@)Dv|skjXxY0i(HnS!ClCD zePFk`WrIW6a+$(yLHt*wa9vl1wIS$jzX`Yx-oyIH#0|HJj7Jp6pTg;Y%+n7DE{5~8 z2KB7HI|@}EZ#a!A+?!fa1?JwXhYuN4Q+95xxECCd#dMnmc0BEB(^sp8s{XgNr8@__ zT&zv9@Y-l7WA;xxqA!&u+E3cY&X)Kre0yH-o&%RZ{Q)CMMyJy;PYjHT)fFO`6sVo% zGOnh=Sa<^KEGG-Sa-5VQEq~MyIfV*>n5DL*odNA6N)b#(oK;CP{&5%}=k5Jsh*YNT z_l&Z&OE+2tv06}a%cYbzrD1RU8{1u&Q0Q3o1K@aqzh_Il^nza!8mQ{Hli? z2p>7@*N7+{JWpIQxufsQitX3#|C+u&4Z`(pJ} z(|9y0s_=A$ko99nYuj%8&q5Pdy#U(y`jK<+;H!_Y{r*6|9LoDzY6A2COCF5!v>1>= zMzt;(Y6P1ATOIFDJ0VXcG^TT@T>r!Hb}_0?cnNf7yPEcJP>V|Pn$A4*)0P4AiTtyQ zO{6RF++3M3>==OU zj41bT001LJ0jLLnZ3u4wcCS6;eDW%$9?=t^gDQR--O`r)7Sg}gX#Yzlbd4~l-2gXc zI~lrzPHFt5Ly*Utp5rls<%!70=q@QsAh{I)sd84GY{~aHJzZ$oX_%wNL2YVyB7Wq> zMa!Hl8-*-U^<+8Gatgu?;bxS8|FU0G4=>_@;aBVuO}1l&8XWE&h&yeRaOT+3f((RT z{Fu{4w+CWQ)7oPc9QmnRU>1fFA~qLY9yZ%M^fl9}!;E)=u;uv6=h!4>s<@uBBNy$W z8VxSf{_3R0gy2&+h@CeSVH{TG9E+-l0&rUe-{2PU3*x%qhP-U!@qsSZwiA+e`FU?-jnFz)uT>C%}Q^9z(k}5D+&J<&@dY;k|{@j^8!xYmjUkemlYn%#17rOCZ z(T&(g5Sap{`doPa!qnU);j;*R@KlM@^JjIQ9+N!74YHvR!$oHQF_-JR<55F&BULZ1 zQ;mmmq5|!8iPm$BwgI^Af=}J$Wc0KfA6qBhTG@KPt2QOE>u1TAJmLVUo5kJU2SR<} zOWD+izH@q~aYp>b?|c&`XvRH@`hd7tS2oMw){B#z?9qUUpWJEMvOFJM!pW6>lDPxU zUtZu6XJN%17w30u3G=?GQyU{y))w82N$7;e??*hukSXm-PH z(gE9h!AaYe&|$O{d_Trkl;!Jl*>;X038E zJgJHyk2cZG1C753>Lm-UW;f;DKq{5Ns1NhbE9Z?PUc?6_*E^#CQS|?Rq$jG7dorsrC^7N z(jshsy(fXcwaq#M0!2i!bHEl&==_{2NN36;V$fpkw2B{wpenrymYU>+sRN8L9h2e! z0+U)bZHiG??T9|%8}WPFD*>`4dn}m;k;Lnr{xAF*A>et#@)HvtnW3ZUl4;L ze?TSM)|O1Zj$L^ezW9WwND>Ls3gLN4Ar%N1&@z=H(XxUh<&4&MR23b4%*QEAoHaE_d@|yf4;O8eR8S>E4USE*4UCpwcMC^B*G&bU@hb zBz_sbp0mj1?`@2^fC2RumfhGehTND)V1oyszd*?tmOWWk)0xWu=W)|mNF>4xdcEnP zGoGKwNXW_j58+(I@9OK+Mg`!WcAqZ`Y>6GCHdDT?@$#Tu?7JYV!FyiH%0A)Z?8fSH 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 z zG#usiqelM@FFTCex>`JQJ_=&1pGL_Jcn$Ed*||CC{hgWqEY)6X>6*z;a%>afo*#k9 zqirxAa~{VSiwmNDsfHdcIp)g%peYKbh~;TcMSlaPyvC_12eObP5k8Au_jDMnhj2$M z)(GVG>vB0U)u`)aa#i#|#~ffoxc=nBvsyP0jv)=2wFV*#TE(WRi96WPXz|3w;|tX_ zhKt~LCk_z79{SrP@2bisHNELy@afv7nW_6JM1pbc4}1N@SVUQK(t-3vME|cYc>U@$ zUTvz}?>m16x;fBLdlR48x49-&kU&769Zd?~SlM0H6evbatHtV9LSenI~k{R%;JKkW3s+iuOAh;d*dDnix6nFqjvPgFvExWU(r z^fZKPz1a^4UH88Y5qMWPz9{o1K9HL5s8XYAQ^su`$;A8XyRb5%Z4Q)k5(D<*lxa@3 z=~VaASgi;ket~^g`Z%>3Lvo3}Mzc`YMxadIAz926oDRNZ5~FY!%<+IdyQ({UU0ZG# z4a9nD!KSll13|`Q9pDb6pea%gNs=1!w625Ry`qS$66E8>yS9L#bkZCwwft8L-e;VL z16<^~hV^CQtCNf5!+zpqd<>-cZxj>q&jP@IWwUzk?6P0)a4el{3o_l=JDZ4g zO{gkR<{L^T7*ymQP45^#wN(#zWueWAtOYxl?c&nLmXN&COGpKpl*QG4l*8+viugVyA-n zh>MsH*Vjdk3ho-<`!T=6WQ)55rT5U*Q>dqA$OyGAPRK9OEqonM)7%?;s=(<)sm9h9 zE37)VrE0y%d2IXuPf7q>GKMDe#eJAxDK`MG%E6MIa*}AOCE(POp&sqm7re&4D1cN} zDE2H+cmQeThgi&SyaNM6opbt)Z^Oy}^zz@qy0byFp43q&>&5CveU|gpeI;nBT<#+F zfbJXTkw0%>Q}s8rM|tTr$rh^I-ST-4J|_>v%trk)Q{DChkI#Pi&W*yp18puPU1rPs zy=kYHGy5#eQCiMI(B~>$TYu7>MSmo8)nX{Nv;>)zRckhGTKJ%};3z^D%++GT?e{D0 zgbnArrT;o_Y8&L->rzm9h2YrG=5O>F(CoHcAcI!>6~h4;sTq|Nv45T^QJl_?4@!le z@H%jY*H461`)P?$9#m^hPK!{*+ipXAYtw?_>Ng+~!qgaJ*(jcqMuhQb-$y81X=Adp zM*^u?KKBXY=ZXrJN((}Y*mmrX#pRVG@5mx?$~h1B{DG9NYwp2a!UPCA(ju;Bz)R_x zcWCb_-J_6qrBT0I?EQL_vN^hmwBz;ly-zEJ*AZHZvZ`WuFFtv!lJGQGh?_^!z3s6SYHB4!J6AA95n2p~SzC>|CiNJ8S76V% zGlnlV9o)L{KyV_hS2^662sKI2oCFZZliSDZr8}Dz(Ju(3my-hP3PyXD+ExE9lL6;5 z0H3+bJj@_oq8|*!{pIcRY&GLf;K2^f0xmnos5om8y*=*{+y%msS)Jn-7CaBQ@_*MF zb@CgpOjbcL4tS%k0R`mCM}B+KlxV--uBlvK1Pd_-DS}%|&GL1|s6QUrM$(q+bwlbs zf|4OXJHOFqvOfcP_P(crmR)mV#>Jxt=~dNL7qw>`Z42Ab5+xG@gmsx|ypAsmNpT3) zHse<_^p@%%t%>d~7{_~`?!wQS9KX6+8e@*%ksZ*#6Bo%nD~;rl3V(_5;7pH)Wny65 zHRNeA`}j%8bDJS<(<5|a-RIp;hwnMtsYnjQ{@qQ09YgZUKXIEpud4T^FSk=6%U4R| zW~4BBA9D-kj+7YA^GAti--P?v1Jk+%&z2nvPoA_LJ(NI=s(G<*&`!SGQ4}t%lks$J%~Jl`k?xw6*_7PBqoq@CQ_v;Qq^sm#7g`;`OD}dpMb+_14UMH<>uRP z6g05d&g5aT`V<<|i47GYtBo_xir2?)7(O4keQJ`c+1!n4I<+q@Qr(hK_*)V?uD%SYc#Yn$2|ovd$4B5N zZ28B8AcYFDM2sG9PjYcczmR$GtIpo@J*yvEL+kotPA$5h+vX;dJD(R{-=#%qplX*DC4 z=6FZP5ljmhJH18|MkLN_Uyly&is#yB`g7S~(dAtNDj#la9^Zz74!821_F27YnD|*n z8y1!<4baQ-!E4AeKxO3&@C4;D6io~Mgq2lh^TOIx=9mMq8;$^M4oulM!*Aa%kYxGHA~n~AwMF)K6O+^X|G9=(iX`>_Y{;u*@jeX=3*r}SO9Ycv zLtZH?hZ_K}9giA`{bOgvLEWtR2e^?EX5doL7oTwRey8? z%**_BPG_i}EKC4yXw#EDY~#$c=?v>%Bj%w3nwGe?B<0cIkkkr6q75o!^5dcSbK4o} z+T0rnr_)`ZJYs-yV4gs$WBt?GIkBg&squVF&or5PL9sSq+=u*H{vb805-NO=#M6my zPcft>*j6R)FC#p0dfw!_MC@vnl=I!Hs&x5c1(w$NDU5Qe&6H3oI{qPtq<(GredUhl z>2_xjh|Du^vsMp}vVvVc;96R0E0)Ac4XyA$^UC}1wagx!wMI*%b0E--_3nowz;`Pet@>(K6QWc{!EGy>^4OSZw^O z4Z&;4d^_j5lT)l)!=K5v-fk7HMf+g%KF8x0G? zD_?onoZ8>4aV7eZ7T5=Ft6$Xo_v{i_@HITSX}`xbUHu)Eoyfv~Eep z80|jtX2=7JN}%eKE3r1T5*&r!_sD~T21J(M$1;_s<(v7Hi2Xmo`+&}qWgAfHEt=6h zy!yC>{iCm#*T(SR=MwOQbHvAbjA(rUoFV!hji{7|szN!xij>APwq6zw%@yN8ln0*~ zd5qLYzHAnLNY!GYK06zWM9>5XH+SE7&W{$eyFq@dpJ5WrV(^VUox4p<#JK&)GhTMf zp+XnXEKp}y&kfh{y8f+^}T!_yQ)FrVq^t>YCATbT(VRo37RXWP}g z&_=+NZJmsxnlROhW!k1rKljZke9oFbgbuRI^2|9O#e7N4htinK$Zh~J_)qheMJw_+ z5Mn&5@$kdTQBSq&fcR_}6-!R+{@Qk|_{)wQf3q%DaPFb1XtX@2JAR!A51l`RW5kzn zXW`;V89Ja~LDC6=+55}*HY6TvgytR*(k- zQn9?@rlB^;`YW+5c`id_%bxZ9D@Q-{br77$N7t_9ogD4ciljJv>H-sE_-im#Aam_c z`Llqcc<|is&G{Cnz)qy%Cc?;_!FA#mV6e)5pyvU4z)O%0SxE(ggD~91LRHi1`&6#l zL;c3HX}69SK|VX^?jkq9<}d5# z=(=veFuw^i(44I#6}$fNpN6klfA;Ok8yL1X@A5|&89Oy}!xNa=I!90u$lvIEl8IGv zj4r$ve=7KE7r*A=&3F~IseYES1~{fTW|L>khg2k{{d5Js(=hdP8J?ZQ$mK2)Q~nCe zVjNQCEfmx({=@qVYS}lvaIJj798VQ0aBgLnWOn|L*Jd1vs04l2VYvdDLt-`T0?r-b zlZ_XK01t`AH#TcK4B4TRxAd!qqL7A`uEVPty4a6Ff4dqB431+_BfU9oF;8J+a*tRg z(i`y|umcnIrr)|hjN^mf<9Ooi0Jt=RI}B%KsWjh#=S`Pd1Qm?_d371#BJcnPn=A%X zBKA3T`6k=k&MD4YIb|Kq1b;BPQFjUQiD#Yj@Y)M<0)o%+dN9L2sw3ze#V_fcb-vo` z(S%3kPlcVch`WZM8tbUJRsu^-rL9+lr^7rQ!isyLLPmqXKeQg^eza8Ox>iOODg7r* z-7z%yTVwa?k-m-Wo`@CG(x_{A9XqrNq&8LyzH@8kyhD%Yd$OsQ08M%hQh2gMe2;+z zKuEmW%sgV1RohSDFpYv7cM0cH+{&U(06BjP6N{(q=lK@EhEtl)wDA{O6p}~8;)mK; zP87#5Uq%uqa-4cx8&e^fB&UI+oiOu2yc}khBu2sZ8G(8;VAvz0SU;MMIOtnYD>d?= zgQ_ol8hTQpIe|Zj45i}bFE5EYD zShJR?Ux7rq`@nq|lH&;l#x z%b{6eBqqsiE*P`-ua!M0dQ8U2MuT_$D6DN3!zjy9pko)m+AUQL7Wi&pb6--!C?^Fh z8r9YR9G5)HlVRGV)&fD*hd~Z27O=HH2RrBH?qXmu>IZMY|A3rVvY_kwXuJkSs|zkQ zb%`oRelH0|9D{INui6!nIu7X=9?TJJEUPmm(@21;Mgpr464#|E&an*o#R;gp(O@X~ zd-)&2=P9j36YQ#O@!;FlgoM&k?Q8=t;4c#X!>!0wg3DG}H1dhm_@Q){Ba9Dr)I6gM z2I1*gr@E{P3xAFeeFO^Nk@cE!>ng!GY3o6gZwh$cjjMdV^c6wipsZL3t^d?%qc2tlMjr-$k` ztKO0njt4$`j~(4}urH{tCO*BhD#1WtJ`-D|9o|T9M~v%3cwcKa4cR?KR(UR_94wpFOV2vOEfu*MG1#P znbLWiE1zYaAO1!X$jL4EYp4XfmS*NcdZgL8Cc=fA6N=&~RO!d=j!S=lP!Ug=&LiZu67%^6SoxvWla&H(FDQd9#GA`&NWPdV92n4X&UZK> zz8(%_FV*rQ+ZrwRTY(P*Rlw46qKD5HHfD6YB(5Y{Q_n6g*`@X z+`Ilfu@u$VtXZ*)fzbgie)~}PL_!G#gpi;T*Pz~98t1y#7=Eo_{2%Rp!^b+}2ZLAi zY10N0C@IER!`+gqkxYRYpfmDi59|RIic3UdWR=Wf ziijf3(lspj=y=EDh5PLUeozS-H<>8(aCp_>l<^%ZM7e0mN!agRToNz@)>CGv+h>ALL@xM@$14hTI10 zgXVB%txF zrs3%EU%e>`hv{ zmYCutbumv)@OjeHGZL?%uXsv(=ncNjjxM9-iB52OVtk_s>#6WSSDTJp))B0kl5Aj^ zIc31UmcZ{cFn1Bsv}tO;y<*dwEIA#jW~#FK;umdvwDtYzsXvw_`_+CX9uT4cgRK5% z6gYHz*ZSkg);cq@?-m?^nytH$e>S%Vt zz{l(&y`N;vK`grPJu_CQ?#Q6%>91B^QN)j-O*12Yd}4(AB_S2f?x|r0!uEfb_w%Pa zr!lc&AjdC-gFB{%LfF?Ri|!vvn~_gH623uuNTB2u8tiZ#PM=(f;FmV_LBZRPcC3cE zCiCENacMPBx>D|9;i{E*I%U9V$)(S=WE}Q&@x^Yyo?QR64d(}-0X4UbGm_y1{R-4~twC~u zlH<2Fx)oSnOA9WglnE(M)}9b4s8kHxPDy8KYsZrrs;ruG`A2K&r!899HBaG;$6*0J z7%-++j|&t9gSg}OH=~<9<@w|JWRazc$87=kTdQ^?D5DUFSiw@oOV2|@1$HP|KL*jP znmElGx7}@l$p7XP zv#TIc`)DT<%*P#!M2(=r>+W_FkPnRc_A|L^DJ`pg#{rndSPtHQFQb9mknn4@DSW?r z5`klMCs_g}?Fs|aGYd&D8 z#_oni$yU9VDo7?O;Y&f$i8=5NB63`CYlZyk`WUkbw?O~dusZlhUB@V{n{{~Jw0v7&c%^{?c#wKJ?H;N^t>U3Bd56rxRGVF-eu4m|V zEHqS;y|mKc47x@=FQITz@AL#6H17?tBK6)nfaPeBpg%|feAGnnvku40V*?{XH~hes zkGN8*kmo*E_LlR=U(>96a;h^p-g?Tt+}3dvP_-W5_g=aCv4-uAx@3dA_~T|*Unrvj zCL(uCo*6K55{)OM;ieSpj6(ly2rwJnoLvR?p4xb=)I`&AYf5Lkhj%*_+pSMq1lLeS zu&fa_UqkTRd9*gp!Ccbo4QG*0x>d4+2ZeYnHW^W3?ZRS;;2TGpaMzz-C{+Jq@yFo= z*dpU72p3KrPWY;5oCYVoA@$6HmC+B~u#SY-u!)@A;t+Js=}&~@NZO1GymLSpircQz zPF^PW5B0Ay@S94$Eq)+W-$#FX$&QH_=#4+%6pnK526vzh9US2lC?B2OAtRs|uNqil zs#Z>a%C(ZW2py7qJ48Me%T#R4wlpf63#cuU<`)?DxeRAiTl$ zy5oi@r(P{eTe*0)?~vS}d$^SiJF5!Ay7jGeR=^ z$OmX+iN`Fdgo`-ovyW*&gqbBVEdu1MKX$pyBMZsoTkX$>e_-#QYk@U2du#&mPb}Nh zPzm-@lv7nRG@((qB<9k^Eu&v@y+Y%S0t@_6KKNT%fYF3fFI>~PK?1`?)>w~M|vfw$uL>#*e{ z9vCh}=uYP?Yg>v0Q z$MgF7Hq%J}{1@qYWj2i^1xt5&7N*zD2f|c=cAXfQc1AoMMAd~d(87$iA9RG<)25b{ zJ)8zcN>?2WC~q|8JCM`YPMaVNQ+26e8_-~rq;i@3HWRNnybx9hm4%MFb?Bf5v+#}V zWtAmQU9K6Z{<8)owUq-zBSNX==pX!`lo_RA6_%=?MsPwC>`^-1h19ECLnIB1%JwM4nevyejZx`0S;$)d5s7%tLX=0HZEnS7 z+{DmqBwQ3bM8AR#FFXkPVQS@us}ZgyCUw?rOt1!O$cKTtSpj^e@dze%MDl{+`Tpct z=f3_U%nnvI&JkpgTMQ+1J1>32lK>@mXmhNZC!YfMY;m>cEN7R!u&$u(*p5r7=YQ5< zl__a7=gs74x!|IAB($ARu}F4PRVJ$CFx7pfRl47=Rds$2cte}n>cvFJgALp_Nbpgb zkkL}Fp$st~dL2J7uNo~-s6P2BhxqMxWZ$bXwinN!EQK6n?t(&d$N8h`F(|@RxbABm zNg90qaCM@CwOe7KK^pg*5C20nLQ``1&8N+9vJa0wgHU6C=qdvOg9G=2iGbfwg|K~h zKbAAOjxomIgp_h>4{VqY2kv8DmS)bEjvcNjtVTe&zB#&)d{@yRJKhq(J!dTl?G5i} zkKmXC$?3+>V&nu79lZu67AVGN5y05bf@L1~bNU0HGD_dTUbJXTSV!D} z>~_bUO6PlN72Pv2{eof@<^37gs1B};(_FXB!7PF$@{)UPwkmsCSqLNZ{k>@3`d4}W z7MRHZ>?mc0j0*N9W*c6ocg$^B=tY6qXg+&8gH}uxBsKP`4<;}QCVP+=h6n3;2+rZI z-YgfU{EI+uu$oEn9retW4utuu=*MFIl-PERN=Z8u5s9j3Y}AKH>P=%xn*{1Z3PPAq z<+4=|2s!6cQR81$);gJY8mPF0h^{^6nA(WyK2vaGF)KJm(%L5W`j$T2$E{DU^e!tz zU+}%0j!!rc*n{XX)fGy*W(bUR@y)Y>O^IBzV6v+VIEqiyYaU`U7nMP<3EDdo8iEc5 zJ7pQ0y1Gj7Ma}+c z!@ILw%(ZcSrO@Cun{8*f)-Ye(D_ z{V(+VXG1pRmrS}9bb!XQsk_@h$oDX3bD1wJ-|48K|NHkv5+7Y;g9CS^O6&Qfr`Zcb zV7wgxNPZb7`0ymVLt9HyXxvt*hh>t3so@6%Pf2OZbI0oVZ5DKQJ~^N;mh1HgVQ(u+ z7wh3qp)zn( z`byVOUJzaw$zsC~Es)-8s&`$Wj~%mzr{M}eCtG)EDj5fFFVtG6XTAjGDxuH`K-PMg z-0*LLDPypSOYx=VG2GJYt9pa7eL1Re9wfi^L;USTB)p;w2Dm+wLFUivoP7SzUiVRa z5tY%vN8h^DZ>>(iege6`zf*%4D!UE8N$nBKLIyuIiBA~OB8|#Q8#Qyj#VIFy`u(7{ z<^+c~;2x1iFdNR4DGE%`N`mU4?3IorBM}R|+qo-FOfpBNNQ{&E8Ac*FO7NJzmU4Lcr6UFeNI#^*J(9=Qf!A!uPKqro3Y9KZxd+X_Rhrgv1PG{4ZxIO`G_{3Sn4#l&}?6+0>Rv>x~$ z!P-aGOEcK*yk*wMkwMEoTsT5mVqkuDPh1we>gVs1#BL>w%52!^G5_R&YS$!AKL(3L zjiERVA^;(V@`N>K3-(**vY>w$aqft#Kz|{1AkwG(FBh+y&{}fwzBm=RgfO%e{P_0B z{aMqPoJSpbA+AxZBQ~=Ty`Vdp^E$dq;&=7Fb9m0)_Dl~piwX0N3SSp(HcuEYyjXtM4%Uri0u(cFr#f041<`sAmp-f^l;H*)4sKy=FG_^E0v|Z$5 zr2BtES4Q;4P)t@DI*I2@ZG~EmBsnh$J;sE_lJV$4a>q+HE@sl!0DqMSCLKLJ+a^4O zZeJ-<>Q8&Jx|h)8*#%x^%YXk3SQVy`?(^>M0VoDd@4zIDs_m-MuN^Rqm1$dHQ^1gU zAd<-1e?VM_mErb`^})=t*!@#17JLjG?5QY7dr)Mh)756RnnLfwpX_ayc=oz83Auy$ z>EmzzK0k~ynUThcEIX`0#+cPx{kc|RTBi5>XaWoyJ4D%SjQ@6cTz~U!vPnMm8k}~7 z2w*@(1?T+8EMtWi4A|2DzN-Q%ivYlA;}uHXNW4 zku}{d_@iX{4GHX=hFU!f9w6rsvZk(qT|hkbpzclU7Y@eu@+o?4AnD*fthZ6g!7`q! zXs0nmfcIRvF5aG1gkAX$UwQE%fLG{P&B+D(6DE$6!DQ*zqVHj07q*&YBrxf|>n8*Q z*M8Gi{P{K*rX`)$KoYis&*L9N37(A0Dt;>P32@oeDX`1YUyjrT9IOR4h8sJ;MFjN2 zy7NHY+HWdB_|9m{l0f*DUKQxTuBrv2ha)53>#|lB&I$faGsSZkhiVgG=Emn0W`+GTF=F8Us-p48%K9qr&HiNvt9@yHi90Z z%l6>+DldM7r`>peF0J%Q(w;Xjov_lxF)P1gD5#qgWJF&V0x%X;8DJXpdv#{!HUiqc zMOep2;*LyFQ|7cH!~-+sS>;+$2o$BRrPvY)YV^ZXlG82Bs>nK8#HiERz&PUP1R|T> zhJ?d%IQSwN5+!pC`drR$x_C=pL-2j6bNFn2$XEf}qk`8OtGhsPaJxEjMVgYzFMS0F z1m`j-a+3_2J_uP+s!VX`&rIN>=&C2FOs)#|4@mT%Fy+y%a4Z8tE7WW%z^x;i97qk ziIfJOUzAwY{8LI@|XNeWh4CJT$^n8@YBpBLuA7C8&D5UKI%aE-1)V~LAR+=b} z^-?A35D*od&9H=f5vknJ1J&&Mz4jHY!FPbf@`+eA$QvqYi+bEA?Ga90*%wPzQYSwF z0HsAQwc*~uf77^W(ERQrHvOZ}#;l;-eAQp&{GTaPV~y2G01&w0V#}5gN#QR2(h2iB z6QiYPdN$5=;NJzo%uWkE9`*l&Dy$zJ0o;E=xG_KqdWVz_A#OfeX`BS%I*#d)lTTeg7SKQUe{!A z5)IQM5oBE=7+Ujzqg1$g$+i>Yk^CvBw624KEct+fUHJYx%KR@(h-z%bZ$}olq@tl; z2V%EYD;)FP0%+l72gu5sdKeNn6+gSAzHrsCirDc^(^Ti)bTHyQ^3qXt_7bc|Go8c2 z@mFlD72LefAsyY!nfAhRWVTsTP@a7zQ6SWK~0>Mom zn@jF)9#e_Z4uhm%HkL{^0R%@%!G{)#4Fv+n{ow|WI+#SpYSEo%0DYiIjcPeO^s)wth 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

WLsV6{9IcaE75*u))pb$@}o8BT?k5~|% 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 z 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

    ??DX#CmoHd;hazR)^x&)&01_LC5`hAo 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

    G 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


    r)a}5hUf9g(l2kAtkMzJHEi;c89Dn&|Ut*~g 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
    naF01_gy$X{e~si(Y`Y+D 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

    {_BvsFUHZ(sk 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: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�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%0DZx>2*pt&l8NNvT8CWJ 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 TV7i;P2FqJk*o?frH#g-@-iLj(w6^PO z>1O-%ZhylBjn7C5CmBSq*pCLs(*v-AMO5650n0L60R7s%W7KHIKpfVg*QSCuZ_6?n z?6yDS(kG_G9^jk;?0Girv~B;lICSHr1iN|P^17}$ABV)M?Jwa$2XKBPdsJd74|oMi z0!T&4CKey&B=+raQH4BMwFuW-LbL+on>d|!K+OO_YQ^C`1D&LE4Za-#C7Q)wYbr3p zeGhOoS$;bixOY|T+=gd*MY{}*o&(Ed#W&ph(6+vv=D{zxrg#@3r$I`!>mT&?049S! zj?A>!w&;{)XOk*rGusN8DAxan1smvE-sb4MYPlEbW>qOB^K*0*6u6P}XAus7YcrlX zh?A!>`=ZcNqIIzWmc$tG+r$7L(8Kz05d`+&6e?!^l&H$5TWT!;J5vJt$E z#|h@=H4#H3wRzOKC;)o$2hVhw$DSS_#Xnotj<6M85}?hzjJu_K95H1elPe}M@U638 zEl=)TghL2FIGF&G*Zy+x+j8;+)63|L9h$B+U*8IU%u#yX03joC>zA{{1j^JRO` zSlH%}9nyyiOqP`AG~mggVP9~>A5c*L4zeOIGVyLZU>Qv@Np8SG1)ut> z2Egi4LI8b*ET<`RkRS00?AjvL>=(~;yUaSY6q`{UP z)P=yO>SyKIg0zt4pf$+d=H{f^hVDmreYY89+rh)PO~o~tQ^_NZ@=D&bKT#ya31*rWtzpT!_Tbuw{u#!4?567hShhQH-iSW2ljCQ|9Eb6G zf($~{k-oZSx1)TPUVx<+)oivQX*MdF$`W2}=)$JTNvg@%CQ9MK3g&$*;qcEyXTS1Q zBe$?@(qh=QEKqx8beaN)EAg+UD5C}Oo51Pn>sA{PLFr*kpRTRk)^GpxB+!qv82!Hm zo1#fNZi))V{IshfZTvr`_Zt@>VE<$w_!*6y!|Y-&+6mDYb?G_RvI~FfoM`VfuVk|> z;qPz~7&%=B%ckQq%k}W>W314+-A_%rIMwTq+5@BT)&_iUx13<&GA4%$ibOD;w`B8! zk#`GD$swn4c!~uV@FJ~5nYJPOCoWMo#};zt5D&WD#d6>N2_(On?5xCmDj^ZqWi1;l z!Uc(_g7Pmi4d%j`~y6)&O6du!CpC z212Q!)m;VdsEHMgXl_|S-nUgnCt~j7lDq4Xz5Fvlab5T!$ZkbiZl_#fh!&PT1;8J1 zIo7f?W)t)V_cndw;S^2DYjw@b7YqF6g>HFLT)?plI-$6uyLNL*`3?L4tKk-{%P;sc zlhpMRCN@|E7q>ju|NAUu1xyOrIT8~TV2@@T_C1-#>A?t+slLVdk^4pK?HU@P_?iG2 zn8#aoyUNc?PzHTkV+2s${aaH{KJGuH6aT#SzcbY$ErYZr=g}O;3vf zS)DOp^Se@=Pu5VR#Y5OE7L3+(nn{jnGVO?hjBc)O0PVKJY(gr~YMj@zb2FQsh7Z(~acA`f+M~$sTDxuCu`S-Ee6#@CmKx#YVpZ|g1A>50epj8$H zd<7AH<@)Vukvw&O5TtSd{zQQ(nh8n=>HqM@w#EP7K<6q|fnn~5ozwFw&eLkr2 zSe(o_U;$tLj~KUX!p|4r0Y0oJf6GTj2TgfnbVY^iQM&f?w!8ZEJ{}-bEJ0@ULHYOv z>gn4;13beN@%M;?PSL?!Emn)Vl#ae#i8}WcW3sJ{3JO4_ddxGfe}=fJ@RV~*xt<&o z*Pp-lbGkIrJ~&>+Epl?3^6E}4gvz@YbsZyFiO__Vx0S42vHzY-kSm#(#5osdQb!ut z)L$F<`zl>ts4uTYlHW@k+(`S9Q}?kgDBM5uG$#vv#<2h;%N{%qE5gWhKay09j*2dz z(3;s@Mcqv7ut75w))#VZo!V{jQigMgwGb|#VBTik5n~+j8GnPblYRCDvIq1zI;Jer zC$*%uqOM9UY(OD&xd<-37h`x0i}16Tjw`Rj98P^P>c=dw7H>@{7W&m|G2F{mJE(&& zZVG#|?rrkv5u~Iv1pxn1C~;1j%3&@aoQY=Z8ZvnVk2oO0ZjSLCMgtp`mTrD=CY^D3 zYy$uK{_B7#QXm{xr1(@VaUFa5EcWlw$7SVeHUgjsj`i|(Q3S>wW!4q?oA6G_)61Rr zMCvnwfSrQy{n7Hi-f+jL@FylXs_NxZkLT`D{&W-Kw$M*bCnBLR4RMCdeWI{V{{>D~ zp4-Rf&6cxWNm`A~$=*+Fk_0fbbkqV5_KjxxX>3bJm$(9p2vHulJV-+@uY~$=O_vqa7&Z$)}*jBXmgc^J5b%kp!hncAnW~5iXKA^k*Et}+2xjFSR z!bp{9AVSK7yeeu}8XE`M8`qpg{Joq71F`?mFNIX-m`YxOal%ZIw0R5}-3q{ZF)#R` zqD?kio?pmbPBLa&%ADPz1Q)EaOB*_T2sz(RY^?@MwTH zo*~!?0YN`IekKh+Q>}H6{H@w4W+Fujc#Tz4sn0~$^`o;U@!#FybgYxL)Gx1C`pTRJ zwG#n(Mq;-A6t?|^?T2zcGNklVXI)2{CXMk>WoC>FZ6q0WH88Sm;!88N*4Ziq_Ba~e z&XboBd69@TdY>;{vCy?85Zc;RYT{=}&m);W-1eJ&L9R_$B@=w`r+QGtJr%$t0t_-a zAHRB`->SkuPq2lQ)NSil8}TL-p@m+pFLcoXV<*7y`4o}70wqN?gi2i89wICPdkXy6 zCbA;5xBzkA>y79CL-?gq_?#VI74}l>rQK9qabpv*Y&2T2b@w#fP=XX@5pJZU_}>I5 zINI>&AZsmRUZhH8rf+~3VTzgDyh&Rps=GK(QCbFKny?588E{Yr^;P?m zjMDxx#lA;(F32vUsz94|_d{$JNs%X(Qtv(=v}N_w2a?1;@d{XSB^}dY>FaTR+Ecc? zRb0JqFlkRm&?N-A^Lh^imw0gqf=9>uQR(Vp7U9>~_FQ}G_|C(C!M|o_i}A6?)ZYH#=2jL7ABYt!1yynWypy>oKdu2Jm<4b9&weOr7B}x6G8u*YT@?uRSI8BR z%yd()H-EsSI`niQs8qy2aYuc$>~}hXEu7JLi1F3(dAhpBC~wl1&UB$RwlsdRSLs@I z&q2emIxD)uS#Mg#(k@`7H@jfTjRW<*RZmES+qKi5wgxfkbeI#uQ5b>wrK2m%)>TZA z@F_nQDFCvY@}itvTGMf%rWj)k+F!LQbszFPQE=&5k~M_oExKdWbMER2$3r(`_J$!( z-?`=-3yE;$vj&_ebz#MEzs8(6smbLr)`(99w}s4q$Fa&kVTQdo9$^HS_wa`;hdGqgGYINP;&}nxKbAB?fg7x<3DFp zWp;U4T6+wBU?rQaPy0IkK%0l{tpI%Qol)#}_TDC}_dnyJiZl8g!AZa&Tvty;_;hgv zhW!DCEi2Y85jtj2tgutZN=z^ko@)5sDDvy|g2G;{RD*zVi(a%Hb3vf~k@-(3LjDdv{zPE!&JEFZ9t3#!K$(mF&(Ld8 zfxZF5Nf==ZxcRak!K#i^FE{FKfN(AXJnPLL&J;`CPAgDWQ;Yr9sMum0cQ{e-+!vdH33Xu-H&ZkJL(eJKVNVUXVn(h6_SA9~9{cs~8} zgQHjh`9^FOzau#ZoP+nCac!!xFji#54hRl+V1NOQ(wq|6v9{XcetUU&nM5 zt8eL;%Z%vgei;>$tT+=FEu7QW;$>08TePok zu|PU?B@nFG{Cdxg!r$MzKI9O4!Uqrgk0E44pw_^XP0wp3P&7OVX>!Cjp4nIh9Jm>X zIFg0>GX$hE{s#H2|BPL?Z6r>>D{gRJBVX+F)Sn3>PK&30#X%c1HF5a-D38$!#rpJ7 zc``A2*)Q1JQQQt0-)A+Fv*nO{(kbD`lB1Nenm^)%dRa0Vi8puF)OO!Lw=rDr8?w?N zqrYK=7Zt9g-1WfN{O4{}tvRtq&CV_iyY=Wz+aLP;?);|X?a|zLF)nurEgWfcaF9Syp``lpu-y>oP%_pUedpwY%)#JkRI>FVoafaqqX^uVtiK0k1i1;546gpIH`r3!61K ztBceF(RJV1&7UD-CwQ91xFA_lmXCIVfRv20*gNACfJyJuHfX( zAHd5NW!W-X!jPVPYsyf`7;|X^AT5Tal0_yclv6f&Qq`g7@uQJ#qVvjc{&yV4K<#%w zAjI{s@orDq(Z70k6G$Gm%<_TFeS+dU90)^ehcmYVS2Vu@TJO#X3vu>#v$$Wn zZUeh2vD);MV)e$)t}3YF5b}0%@+7oVi7(Yhq~NdJac2aqN)%`8#1NjK*o(AqbXMMs zBMyi{I4Qa|VN<&82ujGi((@7cZ zAC$UghkvUUT}MHl=}V7#I+G{Bi_WM@)`nQh?nJE|vb`)xrSns%&zK+i!<8-@`7D@h zC8By%wlHKG{n+yqJ!WLvn#KH)BYjs_`D`inDIR0+;};K(kwn$Tm&XUa{;lXol(({A zG6OP|18Qry1$O)26gZ$}C(O5k<2Q&D;k1<9yE^n}u(yy7o>IEoO(}MJc?99ju=tgk zamK!JP{7Ofg3_wn6J{I}u5qzc!y6@qFD&^E1+;6gYO*3$XyYB*Z3_*?kmG z=kHu%M82qJJ+vdGXYqS;mZ$_CjJ@T2+j)b=zf|fMA%p2j$$p@VRI&F_4mimh?Dz0q zqCpcqC(%EdWU!XlDn@GLPOq35Fr9}&JGck8FCMf(r|-H|uc2cFg0dnixH@iPOvN=gs=0T}7a$_ah*PDp+v=oV zG(nM3`=*y{i~4H zja5@%>>-cjZi0kamPLVZ5DyE=MMC_B&^9Idrccx)zZqF13^IO85+0S|@_dpzlFbc$ z2kSOT=lW1^T}i|_cM3S!O@0giXDg7DVIZblZqkDjezATXMPqq`aG%e}!hipj3Kw9H z{jA`tshyZtN@FFiMhTP@@U(ZGJd{tIt{J1|5_Ip0(9-^9w^Svy(fTMW32}#|FZbp( z^(_U_EA=%`=4eOM=#o7_(B^{rbA|q+B zyg<_MHwD(oP4AsjFw0>cd*umfZqJzDo7$+b#RIVlEVC-zQ6v{e*7p({pp=|7NTCn+ z|0&`=)H7>*c+o`rfyXIZs(mY>1y?AKnlLNr`e%`pRlk%x=8~|$l_DNu zV9FY^mZ=Um`ez-jupE2gFZJPtt zOwSGl7m*8px!T^<5=T3JVZ+}!RdW|4vvxXN}%g4@?Nx8g(c z2!;SjxjC=c?)~9JY3btUGcdD1s*3XWJ0XJ*Avc9iH1h8sO6@zxwA-3!qe( zUUXzBN~qXi85Ia(rt6E6_YFL5Y*<=HGacsy3sO0+z})*xkrKrfa{8TJBZ3Hx{{wRpF4aspeHp0;Hb5Fu*=q+Vhaj%?XCiZeI(QOMQkQkQc;N_3z zzg)<71wj~$n0TU3|2oWh$_&QHrUg1yq|N+?3;8Quzz31cFGC)nCt^%nK`a`7u+tDF z{1_a3OzVG3p6hrxH)#aJp|FsKnY?>{1x~XP<0z3~o^Yp^)&o%HSn$kneM{+9U1a`& zx~2-h!p7w=b491;U8?uL1NJqP8O&`HZ1kZkEbRqQj&wPK5Hu=MP_A!CGG{-^Zo>OX zroDN?3W0?SQ}`U>`U2!F3Y%-oJ1z(^g^!7IkOHJ7JodnPg)qZ`y^J^E76IB_NH%G^ zEePW0QGO;<^&OpekT8$e88Riro2tQ`3^mo(8-HrgvC%+K4k z75A#UDB$Vxq?*%E`x&~`@G5=pTm-fOSx!|=Ov)af1EG^@W#-jVnMbXb#c|++#zm6{ z3|`<~ag3=h$9t=oD2?6hMRV);jZT=1A%kt?&71o~D+R$u?^c{Q_3=aUMTS_lR!y%20m?u*(5u8=>6%w!9Pt*cb4@RP zrQ_8hd|b4X(?wiGJDIzkas!ynu&^>HdZa%WMLE-ATd9@NEw)JMRIR)cH!phc%#fium+@!Qa^(YZgr6t@mcdD-UH z#?ebJbG>wBi5V5``7rZd-*3k*e%uSNmY0-0Ni>aPIZXNY_rO(jFiwUz7*2zx_uOzi zzQI@p2K3BpCA1XZ;2Y7UQ5=Wfw<*!!2T-&fI57td5t0FA`8`E5pM?v)($?TF2J*UxVPlu6G(SmcEbIkdV1191{ z%=2~4n8-wRy+8`&T;agGuQUPzj0468hT2>fp`kW`Jp}=x(%H7sFSQD7A*$OacSK{b z5sqr*n(Y~<`r%l(J^|ia3?Tj=+Z?)g*il4K(dN>oI=iM6MoEk2@jjqIB?d$U+R+L* zb1qNcxQ-AB40QR}30MP$y7>aP%)-NA+HsgpwF&=%YncWL6y9YaYmZ|)2W4pS$Av|R z4~GDAbzkE%H&CTP9Ln8RCh*wBGHqt&B6$LPE@plglf7(&|NBLnN?MlDXz3N4eWqdk zYqKl=@519C>D>m`%ddRPW+Qf3b|}@8bYoK@`GwvW5ZCpywUT8Rf{}xwp-wplN@7f* zb93kB--)L#4Q zHpOr4CEe-xSacDgkm@jE+Jxtw0i06Q*BXBgK!kA|z}$;PrXLJUhCsrLH4C2-eE9>Q zM+p@XyXNZ_(uHqx`(N)XvTo{)x)^;s~;l&6=kvMK20?h+DOOhfMl{P zmo@w@9Xxp$Y1Emo&QEIHss9c0$B|y6@0-sZ_*X|EOStgq83=Y+>-=D8b5( zWtousGVn9SRSsoxFD@d-%-&3o0oA{b4lNYOa>*-FzwNZGwuk?}fo65t)^jV8!y`}b zRY~i|q(7L*`?gHx4V4r>S+-UcGPPaaHw-8?zjKau@h($cEL=ctx=%FuE*kKRf2}*0 z(695C9;hOmKXOuhY5(9-mfLQODv`0+XkycQ%u^FpaSW$92l)r^4xf`i4R>8=M}Jc* zatH1N*2>=;V@jXr{T13=5wMi@C5}m^S-bWwj`^!5CSEyT{4!xTW@=^5bg)aJZc60k z=yW{Sss}+-P&tjKR|KR0oU^W6khcEUcGekNrVD&=&>%Hs?oXjr4Y~=5&nHZOH-}~9 zRtfXx?zWx86|hU5%TS6U&t70H#4w*Vlfyj7`+;{5uvyRx;ZNgD@PV1lL(G-USsR~t z<+l7ea@80P7J0g4^|K{NJ&@O^30YNbB2f`}TlkdW)eu_QR;<0J?nlXBdn*xD*~ZUb zDJ9C}J(;6GX*5R@2o!m{%;v2#@>q%SOnvpe$c(QmZb?`-Da04#_pW_S$ZX->5oQOE zOhKlxw2LCN^*k*zDCXEeCB@nwBK+j`dNTWb|w z4}xIkh^Ua->mXZ}XI!Z;lA8aviRMQ#s)@zXp|FsZ*z2S(yIz@hrH1BB^M-GXo0u4# zgHTDTIyTUm$1@T^H#$wB$2^N1rkxMLcHDF!{B7NfF)_;mF*dqV?!xS%i=;#k4EPO3 z=@8c^NLi9hIPC5@{X^<3r1k79I_E&=#o3z6XIaq_An(I7Qs3Arf^EL@59C9Fr6lgX z=stSbhdJUoi3k)_7=yvEqm`hzk5F6rQJ#~K1D(j$*`)`IW~?0sK@*FMXLcO|P-8EG zNt(dkQ;RoJq67{Vpp+HoO9NaD1vTfoJBEwpoJY)RDL@2A_!9a=xho+eeH64V06{>$ zztv_`UiFN%}MSmupbbi`>EYF}I_a8}1EW z-E07_q3M z1OG|nP?(Kc>z{cy4~N}w3GcqU5qR(+tF$P*_+I#{&@%j0b)T1QQ{%MF^V7S@u@j|3${63zE?3EWl%F~Xp6bOfB zvo)9X7b36x$`xfm<3Ve&H&J0dWaQ|^VR`hm-uPd&A^4)*IZ?16JimbxwO8Vmy-!sJ z@W9qfev8M%(wwTNO!EO>?Ii{_z8jssbyd7PlMJXv=60FE6+7q;y~vqUHNp^ARZWxD zI3tIy2NM#EM`}{KL#CNtgn%w7=KJM?9B3xp$~oFpI1l(roZ$4Tg~x}`=N}!oB0d}h zZKN=7vx;#Mg)TSH;k_8K(-`C`px^C7v$M$|LOZ6u@j&7w=)N4}-NnF6as!bc>nk;v zPF?i$#pq>f{i#5wxx#B@kAG$BLbb!3c1Um(5w)r2H7(^b030fg5!hFvZOO}PgG<7E zO4)6wIki84lpkDS5^u`7rmv%G+hWBZjbnD2-pd~!rgBjF0zLFDNHscC7Z!|hFhFx) zy)BQ+6xo)L2#$7(<4$hpy-Km8OZGZY5ij{*(!{*oOu~Y4@NpQ1b}f4t@NK61C-S|k z(fgwp4hSOZ-7CJ8aOf3DKs7Uuk@Xst2Mb`mnT*CI;=j(17{n~u$y<#ctHvTZa8g5I zagJy;ov318i>BJ~6TW6fC_L9X-cIIPypc(B>dTLOgV~vu4*OP9o2`g=+_FNey8br} z)p0?#gfaa$ODqfSXwoQnIjt5tetkjCd-4{mBpHenX3>L0Kx!Hr4K^@QDWmN&+m#fDxM&pzxO>eL4v&hq3dhILfzF+W_Pz!>F{Z}(-7hoVfS5e` z$W#7#Wg>f7AV=S=pp;5qOme$^QX(KHkO?&z?L1Tbsl0EMEhvkRj28N{6x5K509coS zR&6{G4+XN85pl#e$Wlq_;?#~$6MxuqP`mt`3Zqdgtw-c(gIObY9TlM}VCV4|TG=4u zGv$WU00o>1g#Wh9d!V}6&bS8FAC_}y1SLgh5_nD4nL~@sC?h*IThBl#e8kFyVZKw@ zFOg^584~W<#4cBp^bEt(UKKJkNsT)!4Ys4OqN#|6hs_>gMwouh zH0(fNhF66#B2D;txM%@Uc`zN|N|^YH$<((TT+Zd0Rx zjuYt`sjcPK2-PCg=PSqR^P<4n{|KdB@s@ku;F7cK%ELzc{{cy_NRV>B0=m5(**Y)C zZ)Sq29e~s%F1UXNIfZ#!XCK$qb|Xm^-B( zN@L$Wqjdy6=W`z1Iw7aEmViLJ1f+Tn9@+gzaY6XNeX~0gDtuOGaKsPyN;sg9Rri&zItwH1_`z@iT1l(a*ew+DX+F3zv?Y z@~TKT48S4b@C#@#0fj?P*{|NRZ?=R@rn8f{Bc(>Tu=Uu;%=*-K=j(#FfShU>b7<%1 z?2`SS-fHNeqsFmC*crRwXIp8#o9JbXgATTk4#Gr`G4IG)%;~B=61u?H#2y(jDjWn7 z?%XDaxc@>gLyb54MpjouOnfqiom{9>Fb;B>NTJ8KxxL0Z-E&a^bywZA8F9Tudr<)I#FN_(m21;v&Z_?pI;O4p9whE{>k^P3TL?V6Ufu?!%S4)bf)^05 zBfB~Qm4$aJTD4P*<*J5kHP6)K;;Mm4$NSkEMCG^`^hq{x{1Wcxmc0enhXqq;j+e-} zP40X4AX~FO;eIg{f2wVX6NA>IE3ohp_z0fiIh6XC_6~BzZz-A^)^Q0ia>}dA9;g*DNbDPPOpTMr5*;agoq7x}JR%6_L&mrL z+{g*VrDXYaynq)#M=OT)6Z%Qk=B=1szBz`fOO!yvi5jq$WBs5HgDYioK^Db>86zR4 z$@8zpw%lFptw?;|c7$mHNMjwiRkXm!i6zpkriM`NrXh_fLx>yj?tO3I1G@kzhgL#C zbJ&_Wd%Kg`Ens`TXJJ~EH!`>Ktw^~?hB?cuD+G8)?W6&Pxxh*s?nL|wSNJDgOwjcYd=M=Y1#hrr zXm6UZrfkZ`&ISH`YAf=i>Tvy#HYpadSV?@?tF5?uw@YrGc*iGAq)DytMviKf&HtP9 z!jcj}NSGG?RAO4-%;_ELa5vg}(Qdcam)$91Y{0cvJDLo(; zRNPYNyT|!>H`s*s7@SB(UIU@_isj{w&3_GX$X1HtjD#MMT+pyf0JDOFBa({J*yeM3 zF0TRv>0hm7t&a3&?Y0zuzN+D3IqUqu{uurtutNntL}OptA3I^t>1HjVVis& zUrik*E0cVje1S`FyI#PQ3lpxzmplO+%?LXo3JfM$4xtHe>bZ~_VhD4V#51d>Zj3%X zx;}#k^6HtX3Y2nPz*jHm*T}F#r)ZR9=nn%~E==tr;rd@7kseIUB#^~sURz7&>x)HQ zU+3S}PbI>sr6XaHgI<^jgas<3l;{R^*xg5*FyPLxoi{`kUXQ?9VZw!8j`myK^<)MA z13*bsX#`$~*g-jsJeN5aok#}fDY^yk~0Uj8fw$1UnJJu$jZDRiy0cF!S% z)Tc!mEcBOG_d^$5;#ia?MT=XpuL-zxtC%yLs2EQg=y0;lW+DCYLEYRlQF`*ssjR%U zB>M`5S198fG-?|G`F|hI(-cv4(;Z&WtqHP%C(^qsBrw*0idkw1X4J>iO6c_}A(~zA zrr8QC&nBvp97yCLdT5hDVWzz5Y(}OuFoj-3fZ+2|;LT29M^9Mlcc&%9{nqdcO>dxq z1LfbCf^cUPum~S;=3Q<^4NLC5%?29UjjX9pDAQbT=KOL&O-qY2T4_`llcFwzjDfB9 zx;$7i{g)!&8=OZS568oatL13R8yfV}jghOLz^twfPd`oT2krl}HayCo*3c`I_1plj zQvusv@hg23Y2Q@{S#LY<@<5(1qqtB3EdOdVRT~upteZa<7WCSa(_RM-`ojCOEl7@>*2 zdlbxCgx_`hMZewXk#trWfpfgJbFLULhJ^4Sr=MVQp|^IYSEkBpf-kzelu6nh7z#TV znq@9J*OfW^mi7W&mv<-Ghyo>Nv*lw|)Yc^6&8tb7r{l)2Q%?B2650tdZpQh~HvY$s zjK@7pVysrLA8rG;!5&1Sl61bIoRsWF6!DtRw9EE^W<;na`0};-iM&6QAt%AGB_V*! zInTvovXKN+)M~Js@^5}=P9>kp636A!w-QkRUmHeNWK}e#!NVBKSY0OFHJn4s%Xge= zlT{@x`T(#c$p++(oXxX5H%$rz)Nysk_}U8QhKsSgRWW_R;o45Mta-@I3zlz`!6K^u z5l}AP{^BlEGE{g1vDUlt;6FvHtQ*R-xiD#AP+qNYNXi8gS-c4$-3-+fi+h0SZ4h6R zoB5lr0V%E1JBr}zvy6wO!6j%WGn8s9VxPtk@+y9M8IALJWt5CpXcxX#+pz1gY&gup*N6@tU2`@AqsVH6Xb7_1xyC;C26` zi3aWh32?k%$~~08`-`y$zSn_QYP(N=P>ub=`ipWI6U$xEqTP@Jtj#%HWwGeJ5x4`% z%&Q0&I=@K9TfES<#ggVyVfXJYS2|~JwM=Smn&XYEmZmb|kt|TP+?l5qMl+@6hoU=& zeT4X=bbtZbOa4`6M=cp(AX`)DPV>WQ;O_-4xV-J%#((eRAGNxGSS7QsR0AVgIrK_< zK$z>EC3r9;!&~v}bwL;N{V0x>x1qHc^s*KEK9x#Hluz>`npiZDPu2KdBg!fXJ0M z_@dIYHO4Z=@O z@E}@ff^^gim=9E%K}2Or06aUQ8_b=e&q9b_4;^h}Q@(qw=>IGu^0guULO2#^<<9Fi ziL~*pX`vzi^*9#Te>#}Kb(F=SH|0FCq-xVX`M{=_c~rZ^ZoU@*$KiKdl%^w)V3a>4 zl;6>2u>}4$&R@<(sVoM@#EZFhi)l|l2kF}b9t;|FGrdmNZ4{ct`hQ~dsL2n!jIC7pc+v=hLnS0-bxwZpi}eLhD01h z>iOEbSltSn$Lf8Lz=zS@rDRc%Yiq_fK%0{>7gF;z-T=MgW;HUU?u9FL!B%PV&<>+^ z-hk1f$E#}-a=^LJJ3q4d}PDoMVt zJs}3$Esh)Y^BqQ?C_FQP`2v6j8@&z-sJN=R@5$VyDj7wMIZ|0B0+Zc` zig-STLi1+`PD5$4us3M(yj!);M<}-wXzIus@#n9*B!r94o#WLlXm`rYZziI%LYf;1 z+Vw>LoQfzSjl2FJpj(_P0UKMqddQXqXAG(1Po?9miwrhY)-v&CkWW4L<0F*kbbicO z*;~WqbI5{CUdb7x-*#1hYmwWIQ7&5f6+Nr8=LU6|u$-sVWb3}Mjl1@%LI=nDlAj+R)tzYf_4w~s+( zN#mV`tKBTY<`XCGL#7pnId?*4NUA5WR&MP69XCFL_orA6Mp*de2==R07QcOWT7)GN zAc11%5MOjY;1dsV=&WZ8GKGcf2DWj7NduHcm3N1P>BUQdLGN?}c>RIe+C)`%Nn2|F zJ-wF#evCSqo|xb_(ZF88EmC2T7cF^0JB_xQnlQvHI+zC`eU$7w#LbORjoeU0KWJK$ ze^68T8Gp}AE&&y>EWwl+@h=Rr2k;{(VpL;)m~UTo>w z;y-fKw)RILWK=)G)HpRXf^bb`8&|pFQcvw!sRfvP1CfX6JQQYY;})-D-s2Jvnj?1w zig}wv+M`>k0htA*R6_I>g!L)N8Nmt(hw`y?U$K>S-l%5!M9-4B^MB87JbSFptH{8$ z2+VQaphu+d%QRR`OA1jGMV49fd9sl}Lq`TM z4ylZQTjDpGk#6lTAQGDXOK-F_!!mTkxY)3EIVd+>`rEGw)I)Q%Rk!YPt5s^BL9j_* zuC~-g5--F`F!UfkicP5sh3(`H+%{EOk5oBMG%X-R@BS}1jKQ6M52t`kk?YpNr}?>f zx$v!IDUpc5$h_3?%3lX>#B?5u(b??ipaT@KLvmYLa{nXIW0j#umdijMa^7aj%|**% z^`{@H`Md{N46A-L4s_?Ze{?S?+FBXvIA78eee*7romMd74F*o4qY>sFM=@vyjM_-% zX`|k<>ZCNyD4LqSYiT^j!Ib=eTbOZ^-a4`N^@ElNa1WTU`^*Y7+lhW95Vn$um zgHo#(_iNMy)uQQt|3(#Cz0UX>8ltK+Y2UDY0o(c9TFDtOHIKvJW4@j9T}eyR^Tt)o zSLSvV*r7m@i=eo0PI3%H!9mUBcz<5h-gGkMQKu+lZ4hfeTE z-c)W${8C6?MFe{6l~xr9SB z+oqsY`s3oFdp$f3XfUvT)XDk8Dm13O^ji5?_BW?tm|49nO((tKZyYe@5Fr@uLik=GiH(G_QK= zK4-j*z?r|cdq?%Z#$Brc*Tj?)H1Af_G77!oZ?pp&VJn%4Z{0VfT zD#Xe&=&n%xJCpmSlI!l}7y;4cdGxF>;C!OtW;CzB%?Oirl(J2896z1ZA!i_5xc=Ta z$ZZnQL-MPf4lat$)|HWT8um=-LPtal;f{bW+D8lZ-&aySA{AjeL4zVu2jKZExX=5U zwb1Ht!nt*I!7aCTZ+TXtC__XJ&}yJ!?D(G3OwL1*UiNCj1d44?D50z2%ZeS)nV^v( z<@~+>3^7Ua` zV3c9ZvRK5+B4-zfpT`9(ALEg6JzrQ?;F*R1?dAuJB!rJ)+|k0iw;2@jMusrXea@0K zJ&Q_F6{9lP8C68!P%<^EQp1o(wY)D*LICRaiVft>vQWw0x|noNI+@F#P?v}=_M8a~ z^hHhazW5PUXlz!Q)8C_Xx&B$!fTCc)Uk>qJ-sTl=^LUdwHB4-H%ry3EBKJ za3-vw1DA_@vs60BQ%2x|zOu}U;?KUf1qyIJ9daX>Jh}2?&fP{{k71zQU~@q_2)#bD zJ4#6Zf2c>Dk(LDrF9blbucO}#)9p203K+#-VD1>@Cv;3Mm;qh~lZ`}l8E}7+fThVU zMLt?Z6#MABE&6-s;W+2=S-mnYbmqk5I3;z zvS6;3)0;9JG2Z~bqT=f|fOkA@DPRyoa@&>Xs-%oaZ-7<6hd~>P{&ip44xsCXFy@~T z#areV>oks)mbXcvpXAU&WDhZPi<#zuH?FsFx0S-d+G3H*IzRc65K!?f)k|r_v}doA z1J+G}GDs97i5*N4V1VFpjU=$#f{Ojf2U!=H@1yEF4Px1l$86l^E}f#!&3O zZmkHmR!w}b=-lI0mspd4Cnas{4lFb>|3ZLV8}gby#mP-Mo{JS#=S5A!9n-VBL@m=) zid(jMujHFk!jS=atfZ6_j`86-0fR3Q^Xg1nnWpT)zSI{G03#0_$x&f>Qo`)QWo~Z5 zb5vCyuK>H>S;)S!vDJ9pTivgVgg=z`IPwef*RH9IPEjODuZ-j@>twYBi5N2x4BdK^ zBc!~>@SsGoxTWDo>FTBtE91~8M0JK2bn($Dj#sufwch`|ja5a|BI@XUb^|{9B?2uA z>fFClYX`h*Mdb29bImr1>U7~ND0aQ5Wuv0n>Y79%@AFNX#UPyGiV*V_6^_&*I4(kq zPIC*Bl%B&Cz$l3ywBNPmlZPij|Ce?%h=wC?!Qs`0Yexl3X2C zR5(vmJ-mLL)}d6tYwxRonzl`MjZzY~KfuI*de^n)CE_$R`v$BYx~s`3XZCa?98Wb| zVfe>e9WMS1MONn7hDtt5MSTkO%%)xse3d%_YCSiK!WFB(Cp1H>6*Ygypow6~O-Ay( zh0qorX(D-vemZ^@@tVEwo(~$N?yygVy-L`lCGo{Kpqs~nKRzQMnKLv}#KyMQmT&N;Mf?@bU4BXDBRDH|X_AvcJgrogY`*aQj_kwg4P~F!H zsL`!&JEvd{KEmEuN20xG81hUzK)l^b>>MqI$FgOnU%O0CEjt-a z?|(f5jzREpSe><>2)(H~S7jD_?5u+rfZ{xl@PI9W9w_3BW?7fWl`~Z|%#^+c$s!#Et7GU2r$6=*X zP1h$V&ur^#QZHYJqZQq5+Y_ihJu=wgLg{}1xNezVpf5Y*GKyd`M&hp z3VG2}&a0n7l?~4G+$I;E8MaS|r6a(l<1VhAVJu?F$G-yx&2TBlk@#!diC2#|bh$#| zJ%lZVl{CS0z`7`uFhM|U-AIyNDL$1)^2I9Yl>(mrZ}Y6jzEt=UN*vmP$pQcz-EJW9|$L(T^wvoIwP8XBZ+kL?ol{3=)P_%uzX5@0H`J?5!mb5>A)sJq5 zy=m(Xa+n7F<*A-i)85NrZw2o9vjtVyB#bt!@~2y&FtbPb)( z)+#+a;c7MT3cD8kr4hv`88fIUdyf=KbwT7)f*rd@0G=SI~d3@Xe8F3UdnlGd;=Dhcc z)BSfbJ`fbMGk@~J?b4M_It1)PlAyT>tMRQQz{N1Cnvh=Y-sTkDUjcN>+*lhV9>ej0 zX7*8(N`!t`;O6Lhc4BnZDG`SOYDmMvk_EhTpX+s9q%}hH^1IC!YLtmsl~m>ky%vrm z*l5^7(Y>zyMHQ;mhmZ&T6Pns`$rU~?5g1Z#!`jp8uHY>?KJ@M8KR`=gU7coD5wcQr zi;GsA)B2(pZ7w%AV}BwAbWeY26rmshzBVQ$g07kHSP+8~5Hd-SIEeKB0RCk#QM{3p z<%xI?@i%xYnJh$YCnV%&a^@C3`}gH#48~PxrO=<*1YP#@2~(D+B}wgDE|v|Hv!U~V zQz^n)StWL>K7VLq$O?;IaRtB~y_;Tdx68KeWAt1yWgRG}o{ zZ7FlF=HC#bW=~eu7i6_Mo+Ef&62b&V^D%UXJYqYUB^oYBM9FQed66Q?W60SxUn~=_Rx`~4 zgCm6JV*uNM%xbIw{Rm*G`)DtPAUJUp>Ef+d zXH&Pc0cVBLubshmG`ZL@pw@O6{xPkX3@5onKD=|2TuJH1jqj?v?l&@>OcEDq50_bfiND;xLLf_Rm#ErY z@06RXQbBK9-H@26w@MgngVUECpn6W9P7F6xb;bs=r*E1+vwy+|XhQ)rcr$ag$mL6M z9}35|m>qc-8J=y}gw{yj_U9duG6z7b<977kq*K98*vWH7NrZ)=Vz6c9KABwI{m+rB?su;oS^*U! zlLP+25h)B!&M2|?H#uKs=KdH$1|6M2XLzvnd=Rb|FDUf6S+MqVc5B@Boji96dDR~w znM>e=8#3xifGkH?D5OrAq(VDwdj;bar@oFEnP`?A41V^;Fgl@Uuy3g05NrH<_nj3( z8UjyR4d9cLsH7BsG0VLqI9dBuu@(W8LrA)8K?6%1zeJ>8_%>87z`T5zs3SyTex14= zym;1zv9?Cc^oDQVFw2Z?eAMYjSbYD z^YS?9?aiG#^9hFerhRFENVR38OHHmIpguEDCvDct26ZfNRh;~_ zn?EAtbKip#!-$bm(t~Hvx&3EH3GO`Ry5We<{#A$C1)kc(PBz0oK1T?B*ISampYu#sGNKN&G7 z0tE?($?8)A2f-VJs8O>~HJH zqvaeFvK}rD;lnlXh%>%2qUD6%p8LO}{euU1m*n2>Hpqis`3iIlMfQw@i@uD=l!7Im?BKh_8 zp!z}D`M<%MEz`m)et=kAXn5*5poiwL-@%fdr@1K?T?@9UaFx{5y-67Mr|Ka|I}qO0`}Hq?=1l?ChzqRsUz?^ z*_RY+cwsXVK`8TFFk!2ezuv!%!!mm>le-|FA~exE9-jAdEbh=SMWuA6$i()}*x4J} zz>f~<-o;Rt!0ap>m5o~_Es}1}902M0H&j(^Xt6C-K2$geDU7C(=%87t1)HXvW?z;g z0QXl!S=ay{6S2;jZ7iJ0$QvM2LEWPV9_GKxqQAD)wyTUtz8?CvIM3)K-Ud{A1V;vJ z{gYt_Rpmr5b=}eNlsN?BYPIPHvCqxnDST(2*LUBT!TO+{EG1rB#F_vU91VEuTt5fP zzf?sEwqY((!T*<9EzjQ8nw-+F*{noslF-)&Hj>!GFk*t&f!T&H5`ifvpnTjo=t37M zsfAL0Iq}-c@_~t0fvL-VUGkSQ8u2db#`O6H$4zF^YTu*1|2K$Ygk2;m*2rQlT<5)pm||~MbQVZ;+d8NRuc3w`j`m z*|^xZ%8W`ZclSJh=01S>))TSjwnGcyvcZH_M;-X7eVQe)05tKU?hpY1mbW#-f9)8z z5f3MRSSycsEj=dr5Otd$!dcJ7@1uW359=MPAAMi_WwZ2Pr%cpsl$WgN-mTImtt7fG zhe1|mo%jM*wQs!)ua0~>lsYrhd$6QdCB1s!ZX*dK*kg5x+SUpJjxU!~nqp1kO8L$EJ zFM~a8cqj%A5X0RJ3g5qA3u*hn*Hdma7P_(tU%VbF7ZFDtkiMvHlV;%RHPDbgtz=e$ zaQA6&o()#8QM9Ib8&ILVy7SJ6$1N#J-PnT&^k+r&8*Ewk=0F-x@86vD0?N8J!`{ zdy;=Crxj+{lEOXqcS$6IipFjgZ`M1#Ng_r9@k3}jq+rIk(!U95J7dZr9q|tpV0Jc> zDcJo+)(p@v4AW{U(lk|)duO3K6?6ti z7e1KY408&v0ckLstQY~UH4ZcV%&NR(Q)y0L#BpZL&_jlJB^jcMF82*N2qh`QFwztsI z>VM4;ke0Pe<|D;#uMjT@&1aH&AzNtk)P4^@q8!E55T3q{SXdwKa~_}WGSb33FRo}) z|IyVXTx^lI&FjQrXgWNiE->kZB`348a19-^buGk@MZjhYOy6fGUDgtVQpDATZAxB_ zwmiNu`*WBG`+yMP&Eu$?mds_cw>t+RnoGC~0D5$!_;{_Uq%n(jrxHgRloQ%V!cM4p zsQRE*(hBk6V3!dIJkAqN|=2leoP*kM!k<)mJ$)-86&u3pB|sK^~0AhUox5j-qh5B{uo4N_dysGyQZ@$b9| zvv~ng#1Zuxi>}gI$C~d!a-^#p_UI;VnNhjCus7D6^)dr=3RvFViUqBkuz~4nNXZf5 zt3Po6ATSe#6qG?$D&zxrKyFD@06s$YY+6Gl17yn%R%YTxhLpTQ_OZvdpCol=I_IyO zHu|akdz1llhuO5FkJi{aYi9ADhb)JU!d=S$F#%?^Y=ij!NVMAWb(RW!Qh!!G^JEZS3wfj;K~| zO@sl1K;_RTr8MB_an;@pmSIdz%ns2(OfcQ6CPB%lhivH(UC&KAn1PT4&E({{BrtO0 zEU{Q=1%)OYgeX)Bx^w!aQI^cfO>G(B7_XPXrH=T+dfsCQaHfy3IB7_({-RKdigt*I zev~x)dkQud_!_ykGN(3p=7t>ag-V#RE&2k+4g(|cUGQbTws1x!7WlC>Pb3k}osn4@ zp`4VDm^nJIS^|_67Oqs6wltUnvI=efV`fZ_H_vmTB>ViMl@u^C=OHHNI{igxN_%zK zrW*MiHh%6uMohriNhK&~?4DU;0tIUs+X;3^VqId>8R(0E=ZU+c&A*`Ur^+uXQsx`F zPIYaVF|vb?@-)J1&GjPmO_$4bAK`?SD%Trsdso#DBMAqI0nrRiBq9H9@YxO>bxcuC zdax~K&;?L(HH%w3oa4FbX{@BYya<=V>49K;)VcVlt(;gMO&T^=BV|f;V6{>&uXkFd zw}>oEKEu!|6^bAOXc+OH6I#v)GnIj`s%#%EWd__2E4!*^AbfFuj>@o(8-tb|oyAt% zgY9UIJSh}4)rU>n*yZcWK$V-nc&jav0(?`eruNjfBNF#(C!Jyiuob1r44ywTdq|p$5EdQ@ z>WH<`b1J6}SK5ecn$lm&8Wm$8ZXpbwO@yR|(a2XBjO1hwPlYH<#m_9Y%o8{#i63^O z*%`=p2l6)M^ujr{;o8e?7Lh;1hJ3^ZO*mi{ta1ga%1rK9mWefb!XsH!JFe$%7|Th2 zj!r;KD4Vi3sSLk#imBmmpD*R*JRcdW_+)VhOPG?X80#1G67I=UeX3I|qR+4zLCsm5 zjdd!&X*8_cAc^p&56zutWiaO>7-}XlZ=I8=@wsBLYw7uKw^pVNB~}(02br zi%iZWH3uSSRbk8j#-DjOBTdbSYL2PT&llOhwn=6xQT8QpgTZ~iOqz$-p{yv4 z5ZDtG|LpdW+fmt||8woQfbb%Vij&y2v6|E6B zi^IYVlpyx!HjA~<^awbe{#vD-exNX!beImMXB(gbWeEJ57B+~SSnupSD)YmHr^yAx zmxDS)`&*KnolI}*gy`U?C9}no+n47nq1#y4(|#-IS+E zT}ZgWcHQc1!iRjVS!zRBt3v`lUH8j+pv;(0>`P>>hdM(G5-gDaNvB~kVvuvuk*iq6 z5LsAStprwz)82&`KeB`zdoIa>?VGKMU?Hz_YM>7 z!nCR2L=GZ?S5>?z)mg8WDzw4nKO>gFl-5kZ?LVayY5vGM_ic>8N9m@Y8=bM_k*S`s z{rK!9lHNCX1BM%Sxj((i)6v)ctN*?koMEbZ>T9|>PTM>bX8yoLy_&pZt9=(hyF*jTfzxJ`Y{rXl05WBYK5f*w&0gDwqB!j`0@Cc`YrtHq)MLco9F3kl+x_8M%fvj<|UpK z@=9qtEp9`xyP5Qx#8a-NGAO{q%GxO0OlU&(TtNhG8qx&IG3&~DneC=h@^Opu27v}! z!g{+szREbC4~P$@`I+ds{Agi@OlKyDtzZIxP5YOm;rpW5lJ0-WIsSRFPYdmRZFA43 zPw()%4V?kS|CUM%@4xry&^BpCsJz&*RwE2w6z?KwZMj zZ#ahMxo&9uTSNAp7mU)q1lbb23K^gh)*x8%8IJg|62mXapF2!X{;S7b+=rz%?Lc+c zgLKE3z`AXBL=B8SRazreorwQ~UOi<+E+Q3^FW)|m1lOhO;4*|Rou)a4=oX>z#*;XL z)(5?<9lroCeDJVHCP=>|fSM*{QHX%fHMwMjx{yWK{^FCjD@!6Tj>6+!4pK|)NERh# za@WL!2JGMxJZ89A#;DE;SIlIuCdq2`^4IO`PqBw#DBnl7;dsb|ZtyYn7Q0_A>MY~! z$X*gW^s@>LgsDt? z+u$E6=0yR0_6+cjqkcn^XQOM$S7Uo=&-#F!(tDUSe*c4?p|s3iL<;NjWqK$OT8A;8 zHsUH#QKnnA^qutN4)9LLKx#E(<B!>q^hQb1F-(xZB zX3=Y0#(&iovWDuN&gPku^n6vI(*c(&vLJ>}d~-^nmij)mb{+EPq7;)3FHLDe=MCpv zmiDKM6)zI~`T43#aM}y-*sT&h zDg0`>DaCDg!SYZ(6HL$gmOB zKjzp-if;;nf#kh}-A3vJPU9Qw{rTEJg?3&IKnt%_9^ABOb~R*57`%aouvr%w(cYzo zf8F@49soq(Qpr2kG=G#Eu74ucJaiJi>rH?J;{Yv8p>~4n?pGTHscX)#Y)Cral&PHY zP-2)|#vUaY2j>W=+)3gshX^~Q1`xOI90eI9sq`S#+lOw%V|XdRgUOG<^iYJR$T5G0 z;WyBX<@>Q!xz+}NU3z!X8lw}z3MR?CK z-Wa>;6Pj*&Wyj`s1>mH2zvdo|rH0s^Ac2Q`dfE3;+x z_}{Wind||{7oAGVVBqD~w^_ResS>u5&IHu<7f*BDgJTW)9^meqpHgxi;6J!R%e)N_ zOq&7nV~k*jll{2)?#U-PkgWK2Fsi)Nm0FL+EC(EygEm0L9SDU}8UmI^ArA3N*_ato zjAHf&}r)6KZkxUK!Zmfk{eY7VlA4Iq5D=1jH~=B zXu0+?uqrS4Fg#1|YgO54&;bigWdN{|toS%l9ds|xN;!Bb5vwt|jzJ`SiX{f9 zO&$SoXPzt?L&l};u$aN#!UjPU>c{+O3Knk7mgfexYb*ES7fszrd|++2!eksBIwJg< zSz0c_5W91D5FePEa>GP=*z>ZNEvJi@o}?pePvMF(u~A$MS0yVXEMlrJO&+RXsI)}( z!H&oGhdHh}`qe;ItKSvLkblr>-S(9GyYH3q-GHtd49l!$&6b#DX6lj#oy2vBLn_MD zf+p@DqH!0RZ3v`R8)Tg+K1IHCzey4(5)=iG$dhAfm?%N5&mvtd(WP@X!qk>My=XPP zk8w)o`u9v^EZvTm!jbIsCSz#i&Q-Y{hO^8M>-%%s?6{KOg0&Jmn|u`4(>Hx4&BM=P zK@bFWx3Uyk0EpsyC>12;SPgIRZEWqzHnJah7|pNwbvU4r8R3?1P|Pl?afigc&c8Es zXLC8`LV{P-GTh%ron?`1?!~5hs-U4dXZ& zDDF#hx9U^ImU`vomO`_yi0LGiqR#6Ac+d!f?K>i$%F#1t<0@$9CQmbhLpx9bw%@zGRmUT=~+Ndvs9Lph%Q?B#;G3(s$T{UNX(jD7jMvNyhP1viPBKi$uaD# z&SvEtyyMED69(82Bmk^KaNdo+N@tj(jP4X!*{lBf<*3rkB!@(3W->wK&ZI;3fhWn6 zrA8xqy5@Ghw=}w@wT$A{FwWig@Zp{6kXWjK+!qTNV*;GBf7yPP;2FOr<17~Vj;nbu zz+nPTf)Va=$h&a#KA3xt#XEU~70wcQ{%PW4t&yv7HE7xN^cds?^+cxW&LUJ`n*5VY zav-mHQD^Al4L{Q=T5kWW&^p7n@Gi=~^rljj-6G8cn1NFSB%>@yyxm0@AV!7PYJs}xf`j6xQ6fwpY zRWDh~gnqT%eAI)ROv|VfwuJmByxa(+fP*06INsxba6T9-E*$2j*h=+Cm7FhE%(Nalo3D$^c8n{RUNS8 zU&Nb|U48J<0A_tz%b)CFZ+7%j*<)=td5Xvj@Y6|gL6FY2k&HD*tL?QF# z_*D=};jcC?O2c!i_EW4hoB*noMES(*+h(g2+u%$0v4bm;v_fK0Rf>VfT*w#nQc zz9r7JYm{qzP(0ulMlZqI5dt?Xet-q>a~>SapT()?;)IXqCT0>(!5;&k_)Q6~>Ru#E zds@BqGlO)Wf5&Wo@0JDC{Qt^AzW+aafTW2Ust-`^?)H%J9&lIn(<5Kenuyc1a_K!E zmlWs9W>x8$;D?0jmDOb$jXdBwY!XM!y3Kp@l`E~bNJ}%mgI6^i8@ys4r_x`rj#E4*Bgp2abBM7q@{0ep3E0{#zX`GbqG&1vw!m}KqbFK9 zzE-Sh!kJU-vK#~$omECSZt^EPfZIpZ|k$J{SsG7|IT03e?q569ZgYu29ltU`WI9|eiibo6-14XF40<6Uven$kF)Nn%9gp2^ANo96)U&}L>SmKp8^VUk1UZ%^2YC-Gsz+_v>B*jENy@TwjTW2J< zzaAir;t~tXf;|MfC>SEV(Epn{%9yFjrf-$gogc3~_d|pu$HkW*o&+D;3 zH7VqQ`Y0|x2Hz`@IMm{t*>S(EAX0q!1pNL{H!OazE}!yoJ9#ss?nNHb3WZxaO}_V* z6%lQd&>*Jx&{4g#6IVTVOHT(ChugcT*lcK6f6x*3g@OkmR_Lis6Z=2bJTp9JDLdwC zdL5KTAmp)wx6d{~qGWVQpj>Lf`Hve=6DxZjs#qznZ|O98Z3wJZOc}!GNY=`B0qdd9 z9PP8vc>d`ic)>v&B}Fu2iV~oYrC(-y664Y7OvWJUF*0+hORjy0_aHGK0Ortni9yG% ztR|p&C^*{5l$;)TWQ3!xj0~YKw%w5=p0k5MdRwbvoQDvb z)}z93RB*cX;*va4G|#$G=oD}E1NJ7ZckJ^>T7xL(!MiX~FatF|)#?t-lQG5|uw{lDj`Cvn&(h7bBC@wvBp{W`hA5cK<< zRc;1@-HibS0ZhZr&&2SB@djN^;(DhX_V9`O~J@w*YsvN$#FZV>-QYy_Tf zD-};a{}Mam+afK)^C|ZHG&G)FH`ZhrBbt)wOQRVCxU7$}j`uswc0S`=eu^Cj04Wf5 zwhc%uY$b6f!=HeL+`g3;3uzXokq$LxrHi!a^})5zfaAS1`MkdM2h;%n8BVym+t2Cj z;r4edhabJ`Q9A{nPV(fi6t;8J5AWvBc1L|L7Pvk=oLEm+VRU5t_ZCJtV!ex982B{0 zcVM3-%OfaPvu12_U2ajRBIEYNQ&2=Sa-M8O=kXleD?P|+9_753IC7+cJ~s|Dc*J0f zI?4^}6LLW0i$hJI)zg>w6hf;?4#5Q+Cqc%sHncI3Lo4)QYsv#M&WfZOwh~)aZ|a}U z7NhWv^NXE3F$Tkz&uGz8_OX?P;_%e#jd_38_>V06YcGtXAn#bY!p}Jy#J`!B+#x}5 zjLY-vwiZs~>y5VJ#s*XQIisBS`e%Qy;mg7eJxxIkQ_;;~9I+bK%IE2lQwsO!07_q= zG^V!PH=De$3bm}_>s~pgT0u}`ELu$8&Jh?_6;`YZjW?o+VkcL831_UTi^fs){zhZe z#|syTgBws*=%&RTix#KbT|m7S&9!c_(@iD#0GnOj{{{aT{K?2Im73R?+HBg3c)69! ze+HvX*k~>T5s*>UNyW%GE~#~z!=8N$hOCG z#oZj5Ive1eb>L3Rnj4HyrBsY`hBJqcl9qez;}M=Eh;we*G+O7c9*0wNUh} zdb!j%8tSV3@y4%9VqtO2KrOmYrjEmj5`L!tuD3JDk5IU{!R~D|C%2EeIT1H?@vNuZ z>uc_dYj^YC>-D7{_bMq^dWsdd2IHQdUZeXC^BAQc--|&Lm#Lg`$p`~ySWtA!M9mug zd(B{Hi?qHgKF~Ai7$TWxtuXVZRvlq@iL$fd)(-4?4F?(VY^2FXn>-)ic<&s@M2; z#%YEjFE^UglhMxbAhk@Dv>X)D-Efbd<#LsUJ`5#DroMmA_qp}1u#{u0 zRTitgS?lgP%wPCnY0B@IDiUyGqW+$ARtRM904FbogMXoiLWM#bn7)b09O`0Fk&Dfo zjfwrL4$JC8=dK@y?th!?ymtiPAdp3i>TS{Osj6FwVjV%%p!dM`ApGtf(BqyweACi) z&d^S?(vZ1~Pl!J<%b?*&Y<%(aQ9Li}MX#%c6y@G`L=$&)^#2Q0_(5TZ-vJ{v$+3MK z8?M&mo*{R-@N-OcK4Bj+JNO6iGK#UbbY6a8_RsW{)W9^4PV3SZ%yToFm>i%Rs?zFv zFN2%qyC1AT4EeJLp@cEtf4CElw*VfpaK{~rcSTy^D%+A5zhb&>VO3c9-Hn@;f7)ro zPABl9i~hx6rin8N6cQ%*9k5@z*lT*zga5abJ0Ays|d z?Ts6+`{yGAWL?Kt{p0;7rPDs-r6#-qolJj3Z`tI5p;Esqw@-lt!Q9m?VTx$Z_+z&= zB(xgRHm=C8J>S1yL3<8bPK_#qD=MkYv>3+k^6V_)L=qzNrX#;uYoPO>h#qAtOn&f= zu~e<=mHA<`BpvE=BS=1_dMExxukFWsIHkhCCe(0G&Pjsp=@?-*7p)AvTe2PEiK%q9-)=R7Y{|Y1w(7lt5FAnW6k1Ga zO6y1(ibZ3pr3lT{-C!0CT@=7(>n?_V&`JfLMa?hF^tJc`Q3Jh^b_h-Ezj#~ACgZzx zLzaAwdm@=E_&1Sc27-21fD-HeTZNAuWSU-K?m3&ZE)Mn!1Nr7H^%K(>%k`%1h!oTp zLup{afzti;E-5j}rGZ`V=(Ae;iPh$xeYpO;ckU1coG{ne^Z50YP+C+hcOwdeoT@Sp zJ`vQn_-F}`a>5hWzfvm!TETH*d#3u?9{+d=J))@o+O{d>_<~YIJz-m?E-fz<2vY^b zc#@u6ZfswI@F=YDj+}_V0CHGNkm|#Pi zmt8&sW@S+)hD$IEjs~8B!6|!2I}rU&UdFoyU4Vp!%KTg zCK(}vQ3l-M9vhf-?7trcvKd~P+FP)SQo1(O$X3}oi^>RJJ0FJfvJZ7HEuD>GN_Nbi z9RN}n8hqM&l$PIpx*p#WMor6+A-Ue(>j@j3WYcG=7E=DQ%_w@RUkFLvA3ZrXA=ls) z*m;zns9VQ3_#xp>3HBwEtobF&89~ebBj`YSZ_y2mEVSn#l<{x2mf*u+Q|vE(mArjq zbD=94uCE_(gP-BanHn2U`3+##6(MBlM=melyd@X!10OHldF%jmP;ni|J@t*cG%1js zn=vgN9g5Y}pxDl%i$g{1)ahNWrz0(0;kmg1d+``$ko@z{Fqfz=5u!@<4wlB#dHn%G zfyqaf9R|1D%;AStl;R<~)Q5RH5Jcw-xF?k^$XupLqN_2jc+*dRkA=?&k295W)gy`y zRU31x_)B|l3*ni_;dtt0n5kScV2M5z_0XW}D~=DC-9EqG48puI`v1u0v(yw02>tl| zKcr+|v_l>wLrW2PAJ9n{jA@xwY3S^M)@N@I)?R!b6|9=qE-of!c_{4tI^fRWAN*BK z^k$Wn6n*%(_Q93as~k;)S26-MTN8TSF*@aDwVy4Be+C_eFS`thgg_Wo2Y+Rj`_OaM zpKl~Mhhb3LrZ{2wxuI1ogVRa4>1hACa)Wd;ILh;L!z901@xoyu*-yIUb3sGdz9Agr zZ;mKi3_o1Lz9tw-YgbhhmH^p+2GAh2=X#Q^idltV6^{#JmOke`k41*u91B!h+DTtA zhzK7+vvlVWFMZ%SOA7pr^)&vIuQ4_;u##JHungqdg=YJul z+36oZD^*L3jK5x5%ud%G0$W>Pz8U#q_6g8xCG8${C)%P_iFeLJVmo>E?>JgfDSB!t zcs^bhlfOgGBnFrPfsNW8r27-of}yO36hav)$sbzD&*0H}1On#^WKRHMCy0oukA;5VDrFi;{b+q93=f{P5~bV^6e|8D zYWwH|u5`QYx{gg6Y*`Z^5zNyiDL;c`5~hIK>{14}j)4SdZbG1G=+SB^945uyIjlcq zGNPJ?Hil%mbxQ+8Tt;KCAM+7kJZwq2pX=FvHx4LHl>~9ITxI87kTshKo_)B7q_`|9 ztEB4SeWXzkku3{3N4rMy9bp?ql7U@2O#aQlru<%J3MA^NPOmnm-AUDnKj}Rmy@f;~@=-TNuTLlK=oF^#P~_07VFY zkPcuq|8}Su+E-Pqpec{=22bRAye4D^TpN~8KPqc`A-W99VF%{#^0AD1ex1-ATfg&^ zGHJVrSc|2n05x6@itJOFrFwM?Lh*s5{d3w$iQ|B1i5$ZpwT^>iA?r(%-jdv$dX_g# za(-V6+U^ng_6FuZLlm#ld@H}!1uOJit6|_m%W|35n}{KO?4yf2D09@f5S#0j6+yq> zn{6(Mjmn7reNjp~w98r(P}tw|N0DLi@2r8=LT}V1I%vc{A2yCyN8psknP^Pg+Tw}w z16mY$@3q^xKIj$R*^1Li1M*&kz}gA;Gx?TQ)L{xUpHX!NBXv~3?Fk=gAmwe0*i0{q z^C1clRjzUo`^R&1G|^XP(Vma5byyKR4lSh@_yr7Nt7`l|gLv26xjbX#$KFkS8Qc!C zGoQW(U4wG|DbQ4#+~I-tgt$T|RTazgcjb@tN^-je^y*ncaD7iy3)54j3o1{{XY}XqhAb-n;xlniwsYXL9BnL)rJj%uATy|# zJ!p(2{#m;AxuFoL^aK_>>PL~KN%qOOZK71ZeM?F3&uDq6iJb|G0QWLmJ->s>5gOvL z-wFd6QnT(Nx(HyQq*Oaf6R@k*qM01at~X-SCTcwMzG!j;@yVk~iuM8i0BOFQuUGL4 z1}ta;F&?cjjNvUY4f&*!&S!g~lP*=&8$UK5+!XSZosHpkGz{~1+o)|#&YiFlq-^%{1E;6WDBR0K zO(tO(EgQM*kd9wUKb3Bh2;pc@oK2r>IOEZHcU>4Dro#`6VrpJkUEr0k)=X)F;+Di% zZEr`mpuS!>P6u-dnlh6}lk0 zzpi*Y4Uosx!%v*rZ1$t4)slc^SY{M}7t`m+N{?hRj<90Qr z!i9i@hR)&0|j0Qv`YH zv`V;o_Id+IW}or5O`sm0&OZQFO8`!7TIQvf@%w;v{uz;$V*rkV6nMlzhrBDFS0Q__ zq55}1kW{e^XwK>SEFo05cvE&eFy^vQW>SxR+N}eu}TyUn9VG#*&6l zcZ7_XV_uiz?Qn6MED610Z_JB}{CWmspqkYZ$hh%ocO>0r`1IY+>W1X*T@pm|-+)o3 z9uixwJyi1+;=T#UU~o?Klecp0{ir(FKQCh zd*3wkTRIyU1C|qUQKfK*M9Ezq2|82}Jl;>D2;z_vNpzK7j)lJ(!{$>nM@e#ZmR^f2 z#9wslobpQbrzJg6YiX0+XU_#*goJ;g_!Dd5{<2x`aK5*y%L3xJIu6O;?`rLn$5&FX z@U1$69)lgN2_j+W(1q4doW_R>Wb4M-r)6e`k|?;MgB6VuD(EFOTL9nv5k2ZeNMOVaf`ZSY(f;gg6MBr;(yGISw6 z<`VN?{mO18{89;G+39)cTAz8xTc`ft-27+(WT+)Sy9eL3>d5pCDEaWfzgvo@xV8G> zuajS)Guuib;*7cdmEe7TjQZnH`qB z+zKF58ix&$`d49EIaSO;s77*i=ipa7F3Vb}W=z!lKz0_pSIhM`d2q>+d15uZbEnJ- z48Z15CGDA;jxbACn+#Gk6lqJTfx1ij<)T}zz{wf9nJlqwTde<`3NRJ7@Dgn$%xlt~ zjT#`nzGq2W@DJxQ&R)QIkDm74Z+2SWZG(OqZ0rKR&gGO@ULlBx9a0z!j)^Y@NzNzJ zRoZ{e4i-#kdY%f`&U^G9mwl6c^Au^4kQcw0% z-Z2hxmfPznGv-5MsE~1^dOqFgl%i=#Yu`G1C-UX8odd0B*Iie2b8FpqM^Ti}$g05; zK$(v*BQXg3-huCNCq|@Efs={yvyT&5)JMd!pLu5#ckdMAY~Af{assk1TU}?VD2+JU4-bhgOal_CvrsV zAmdp2Az1}M{&mO!6E{q5Micu?39`5exFv^!H}PiJbbxXDU>0`01z?D0nch5>s4+X> z^(!hV$KY8Zj*^IYcV*`4H8wO#*;zi)Z$wA#HX-~ZRxCcAD6hg| zI;Az@Sts;~yC#BCdXj_R%_s=;?rg~4md5XiZbg{XmgPi-^dk2v62tvOfaqFqlLctG zF$c9DyqCL?&hg^!nO_%Jxy>W>gXEBMCnL4d9iG&weFISLEp85t(6PkV9uF`}H@P{N zKgql1(0;3F=-o)GNw|3+q8$(eAmOp#fei7f%*|4_@~=I)V-#pE|_yKFUa z7)%jp!ZQbj%Vk88;}9BROlR@%40`mC&&#xpNbFFn{>3`PkZN!ReZ)ip9Di4M~qC|1P(-9 zY(JhOD|OV}LSc!TmIIv=5AXW34wQKxhsKswTLGF&(2X6KKG>dRSf3IG9P0@qpU7B} zddWKJ8#o4nu4Nmn#~hPVeVl4~4Pbp9*iPv%{c{j|g2F0yB^KRd&+OCEBt ze;_Bo-$bvOc7{ophYF*vZp~Ww5g-CmTPZ|ObK2CM*(<=s9chk7@Yo7gD;>*~byMbo`FIjS4140e`OQ z$u<@E6{LgF27=q>(tU|5A1=10bnCwBhIJU|W2(_3&YfEc*R3iSdXvMEQXV!_GfAnH zjc2@b2|-6eIvv>oj7OH&M_36rD%PS3?Apn*U@5PAmW}>#nKL(R1)Z6?p+2U?h)Ja$ z>%`ek47)V*4zmFn{ikW!?x9)xiDK$R1%~&}@nPOsMyqMWC<>KdS0^FImy3#RkNQU% z?ds4mf27NEf!)mC_@dzF)QNwLk?GC@1IKx5egL*#5G9Q%56WIl9rU0;p~9p3a0Zz* z#YdW`xP%unKV3~OZV%BA{Tqeroq3i`QTCZNWA!Ts;j81n@aJ8w{2jrVby;aI3hHb) zZ#mUOEv_70>Dn$rKApBj_9vjL=dzE@=Y7y!=udpQj%nx(m!pySo$TS6Fq!>z03r4o#!PHWx);GaN`rR?_+rW?X;V`c{}NCoMrqNFqA& zQ)A--j+J|jLy@86n~Pv(6XRCc6NUQC^ssJ+X4ONqLl{Bxkr8{N?b@8Pf<4~2P&u|- z7@53e%u_F{tKiYcHHT~YB(~|;Rw_O7bBqb&w%NvICraShfKpDWndym>_BK%BDqROp zv$JKON*CS7n#tg}f_ul_dUOYg=XX=nN*WA2vCC)}bTl7PCSQOdkJS@ln)b>Qe9Uzk9MyN$~g7 zfCxnlBxJlt`e}A&&+9qf*0&7dzi9TT8dt){rQUX(6(YCXl!Xg*n~p3X6bQuO&6`gz zC23D&n>V%zI!g^q0`V(7a2hCU6X38&ZP()a4~-xc>jKV%gX2-H#=4BD~V5Pc2;hTjhEBTDU&+H*`6m>W-=G z?T-!rh1?Gl%2!pMs`9Vf((!@zTt3i^MH4=?6%p@Iu_K1tt4y^;tEX@cy>FJrsTulI z#})3bWX!oY4*OCjYV5xPRNhBn?+gc;&g>yt;2^}@Xurt&4Gvvx7ED4Etg6lp=Gc7Z zbB0AzZu&}yfEj*Sk_i>=4BT^W?$G-_1I`N2|jB#Pj#|M-w-@ z`$k@2{SD57)(fHt^fwYalT0kd{7rlfo3@>Qq#>wXpuqE_h_Zy;!g%Y=_G0LExKTeUF81qbKa(ZS6jNbGXz4IvWs= z_X`)!-}22px_9W)SyQZPMY%ifyV;`vQa2m-SUIR*7{&KbJCc?AAYCEQ3wV z_<^&7zr>!%pN^J*nK-Z^fWDuPN;V5=&JgA1g30D&HBBHRtBUU-hI@ArE^MaW0o?Q3 zN;&V&ujY@f9LZJqf){(*1Ie$=zq9O7&AaNBWgBj||8e0RV3-grufjVzw`4V8#&e&C z2`z}4v;`?rUVSBU0g)N{-|3H^R9~oM+a~jJ{lS<-2rRzu(@fJGdTfx4#!MmIkF^~Z z_g*6RV3eNGFWN+KO7$v%0>5kfX@75csIHSd388hD1uA=iGN#~QO+7BeL1V>!(exWKvNRM$%=-guvjCw z1d`G9<3j9dmCqyM4IIx>0^=v%@`;AC7ggj%-lroW{cRpKlOfJkYGT@4mNau&0())q zgY`d@1M!X$Mug#q(86i&gF38Wnd%^0;z9RuZJ!btD&3^J5UXx`_Q>b0?&^JLtkHOdbUu&`C=ofs)y zv-jW?n_meaGeA5rQV!feKUnJ+f{+nD-KbKdi)9zSP=*WZ9CD@(pz0tmwh+gAQYfusF^S+5Zr;L-^Y_%aOz>%`NuP$ z*Ek;La2^wOjtfY&F&p|rbTb5G!#b>aGR}@zEW&PMwysKq#$fd9rA<|mUe5T-M>!wd z^flQaGgoP%77?@FHuxM}Bc7Cev9(;PI9(E?|H)N>6Y$vN4aYnJ`S2Wb11K@`#@j4~ zZJ#eVsfP9S>-9>1B@bpJA8vi=w-MgdJ;Pl_b#C5$o6hmTFjllYDhJv$_^}Xqw$R`8#RN7L$$5iSfMHDu2v} z1}uzz?KQIq_MQt;MN%g?r%ykmI`}k7qCX!^L)3KzEV}fiMc+a` zBpG?mruxCcy=Y5Qm}nE9B4o~2#@Xo6+`U@a%sJOhL**LxO(L+78LypamOK128@*wu z0Y*g6j}gksHxG${Ru1dqWrfBABhxIlO{C&A+ctu&56BE|y=m`BFk9~?s8(}xvCiZY zqNup{Zw|eU4el1i3ncZrilY%AU3-?V8MVpOFgTtluY^N|Wc!~8Nc=nGStSguI#%4! zhy5mP<%Lp62~#RIH?)rK^Kr1IpQ|xM(_zR^hrsB2{u}NlQkRWD*yp|p{N3=|ROVl& zjJAS2IB$a-;Q9*M^LzSWP{0HnmFT9orOY!p+i|gaSIiVsKidHt&#RiAr5;62Us%Lf zPyk23J&y20|0$YG=-6+|{fYZYylJpMh0?Q9yybIhAK14F6Sf;o#o!~#l;BPULvFJW zbABkL0IzpBL*!dpB15{FCx1~sHW~Zw3?I|3$1o$bN6S~$rpy+mlBzE_~gkg8SF zEEi$4ySxyJf_BD!TPA|SPqvlk=fWFj6BlAara>^pH1lgQGduI4JU~V}D1=r+TZW)1 zC5tbKBkJ0I3upl>j*4Z6o-B+8P?oZgl4`#qU@02iZ(agM!8C6!1V4Xpz9?(vT7ETS zZrdi`F7lTWHk=YGGZf;KOqYO%I+Zn00v^`0o(6DhR0~>ainB7SZ~Vyww3-eR>qN5% z5X1>ydroPez0s+Y&IEXK8w-L8^9~Fcj=3n5uwBO8dG@`h0$=K{zKky9+;$B*EE^f=-AY+`-?Gv@X3&pVgQT zy($v4K7k#C0A$($`^CRmdb=dMx@jiCyvTLkdz$nnP}=Z zgM9E-N*dnXN@KNs4c{~5k#pT)Rcn%+EW524&Yq#K)?e0SSFd^Y!hif#B}k(kSH_{$C7rix{v4i$fyec|nEk)fGcsphG6%b^q_?IngA8 z)1pI0`JXx1#lG=CzOre{@iP1DPL&*gayT+^k+2-zp7i-pWAfL+aG%SxFfa^nv+7yD z&Gc0-y%nvks1cXaABe1p?GjdsS_!RvPar21C3hUK4t(s85OAPmbg&=&g{S17`tmYx z`liU`f%b9@2KdvukP<1mm$%X742ZzQ+BqK=%ccYUE^bRAbE?Vod%TPXPR3j3jh^;a zADlKOnVq_rdHQUX*5tNbD^G(gNG|=J`pBL+c26ybrj(oL>|2S&{=BCgE{3Kr#;wMH zqHlmg!qDqW z6j~CDJXtH<EM*lQ=FgSK0$gRbd?2(Vp#24 zck&kfz2D)1|KKIQ&>)-80dG;FKN>>BZc-78hLdOXy} zr4Z?Qftu&=a*#w(t0LAPCL}i%C>DwYb;tn+A-UY{f&O7czt!60){Cu$Dho2vX=|X< zK8~Lkizbn45c1B7e4|eb$aBs|<@79nbiR4(FSy03 zFR{oKA<-I*s_|r1HX)FW{o(F}CG;$Y{M|pf8H^*TNPG9greS}Lt-Wl0YvQP^kaJuW zN0@8SmPBgW$8z1zqjz)BdElT;*`u0ZcdBa8QXHdMDBmUOnto7*huD`H+WTZBpIR5!)YcsS z@NNddn2k*hAqwIYw7mq|@_?#XH`^X%2~VSznXcEA!|CCW9%mi@S}k7&cEtZRX($PB zb?mt}wBghHjLt1!hNdpGraA@OM%ac5YtHx>hL@7?eeo>*%LTuv#}%_o1i_W;&y(6V zmpEc3??&g14~$i{RU+=qb`vL|7`7Q{lQX6M^JTQ`tUj$?CqbjdzGv%QN81M4-VIM= zketPc@v6GX81VVM-L21mO+om-dh1cg?H7DyQGU-qpJ4w;4*J4ex-I+u(9?b6HNM`> zo5C1A+5xq)+0ya63+l|7b_*TAwPq)R*s7M1ei#gBwprfeFzQ!#Apqx!g{+OKM`z8t zHNg@Kn+i)*#-R7|qbQddf=LZ@^TK__>=~jbR2>(A*8|Kx^=Bw7U#>`$4iKkUO*_YA zV;W^1a2o8EzVd_RkMlT;%o&w|@y~*SJ4V6rXFtKt7Llio{6|McJz9LVA)tG|@LyV* zgvFqaf(Y<=!vyo4_5*%W>QDA}PvioY#;pdv+GK$$h-XF-zKsRgXH2PN#{yfB^fJl6 z1!DU2@)^C(u3m_mAf%m1V&9#1tY@ z{xb=FWrV=2=sT_3J>DLg?a&U+5&Sf9+e^ntbzD&A^&YS`aYQv6djgh>UMGYAfMZMA zCqQu`vHiNUe)z1`AXFMGO@w3o3@1RTG_wZx>?Oe!s*cqXvt91Q-#YO7J^6X8inT^a zluPaAY$gj4X_ugZtu{12?Mj#&sl*10DG_WQnTd-DOS(NITg#I!zUjZPP-@fl&?A_1 z+_-+_5lj}pYk>#fbRn_<)F8#Q;j5WP`+8QLs-wkGq!th(l0w(g)o(J}Pz#+S5PGHK zk6=;u#`|&TcVa&Sxpt>$#ntC-Gte-c>4HO}NIJ==DX2TZ!Z9*G!d=*v8N^*pEyUh559QHmr!~F} za%>ATeOfOv|slN-_}n`WGD~C{cl< zGP=aBVj&IQSpfZGXuncYMb!{Pf{Q>yvzK-Om)z*bs$FJAcQ8N`D+KYY&BhwBk(*tORMCG`X8GeBM8Cn za)9V>O8Q|%(bnc({$rgVLiR0Qe;)Ykpjd{fxdv1aY*Qyd|lRqw!|r7;$>s@{YZDkhJ~88{h7;f{2QD_Zl? z@I)0hBUc5n7D+xK;t%apA~PQvL%!?_KPuglCH-Q7Wpg^m!N6`bMy2MfbaS@!=zbf| z^tQz-xYFZT7h@?0R@SG6g#lGQ)`^-FcF4dt{MDw}l#?Hg+9ZZ&fn*x=Da8HlJIM2o zbP_3|k=trH{93V&Q3q&|h1s_eSVeYpK;QyGB+Z>`J~H6IbdNCQlIygdUMYgpzey8X z_22!NG$>8iL)7qO|VjLJoU8WG;H6FRWq71rghQBMR zZ5osn)MIN^UuXCcyTf2mIIg1>%)n~S?6K$*Bg5-rhiTD2*Uc2WkAAWIXYU&41Ps?T)(Xx&I*bUjD@X7`f?2!{aliw5J?^>(=Zxa6hovDNu>q znwIF0qxd?%`cTpQ?2lR>64dwqUto$I&mueLjiA?@y9J#>K4b+APJOfRKI0l{NEVs`l@vQQaEl7qe`mnF8vE3WoO~hpjVu}QE zw9yv~d!;7**FFZb;hC1iZg*a)Z)rEv6j zshM)s26irKIaw18qV8hhf5ks1)mI|yRo}Bul(03;GvmTQovS7ZL!e6*PT~?WcTuQ2tQC=$V0$$kXj0oEidq`MLf^*`#)s12V{}|coh#b z)>7|kR3Tg-)ZVa6%_b-kPw-2uo_?XK|57B3?Xg4N=&`)rHLUI65*$<^F8^cJ?yM`` z&Pt2;!1XQ?A*nbQb*{ifjnS(KI&R;W9U)FRHZM#_>FgmJBwV;VtJ}yO`GHkMigF-} zwqwm%mJNoi_!Ul=9D6RSe$qrVXFoTT4fmOoue;{B`#|C`10%(SGU=!m^b?|drTP0Z z(WE1XAoK&LM>9||`N4R&_D-I2#tAMg4|ZoHH1&(bOHgNRx?-B6bykv&nVwtQYJW$^ z#vufz^>rjV#TSyj(iL!YGn3_#RhGK8+Mo)a|0C3GpuAQEw|l-Uj4vIapv88R?tqkv zGZVy3!UlU1a_-WW)?F!6E0Ml7C0cd#?=ao~G9n*jv=PHmfDT?vDdPs3+xiAOrHc_C zTI=d$pcP^B;8z|b>2LBsfe$0Jmic`#mwazso5DntT29~stS@yy;MRA3PVXVej{urQet;zPO*jw4U zRr8=j?(gNl1fte)KtPTI=yMqesPv5yg z{s13NFBzwcDBVwtIid$j_DFI%kR{=BO-L~Ia4npQ2x{R{e`l7a;(r*>U%n4 z(rFhSF5MsVd;^xhGnx4E@uC3_YAv8xmCP(rTYMd=C?FR`r0=)K9?}*I+0oM^!_8dA zA!>mWXIjFg(f``{BLOWg{9$wXrEWR-uSoR+L4DFKea*Dg{yp8m+x5-up5=pCY~t_enPpi2b`J?xuTv0R69;;p(- zpffG^=M3jtMtQ*~&+`F~_Z~Mv{R-9E^a;I>&yG49qi>nTz6mQ8xW$o&c1G44u()RS z%08MtBeJc$!7w{-wuUgd@N%glm4T`s;iAf5z+Fd>2MBE-1EjNSaW5(7_>0GGm(tfy zv0L=~5`35%1NWYL7vUpC)BzBF&u?oTk{_@I< zUFtuaeSE4hjyg?3jZ8k9(+71SpyihG%Ds!hkk%595u09Jy!pBFip}VxymICuccqbN z!ZbzZ1KHGlPy%Da&69tp>>?$qIscLf@S0nf?)4uDdtfkq;Y1}6nNOR7-;|4qwFPn} zINwm^ZWYkD-kfT=pzrgMy8PoS=glil-2>zkN=&-ZarLSOD8WOKxfvL#Zy@T8ERncJ zm@O3bL;T*Xhxr;(OB)99Dy$SulR!khtZtC`jr>}>-4|JV!9M`R#*UQ0P_nNi;=m#Y zcgNKHe%ib3M{;wm z??Rb8>?hnm8ycg|i0~Nzx~Vi*rDVi~VGe5NwM4&VVM1Yh%o^BoJhK@lv8+efUwtq+ zl+>oh-Umjq2q7(}n{f!1_V{8IMuA5_HZ!v}LxIT?bJ&6~#>ixplmm2gFDJZVfV;jB zspP$V)iP7Pmf8xbJtJNLZO$nC_blkKT;5F}+n6DmDePV#ULkX#hris@Lu$0bh6>r4 z%!g;Fx-i2jBbAJkKM2YtSOuSV=u4$_A)=U?%SZr4K)Sy&3c1XVMsbWG7H0vjFu zV{2n6L|{d|}A&%4!{ryWS^R(#AnLF12kTckNZbO0Ct=Z^alN zKhnYM-0x(Bp8!&y#}N)Nyg0U59YCf0{$OmZ2-xUI2x0Y1(@q{LC9mU0Nzcnbk&@AU zMk|#@49b|)-cL_9T{cs(U*R-5z|Be=t|>OABoBB06u9(hY(Hx}Pi67#Fp7dF`MF(P zuFTRey;%EVPBC7)Hnp@;+`f64;;*&vwUCAKoaBPZPmCVdN*q@4OXm~MnDr#&ABlyn z6FaVq1z2qbiN!A`gRi*~$xwxygg8F0a}SwPz7rIX?$+o0i2eqqN9kBob~5^(w_dc{ zv6#n^zAdxT+nR~7w4a9xB%IY6O9gTFwf25D8-utQ#Qx8@Y{(lJ>lZ zo%Fw~5G~G*0ZXiQXk65g@10`1nrI+5H7`YINsc+8$A{&AN$-eysy;5nOnko3085>y=t-&XmT_ zv9XM0G45_+8sWb3XV|rs3g72!$dcre$A?p+W|`Uvn4K%u+eAqe1nwc=luo53!)EJR z8zbido=_RSaO+<69?n=SJ;+FniD>MksBJk$Bh<&SsbUSEmIX^;UhsKy>P5_qlzoXO z@#<&X(|C7=1kUkUUO5~hnR!l%44&m#tA}OZy!)@smj`=}mpFs?;79b^nD+cYPqn_f zGyE{oU0uH4KVTa;7nJUO2pGvp*o zGK@;CJ~#+q#1Y9U8-^H}-7Zt`%rsi8+ZU;5|I~er%Y+&U0*Rc{5L2__d}Wwdda8{h z*)|#aOFVY>^q5+rmy^vjgZ)UrM$6KFBqEx*mX1|AA3Q@}3#WFc`36%&wMlT=6V&CB zO@H_a`?TsoIaRY6`1w#td0FO#I=-5)IgihIKqj0o*eO2hN9IMD zzzbEI`R6QA>DTbS>w(!(s}hIcFCk`bz3%9pqn^&T#tMe=fOAo@3sYUb*$?o%~oVBrs(5HXZL+Jx`&6AF3`o5wS&HB=SAZhV*0F0fEB5 zfd&n6QH=0`8=SiQUFGLpF2YLjdSWs@vhfbNx3v%c0$ z0ZAs+W`#D2ABw1?uCqHJ)?8EvvLFgrY@iOy`ord;SH#Mt&pk^-q-xE$z@UQoAKP zL6;wJ!ftnPCA@|1azxpok-i>DgfO(|C7Onm0r|SwcC5}M&0L8yY(cha^ixsUF*Y4^M7w{s{IIoWc`f^?VQi`fJoj95HM zGm0YaLxdBz(KYMcWrYWLPaLmCq(kc^1g4iSp45=yqM|H~!XVZ9L%irt-YR8^_WCEn zL*Ya3anRf~5ZXY%PWTrNj4=IIM0z0l1p%X>qeQ8I#tkG3&$14+^CF7}j|5bQtI zV$754R`-vlcl(&*BUj~do)eiGDWefo7L~&U?QsjWpwE7!K{Jh|fy6CBDJ!i-X)Co< zts|2rjYd(ij$M-Z|D0JrLA@toV^G1i5qXQ=c?9t}-N52E_vkOrgQLE)rgluT)GqTo zDYSBB_&j7i9L2f)J*kyCjQo>a{RyK-(S@(7%{0fDAxXm>yjV&jB94>Xem#v#If95n z?>3?ZF2kCOOG3hV&ZPz&wc<=MFO*isZDr>Jw)OxqR)^mb&6t{Icoli!w9bbpTsD2rkx*O z@E4`>+Bn(8c{#ndKCOmyQ35E zco)Zq+ebp01c=VD1nPLL!676EmF?PDnWmL~VX~-$MG${# zhvB^D$SpZFK)1boRmVnmc`_kQWkR9W5Fs&!)4{R#I~s1tyVws0vk0Lgyc^LdMo=*& zg17!d2rOK9rV=4kA6Jq{zTkt$L%q6P*ZE)SQ`?xEq@t6)iEQ`u(VbWRJJGlJ*x$Dr z>N0!Obwm$Tz4OsN?H#-B?su+D22!gUC$GB#NcB;$su)Bs=4H3^i1oXo^IM@P_z)Ej z7u=p7$&;fvTMNGB zaxXRs%$2=%r0v~F<&vMc7T?u7i@qn=RE={OgzfI1d$POtdBq@-a;2tF{88Z9rAvBf zp%vF9Giulc7c({ntPsD#*N4c%oF;J)O4=-JR>|soVs&AdWgO=8AQX3DEwI$8dIVcw zLKII9Nj)j7y9^q6Ky#RQrcTf!tMv-hrmCbp&9D#fS<&<;7H4j!6=m=5b-wO#z-voW zB5eVBnE={>lAdQSGaw*io9q*HnO65uVk9M`%B87nUfzw8EjYUy^p5|+2~28~8CyqG zYr^9r`2AwbzZ4;*TS#LL*^%(qCaK3A`08XKZQ|fcQmMR-$A-3M)eVm<2$27Et5vOn zuDo|H3_$VM8IZG-ZS+{PB**^E?JCPFdZh1i=Kq$&Q6(upmY4GJC#ze!(w`UP zq)YS$?)!(Wha6+Lm~#``=U16mjH<%?^&_(dit8J~w?}r)sGOtvG1LuAb}#g&1*Z$sine zMdDY}n4PSN>R4GZZ0)9J6^e|sP}0Q;{yC^Xpu5KG#Z(WJV9;?qxCX`MpinC7GI28y z^YBWyxtJim1tgFg)8SlS$U|g(*l|e%sObg{>?JRt3h_b1UG9g%_vTR9;G9yk(XPt-%UR_o74tiGh zhIM9ZKtL=-%IqI@m3^`j8PvK7DAas8J=!r=c@s;OOg>WaB$oKuH6yusdACcos(7h` zo@h47&B~(+J7f1?CiWcogt7OB>lF%vbAuLuR6cTF(N~)a(&T)>%lU_oNmPhtQ-2kB zt&|9M<59*mi(GCYJBs9_UUA>{bSCF2>wNdA)uOUo`iQneFDYK*Zto_o`93={S1xb_v?F_MiZe#pE4 zZwmoLMAh5!W=n#(;3^?1zRrvQu8@P4EkqNB*k zKe3=Xg*x++Wl!U##_-XS3l-{2fySphI|k>eonibTaF?!c5jKRv7m^}$iGO0>c^#Ow zW>%E&cchjjrY|52Q16w%V3doFxJS-FrlOjF@hevtY;-i}inWT0dAD@Y$BF`%zh>+g1in<3Tgf5unZpuR$ovyE>5`% zn4r%oF@un!dR*Yzd3GnH$44VSWv$@Ln}H@U`}hz*Xax0^eDTLa6O}f`qi$fM5f8|u z0`ip$;;N*1N&8YqRIv)p1e>*~dINAc2LuY=b|>v~=nzDp6H>J5<;;M=@1sN&9@n_? zaBp*v5bD>6tqyln-9>mguX1~~@@1Xp0n`8GizUqCNnY+O6h}cyw=(OSn^;21oQtzx zU49vD*hEv($WD!EWGk>F$kq|bGR6&T1h_BVq^66fqO*9)1jVf|*&Mu6z?`dPJhi_Y zelan=C)#=gufX#sNHB0FjYMKxMsRb1rs#V+#inmaJ-&9{IhG)*gCQpA7`o({Z6!Om zeDs$yi#!+CGj6Lcw{QOD@beOUqwTM@oJ3}VD?l@2{_S=Iz6<}*DnY$pD={LDcI{Hk zJp6tt?(cv$%<;bzSv~Dj#jCBby;q5aG)*3ExX){TcsZPZg=SY!4hh*~%uT0PGm>a9 z(%XX?%lux6)pwWcbIjqnUqF-8q3tNf?iUe#dz}dG=WMLq^5PwnR-1Qhq66)h%5Rg9 znH1o{RM`$#8xs@9n=ZpjfZZk@z5!}2>4$o8W(?_$m=@xj+id|P>wKh?d(j4!s=||X z{1d4M9gb(Os8m_Y#?|vKEuCVUljV-@WMulYOEms9s%USmdjn)GVqxd7;jc*O1WRjt zf)bau(eP=S;ifZNS$~~)ef@JN1%QlS@((*y7p?Y)$T$|vX#L28 z=fv(4?;G1{Y8m0^?yEjW+Ysfy$FPG_V9nS>uxIxr(p~@@`$Q{r=}_#aAT2g*`@qvR z`)Bjj?0Sx?sF#JMbVB;_ru01ZgJSZ zoN1y?_68*}RkzB&-)E<7Kn>!6Bh5Y3;8s54N-P}_^SMYe_$%JDPTRvQeENtWQ!oVq z_~Esc@kgjeL5K0GWoq~eJHx&B37iqxvm})ZDUn9}?&xz;;~qCG1_*zy@+U$}^i15u zs!5(l+FnCy16Fc3%+;O_5S#C?5bqA0ai6Hpwh*&tLXb=+I#{S7G}QZ*ZBxd_f%wfi zPOGtPdpj95NP&_t=i3_l&Vx^GYk>K5C`d2^QdrQ*d*P=SsxrJ>?=$|SPL~ZR&5u3< z(LPz*jgjRs+`LR?nfXa_f~L+WbZTU*ZD1Nb4skGX#2^ucGbm>=vMt#9)YK^lx5x$? z$;V|nnh2h9T!)e^=NiwT_A8dRBQxIX!+%W8^iq$WUTpBqIPe z;=pmdZCxc)-r-?&5b^&Q=d>#sp0-oC)^)(VgT{?+5rl_!Irt~Xc2EBMna8fYN5;bh z)xs_Q+ZzYEZ^D1wL}9Tmh{tAB`|aFtfZLNtU)85!WoUO!XVEJKm7NJ^q$SD|?fhiW&x3ldr9|ykjJ})<)SkPB}L{Zpm zUb4}R$Y0?uN8P9Z`c=2gpq#A9<2;{$0Sz!0aApCA4$dLBWnl?rC7h&uFI~J^x7a(_ zB`d)j6!ddK-lJ7T%L_h86AWPo?p6q<*6UEj6v#>+7{?R9oje4rtB1MoW-wfTo@vgkg zlPaXxjzIsd&d=#0cww4;-gFsXU7I-Tudo|2w22@|IeZW=P#z3Xj}|M_MmLP-4Ekn$ z9e5;pUC_os0ypVxD`JiPs}uDVNG2>ImM}hmL{pu@mN-o5XzQj-=|P^R{x-3yfnFK^ z77E(!V%p+OXfwGbww4mX;A>CrSitHKx}RIXSpQ15D!=r(<`oYAU;1gyx1_EPzb$Mr z0Y5u-l|{(5RUf-zPKk=JXU)YQoG1QzlJ9kF5t+Co6@T`=#i(G!68U|iDtjcHYF6 z!k%&KMF1{-jrnzq#<&KXscS;a(F4Z(P#9wB2C6gKDhBHMa#GE2#(>9Eg6X8zG;#vu zW++C_CglnFNzu6+J(;Ub)>Jh_7F}WuWX|N}Xd(y~P2rhm^k%|K-PdoJ+g4CU-aW=mPNv&K-7KT0(*68j?&? z^cHxENxJG&$y)OHXUMowk`!6CEm`%rk1e1GDI+2Lnyh$^gKoKTYHTW+Hx0liEwTezP?QP5qU}#c6^F)NB z4~`~>pO1^g5O5LwyNin3+B2EqF0XFrE46=a{DI8v6{7xmTOA`|flx}CZ3X=8$|2!T z>FKbKJ8hmk8EX6D%i`dWdjV-n5`lzmJJz?+^bb zh>UM>W=G4Cv3Y^NLXII_lm`eqB?_^X)21JM6WZOrfW$*ntZARBx(iy9|NRezJ9bH{ z3tx9I6!sryc!I{z1@@3(A78vlHtGJK2H2L{AdP^!Fml?oejBUDnPJHY!FWF558K+S zR=gZ#fu?7X7|r8~7w({`OBW?+lZZ;aP zq#=DW_eZb>dIfKpzt)$hN!GUxFZRAnz06M$fut~*F3Y>b{ zU$?$|w{${QtZjvp0Np2-e^Q5*3b%=`&MxPzLy5F4gfT()wB$LOZ8q1RRv`cSdjQyb zyN3J|5L)C10$P(LrgTLn@}rn9<1yr@@Oi>z#1XrD15woeooA71$vxR)sG+P9lhmDG zxV&)8fVR5D&^<~UPND;&;y8tN0eY4m1 zxr}!o<;mJc;QJV@szx|7$CHu|**YNLHuvFFmtgG)^{-VS^x!by=gH#2E~)YVc?!1z zXBW_iZ_q3Xc;(%I`DYuEA+lXDHh~_)`d*R4X{SuCgyPtKqt+MP_C#!$>Z1#dUD$cP z(aZ7LNi^Zi)0i)PL{*Ccc$%?`Oo}v8ysF-IwKajPyXM8ivH1`6m3QwX^w-`V6qihd z%{CgEw){gpD$Iq(E9>&mf|RJF38&R057H``r?so*wkK|q?W<-*jP%TxsA6$7-Csyg z3wlF4he0zChiFz0%8I&k-Dg?_Qg`+B3@9ZWY+rA)=HzauTs(0|Lo*YuuToO}uHOhQ zlXDmfL1CEnzJOCGS%QOFE&&P-g2X7{%#~L6IT27?12vDowF0JxXt}%U#4@3@4?y<_ zXXmBb416Q7+8HqC z5=E!=T;eU76EiDNj{P^MHG5D?YqFB!%KDbIS4RVAAZSHj8J>U9tEisBCIuv!E4ET_ z4ICicjuU`Iz)?S=7Sou|CK@nF3**JW9RP0S6P2P}bfl}p*{SnQ2+Hk^ zWAyJJeChn$0G>Qm`Sxhc24E}{_H{1hje+CmIp5gmIiLO;Y3cJ*G+PiqRk?r#$Dmf% zJMgpUC!cGF+dbXM61gMWwNL+qKno+OMs8DI)g0AKJI+3+6StQ1`=7d)l8N`a%uHNS zmK(t(NLk4}o0#8zUnS74R0j;^vp>ynVN-U&CO0tr=qidLNb@AmQD7p|5FGh+alt!1M4i?Lm+y=Ux)&$<{5tB4-{Wc8?HTR){jt~o;y8nM zF)6PTO74mUZS?bcp8sAK6SVlRH9IP4+IF7Zfwm!t=?w6VT7z4I|E+xNKV+Q^bk)&o z5ImF<4_&laigY5QFM_w&ur?tHN$nShC7YYC=faMPKXO60yy}4mhMk=8Ns>VVw&Y!*k2C6&~*ZHu9y>o#JsG$^>V*aqT>Jq!%4e zK7h4H_M~Un0Hq?JuBFyw?(Ul!T>Z>Bp7bWE#hB;ZsHJUnHh_%%wQ_Vb(!W74!?{2T zK9(Ier3}INo&LGZ=y?w&d{4cnex<1uo^0a60+GfRmbq>}k9q_cTfAa@IQ0OQ^^D*M z)}I>YU!9|V?BJsJYzntOJ+nWKJm=A%y(EdwIY9A~KwoNA3_2i)f1$(7Y$K`jOmP;k zLPpux=bK)xF3&9d{=~hFtK=WeqW@Z1i)gK0fK>`L4liM7&A%@3Xw4e>_N$e|1cbjn zxC==W%T@(nC#@e6xPjm`sx02!PwP8>JEQrgNEq8CS$(bGpziHp(XespPMc<6BoT(K zdSns`Lhac$?pDlGuv{DA{L>8%Ij!MI9R?1S=w+?;-b%XI^HPcEA9cg5c{2y*@IN^7 zOlqkeM|pwt0EK2xN$!;%UC~195RfrjZ+>95+ET%ToMkCR7-&Q&keJdLUN%8-U-L0ZkJlC@$Dk}IblCiN2)Lc#u**U+G>T-Qha#YrTIZ3jq+kfQJ-;%$**v$Voebd_ z({K-f=#!77**AEPRn%1&a68p`Zlr@GU|J?Xsc2vQ>a*;!E+Zv2J zdWJ`)EstZ3l+1Yi8Cu&(;w_V)p%#B$tHMEQP{uYs2gj^;rKnhul(sa+37_FA$7!fg z!a_{wVEtA|$&6k~JO;Kihx^X+TE^p4$_e6a#+MJyXxcsrOz@2}2^R)1y0{lm z>&c9PRryYLy(Z~vbJqjHB$7CMqQ?LTJ*o2iEl`7Te^1bcY*c3y-4skWC=hX;(oapk zrPDvF68iz9WsKRy=-7$P`UxCJU zO0FTUH?eJ;;;yr+ViFC#zoiWSs3v2fmpth+4KA=k%Y|j{25ydC1Dc=W+i61Pjo9a? z32>9#P79Br?cwZYEQ+o~9_d`>DV7QgI$W_w<3x<`yBm+6d??21J1=_>1D-{YQhPwT zPn$MjwOl6`-rDn(q@I6AtJ(vOla&%_`qIhZ7t*r0Svpl$ueQ8RXhJj_IE}5Otr*u7 zWHov%0gI8%G@D7IC&2lnEBJ+LF^k}`>mu8>Zh^sJWAf|A3}^IxSLv!`D!yaaKLviV zm78P?=GZN&GW*4 zIqtl;5Y?(UN>*!en3)SzWBLU><}sk^$=NsNmgK;EEG446tW-RUU%1T(#ox`|tWXie zO#mu9uPZ>Sq0WYwLR-;`b|5H$Idh-s*Wi}Ig&f@AQo^=XX#D@4&P)1;$>#0TK+tmI zZxjj|n9Q9iyoV3Q(<5%(vd5td-wtW6&jOkBD4~S@oT1-7X_lG9kRZe3jXX`h)wzd^ zGDYTP+1@m2rHBs>D7P6I;?bwK6E)F1qIOrU7mvnv*?c@=~b! z)hgY-;Pj|}N_K4Xa?zsqf@+6W2=W~7QxHJdjCS<-CBG_DPcp* z`9j%yXNvYm_P?9As_1J{MLy(%I?m9k zn1eI+;!<01^zq-Ll1$7gyTkB_L`g}(4saU|w>=z&gM%PG?rU&9kiK$`-q^iNE3*Dv z_kyYkJGNZHI_@wEt;&}Nx~L5(OwSMpa$*Ubw5uDbKHg{B_;Vr5-OFb%wIAqI7MaLv z{?^KA)^ZsQMB3=x#V!fJA$nPq@HFXgh)}&!-z2pJkzrMV$ewE5Ht3VEA`^BJqe5uh zq2dl=rJB)>7ykC(W{_MDn4IjYM{mXfuI)LF0bi7t&QMH)6itT=YjY)5M-F3C^^Owj zr%gV<-S7a*If7eMDF^WUm34O>qz3CGaEUCU3>w_~a>9H3ow;EVNdF; z28DUg9Vsi*u|A-P4lwvvVYOv@+3SvfwP>u-6wnZUhR|KaETL_qkgj|UTrex7FB+MJ*+c+>S8r>2rb0L?6-kp4fVLW9Fbs?`zsEJ0m?N=Wc9Vne^VXHIY zq#~*$g{(9*WmuN(256&4nC$(eM#hlqdyVNO{|l?u4z`nkmrsx@QjflM?P6`S+sPQ1 z1*?#zI32}?S2vbt65k> z$e7JW@3Y;kZ{p|WtJrMuBsa#w5_p}<9nG941DU$wJ3+l>=dSvGXtP#C7Sb1O`E@f1Wv*cQ>aj*cq7fG&Wu z`|BFA>qpWIxeaa4XH0n;16jSC@ddSh^ zBGB2pJ=uzsqr6Ahl?hpyJ=wp(zGi)y~)A$HwhJ;3+qN`dvP(UqRUKBtT5<}n1;GpB20eh@dc$nze zG)4E;!%{;+znDBkJc^d`z4g_KUD$jL?O3il!mba%V7Xy&>Wycj6?}GTVZtD7kEe@cKD`b{Ppf_DeSZoU z?0+>Y*(HpTo7{2=Hv2uwXc8l8?q_&4`dwtdO$!_E%NZNw-<0*Ar>30k<)h!8km`6B zel$EJqB{Uf$ifgWFHGI~=il6cOrU%O>guHAAk&brF?jXso^TM-T&PJ-M0>wV0=47! z=RADF72OuyDgAy@+xZqYVsUty)e&Tk2WTT2j50QY9ng0HMsoTzlLuZ^FWr zDeL!35=<-}U_JQvmToGaA!?zfdZ4e3@{)a46EQU0SAmuzmy<% z5@78-AmX!q)ns}L3k=EUs}mZ&W_^$){!Vk)3A{`0^5r0o>TIDvHdK&z5Kv*bzLI72 z`85Ni)-EM@W3+N9$dK7O+qbxx)U#_wl<&Omu%V(B*|C4t^_}jWVj`=ISjD z&*AVY;pTPN8~?YupL;vO66Qx!*V#%x13#esL!L!otddBu?y11}oN-5o#gaeSJ00hP z^ZtR3Ip7+d5V`DdC7m$&IM+rqbn8Nt3+2?A{{SO_+1^>DHbxB3Cb_6xH3r@Gs*`G`CnI{@7s z;Jrg{=Z){sF;{v0L{xa9*|4|Stu+Ehg@+FhBx1E;o<{tG*fOrd5Q`y2_rPM%qhr+H zgGP1&2pX@eO*vn8b{kA6uw@X04Mt1qYlH-;Za_X6O98PY*!BApE-M z+?>97#LAE?I1sD95k!k~%2cgSKkCM-hHE=+v1^x5VPM zeMoay2T+W%{LU^N;QiOZ`}R^;@L@YO-~T1zs%Ig?k~!R)CGQvBeJ_eRsvPyg`Uz+K z`=mO?nIl0<=cu0lxQ>3JMzk=C#&^W{87axRj>HijKnx0!IS40SDu`ab0rl9-SQyB~gQ5k|d!DrdW##`3V^RJ2OB-NlOc^imgd%bsmu1D()^zCF-tmXEn0a!Idu3nt<|(Yw^p^*iw7WsZs|HH+(EcZa$D`Cs7Kn3=Oe;vPKsP(ZQKcR?F3PT);<{3n6$n>t(9>&^RFvcyAQAI zho!o?xklZN2l8jPAa_~}C-eubI#WrHWE1AVpv*$%Avay_tl-?QBD_(9kp|}>*I()0 zc^Qnj&FQzAqo*aV|FTlLtT=pP^don*ud{WM0nIS+9~_j`mpVatUO9;{rwI?=4?9jD z{P!*Hi1^opDb%PgdNs}Re$n)3Vb?U4#u)CwP(L2bBtf{5e=SZf%O(UnGuZ4PLA1Lh z70T1K>cl0p#pRAfCv|qMbb(b{G7Ls=I6G|rg(YTM$&3&z*@tCn*^IZ2_a1^LultL6 z@B4rDg8tvvM(Tpt^){@+E1=rPb2dcXbpswRAQa!HuZ%~oDFGY^O&-_QTPE*emO&`L zrt6`D^Ag*Xhuq9kro`ZJS(VTnl8paL+qWv6BP$C0^v*KW1Ho zq!^f3vgswq5BL?Pd7VVE!wwKf&QVgqxsZ`Cxi(beLbwd%##>4)uTzgOxHSzM*H-bb z0|#*FNQ~w~p|4q&q&0P#L)@AI4EJR{pyyd~mHdOjWJvW2`cKB6VHSs=nAH;qfzG*w)?G|SEn;^NT zNJQMrC1W%quWiH84CkdNdko&EBjsL-;3wK8%nm!st{u7V6}*rZf(J-;z)?O4BiS6D zI8g2k_HTWCR!!7)}S=39(pK;@7jYW`?E3E@oY!%g@TIm0Y zyQKF^>dDJLvxr}H2E?Kd==r9&SD>C?yrt!_14ObmtqPIEg=dx>AJ+&#!7+WlhKw07 z5egr+=Ic*_6Q^$csb0Z4VI(I&B8*L6+gnUn%EQf9irW#2eLYXxE6%2c?qJ63OX4#@ zjg>&rkc+fFpvJL&YByVv_(!14m(MbvKc&XSxX98uHsnX!O(7CX7&Gly?^hw1T0v&=N^jdR%U z`0w_mI6Pn_sLmLLW7tl)|Uz$5p~sxXfGy5d7(t3S2~o`EN$dUOS5h zu6i@p*JUld##<3DusR+n(9gEJrkXOp35azm|4aERvDJQRw3zNW2e4<)7>$iL8U$c4tYaO-%uV5$}H+g;ktoN7! z&{--E)Bh^2tX#Jf)n;EB8xf?`xwpP6kdxkLLi~TQ0oo%&pT}JC40liXO|Pp%mjdUh zi}K~p5K8@QYf#%5h>~rT0kl5gfDIotSc{tG7lIuHgyzk&uJYa?@?|VTYx_iB6fp57pocfb z(Losf_9VSR5e)hp*c+QD+V8jujsA`xcV)7;ARGwrRoL`-EHCZbVx~@H z&G=v_o>`jGDlrMwg&>Y8=@9qQp<%lC3YST`DZ2`X4{~f5BQr;Oiz|)MrFWgUJqqW$f3xBs%x$jo;a}K zn?a)+(LLw{EQ>@pUq8Q-Ja+9ocXygZ*LxI#--o|1InX?fDNk906^`1stmNY%dAd>N zf#&*Z#-pJZv{ym)FJ{+QA+&Q?{DlA|aOY@BAMeM0S(16B1<9HnScB$B||x zI1Nt`Udw}zTVdbN2y+xpA=`C*IGStgrG?N}8l zGzo!O(>Kz_WFF;+I%1hv0^+ZY&}#eUn|7^kKPuCJI*XIViLt`l^Ip#AJASb7#|7ft5USitr`HyUbuz> z6wEiOI$Clf;%Fhjf~q`aXfCf0ff|#kXk5pvAjV2@%bJp^`!#Z6xc7G-joya-&QcUJ zlR=4DG+Ol{7T?K8Iey8-FuTgex9rWqqcqZl9FF;{^Zi&+jULYNjrjAW8PiZowu(Cnv&KD?TS)xgGgLu zc9N@OEK3bAKLy3Ty3PzuJ9>tcIab5-1uQ$X7{Vr$b_ldF;CW0J+viymUL+fxc+41hb(sI$rJ76;!y4yEVbiBTj)kr8et@(vAAK=d?o&sCK|*C+P1L=&TCEJK#FqCpo>QvODBJm0iQJ0{9e$oEd==_mL{M_9Yp(6u8 z-bWt0--wjS;m&c3bWD~lX}Vicd!_1f_d*gMi7iroP#OS%sFf`UMC#yr^$w=x8hla; z{C!{1CRXOk$;UK8u--IO%=PB2UUPj^m#ki9Y@)J`;=OQv?lz01l-!|Mxn8337Hx}M z-nEE0OIHXG_6!6`#zZCuWg2MPzlJ(N4@!~gLeE)>YPD5O)`FO$Z$?ZCq; zt6K`v^DFytuwPMGes$ zX~5lR$@FGS@J{+**;^^QuE>}b<}KM&A#yGRzOu@r6R$}e0s<9AXZ*r{SQz3~!OTyu z$^zsQ^0hEX3?XNxLpH>h^+yl~payDz6Q(+Jc}2K_r|`TL@+Z)^eq>9IrJ_C#ZTuQ% zc57t2W@`D1tgpwfEx=J^+n9wud>-(>tX`(1SJ2m!F2}>K=lgyRIbSqop5@m|YCIhM z>G)Bg2Ol;ClNde96_lSgCOC@mX=FEKUPO~E z$9+>}+4UtAcg>G<&z-PM%-nHGj>@6zmZO$v9EmV`34$$a|LgN!X6hXAackf2*ty!l zDCw23aQ8p9jdW@!R??4)GKByWNBI%+E_8n6m@>9l+NU%w_hb3Ic7_-)=OW$;wIOat z-dv*#CM0-Ng*MKU7I>Kx6LujXTLbuY3|-CIx3;#r-+_!;bO0+^&%fm_e=$tYMZtg$ z;S~B<=;Hp;{P{?~@kO{f;+1r~;DZ@&@iG!y0}|E>*@9PvB8>DkFmHVhe_dQTeoE}7 z#`bb(Umj)=8WXYM(dR^8%Yw$>-5NiEg^}dTWjM&cS|7(R@DCzaJ4%@{SKmjtt~)rZ z1+We&c6fNT6I)wh#}^Tx22Wr9TvmX9L1`l)Q#-3?Z-=?_QS*f*PO9bnLGzoKfW|4& zhJ_1LK&MHTqtVjA)y5rO5(ErEBs;R53NIB+Q{uT_!lChs3tk&mOxV?>o54UxPcvZ} zHEkrj=#@&qK^c1&sQfaaJl-PIV8~dVYz}MFp?pLF^T zlv7SaW;rF-x?$#owV0}JxQEP2xxHv4f9@QoWT(SW5N_7uV(CioDS0)(xvbtjp<{~UG20Imt4+(k)P=B1%xo)wyL`r?3n zD#J$DtoFG(ro#h=qWkM<37fJZyG|OIkqKbad#y8#YYm`QN7LtIJ29WGd{h-ThxG_> z_IWM&8f{VcGFO;4$N#;qKbGZArqJdMzZ8gNa(jVGC~Bt%`q`IIILznUnR3g0slG9U z%LQkg8)hYHu8utA71`}5^b2G4I?N~!)G5`wR4(CQSl-K;BIFfzZQVXVpuR}aVJq&{ z*>19?#){M8pnW4dEd%1Xk1<7H^qpsU+4I4Cep3Ln!q*EXKM(fSEmZtz7e3G|9gYr9 ztRZeAJkmi~xHda&Ytz*PMuDW4NGvtJn8O1l*0n#`V6Of#0w|IgpR#e@3~jSuQy-Izh~*}B+G~6_n++N)D2+QwEb{RE62|FM z**+4~dV39>ilJJh@Emxk-!sqmAI%$wAW5K_kJqnBo?e+-xabi~HhP*?{PI?6KDWl4 zu34OI=WrdLtQI85qYW-t5)o*8Z1=JH5&V0g&52MHongFhhi!t;!N*ifI9`tCl#?iY z%v|6Z1(cH~0fqIJavXubx5h(p&bIyvu)(yKu{g(14Wl_so_W78HkK!Mp2J!fpk1<$1#k)$x3gX<#oQT6 zRI6UJ?43ifAWPS^k8RtwZTIP8+wNoAwr$(CZQHhuzenHXzQg~{ZjCZBq9#wQ$lNL>wq&GZb-EoL_@+FRV;mV~{#?&8!RK)hB zKf&qcAaBOvw-NHWq$iDRmJLs}tGqLPkPL{j1_=I@r@!53tw-WW-}8npQys-SpAgadO5;2{PWQz>UVE@`tR5|Ue^ zd)?B`?1mKL$T}AyK99G>0V_b9%vfyB8l5hc z#~2GbT1N+|C69M((ZCdiq_< zKFdVBO0KY#_}s<{&il+^jD%5=O0m#7vv@r^9(LpBWDK4G(DL?F;=7{GSk_KI@6I3^ zsFjpO)dmy%p&IE$OFr`k{hVOpHa!7+xpx`~UFW?T3J`PM@;JqcoXF-BN;}s2U9`o( zDM4i&XcdD0=g8Ns@j+#kptQuM_^M03!vk3@7HJ0S#bvBFLuLr@B0Ep}3DYPN?z?`J zt(ITth>rgzHvigEFoe{;H3iKOBG9yX`_D4)ote^zEbD$2K-~AGa%N7#g z<6#=^3!Sz#ukE}?*ySmxi)yA5M7lx{^J8TuMR@V7Hq%Y9F0!-X0|kKx<_6BLNa6IG z4wKpnY?GE4FlBUpGTZsG#^oFiUGOwAs4IptK55$FkqM?9Dv``s`O{YoJCxGRb}zI zniQhY*X~`Aw6CB1d<#0$8!Q23Y*H3a(#`QZW+TW-{!gMQT|tEC9Li=ZTTdvO^0pxe zCGOgcM6Ku zE<=+qrSs)ByVHLcgN}|)O);NesvrHjUoPyhjeOf`N?^Y2ybwp@NV9J`8C*vterd_Trfh+)gc@qa57*Z58bYrPXo z74j*W3W=QP7vd#v9&|;MyweJnt!F(?*cy)QaS&(fT0%^U_c~Gol$X zVysD}{AfjEpIC})HGm|*VHb3ji|d*w06lp-vJY|I{Gn}ifM~#$6wUR!rE4-?#K6M? zyA;H-5@4ZFS}JX~tD|!5_HS-rGU0q{M~3x49{L}DVAECkL=Pw3L2>1(hl^p2EoIL4 zL*>DAA+vAK8w#!SP?v5dbg?+K8%Dblz!DM3Pp^pfiv$yB{g_NS{gD<)3~)c&SrrI$ zY~9QjZ+z^bcK?hNxN-%tItvyH%)^`4NspTiYk+Hxo*jm=Ce59Y8+|xyK#{MLj}RPX zi>nSC#vwUcff4Y46smC@zqeXG5&0DH5H|rwBS=AF%nfS#fd?NBC>>@*U$hPQ!p|dW z6{fnZ;)VIFwjwP9=GZPL^7Lri1w(@jlqVo z)nIF5(0AKQAl0gjmm;z%@gkbC8u@gsU^tHsrD?gvxd`e%|b1~n*BC(b# z1SaLb6&dJ@1S5x_vSA{k+RgSWB4ZtVCVuM)KAto9g9B+heus#OVz}<(~#VFvf5_mwn+$~)|yF8u@2$^6A;^-K;YaIyQe5r{v z5-Fg#-E3PL@UH{9j2u0y%O4XxP9s44?h#0BEJ=DA5Im5Xds zT$xHP1DdL;-Q7I?wkS+z(%uC>ruvc8X4RL`-Smo!AL1P|SiFnDSO2RsI<gpIy0xOqapLQ$w2B3jT{hA4ByL`I49kStvPGomd??bm-$915FfXRitDQWs%=$A(9*RwgtVp(OG{p z`CGgxih<6<=ojE|u{DP?hy{6pVJoHO_f9Bl`}h9+?}D_yDe}-99Z~eTjQtd;@2~~0 z8GGxM<4A*XJJF)M_B_;qgfHVJkG8k38_{++YhPg+LlvFrClK$hdtU_yK7CNTsss7a z-DY`MNo8*he=$y5oo^7_(wO`iLl|Iza{Wa}YKwAN<8#p!` zMoM@di)mfC57K&jA7%4np&d;Wb&}x(RajEqrnFu?NAbVRy+{bzVSk$J4e0d!QSWMJ zgLEBYbb;4FxYmiK$XF*WJP;avMF!j^CKm@%2JGn{ykyFmV=d^kBYRY@R7%pE6@S|* z%bqQIkgH3KXqP!ECW0+EM<};FFs=c^dfe;?c!t@;1w=$Q{IV{qV6`kzBrA2>3)k5R zckp4YBm4n%yMnCIUj_2q>K9ILRu~{{l~XB5RMKjJef2*Wp=f>4zI)i_FuedamW64FtKL;O*vzkVZbPz#HZ_kkj7dE4v;W1g$ z-dGNLirSh-i9|`jH+~d52h0G^fUq<*P%r>bJQJN*wY)<7CA`?XX<={&(r%{ACN!vl zo-63|$TTgDje{>?~+s=nmL0dDV!_yr@tP(vTMkm7vWywVm^ z9WMUOGg!zPeRc2?64pwrFeL=@)-q4`mW|NJdQ3DO*gO9iJkr!*rD~or%@Q5N%JcE< z%sz2A4|Mbug9)5wI~)XY_g0mfE0d11H|VMyBVD(N_k7B(D~d!f(Y>i?vbifq0981UM5GU@tN71es`0MH+S zer2-vefPP3-0>+Rc0ukb*69qE7XIK=hyf^aIif6BjLUI%c9;F5JxNzHW&j zH^?c=o6M)$HyQ&xKsV#g@;3hc`hOL`n{~H841;7qWgxMif!ut$l$O$oGReOzD1Ys!u zgcq6HYh78Yq881ISKv>%5iBe|@9p+S5q{hgGRrtq96AOx=>48g3dx|#&KM9|YS!r1 z$4?}rQG1u6T0iUc@UAr*J`AV3>xW1a9M~497;6tW7e7wf>EC!Dt)*Oa(DwzJ1ILM~ zz9g=?!5O~|^5m@v>fBK(kEQs1%d)Vb!Z&g;mFI>sOpm;dYPcDlS@5{Md4I|?2P(G# zG`Mp;x0BlT*w+R#Wl1}`=rFGAI_RNFA?UWTpZwweDvYCLuTY*8s$}LrSCMwV^>Eua zyaz0Zf9nK$R9X_06_OPwIWdH?NUb-k+o@N6lLH~AA_LCT%J9p{86SzaMcOWBx64*YwQk)3T8-j%2Js_O zV$B{nm;bIMe42Yy>8ejB{w1XlFd}{lgSvaN+?jdtprAUb{UvB3unXy4Mg!1V5%8qe zsU@9%pzk%uc7BY=gznmAJL{)G|32uH)(N@({Op)DrAf~)RV(?ORJNpsbDo+jpTk}M zz<>SZEbWz(AZiB*UaLp2#P1i>(%?fel~qmiH5u@VA?_D&mi*Y)KU7#k^SAAYyIYJ zfZCEtBfUf_lrY|{NpZv7Re67}A?NQTZ@`SzdrfNo;yMrkPELK*0j#vjwgwGSE;ait zd{9vXlM@4T`^EZ1Dj5QPg(qk_LuUsmc-sF7d7DcVcj(CJC;D#V*9aBt*8z_Wu4rdv ztP)-<%v`puh-wNef0CDw`u-}qpq>%ICm?SoIzi49)KCd2gHqhJeRFVKdOrjtSmbXW zxhG!pnN$aJyjA(W2rdwn){M+gOwJT}u`lF$qtCH?5H)JOHs)`}MFO={Y^hSy$d8H# zpCbhhH@hWlUXMp0!rB)(edI3_No=(qEl0b29g)*Ab$vrS>-%7uq@5;OBDD0ohRglX zPzdrg|ETWwd@W*$Qug??$jDc85T)~5@S7KXk6nv?jl2N-6p>CigriG9IJ3i_L{{Z3 z0QOX1%4+mjkK=e|Ytqz1uV^{^SY{sQB-#Z^8(Ox{x>*>3#52kPGtpw+0Da*H84bN* zcup>m!Uw_0RF)Nr2`HqNA>p|?{d}^I$lN9D?6EHsLKN0<{8s=-n}mr=PAk74u!Y6U zn-Fu3@Aevf`6s?&ES64;Cjdj8@BD^1A5D8>RB$WNiG9HPLHAumm$=^X1;>N=sa#ga zjR4LtjQyEdGz@pU#G0H!Z~byzTtP!|t1;3h(vnEf$VINi4IhNS zlt7*>QR>nvBIHP_x4z5%b-%7Z%=^Q!`LOQMENO5f_cIUSd?{gqyYxQ5gYaE8qav2$ zyAag`N2H0t&~R$jQ=~awf z3Xubp@rvkbj6yDfoS9U~>tqFAevA?FKJ3X|RDR=tz`AhQS$48(6HmwPZG^J;_+D*H z2>oR>Ys2tFcyg2giFuE_x$P95Y2_=)UTag2NEkU*yN#Tm# z@Xj>#vW26%qI^8+*>(nOW}FK6`thg|%Mq!Si1fglusLY&Q{j<~9BRF=*2(uGyrGEI z?FaVz2b;%DCiyz<(VV3seIzNxkjqPs0X!B4aJ2Afrq>KZ;cxy8D9p?yQPGXTx?6T=W)0LaQU8 zZ5z1i>>SFH*J$A&z-3}5j#n2%XjS?${>U(M+ZgxmnYliuS2Eri0ook_lRkR1xFFxe zA~km)(6(=CYam8N2A;+rdWQ#%>?tR+Va0Kf>{bYNE)ukY!CNI70f_J$2^$alk#v0fJr3UfAG8bzzVKz-05K34}vz9`iW zX{YH#ZAjWrInB@4NS4wV^YJNSyad)lMwligJ?s5&WmIs&L|wSY56UtJ8z)& z*2^6jF9s6elbx7h+N<^Efy%qUxYvnZWsuh!za$m{LczWRUc~z?=wS4W$8)FJds8i3 zvv&z`#V3xDZGOv_FQ19~h`aN@$c@GGs+ETCGBuEO!$;52l22*C-ByN@q_w-4Z4IGd zc%~yGWC8M)$n1`4jl|O(x-MOgPWkBcWegDsqQ+W5CYLVzbEACOqNdh<^ZvTQ4mdh; zi?eXMCiV~cJv8wWKR4{6p2KbY{jFj~IK6eM`+F8bJ88&}zi!L8P5`lNH|Vl4;fhu3 zCfHPF+fTl~82B1IGWeGYkmi~i@D~bqz?#X>J#XwDL>pGiPIh<814}HvroFF;^c72} zVu^T@Tq3s%rYvb!d#O0*O5>7XEdw-e!KrUJ@HcEAjtS5t01pMqBCr69#m~NV&55DA zN?CJgp3QF-CNWSu$4)0xyqxbb4VJs}}XEnd+j-JWX*<91HBBZ0&AP$h0 zt;nVp8A~ff9_9qMQ=g)?!RlL%B6hataP$&p9~r&HzI1ylYV4FiqFo^g{CScp5hqUK zD_-Z$eb)4ex%yVfJ6_A2m>cr?JKOwGZDcjEmctX9or~kT3=GrQ*@jh-pmTcerBZH< zi&c@dA&3&}DubiE(ln$HMIL-_%xcmWhJEz3-R^L6Q50YwOhPETG_Wk;*4ucMX2aFB zh%Y`Y@Gv9298WkEA`Yd5_#(D%`NUF{Arx}9xEHqLip2`(%!|I*a)<=KL*D=z&w;K{Q?rQTntJ^u&A2%+=C> zFmL;CYd0O4;c)ft1?}&`tS9{0B0RU-nrfc&6{?%Bh)_L2d(keXaBZ!O?^c~ zCD|?}P^Zt=fxfl)FkB}nqbYDz6~9Wlrm;jB7CifRy#uq?Meehe$+U!!dAV=DmhFCS zlhacIq8ip*K>Sx_l#}16+M!)ll^(Mh&-zK0NkRwk1EPbOL9Du7OqbHvJv5urq7td0PFk?XrtzpT zxh}U!iAcQJjqZ{#Q|Y?7gQ-R6o&yd%JL7vmMVz5tpBX$hE6t~{wm#EdGc_hG4scdV zA<4y@t?nS(6-uY0O%M_);>O@SpBwhXC7`n?Uz4%WvB`E=JBpJp$~Rfx8VEm~2?(pm z6D~fXq1gi5W_!n8-WJqEsh2MZ^@>6;y59Yzz#al^V}7 zX&+gR6`#kD_9Vykbr9b@&CL5@t@EDpc?y z_$WzG=>xri`^$Sy^lU>bnbJZb<_{`w9AL?c!TxP%U~ky+oSTHn$Fs(8`t9 zHneJ2sF(lVQT^RNLmwwSVXNk09Kz*X5;WdtUX(?D;mgU1a7PnmDbOVJ%wrPI&#E@Y zaHv?sRqvRrh3oJ1UQa-1#)Zwo0AEkL54bbzWB=`aUvli@AF#2D2Ti z*hO#jo>Vih_TZD2AgW43_{jtlh~)ivTbvI1w>#tA^`wcnR~KHn^NangLI#7I6)3IO z?f(Gb?)1qu+wx`&Vgj%baqzbh{?nmVxxu6A8fcO}`=-$NRLJxy$+5Xj)$ccxYPsY` zZLQ_V*a;I}RC=b5TX{=8@)%s6K>#d;Fc>7h0)N>hYQxE0WHMurSU^2b2HA4hV)XR6 zI}j}1hR{%3j{`QuC6NrT;_?23bTF*B8i^103r`Ii`d_)17^$u{Un=@!C#=+sF=U$B136S0ce+t zf`U76er(K%+IcLOSAo>MrYE$XxmU%?(CSwZ1*8))%EV4X9EUkgUaOm*cH3h3n#L$I z6apBEe9@%BawgW8XkU(5qhH*nCq)SFEQadb>8Q_Yq2;IT*b%g4Xnp zt)X1QWW$B+yG-5bln)F>0yzZnJ)rw0@(1-Gcv4mauTLkDeDj3FdxF6P&|oJf>wH>$ z`V01as}UpeLv<~3=^w2GH12H15pns!Hh@UKBT-Tq5^)tQ0~q1W|0nYT4LMQITkDwmG;;ihcH9p^;_5(8VVj z0JRll`--Qpq^yLP>sEo+D5;i>)Uk{6ATY;d35o}fV`m(o)x}_FLwO66^H}$@uy+%z zZ|iMB>?Ndt_;wOeF74(&K}lxJxMU}@wg98A^Ab`9uauD&zTUx(zgI9jaLGD>gCCU? z$pd&_NtfPyV!!y{H}-G~_aZ6lP+^EXWhfjh;V9#Ud#a!W|o zxid(A9S4-^S2sO8u zK*@Ycw%G|&D#pt9ze4rw_Jf%sq`yc0aTgCM4;dH`ZE|)6AuSj31gs21m`P@e%^Jc4 zHoda-s&JPzTuT#2ZqDz36j<-cO#ri-bpe^QC+C48(*A4<}Rs?ee=liTLI_N5A%1 z4w?4-Th5CmZIUG2&&Mf^l>&6os5IpYGx-^*8sdOxux20MQ!tQe)V{}k%R=>E7dU%P z7Dr6XH^oMyF_u~7j;1pR)SI*zPH=!yJf>dY_vHD>so0hD;wbx(^HZ)tE%C!izujt+U=anRMFP z%kLX3aw|9*wd7=d=igbsqcHc%1i?&#+`aEOde;-3gWyC)5gyCf578aUPV6fsRIR#6 z1r3N0{ARntn~MMS>K%=4$&L(NF7W`&doGzXiU;)ewEkkXwu6~HdN_{#3uQl6yilM& zDX0W#QRK9vY0J_HL@p3PoiS^w9x?(4!idr{$c!2ru>hqWY7WUY$SnrY3apx4Y8 z@$yf5Y~y92{D4DAVtF;4avkhC4w&LY74roB11_;shWBy>awCwq&Ty6UT`r+Z?KO?E zpUqgH5#W9z_)_~=lj@;Jns&_Y%!1Laq;h%Q69S-BrZdENr=<*UKKHnbj zkUP~$x5bf=^yjl7ZvX94wAN6OgKXV>Pi$4*>ddaI%VaP?LM$F~REDP1x?-jg`UTt| zza_avr^Vli?1N4eoeFfklVq5jUOcZv%LT`n_!;+LGrH!=_-$Rvb1Oy&xmshyYK`#G z{+PY$jwFeFFYZ?4yc?fl2EKRSRXGS`D>;DlwI@Nx6LU#MzP?P<@!T-%DUBVJEdmw-_4^|u~N4%pep-$r)P7GeTkFm9?<;CZFx5>~b!sx2T zt$7e)Ar*bT6jvXs!k4|=3<8RONhlw7`6>?k;N4a~jwAr(uYVwnyUR|`8}5~3VcaXN zmD3;(Yw;u@C@w%LRx`GZ$sA)%J&Q4(!S7D8fk4c_d2Pvt8lY2@%-AZaXXEW0yt!}A zxutI)%li(_HL39XII(`>P#cVpL@_K5G=&6Y&*#bx?<*}=yDeth=K(ShA2Brw6T%!Tw;943NLKsA(Jki> z*5*kLYXzs=)N`^Hk|Yo4+4mCzSw6GjV6SwQ0Jiq=}lcO+f z>dGQrUD}^DQ(W!D7WiJoigf&KQ0dq;TVxqL5Hp8_feKPNvg_=3l1`pVIci{%1Eo6w zU#s4*Vs=q|o0yPy82FoiK_K4hwOeN>*u2JSu#RlOFRQ6$KU}V z=gW9>CRl-)C2epswkUO5PD8K$;uOsYFUYna+zz>CipK4KW`OQ7$Tvr(14D{*0 zIv{?yLpq!I3S$tK&hdBmape)EC`uJ}xVg5p^R+X;oLuuMnQ8 zb8l(cY3pL*x7GyPacRqn06(*Ev`b%?9PKj0_3d|2#y4~h_FqeO=VfE~hCY4mqEwU6 zafH7sM6v`VReRd|kMWw6U0)}-+8$fKNiN%%Vkyksmmu4h7Af1QA;2btH2x-R$HDXC>;w^=y zSN3LYZ(6a)K$o^qq!qzmbw%Ii)7b_|C({v2ZCw!LslfKl*0Cu^m%$!S-SGr;A*kv_ z#a5nvW9v{AUmz+Z)h%)vRzIIX-mX-iEkE zX^)+)9IyMW=Xg?W(1AM2CA|LmwkXOP&&(K9ZIVPlVLzD3DP83w3gs@c_}ewF%`PbI zSo!wdQ{>Vg0^f@%APb!F=?}%j!MV1^;k0AzO%AtKzf+|$rQcieaXjd>D9Q#$zp=~P+)EK_ zl-?Yt#*ahsB92~nYfNi|K{8rvo@#I23sS!qcS<(wRA#9U zg1ZR<}&M z<8o`E2-@)7X)`o)Adi+Tjd(bVvFfe02RT3T5$OmThX~_%kN%Yrp3wnR=(=YpW)B;r zg32&lLj5aczJDBOGeRnnBC}$Vd66{j(~mx-sm>zJB+|{zf1^z2P^s8T6W8zN)LOad z;^W+K6J;Tyz0RcZaPU00?X?o3@xH-DAO#PKffBavDvlzz8vfz?WAKYdW#Y z?hTNFsyNs~*RBm3>0B>8tU|Ja-apc~$N~o|ja3ID5*K)*g%G)K{<-v887w-?bT-V1 zB3Q58GSsTr-}me*>3t^C?^4B$5AtkEa`xKhF(E%z!e{t})#Y_ZoxD+2}e zr3QpzV<^Jf1@Cpp)J3DqaTu;hoGl~*5|#=Vd@P1Rw z~q|4;`i2zAdW`I9(#QfO&bpz6oGn5Q_I}BH@|F0bG>#n z(lLVe3jSaZwEo*Fzlz&ZrPi%z21I{?)B49S2B0c9B!@R zJ)tO-DkyWKp&f5H>@1xqzXu2M)xvY5nqom)$6Kx*7abb+03z~jXALzszCTlx7G(R~ zhn>6aJj@2)i$@4e@)rs8jd=Y7s`_6eZDX(iasV< zKm>e_Z4I`z{+g|fktn%cHr;+h^&=j}vbm1y#V0_D#5=gUF*29ZXOSi?n zrg}F>MVOO=hoLBZjyMkvtspC&cJ#|7g|{dLcO51(gIvSmMY?ULROHYjP{JX`5XEakS#>@zA-IQUrMm2vbdj0mLtyKGAhC=T4R8h430>{B^Xstc7_a# zEQ8N60jmiVrh>uTLhq->kRng+FJ`!9dVshKi5rvIlwz+S-V6S0kbni?xwz)Lvn}mY zY9`4nP61{Xng+Y)P!cv_8!HBkP7Y2Dptxt>_o$HPK~w>hMQGsg$d;i>AVlg8?Ka_{ zZ<*(KoX z1PO;ZDbHUkE$GPNvkm}xomQPrv&SUOs(o-8cPqTY!yHO8q+a3Z?H?#?(=_Vxhf$ z(T!&Q;E2c8#X|MRba&-yhtan?+xj8-3CT&wdCk6%a~3cKsK@61SQ_A7rTGdKecuEV<#XtfH2=?& zGxn#+{WPsM1>nGM)G5Tsqg1}yz&%m;c&n2Eyz^9i|0Hk-G75qb+6D#wkDt|3-1V+8 zj-srk>Du+FD356B>CQSe3!$7julWAf{rMs6OK3T4u>t}m81!^jwoWpctsELPD?~&} z=NY`fqh&m5@;5@lidJ){H#qWNGMlw{ElHHkp$r0-7vk&}DnU=kJ>L$y!Ob33$SB)a z+!&YTjWu2ULjHRQ#ATKot1m%`80~wa*q&Y3L*~t$KgR`6Macu8RSQ)e%T2#QrWK0r z!Ku@+;-xKXjrfid6}OEI?2`oFl@kXBO{2%ptkq0G!uj#nG9tV@^N`1&BPlFf_?3I*YK*8q zN5YR+948}5(hLj5N7&MSuiyq7Lf$`|u;sroP5B0VcJb3iji%;skBw9-S!mRMCEstc zn@5bBQXSX{uBO^fk~s17q{EP5dM{5QKr9j27vVFX&vKIe;#T}BuhV7C_x8x>{I(q_ zT8?Mz;>gA~c9vU7sBl)lt$y5>`(FH{j|6s7vW?^CzKTtWKiV*55nf!guI1JO(8M0X zU`GAsH=B9(^cY8lgM0ogV@vIEY)iZzhenFB;D4%Cf7-rhMdvv=IZ_|nz^PHlZhoe! zfex>e<+ozM6Fn(zZh*=|3PG+64xqrLH8w1PII0A~@)L zMR|!P^23yJn5m!}IUac^4xoq3{rBYi<36#duti!KRnD+}J%!6S%Cxmm?aN#|8T~Oi z^~X6`bcFmq|JXo|-3zykd;pxWpIuJ^*I8b_mLF3`p)i? zS^)RJ8fZt*J%b<6?#_1v$03->>j-6x?rY|g=_pB8sC-VG!gh?pQfZd zrubtME@Ia^j54r*)Dgyc*Q2)Nv@eaefcq7k9DWb%C=|6PKd@*y^R zW9-G;Y&PruHfvspX2J_ppEqa=Bkv6S@A{q{Bi6WY-(6IH=7 z%v1J6O7QFYx{u>@1Srxx;*Fw(C-MYr6&cS_50q+nz5knvi3`R04^-*3=9(M^OJo( z7Tv}0Y|9mtdm4gSFq?`8i&Hg4So+O#azW$km9iAxU%8=0Ub!Q0&?lElh?WR82ts6* zr&_o*xXmmioL04g*E*Hrh)5cKE<|2+>2U3O>YT(Ow#Afu5ZySy68XK(;3V{P76snc z-jt{u=9>DErK*(&arDpm3AfRomIHut63N>oHKrS;P@BZFIq}tcM`pP$sPCf$lAAgIH`3 z(Ifo%JN>%Vm9%YjxvR+xL~;9J#`Nh-_DZ&qRw4lh6i~_yAU-F59o-j*v?y)R^zx7| zZH%-n`ut|wv^X({XO7fz1+&a6)j2fqp=Q{tf`CG&l49Oi z?a@xUt_CbMp)B+;;1!28YmvTkHpc~NR+OTAflm_Dl^>F@ResxxTQqjqrxbXaw3F(^ z1@W1J0KNrad+s&7PR>o!PY8=zfk_uWnyle4S_3>pe(CCwoTNmKdDVM~wwf6j5EL}j zZA2`Q5dU>5M+gHTS;D_;bYNo3H-qp%l_dej9213%2TdH07}Wji0dsJ8HOqk@8;P_` z9WS>mJNizCuKh~s|%ewWpIgWKTNwB9#^@3)D0Bxr7i<(!rFiE)z7>WS)`JreC2M<6@ zS^T7lUN1T4B-x9XNf3W`t?0;u_ z^!ktW|6u(uZI8D9!Tukx{~ckw{m1%$u>O~X{a-2aPuBldDdO{=u>T4B-zr61|AYNM zVE;QS_J6GZi5lSF61Mw)!u}`if2nzN2LONq2P_5km4@(%h0|mGaW2JNS5>aerHr`+ zlRb3rBr^in!cLaur}`5dxDGX6y>q=H(4~0fON-WQ0Ms+dYtdP8QhY6-j|j2rX;Zp^ znK9JBb7^my4gQXz%J>sb`#~Y5=z4K&T0{XuM%iG4oXfbB@S2&+PNzO-x5I}bj+D>y zeV@Z)59?DK}paE8{!u!2{ zg5chgd`g{8>5KBy-xA)+8Pc?U=&#vi7C`dMI!;fG=l&XY=fJZ9$SS&s ztb%V7kQ65!Ji@|;r&Ubi2x8$kU*?cW*84Jl`|nht$^5A6l!-(>88u!uH@ElGU0z!g zVld0OWYebIFcE@ii=@2}eO`%X7X1B|iqXQ6uLP4G*qmr7hDO#%{{xjNy^OIdLKH@C z^>IT|7-jn7C%BF!laTiS@5fRn>&(s&cHoLd)GYVJdg{S%41Cpy~a(V4ALay%J8XM{Okv)r6Esf&F-v;krUfY zyyx}Y=@d4~OxB8isF-V}hL>KllSLB?Ch5cafDBvzKMd0Mx$u(E!;r_tybK(L=Ia5o zo6DXz-=PXChiSJ8;!lS#TL&RS#cA3TFa1unOXCs*?+X!!BBHsq6Ef5=DB-0M zOg}-#7FrTN5JS*`-Btups1c&?hyPY?aN$ zDjr-Jjb}C4T)jMrG~TWQb+NaMz6KOdRIwn1?^uf|nzM0Ikq_nlNN7eUCK`_X`=F4m zk&`?IY7q0z~Q4iLBjq0>13sFqcbdG;k#fIBK+wC6|_Eeg54G$1S~DI;^!)Z zK}L2qjb`Zothgv>+=21_D?_3%4vQip%?i=j1lo?!bTn{k3Sd$*jB4q5eTN4D5O}vp zTVN;&Yokkj6XoEz^abUAlXdVLas8c_F!#wzJgef*yfO4+dCl&xr{TPe413s1>@;ma(i?xA_jC<0BCbk3m5-xa(mmt_bQFP#m{vAzm(r8mL0+Q#I>I>1@zIj$GrU`+}j0s2#t3Y5Zn1^cSqDXL*>}rR-JL zQ>-Q23#jWBO{rDCxQ?DLo^40y7{c52nCzi+Hpy(NmUHbkelxtbGImvh`kTxXJIKlm zD)o!~=jCS@9CTfWhE`Lj5wE8$o2X27AKxR~)zi%b-%D-k=qKV(twsnZP`(X-2y`r2!TuG0tzwUDE_h;W4nOc;Fax>(Qc+eW1;-T2-nDOk}dPT3z z_)V#+b*RSm+$D5pqk_>Pu+CcB98*I9nQX!(_s@VwRP&QFf=8p{@=3dt%_xcFig`>^ z8Zduw$G_t|pZ`$)#4r2Cjiz+Yo|b5ChnpVNglXmCp~FjlA-9zk#tI*DoC&odZ&A8k8F$V;8og(an)N$Ji#z0Z9vw*89fcD4u0O!@)+N-qY2cRI z#awi;7s3IpLI`5SifBQjZMM{rQXSl-%&j7t{)+92-n~DI<%OCA5+fz}c8b8ZMBZuTtnt@txcKcO>;}j28Iax5J5b{; zGnyf4l)vvc*S+ki1R&&Tpg^67)#=PO-SGm*6@42Z*hUI~wV{z^PoJqf-sP~+aW>rJ z@9zNSB#0Oyi&*tIpxz}pPY6cB@`*i4-)A;$E<#m+Yib9fz%Zv}Ftli4HI8sBeNzK# zB{1F$(0y z0*szi_77Ke|7j^*L*DtpeO7b09oS7-HmQWTraPo%>kB+^0m`m!aNhl_y^K7EhOP>q zD*^@aigNP1T0w)mP>*Ev-x5FBom9;z*8!NUD?;3zK6u#609j5;NP=oIQ!&*QtC)wC ztLA*uASPsm*zArOYQ+tc{?O)GJS{1IL)uR>-+X0$JfFsNRuJ+uq|l-Cgk`ULjo;X&)<-uF$&^m@cQN(NAI zB_OCEBj-6e;Y}^P8K;*cWniBsK&i8T(ogd=yIUC%UXud$zH#k$w#WI*Pg#vfsYY=(j<-Eia$72<6 zhve4)0H1In;1&U3K|lT#0K!J}C^zz$?8NgERGKFQu6c0<{a}cbUdf9b410`N-zih( zx>r%HgTtcZ7Zu{l7cauNE3F3O0H%#ofd_X!6Av7;h}9^IrgXcK;&J(^2zv^rZTjWF zpWuu!{>?e@;hs1--)%g|N`+maPhZF2WxZ;}Hm!~}!aoL@B;7$&SF5jn(mIpt54HsA zhLSY|V#fynM`jZ&I}mqSvkOR+?Tt9^{nI z&oE_)xif#D@BzhaDQ(l9&>d2-ltx@Evpr8g(z;!94m_{D>)3Eszfx94qC_NO$q=#7 z5p+Hf#LP#B;6kTo`{kW9N$|6klr7#_=z>5A+?sgY&9c5WYWVFN-#g5M1mDCLA8|GH zm*c!;f{QJght(kEHSZ|ZM>>f6Np3=RHJoQ%KfPkUG2H z3m!e%n5Q|SzWf?ICSs^B?z5lD)hPP-7&V#@h^kgQ0>Y+R4xG9l%}{}Ayk!$1*}B3% zvP_Prf9)XcopwjT{Up<0yPWV4hj@a%(8ltM2c@cUTNdCEPyAUQWz_!Evy0(LybdiL z{@DzVZPBjJise%cu$wh~ZYGvX(x01BC~gWP-_!Of_=F9xPdubX5~6@78$vr*-%5rr zsqkQ{|15(?^8~UbUa}d9Sh!wi`s`I|MIqop^Ari?*h=21UjZwe?M?P`3;qY)r@~)nw18{@V|56b9Ja@p#p1{tiycrDd=%I&lFM*yKZcg`F z9a;Rjp0s!O`xxZHN-kL8%)l@LOPS;hCl^#lp&163d@)m8lfXa9uPCKKA~AGof$osV zfp;xSb8qjK^oG{{IH&XK9H#ZBA{`L_h&Fa)1HFm}$wppY(}gRkICDcC`-!vA$M4Q$tI zj^)SOc}J=BvF(xQ?9Ir$VzB47ysUk;C>!tcaKaCxuCyW~%RL$!*R9ti3j*DloF|GR zh=~435hwE`1Y&dbU&KLhg`hyj_mNVU)EF(1bxQTJy;-3H#1YTbBmi76e)v8ETm;UF zwgeBSCzzPB+Pgi76T|-sjgm&0e)z=9ePDE-NY`ivl>9==Rutr-7VTkP*$tSvg|Sz`01DzpC#bVp z0532mV^;@LyG9>@J^&x0pIIz|01$k0mqqFXYZz_v;Kc0+6I2rw$HX-^jFlBAjN)Pz zvGB7CmcQx5;b;J^!G2In{K3rhR&%Jz`JI`q2}01t<+JYxz|^k=Cv6YGO{tHE{bfFz zX4Z!$B^V+7v7Ja(Z*gmy&2`4r4e8GjS+tjbKPHb+rYC|{RwEiU;r0rV%A)895&3#j zdr$#AgJb1?L;+nMnADurirbU2*50g=+T=?1HmKE{O}kpXL7gryW?^-4g-`X@hB*-v zN$V~9&ys9i1j7p$cmf|kjyhe)DP7)ICxyK-&Mtaq{POaiN`%u&BF-hBp&D{R^g{RU zq}LB1I6tzyE^0}m5&i;J(7FJ|11*4a?Q^2B$WDa%TePDm5fu+T+-DW#t1Q zo=Z>};PPIqZJn{u;zKUdFtK|?k5h4@fKBc!-FA(#l_6?l@R?902}%aKR>i!+Jf|Ga zvHI4KOjl(Pr&7$xxLIfkW{>;iO;@zpumFkxYIRX0Vc{vu9i3Z36(aKevFoXG(ClSY znseUzntxVUV-=)5)(eHKEFKM*K%JU;u6#O+JF7fN%5QoZ!OCB9KWp5ZvRbBLq&Q2p zUbUrB{geaFb64__?mbBoQ7!dt6{)R3#&EQUVa`H~IGro#HPj4UM#G=2f;*{BPf2o| ztZ7@)|9;fgFK1u@rVqX;}iZIzmRuLHJuMem@xAh_O zXl^9&|J&72gejD+&H;E+x(}cVLsn3G)IhC|RN$v$jsP!&G$Mtp3L6C(9qcCx^7Faw z!Ai`lU(G^Ve2I+Hok&#Fgzfy(=;tPF!n#ZACSnYSs&vcGcV8U9?l*#>&*Bw0MF~1n0XTzok!EO z)9kUheFA`3JuaiwvjMFfc3yC7W^y=U-KH4SRuZg=6TMy6M95r(0b1X1@LMOoSbQ(E;`i@e z_aY339*aTW;&q6M9~^7ROpM>1v>kL0-Nxt8Zu`hYjWpa@xX*q_$sk`{mW)LI1~HSt z;YA^`PqfbiR_BC)cyv#`TeKGfpvbpbw^_4bHWyAyF5_InxMozrICE?x@`@`BQeh8* zlX8{TFtmJI+FiXR0~4RDoULM5izPPMOrvV7#R8=q|I+7qJi6Diwog2(L=%WGG-ie- zhR>d5pEp1!tWjKKOM@qr<9ZDe5V<($>){E4nA)+^DZy({yu`f@uD6B4AAC_t0#sNF zGSKndm9<&ZcL#`Oc-Qx&+1FeGr1Fk2f9h(cYBYuMR=_@%eYhZrUqYZqyyO!nWG8m% zB8`H|dvA-2Jd-BpXayL?bnR!Z+29&_{~fyTX81%=R=_?2804GlnyEo1MRWMA%&I(g zZ^-K5BIDGWx&0Ew?Qr;`vbp85`QRImEN*l;Fy0~vc(wKKAw<6n({e)JUHN2~N_Iir znz*geLDS93^PBKmY`ofgR{416QF=W|FdJAtazcC#5t?BZA&4@`DNbZB=^2DYk=p2( zgP<4C{U>~30k%74Z?5A9nS}{#39udVe7~WN9QHB7ps3kEQ8ISY2|rJI22P#`E%`-D zwN305@n!J{z4-BJTu9%D!+kz@`VX`s2WFK1ByL9vEbci&PJdTFalN>vzcs%?GLy@= z;p@vcbu7g$+bmi~MioU{S`S*-6%ikT&V!+!2t`&>rd+piNGrpQRAe(O$S7gM_21JY z3Sh_p_33Hv6QZU1DYLZ%0>Zb*eG!Gu>vh2hw*>Or1LakLFs@wM2anWBnWdo?`~Fr$ z3ZKVZE3pJ09uy>cjo+J-n$HOT_am}>ugS=WQ>Tbkq@KCCW9b;_TTmhuV^Hp3WcPKt zPpXknH|{3zNXUZn{ejy^&bs>AFBD1s9hM-G_FUe%OjRrg5Z#ZBP4=Va>cQ^h*Lss{ zj@KAbzy4<;Vq>`0@agMVq$03aDU3YM0-DBzI*-@cENWGp;cCVC$fN^4_p|&P>Gow< zxP0+Ewwp1W1R@?9X^Sd0jT16wKjEJm8tJ?H^UPj^`B}Lon#Nz02fzP#JiO@|oGzHxam7r6J@~9Y8!$aB0qL%Ee>B$YZmpf*I~$d9&;3_U-}?0@qSEff z7ADf|ejZ}bBrend<<{ZD=lawpRcIss?`q&D7UaOMqVt*#>baU zcDt|KX=DC72`Yw5V^dn>P7j|&WA};8BAUgk){cJ@jD|#JW4NQoNnZgy6QC_)BY&qgs5MW4*R*d+Z#gkYU;RC-D0V zRGxsGJUE=7TEIi-U-to@X(*kVabegK>Wvl}M4pQ*TAo_*}-MPzi;YRjnNLtB#B&7kOO z@JjiRz9}7`BE3CVZcdo8W=xC2vJCSJ$OWXhuMK$trpJ*^8jG|d#Py5P@&IV*B{7 zf1;11Fh-E7rM$0`tKIud>k2yo3XGE(A@(Ssi7IN|KtG)CgbrmrqyA_FNtn^!o?ZCc z3s$cvTv8&fv-4fY_{)wH_O}s7-1hyMG?Z7_tbekBX%%%r%kBz%?gYx~3S}G*`y`p=im!`u6%5t7EXJ>Z$8!r@1y_Q8hj3*Nk-a`$>1i--#ET1&Rk;t5Q?QrM zEl&LZf+}g=cdJqEUL@Mo$cC4sJx+zSo_OaOqA&yU!I+rNPv~J^7;3|)0Bvh2b{X`j zkj$ON8FHBdF){D;8u0uxWeG-;=5V!$4MVZ?E{pWRW{<8wYR*VPyQqO}N<(5vzO&)t zMa<6~QnN)y56};?4MRhYEO53GByC$(>1&}VTH^~265?F1%AwnW6vGpZ2=*N1J?h`9 z0Jq|WjE`)_kJ3~>i#GTgGLFfVvR^Rr!7`y|reByQ(CF3XHb_Sj=MV0~0ff2r_HaR{ zXgnHz*!GVGmz8;w_b~d!u;c3TEO@`$Bhn}XEE;5j@}{!psxImOO!?$8ZDsfP57=bK z_%we?wV6xi%(Q+)SF+w-z!apogbOr#XC!*8YQ~vxNz!B%4W{ADx-QTTU?CGJ*mqf&1TjMoB$4bueb@qT&SBNZ2dMkgKr9pIXqLIm~%XNn}OPY2Yj|EA^nDx zC@7vyC}|9o(*TS=h+qM@{8VI*vrlH&ZJbuSE|C?QOx;uaBrz@7>(>yZRSaIwjh;!x zyd@<1+KLc=9aG*VC)Ld7}4rm-_HdHZ(c+2eE&X#c?CfAe+8fz@Pf-Z zvfpPfkxHlBW6S!SLR8v$gjO1Ch{Hg<0!96pMsxf#1I!~wk()9I%Q z*-|Ts(jJyMQF1@%hqV16dE1<&0w+TM7Q_$bJzfP!%`|cdY=H`-xR5#7rR+y{v#2(8 zMrEh`?1Z)Bwft4*!k?Yoy<|)1EqI1)tYrq|!>~L!B-m?`ekauD)b*d_D5R3pspxlU zOqS<@`uV3jXR$j}4(Or{IJwNORx$2R7#8m!Y`hv@tMswSwLI8T4!VH>cn}cGxhm2+ za>E`QONS*i0U&Gz)+S@vLOcwb+qAFLm)?nFS!wE8xwe>s?-O>T>rnqyK-Y8P_@|TB zGyXP;ZJ#N(f=Pqj#>s1;s-O0tvKV;@v03d$o<7R^eKi$uNOzESE>tDONz+A1`1Cjc z7hXTSX~AA+kPl|OFRA1)%k?@KYtB;%mbsvErCTQu%U=}_Ap66izhUX=Ux^?$RanC! z)-k9*GXoMnkwi7^L8ysD3G#Aafa?ONEUF{|aoD$YRBE20^GEitDy;@&8DiN6VM^quTMG3{F`1?TXNd z?G|T^AAal7bh2vh32sRt%{Y~B2collY9SJ~#k}~k1Sz#}7_F|cO;+vk=Q9G8)S!R$ z=Iy5{KhM{+cnW=k{E!0Y&FZ!2eu~JRuSpJVUoIiuW-V|^)NPp_6$1}jgT{;zy+E`x z>ntn`O&OI-5PiTP*|EJwBm49eHu9fvb;{E7DFZneb(vC^6va4O&fx4bvfr3roFI6H zCQsU=OwA74eMC*)JObE;pV{Cof5W1(v;_u*rx6x1szGmHtqZA~1rlBL&25Xjg2N~x zTr(CG$}wA*Rvl4dKfZQc|6gme|174?3^`Rd18h>Jdo4`qdSq>t;`l zi1n1PS>lNxp=gjHsx73(XX7c_s&qy=IjFv$O#plnv(hvI%3Q}U4`TR_kO@wxl0Jg| z`?!YF(8KP@{h#(|UI9eLa;XZlCd10u{s9taikZ91Dg++CEI(%!iqkC(Z*ybA_F4Qt z)PU&+|M_kDgj-zt*Q^has>_HSU@>XK`BDEHPS%ey&$^~aC2KFfNJ6%*iHvayv439b zrkdgbb7VT0wF3ov9m@$n>fb9n+6Fdd`Q6M2G#rzS_wm`rIe-;E|GUDyN{lgyvrPBAcx zY1eTc^FpY(ugFuDdl#Viwl#efwQ`5xaNl9bN}>Fk*i_DvkAX{sMoLKRU*w;>3|>{+ zs1EV8UVI*4eI6H+cwTt?Agz%vF2DFNUzTz-)jqi{b3!442_X~f1&kf(j#L3jh8^Eq zrnEvcDIYwI%%E?e`2x^4YJ5S+H!!~m&e){jng8i;ecI4G_YQaPUXwU>6u@=>b}gC1 z9UbX*-#=?WrB29Ol;V#!u!C_D>SkJ{;Jib5iM}fxlvk|KxdP~Dk<2|N; zffj}`$)s$pV-k&PM{iMFX<~ZS@1Yp|?&%jvf0QyN>t^?TAJtVigM85SNpx z7}Dpt!JX5BW~q%Aiz3RDh%z{jBB}U_Z(cyanrW^5WeI`WxTyKqYH^29$-&!h1};Ja z^P~^AHJR=+iMW|OID+(kP7jz(Nv${QYIOly>6*2cg)i36v#oZfcRFiI1WnS=trP{f z6a`gBw0cz!@uFFQgFO0#A2Mx~w}VxUlUb}JnnwS}>;xwz%K^7{1n$$gWrr-t$h095GtC6T7-?euTAUh6Id32z4>Q`oFm(kV%12DT}Zf!($gx`o8R z%hgrFD8ShjJ&IF_olNRA05FSQn!!lT z|Fz`+Pa713?4v0E1P3m<7m^qLGrv5tix;r z<=GDkkviQxYuv`3J5u}e;3GnrT-qS1NV@QcB^%0g%Thpi-AJub^Gv(}lDCsIX472o zoTvXcuhj{80@hJATK5(Zmxj+wB<60xF1(*Z$d`H~*q$h&=COH_tGC81Y|=sxUT+Bveqo1-;opK&GGL*Se-3`X6p5vk6-T zWd0oi_z}|~5+IhxVNwMu>d5zj;-SzP>U>WX+`xfZJRNjE z4rTav?=?VTWq12Lv-dPieI3^xE@~9?-_d2qf0uA}FS`;{c9P#^+b(Vu9Y>JA*4RBD z*$oNDERa#_Y_cSY5wokD@+M^A9nukr8|8L{aIuARAS9&tsDz}NvSHV1Z*yU1$@mN@ zmP~8_v9;H(kjxYw98v;Q{%Bw8jDz0YNMzrfTV&2(aI5t!w7p zrO$?cW!pf^onGP7Gp`E!(BoD`hYVKik?(W(lh+QPA0-}Q_>0(0@JKD#SX=6jzn?~B z+j96t65`Q^Is~z6CUcSe9yIU#0oSEgDn)T!=gWccVu;(?O1Q*hQgUchAW*P4sMzxO zip>;VRjs{Z&PPQ((sI`s(1R8O&$w-aN4D_W(pzs+-L3is7mCggBTGKpa?oWlU! zdd*C&rPBUg6(3edzOq~*+5UB9EAT?&3{3rR(aiIcKhd2lX7*>4$W1fUUk$<0$^*jg%2@S;?Ab7au z*lK+dg(w@CHr&Wn4WqObkXN9flVLU)^yZG^D{G0*Cp6^L^$dy|;0IpNgj(xEr1+U8 zDj`6PItn}!aC2~PuHhh(eyAxp9#&m%h7ZWx1ltG}r8)(~F`k=;P5!tH4@V9eZ0m@g zsYaVD&j}vjQ6;VZ+EokU^|C(=3{ z1|j`=JXL?V`Ybe6p`zkpzo_rZ7~OCml9L?buciJRL)9`CN39& zQ*O(o_+=!hv4Fa)64FHfKbAUIO2aL{W{cx29M@LSqlaU8j~uEkXeSwJM5+`59EDf> z3-v~4;0FI>(A}n5RCec`3`VPF#RY`s@4?g1J52!4h~}0W_6!&bKQmR5JRTNM{h)oD z<&W!=;R&Qe!aVXYUn=I*efOk-<44}bb=<8obcIA--0Lx_T~UwgO^FxW<^he~ z5RLO;#l!ibKvE~Wr+`KH^(G2v&?1gUm}}Kg`}~5rP8bI4S$!QKE(_{}SnXg>c{Tf? zeggppTt(MPhv?F7r55CsjGoS)3kn9H@GQnuJBSfkd)9cx-BZf4%L$E0L#4l6vb^lMfHf*{ zdfWQAE&t!*MC{gmAm!L#jCc+CeU9xM*e{I06tWLlEyZ$3bHtXZ5m>-@ETdddHDb_3Ux9=}AoM$R2Vt8@M!2NEHD z{-CboV&AdaD()4KlB5t_<}TN^lMlkaziPH0J1iZbM{3FavM3*p=T8ASyO#;Ok0y2- zs^OSA35lz?Wo|UaRsJn+3&ti^k|$yPmjp4X`$ERRRGG~jmzWv(EotW|X8DKco~~or z=h6)aA~wr{h}wpWjjvvyT%a>eILTAjW8dn}J5eI4#N!~g@Hf#WCro=m>3HyI$S;C+ z#k;n~&TLz?7z#d$vdvkHDnz8rNoSrwdRMF_M-tOE5MX+F*4_$>Qls2OMVfSH%0-SdW9KWGk?7|G8H4eagXvX%K(>!Mm%D ztL_$peyQBN-CW`w40AgkLD$5yexitP9DoD~*sM|$hXuqS0weRJgbr_;8OphFTDm$> zctKSIAUV+s#XSwVkijD(N;80SRWf%rnh)Y4+T&p1S7HTqn;wVd0Ye=|;_>YFdH~!i zsm;6c2>niPiW`&>W7XwM3LtuBo8C+JS2x@ysftDf z;%QbLK}YTpb;}xCWYVu}D0gOY&Y+}T@(njfFUOmvk51COL1-kdW9@NalKjEA$kn?; zjT@KD8JDEl!Te8$?4@646uUMv{|b^|QP6K&9{Q=~e}?x0=8Hn^$Yc_Efx5LiBVVaR z3hljF*d&nq;&2VsG<)Rexaq;J#Tp)S>hq~fHSNX4gr!Z|YbV|;btV+Ed7$m=hTYWb z7)e~?3QkAgg>*OrF{2xePcW1^#`}mf_Or9gizE(yhu&PAuJKYv%?hqcH0e=c*#{7( zK3|4~-Qv^koq=9@t;g_y{cw%Jb^&-HUuFWZOw2h`k53%C{t__H*^0y@l<>$L2+v)r zx##+e;t?=VOY#Kezjh^demuc097|s4{*{IXS6Kk>{u8IR8&h6p!Zu7Q5#T4Pmkh(} z7X_spxo-lzF}OUgr#TF!!5PB7+bv)Iz?3(7b{aqIhR-pt3qI>t#m+Fq56&Ai*ret| zvj1Xqk0AITA+Xb|n0Z@?s8I2q=w8||)C&C{XxAmF7^<3TIu!(AIdOn=5n9zRvsm<( znBy_FaaoD*eKH z&_LMNu<1j|u!nHJzuzedozkL>FLAhch(drMI}x({`v+LC@2~uEpUr}+Hxq>ufsVaB zHl)IMW-*xU#(Gj?$f*K6+-nBKx#Q1V>E{DeKrAOnb`Rs9SU>&Z^?!xU+oSvV=gvu# z$ai0e1e~zxeVF{B{Nle)xsU$%?p7y3e}2JP^_D3Uno#A3NtxX4)Arw2&j$#Ia%=%T zKENBZYN^|%oeZa_j(bCubkK)>V~7NDY8K_iH^Sbt3?mzs+Z$`g6|$e0H87a&jKz^L z(FVaSoA85PP8O=2@OfNcx2gAnhmqf1ELAGVZ6^8m3kk`N#o4mL{cUd|+-o-`m{5 ze04lwSP9M;@~j*lvCnt5mW=nq&_@-*0BYoBVz@YJE>P2GWHDtPD|ENp%Flf`J^CMO z3&<;rJ zqS-J|*rL0;0pyiR2J}g%ZvBMxE(fPH1`VG4yt}%wWP5S!kgHvS!%eE$RCDKvNZWyi zF6@w>7?8nEQE6RX(>`nHM-)g9ujbx8RDjspqhdJo^-6@dr7xoE=g7sX{a7B^gEx9tyL8vmA+5W7@pmzDmnT z%(!y~i)#+hy_+J7Ak@4yUV8Y`Mglk_cKBp@U3hD230L0o<7>-*)ncbpcWAxSGM1PK zvKa}>i{Dx4bCf9MXVfgtH1UiFZ`u6QxwH2@QE=tdnxNB$&gTO1TI7%co1d+}Gb*Zr z$PGrOYMLn)_;n9kKTd=Hv?MzAA$VzOjk5onpM@XDXhjf9*H_ zQ72W&e1sPM!8Z(V^`cK2_&FMdd;ZI>V?jc%Dhaucxr`*}gCHoL4lPW(8v4uW9)ANi zAikwXS%3WO_`jc=I$}TVnG6P5ui%(qM93fN=i*r;|M?1}?AGk0NOt@)(zUFUBTC*9 zaqzAT$_He>ziCVOg^gG`2!`gIDSer|N0y?<7i_-dfq+~!+HxLt%YUP1xE$GyQxPZD z*zXy7!>j^|nr6_gC=|xE1BUETRS;>GA(}3>F13@lWI>wkl=eOs$6+1KFFphdq)}b> zUa5E%Oom_e%kUlGa^Ihe7KA}$D;cV?6s2mG%!S^~qxvly;!@?w=yz!4Y5wnfXdA)E z2@e%QX8G@F3?_N0r~88LvXb#z`l7 zJ=`C!UYRL&rvhkErh16U=IXbFqhH-~kK}g14dBxR{*E#cJpz3ZRlS|y<32*|(OE~J zvm5VlcV9CI0UntGcO~hZpmv-5@g}sr`IK4L;Qt*>P9G$VaX<>JyNhx!kkD(D;7<9- zP`Ek#J3Dg_o3S3V%p}Nkny&>4qp=g6mz9S}C`}eA=lxlsKlqG#6WK@JYCF_2MDru? z!eHnI3jAyw`cnwB`tj|;a!k7CdMmWlo4c+?BU>j3L#;&)m9Z1~5hr%`+FHd%D>_(4 z7Hq+PHp^zma(cLbY#)z4*Xi#85#r}qO3Y`4XKC7@$TFSlD(FV7;JC^h78VSS!N%XI z&P_$seUr?^Tvd7o3jYYXdt~fUN<$DD0jxP2Tpvy@+*zjTWSrY#9*`$^;?y`;oPx-j zq#IS?Z&v0LdGb@#j>zM*-_hjuck&bU)k31H=4ny&-^fWA%TF5FTu+KUKgbfPfOyf) zy-d_4(!6h`6^I~=NtO6N-{8U}CBd8-4`2X^&jp-By$)>QyEpgyZ7M5(n;=Vx^g|E! zCvR4PQeB7LSU34Sp@9o(y72e>z{*YvX0v6gA0>^YsGep1XiIwGaqzozT1RrG7_}30 z3exNc_n+g-F=LtL|CUxshtym*DD$Iacz%@SvLd32-1upj%rjzF3OTnh;2m{pY}+na zr2Y_dv$G$Ivn|%zQjXhszn5VDo|W23v_G_`9@C)z9=aO6Z$p~Rhj7iG#KA76;W((;^- zzFBRmNs#}rEu(? zoDC7XZgu}n(Pk$ZMAZWNr=-7@1CzMLysqA2a_Y(VUE5lBC&XuvNw@8>d=kzuo!gCR zshG11N#EMb0)v(r=4Q|oW6|~c0zSsEoHW!ID2LxAVU`cORoGm%C-wy+*snkVc6j2} zvbvSByu5AQ0Rb@D6X#_*D%$V3Rh_{z_=VFN9Ml;4KSn`jGldKe+(7;HDKknl{Gqo; zt0}B?J~JrqP+G>r?cZF(W(kr(D(TgSl0M&o%w`8_gwCFp#d5FhH+2kKIii?cwF`9% z-RS4I0t49Wejo~idPn`-$Yqe_6-ND7&Jydc)8WU}{QihehcW7kPH;!=8OhFxB!TxI zTSH>Mex>tH6*F#saK2YLXr@&ZdQDc7-1|*#K!?o|iN04!%~(Z~(4eJZT^V_Iv%hSM z0&{LaOIiM;p$(rakm}oi9-2_W4hyNhINkYyrDD`k@4@*?n5f*-48ku9uLbxh`1|Xd zZHJUT+|wM7gH=3#Tt~%gv^$Ue?`*swlyVm@vgTg8KCcnPbp3`WoRP=X6d%@VG9|=c zAfF0$yz$$>`?r57`+411<$vt@uH9PmLjvV_XLS}FRAt&(`8zAn1^mBjv^Yk=u{J!j z{vPu&+s-V7QjSxU7;Ole+XkDN1c!MgUGHl#Lkyb(sT-CLGgbWL&g`TXCU4hOg_}Jz%E!*yZxtB)p_$anyf| zcc8ONI#$0kc7D>f^g*3#Nye|BoMNKlal#+Rb5I=KY<7-_20;=1o_@Yro67kiM5jby zhv*k4S+Q`fmjTcb8%8vyI$fY0bW|CWze-6;*IN7sYZHk?#LXRYo+?!4XOzzmD5cAZ)}a)ABtF?(JSwpzfmJE z-EOl*yg%RbwUo0HPA`r9e&;WcqoU)-J{r0Hjoon|#5hlNuMvv} zeqmatijK!|5LD1~7Hvi|o*g#4xJBvG`2Q?%nyFx)3+|JizU9lX#B^x5Tz$tPK;<5f z4=+Q~=M%^Z{(&G4<~7`W)bZ=3<EdE47!|NTZwq;-G>Hev=mK)*4# zau}b0>?`u$(RAd1THS3mX7$FAZ!=KI&E#KK?L3-SttN~!YOu;EER=nxBS5{)q_3Wm z6V&Y;NSeKwN&8R4f|HpMLXPp)`2zm(XHr3I@LSUri4fg=I)gF>6=(bf~IJ51;M)OVp3Mj7PP+f~k3jXmq*g%%gG%YF^&g#G2$v9RlWGIQxT1XUv9 zCbm=1MxlVa$|97_JFkl;QCi$&<8YWLSc3^-FGb7`>6~Z_t%atDW4xSN&~37%MWUNZ zD|LI)Z<~|3n+eH~`e&UbYp-opiK|vyfbeLnub>rlePJlWAJH!DgKYl*jk*?_e>FH$ z6~>f%Fazy3$0RITdoN#NR5ETB9^q%JA60|oRB|+Lj8Exbz4XV2>Uk8bM6G3Z*zD{fW6ujsL%C8ToNp|O?5aCG8mOB&*2p$(x zT!kl2yhEc#b%TykhXmL+IoId0iSq${$+-)ZGJl-`sML#6!17A}D6HMw{t&8K+=c#c z8emZtZ#5@&>w|-;$%*R)d~G-S&vP{Z%ALG4BjUzt&wT3;*>~8`#hb5}VTN(l>J3m} zMjr zqQ-V;ca^Fd29Q*_tnUdI0tAlTv>Y(op+%G>{X@b$LZj;){eG{CgQ*WsQ^rmK=!dZD z7-ATqcR}p`#fDn2nrriHBp8z4zL4@&U0V9wd{|m8wph-c#@C#~0KBsCaIvE2`C~Ab z*zy=FPSfuVa`;Fwvp6l6LEOZzI4D`>dQzwqhlUQ%IBq7$xhw zIS&iWxhyRULSC7*J@aLNrrk)KtAN>cn)kwkzGuF^qn!6eT;l%5qP!o^vn&E{2^K51!8;(kVd3@`Sm=T~k=ho}RsfwPr;B z0hFzj#mImn%@lOhFY%~SLL5i~=V?rh3$f3Y4Ks0xgc|2|Ed zdtcTdXiG&l1dP+>{frXm#vzN9E~>kxgsdS*8^_fDSmz3qYJak z_;s%x)owgWt1H~siq&0EhJXlfR#&rBxDD5TjD@^nm3R0e1cTU-{0Vh-oODu$NB$hf zf7qK>xQg}wwK90&enikfO6pklgY0hWzYTdH7U|!sEe{Fw`Gx{|);R-*QPMn>OD!QD zkCv9XwEBWh3uGX#dt3d1hRxEZzD1h%*mp)G)j459Q8Xv31*jU&{$DeH&TMY*RJBzK zfCOF7bM5nxPv=*2!Z{e8QZjG~aLshdJTGyimA64<^9WdQdER`qNQ~0Zb_1-Lg;E#j zBoY_WA9w+ON{Mb+J>K21WWI_7llCkzrN}y3gh0^@Wg9h?B1r<Ec4lBH}Z*LjTZ} z!w200VPO}U#H9)?m2KVTZ!Tc2T3hU3pmdfU#2xQ4F4CTruF!&KyK@HsDltz zncG4Vtdyn2jd91FbQ8Mk`>|`M;)>jlcL}=l@cZpm{Y0|penAND7frdfd5<9hG9vL9 zu~=ylGMemR%{tfAPqvZ$Jh!uOeXZtfFw(wXp9`4g(~h0md;9{n+Hn?!nI`kUDj#7r zQ(QOT=_K(m`1LQ^%$u(dsKk&pY7-_z$ivfnqfPrg)6c*9!}t+MO7$SdXI5sXoHXZB`5fy%4tX8X zil-O@vsu|+0n8bbttrmal1c*ZO<5ieMGQ)u2117-sdH(@du!rG;8{&TZYl;n!`ZvX zc&_5|w2~xcP|{ix_dg3fG=Yd<)P7!6{q? zQC>0Jbbo5E-UwAN`#=NZCR|YPY-mlXt0y)w>IUB#4^4*mf??||eJ@bwBnfC6J0luV zg|b#6At6Hi!i*?7kRp28RL@Ppn``8ao6OIIYiPWQX|fCa(u{#fgv7M@8jcjGuX@ooy$?Hjh+rK{!}opisQ8- zwD1A^k)E^EN;D=pdTLx2+IX(AzB2kr;9vH+d>U*I{v9@FKZbZ^oLpw$x>{CU>0UY0 zdrC3)8z&Vj`a=5y4IWXbigbbBbiJ4h7H)cDQs8T(cVWyb)jp__L!E%4om2?&X`YWn zC6JGcmuybmNVKGT%1H^}jQKPp$jc?=OD*nmUcxw-PiHtxd6kc>EP~Q({EtSI))u(e zYA+kO6pG-3#7Z&v$org}*-rS_o%OQ0dQ&3`-^90un>MI{KY$L^P>j^GClc4!TD!+! zB_w|i?&7dA_BQEb35o}*Ml#?9^_RLYq!TAQ0Q(rl*a^3urT4zJ6TJrZ6cuUf#SS|Z zD6iVC=az6#eX?v|(XnVwkqNRo@c=nM#=lig;)avzk&IenWgJg{>dBJYGZPwIj25Iy&B^bAN zQ;U8Ut!3x@i|2DgDiz;l+SqURp`9r`JQY8DsUD>jOR=y|&sUPC#1(0@MEJAi?>u_8 zLF8PDZ@3eHrs7-yEd*`6j1mZMW7pkn6U@?HW2`BY{onB0Kold``f9M|$*4i}6hCsS z4ez1dkU+8*Q)mHAz5B6wktO*G)H%eq4eB+J1*mloKq?|<`b-PX)Hu1k#ZEuY`jOYVQtnYrQTBO_x zaAWt{Coo)8#oC#l^>+K-6-f^S&quIo=V+@lmq|@Ct|y$J13Md!goiqp=*(RLaom z+heQRCcnVDv2Tc!L|t)^fE?HX3UJ~jEF8#c17OfnXYHu3qoW_&t|z6^If0Q&#-leN zxCEWyk8T z)t>iw0HhEi8(ZhIn0+^ymiB3I<{U`^qLk{fhmsgtnN zg2GYlFhkYWtd0XmrLXFvllzpiU3%{AOS>4$qTB=tl*FBnm%CpueyNr?k3vq@T9;r; z4hxQWJzb^{i(f98G4+qK_#G<~k=k}&)LrBfwl79?%zFuji!Wj2uw=zrX{2}tuIv;m zE5}+!ELJ2_xB#>n7uIya-Ehw>?t{EV&oUMAox0Ac6*mN%8IB*AOq+_%4i$fVo((RZ z4cGzhEF-(u0e$FwyZwq1GG-B73x%PIUp3RWf%}b-oTBNTbzf*#1heaYK9S&iwmLMikr$pywz9R_4dGiY9br380XYd zrjcN_>c{*)SW{%;tGrbl+Q2%_C6BWfRr1Q61qBSGWOOQs02BPWTxH$qRvSMOQ0SvI zmDGt_6QfkzEoX!qQAMj{J7Xg-B?xG>T$f@u9fnwixx^~r<}ASfc4zav(9k(a9s0_n zbxVY+{1>X?5fg3#@(YmYDX~0iSFGc$BHwQeR|?k*Dh$ZlcJbpyY%u>CK}EO z#`u756o2r0ae*-LwHFw|^X44_z?c*xitlv;97iTuBLza5rD{MnK!DB-OH5&Tmg2BL zKCZXbon1lTDPCD8&Rb3{5nxwxrx$pu-c3u=EMY$oKeymOL@rI_eeOE zs2r1ge6%P4jqh@%bjTPNMRr>sYi`}UrmmETJ z&{rheT()W@h}_GL7h>xJk$&N4!KR#w-F`3$xO&FlcPQngPRetOdPm_Qn7aiC!v3b62n-$=B-!oBN07liU@Pp1hGQJajbfeGI> zI3(#Hfz=a2Qn~0>-ynad!p;UD5q;ra>b~R5(fEC#{b3|+g-CYM>9K!jI-J;*>f|C6 zf8)XfMu-bM=W|4>B;2F75jo3n$Ki5J^NGBjHpYX2alZx>UK0Z~Zl4UEu%61b=K?>= z^i}|Mg>LSS4u8PqrQN(Z4FtpO{u19{w)jX>8+Rs!y%Gex4X_E7557WhoZfSmTvBL< zj)PkE;RM-)YAPnxAv?;m)uM#&v70%eBe{~=w`c}ch-M;zUGr1#t@{un-f$KaNeoT@ zXydPGQMP%PXn`1Z&E5~TU_vuf_K~l1>uKY#-*$nMFozI6+`rer0T$r zF@unnV7%uvUo+Nt4~R&*gC@Ia08wuGEJDv>RiP!KckVOc@cnd4EiRhPjE2&t3~!RT zZo2YB*&Rx(Rhq(Voh?HN>i^m=8*bK!mKT*+E|JwwN8c@m?`=phUox1k|>`)ewW zCnw6;Vmn1b@>1M*1^PAKK*XlzUNx!y9)Wx2aP1FWrbu4<*zL$RS;@^^r-g62F$b&~tNXti1 zRmPJkq!!|@o~21c(OE6R&JPni|5XKJm31EDl)~uN%N_=`^-ou3y#F|64F`F3H;azF zNh(NUrqxB8#+(?DhzECAnjb#bNqO(0&QZ1tX(gghz*Dp{;bq7aRN)cBM<`a2FqV5a zHg8S^=4==9y)pj>-#uTZXM}^#af(?h>IC05i)6aiW>g%RnaE{Ug} zB(qH1*Y4|t6Gx^CQ{aj3^mC6nUEc#eB^!Z! zIg@yXfp|JGULE=3QbYFw62aI&>I|oWte(d{*O-!?DyLC^5{g)Hsvm)Sb(k6`C{zU| zMISp!F$QZLTRFy}u7~S3WLgh~Y12Bq`aq`aU=OtXE2F5DJrO+|#~(RD%5$c*oFSJO zpIbcGAKDl2hmbK5GDGJ~+68mBAe};z6&+}dIJAkcpEKbLNPLO))ifA9ctv8`=Q_3e zRyrGs2_OIn=DT&$*?P%i=5C8o4nql?ilQ}eK;5o_dK%MyEuKGRd_6fSr6``ayfI2` zm!C1mk>r=bOt+tPHdzYSZ!z0os@39Pr4{Fh`H0+ey1JW&Ya?BH%b5t=$_Hx}_At(p z$U`U&6j|o_j<;9dI^5yn8Z+9k5hsYJ^Yv;cbZJw;%loWDf$*MC z>uhlT8dp(X7}d9r>m435_(&fWK9omJ$(*jRg0~zFUyF>b8%CH^H#OW)^&F&bbtmbB z|1)4!NL%Q_+Cg}7R*lN7HctRHtfg=(GJp;6k=g9D^_OQbNjrVP;$ZDNsI%d5`H8gns%ASYM{WffxOn9dVn-9E?K=$J5?_HS@GgjC z<2gmOX|e_@7s*~Bp&!dBE3Wu4uDShb(XV+vWyMW;^xngTkpKA36K>}Pr3M;)B_T3Pv z!ZEEq>F|pXO4fgOM?b1J z7mB3Ip{1qyd0VNbMN4y|9IDlB{~<{WE)SIcIYlcU)T{l13BdbOFSf1X@bqW%+T(u$feQqXptUMsIE6;s7HGo_>fuE@&=ZNPw^A~|b(-5^Ul{&mOvtJ3_XaV=8w{B9u3DMNXdcPT*bHOwCH6vt?^eS|f%z^b1} z?L0OnYFN}e0PYo%rF03eW``_dmy9RL!PpF9SiWO$Rnzx^6N2CVGVn8ti6HqR!Lr{e zNm$IlrCWL04J3>Y_PlTQBMCY()Rao#0C;&c#^DhHi1w>d66zYit-=-6Oo~8N0X2wt zlt_zp>CcxrZRU>+tlyP}Xp?bo-FyS=)#XVdqkV-4lV~27>bHl2SpfHh)`v2+af?@@ zs(e4L%6i)>arZ)+>ZU4_^TOKkV~&59sI4aKDtf7WsDo4B+4a zyaK&$r7{<71ehCHrpBx(6A4W5 zqHKd$a}Pt4(Emy+mCnTh;g(rpQ=(H#C^$p>4(V3}{QVMAw4tdOaV{_T2rr~b9Y$ue zin#{)N_|^{BQ8Z~R2*=rfh-SQ%s(?UZcDWy)W9&QMO}-+c2mU{Yf~IFy2vlBK8({ZQ!~rx@7IJ!7#`@ zBtPjni80rXRBj>mRpIw0wb1tA!W1 zsM_dlApq0@!G8-!#j6>%0Avw?^sHc(?HSJ0Vo&R=CvX|!-KLwuQHV6r5}5&h=<%Dv z?cP+IMZ8+5&Lfr+s2Bx3l2G`ICeRhlU|-Tdyh{OjMLnMg#qA)G`CH2#4 z3$v)Zw>i%Sdi18bdpu=}@2>Nq$~>?}lRDRNJ*Hc)|#_HHS@{Y3?0t2=VO z^kjwei~p(E-{yM3I^h6hjbmWrb-so9sy&XDZ>0>>c!8(&Un9xyRWb|UZZEu8kag?atX zoity+5Ow0x4j%bxDh62>F7+lg?$Y$ON}~*kH2JL0Ztl^UxO>|+FD>!>hu7(2dFbCe zW>8VUS$|vPQK=BZh60b33ny5fBB8`;AhED6 zetoX*b}O=vXT+%0K+xH*vn`lieI&mjGN)p5k##`g=_T{f9K_bsPbg4yQim7FQ1+(b zO}Q-i6&V2GY@=Z8v1b2S@+KpXJ+wWo6P-ru;gv4Tr;Uhzb?!~&4&IC;z|#4-FTM1aAbt{ z+HfpGpn}s$<>>7@Q~Y!IF*f4IshN?70p|y{!`Iy@XL#qSo8sng~1-`!&L!(q_3 zms{}Kl-;p&AD5!|IJnyGY8pSOKYPWk@fFm67N`DD0T)28wDzC`?a3Fwmc0ubqi3d` zi?4y_Rxf19l`(3SY`_nw=iErT_M}3>K*Xrm1%#qhY|K~4zhjVsmqyP>M1g(V<{5@y>f$D#9qZK4(TwS#qC#%z;aZz%h)iddU?HeTK|PeD6>KdnmB2=8 z*Fa_2UYTzRnB2wrDWPsCjJy>*;1)Fs^V)*^C#qgCQ7KNKA5?5r$^r;5dE9ML_YNBC zr-^Ens~*}#!al}=6U#j+s?M9mM^9n5Mj%;{v0f+b;0iB1_A(-=%0oC4PW=X<_E|bY z{1u!kzLkOVTr%p)(YXEtCSZFv!9k&0ec#=WhqTGvn0hX?t34#Z{0>4HNZ;ZmDM>}? z8rL;`*Har~)mVGf&~Zk7w|>bK469AhRX7*<#@QT64Zc~@7to>e7*HK-s6SkXwT zIbb%&6|_L~4tqaQLNDqxfA)s!Tj?sEq!m77Va`YsOtTd}A>nd?1L@kOfT_fJWx9ju zU~nozOsl9&!X=}CV>i0RuDuH^67%2^5>`_Q`<7jXC%4An;*1msKvCih&gSs$z$1Ez zCTvfIcfg@@_pw}Pa?puX5!c*6q?jX$)G2%Te(5ba*XA@`>m2QF3ptD_gk-jpZ5~?o8OzGv8}zF$4iRT`kP7dk zP&VC?v1=KsE{#XRax@5v>^e4Nm_k5%u$_@JaX;NBAVHxPLcw;*tG?2bLg?2;*T9Y4JeN@!6^?72vD+~|6O_5s$pGPsryiTtI`9PQ_1y5M*d ztB3?rf;fxk&B@$K$rqGX!z93vJ{<)sUn2a@5QrSJ;B;ife$NC%*p5 zjMfdS{t|tXZ)qa_0&GUbovDR*Hbja!(bj@a2Hk&vjG`*|#tdwPFZb>LnP-H% z^(1iNE5Ox!sjnjw593}M5xgW;{G86&ZoeKJQIShDjR{JAc_MQ3+XkvkAEnA~TDa5b znKl8p-NPRLQ4alevqV5ucq$n4r%cRjQfwM>QI|cjCW6F>7o2^W_IMurHTOUB1Wy*? zgcW}1WcP>LS)i>&I?UrF05ju-=ztreum8Bl!;n2L1}EEA-$5 zgpfP%JU+@26O{Yo1?_n(lA??mmyXSUVq>rHOQo@ool6eJ(l9Jdq3J^jHPMMn`?Fx1 zD)G$yI{j)GwY%KHLx2~KU&8rYG;szwfjd&iHnbGA|qB?QB#ZfwjrDDK=b5T`^m^ z{9uMiaq21%X_&Ql3-nyhrs(wR6ZO;7L~wge2ZFTVScr?dtx&OLh~6KZ>MyXF?3f3} zM#hO)oYh)02jc;g7uf2iMTG}bbF+%BeO&||cz|9alkYy-A5g}3?PMx>Ts2+!0j8ee9X|q_^LGk2c``{{}$HEi^<<8Cl%$(p zm*jib0|JMW*S7eXUjJO*Y-`cA+hBf)qx-;$zG`t1PR!cpi>V=Mi4#o0hX$9@i=2=b zKUoqz#Wj+7vegyo0LB1pEmB%}r+FRfaX>dDk@l^?@B6Vt`9H(`{f=t0=ccbIYqA7Z z43?#&Zhrv%zpfOmWUyQ~8a6S}2|MG$1RirUUinB3E@S+t(4}JP>*!MZFwbFL?uA=A zyblJBB^g~~9Rv3GehH@EKwDM}QRsDlodGr*2>bX}r7qWWf4AP# zWM!q3hY2`{rsBGkbCBj*)G1cERMG#gY5XeBmAsNjSZW8POt2Cel%KtSTW2gLn` zva5+kNO0+hM1C8gD4muq{I_TKYl<{^uQUNy2&+*4c>^YhD~-hWoO9W{I2v zWuC+8=ahOlZqF!XU#PSk9^H|6wmP{#{E%c^g4|?!D%K^EEFOl;v~|!^=!d~rlV6Jy zqvq;pRw-rIso6E`BQBaU{O=oKMNM)70?4slKzRFUYd^pwyWev{Orb3AhHp$x6ce8? zSnSxFrFf|H^bSkVi!loj^MjbckzBvo0{_Gxg*;>Y!)hCNWVTlcATI>aUi$6}V9~{S zP#($8UeWg9im@J&*po#!WS;LtDL~^$2LJak3&%08&EpHQ1T1t1O~2^<{WyRq70G>6 z&e!F0p$8=+HlQ*UXYh=8IvRz|Pocjo7nbc1)LH~067$nFPV<|!GAr^aQ{Kx?3Ki^j zwQ|r92rK0>XE#Hv3^Bk2!73!(Hn{L6ML20EMZmPenA3P2n?h?uT32cGr#XiIJG>z4 zT~fz^bDCuDh->eQ#73P3qEh!YJ|uJT^!6Z-at=MdgH8uIeM22F<$^7sHdYnSsr!nYZ=}IS~uTX*}(yxpW`R}&%jA1~Rw$E8ZHnY`!YNVbH4oY!T!~XJo zxU69Pfyk|%hvkC@{2c$$L!RNOit}t*tK=bqZYUS&Hc*WJL;3@d&l9eGch?Wgdr56u zCMEvuFGYMw7AE-N^h26Y`LWY^%D=kv9~vCC$SFT;ExCnAY!ouIlgEb3uoXSLi#0oa zl$NKunYvcic~Oe^fzeJ(u;%zmf)PWir#J)ImU;uwJ}`6LNA{ z;hFa8t(tktJwQBhb(WQz1*^wyQ+e&ghm~{;s1!qyYIbJD`v55QAG{8sjh@UrzkrTU zVo1Wz{kkbZZu`GfC9AlcgNl2^>3%z448*3UBUskY>^RVecTM;G6BG_iycsj)Xh*Pt z=wS*&03!WZfioIq()dF(S=z@ibJ{UGEgqs23tY!Rst-v-_a|L2OF=u+za8f(9@pq? zq;1A0(wLfsl4W^R!pfM?qXno4&wk5nYwWXJ9@Ma#zmTYiB+-M2Wga$5=sf^4S%g62 z%v=GU=J3!i2*|BMPK#%IlIAK14wYZWsw}CCr83#1ca&8 zaeqz{2hIh-VXo$dD6nZy?c3OU2+)lXK5fb(0mQ5Wan^$V_Km3payJJtUQe_dVpCI9 z*02PlBMho+v-k$>x6U2cI7%PSB6ja34>%Pw`%wxi*`I@t_9CBg2w{E$Mf|zan|n`h zJ00rL-kJI8Xm64&aG(+p@bMz)lG0h{qnT96VLepG7Ee`&@q4n3gHJB3lR(ef)!Ep z2!Xg9EVw3ha;h1aj`}@mJ zLDTX&IER2Ad|4PVi0Rbog0K6GRt7f|JP*+_(eS0H`^unb*Jm6lBG-L_C*&6ciU_Jv zb@uK|ePF$O;RK~_AY;AFgE&!EZ`oL=IfdMKkJ^Yvnps+RBJ4jgDErGrXL^G?_V3Q? zF#f?&_c*d5HmlL*g8yGhWWXVLmwU%IECx&CM57Qjx^b)V7R@$1O&5DggBC2{lEj<1 zmQ}EVCYXVMEX1}<<6hE5;Wlv4mgkY(C7xAlweVJunZbvqJb1Vd;6xzfSWlC7#qh3; zkRiI*xoa&>K=5Psh$o(cLi*8l2;L6SItNGG>xqi0M~mnuOLI-^z)zq-7M?U(*92H) za#Lf8f_?FPR?lAjVDkRg-eP|}d7S2nn|Z%rZmododT|A)B!gE$OUs(rY6&!kHhIqoyioVcc4Iv7}mHYRG==@B|pQ zha&;cxiV9RbuFbbEILLY9R`rcsY22&%&8p2=Vr=u`ICBidcYU z*e2J#NcJmZuesIW9V7GH3eU6P#tE(Tjr)I8==o61?s2bpKvlMy@p zcenzR)d)=xm;(G0B*Ybasv7{8o>$isBdn#ouba4)$Z4;o6(Rj+ad$;eey^7ujiyS8 z2#tXVW!T>iSK>vHy%P$GhfOwZi;qF7qV$}+q=Meb!pa0#@q!e^U3d%cLI+pW&v8T< z(OVpO1!u=k?RsZV+}zJb$?xKW=vmKgb0PG(rrGy z1+XS($w2G&y>_rt&ka=}@9CVl9h_U7?J1HKauc!T2uYD2e2VaJZJTdM#4!d62X4(O zzdcInqmktigmp#Z50_+ngWmC6utOE1n;T{E;i8a(+~$JSqrG=9Ofr^*xk?&kBZHCJG%2LdZ>8hSH%)R0 zSL;YWlHK_yZJvw}9R)B}GYfH3`qO!}3{QXpU~wm}@CNErt}e5rd7HJ zrsPuE74L!=4;o`o7v0j{((@d9!gj9b^E@3eX|DMMFh8@F{upyzkIf+2(h3b!M=vWG9PC3BeETP7Xr63$75VTvj_d3YhZ^dUW}Yl zg$`nf^b7`}c7JacVu-=iZKp$;HU#*yy7#G1u%^a7bc$JmIx4&I%VfCH*F7|%^=P}- z+lAlOHPdONV>dz^#3CMowiS5&Hf#;_!?Z3R=-tg3w-_d@b0qhJNuFv3z67!3@~_MKqX(gTSRiI0#(l8-a~7}{^d&dlDo1dPzkIZ`S0Ve|-K zByLEY*s7H&4`tIRyHWrgSXyT$(Lq+i*bEq@pfWAl^fzI|##S{dletqui(Z6eF3o6ZBsh-@y$<8s0y7~v-$)>SEk0LBGDIbECO233j|h-{%1 z@=@E``Cvr3^844w2$UcNhHcfyOrP~^J4ba3{b5?Ng}cLrDkASo!S*QEOHa}&?=+wH z8ih|X5tZb`BGG8_Zg}KUV`_vGFkt$!ZuxmgftW z+$fr$bKsPsRk1z_2KH2UU$kwjoE2w-@R`*#MD{yuC+a99VJ@di%}3bwCPK-KlL8g2 zi=QiLYn(q;0~|Fh{fYk$)v~`}Pew=^CEqbQ-tDC*>R%xw1G1-oizc|xs?*54eW*by zO#VUo7*Uswe%ww1TRf?t7tBG}|GL8FN?%g7Ev8Tbq*uvziT?c$7Ni70Z~XD+CJ;a? z!{c`et%jU>$C>k1jB{FQhgh-BBTWxGsr7Z?gH{3AY`~j$)c}c4Gm1op;M`Q4@U8mf!wg?(lo~e@>S+bYu|^ zVkqr(d_ILJ0~1$u)4zM>ycpJDl#5Ur*`F~5>vx3pQ>vO#rVKG<)v=p<2_?{wX(6+d z0+SwPRx4`x11aGo>Mg@Moj=3~PqZ}z{5f#ENgz#&@$!5{#i7O5Cj^TF_I2E4N)BwK z8VgO{IVaVJuV2~~%J>?1x#jMnbjPkE(BWBmtfN3=ZNo<9IND9j!k_!j7Z~w@z}TQZ zqENab-SW=4JUwCj?_pa-bY%yvd~%@E0fAziCIC_^dz_1Jz zWDQQ{@oFE``-jNiP0VaDAql}bZp0a%3jl{YN^+JS2A9`|MKWXSQu>ddr+N8}D=hpr zq!~U&g5_a>`XJ*HS#7FnA3d53towudb5g=w8&x?bIC|54a&yWES9aflYM+XJ960@U zdBty(Hx|7*B+mR-(fDYV54_(Y7LWpB7K>I@R9YL7V8)IfS4Ha<%eUmLxUk5~5IGHm z{PHW6y_uMQe5$1wOky`y^O`myLJ#oxhnXr}37ft~;3P_ec16H-r~yB8xvkgxgz3$% z(i}Iawt$}ZTHs%bSHN@He#=a0;;F~`&W%~CA5J=Sx4po6Tv$cTzO%gU%*Mgzh&sj9^UeTF_<9j_3%4iP9U<~)c;?NXxm z!LQTz*_F;lRLZ(=8mZ9{e^N4A6#Vll5u}j&k!~MtLHz@g`(=zlvYoC0lt}Xi1S~Q7 zO*`-;lYvdDx@cMZTwSAtY9%4fhkY@p)tU3BRDad$9m8bzB&+GX>=r>a3&Gc>hVCd&b|o{qG)$ z1k5$qEnio29=}QBAIh%5dU?V@BPh*((1l7}uty8A-SO;l#G-?7*`~Hbn7y-?NRt_iw1_Za zT5n1dl21t^; z37NOlzVvRG-Y&6NAS}Gz2p+RD8@(#uH)aJrU~2>7*6Iu;L2|j{L$f$!)f+UU>1G5P ziZ0Bnoa(Dv@*rZB%PA~3*UNi%;XXHpXHD?qUlGOz?$S0JPmb*LDlOw86&KTbgLM2m&Inyd*gA)ppNgEj`VIxY;<@?8+W+w!C*!rD4WFSm*SA zpTPVfO$MNVsU`hPCmN2hnA3P-Jsf%n!-7G;08Nmd6pWIItux7?(ecZam)}< z$6r^5Bj+c?T-Y{?*yr}WRODau{RT>A(a9s9x&HQWOlidf@?nYB z-n8{)-B}TsQsxN1uShR=5XKH1YsDI)pElWICU#B^&&&-qg`o8Ux79GlaNtKxtGlL` zX*(C>xc&P&hcb>&Gnmw5;Id-XGme?=l%JN(hQN+uu_sYXshWJb^g=+V6)Cnlry)LJ zZo{{vI*871D4#E0C^sJgjn6><1m`jiIxPNr0S&cVV-q?C!81xHt!+8SVez&gK46O+MT zK^)-frCKyI>~K;P=&S?Lxs7{CkAiJ)5_V@hs>5+Fo+G4X#HNXbgumMvNIKCf!E=7 zeehkpJNr(fsYH#yAL!)w++7z8WxeSmiO=|KRN z!9uLNl%;~t06b<~C^kLnR-<@-oA{i}n<$V83f7zsKAJvaw9I6H~t$ zDF9MpgFC492T9!>Phy^1TncgYuJr{5Z!FQIu+3SB{VNjnR?DY!+S?^vE}JmFKpTYC@@>EY%IG-v8lox?PV5lPf?FV^%*l& zYXshx8vD{jZ!o|q)AI10HrAuw>SCtIo5sPx>xvTaG%W{C`D=7pUemk;r6Rd4353(S z6bzFT2imw5KAT4_SkraWV_>3M|b6XmE{fL8S9zLGNh zi8K6==~vo$4NqZJO*-DF5ag|6U>zasJajK+#4`pvHG#~2kwndR4Mqd9J^JzJslxlsM z_^rpxR?a#;3&Rn#6~f{;;X=a`K#yfG{0EBWMWPv;oxPH?} z@{UKqUwU%@v!f$x(sN&jOoIF)-nq+b`S%k$)rf_V)50M+DV7{blMMO zS=QKGgzv*Lnj3)d=oG|)D$a&QF{^Rs09z& zlzWtv4+AT<_WB2l$jgFOkHks-O`Gr0V1PO1?Pl?wu;T=`eZnq?Lui83)lugY=k-}O zkJD|OXrEnHk%0;&Td~5VSj04aQB_;^G>Bx2n0(0tkBfvk0M|SHC5_gEJ zbm6holA#`UKFQ$42HrkVJpOj1yirg{_A$GQr4gh9QHIMlGkXQx>zKT~wIXx0&va+>Ow-IPNyqGpt`O*NBPB?Jazl8(6;i)fNSH}zDER_2aw>AAdXxd{6 zi|y8Zyo5)9n*hN|(PPBeQRa3EV2aqVyfER!H?xqv7%YI@MNlGCyFJvq5G$g1q*tEg zDi!H57_o}-i0W-amDthgDpiq_ZZo5l4XMs;^4Vo)3`Vw*Nxc;=i!h30k!^`@@!HUC zJo)$xWA+JaR&VU=%cP10+8-7-bo7l3b@?~f63?FrF~!yZK|sF0df7@c#>gYWdQq+> zh{o-f4^)r1QZySpG6(U)>WSzygs7V#InGtrdj5cHqRWWsOtz9?C+jldzM5B_-Cw7I z2c;};juq&!An%mWVlU?z?7v#r0?(yOS9@|JBp~I6IfbD}+)_8$ zNt4_9XV>Jt%{qc)z8AiKy*hdNW>Psnn}q)iCR|WOxY{-t^{(==^>X$Fo?lBBfx^u% zVm!Ho;cT%xXvV`ySe<(EE)f-zm)yWIg-%?-1hHcVhsv!Gt8?ZF_|y0{1le-!^HQH~ zm7!zdAAr52L2odID0)DT)yPElIqNrIa{yXJ-sRZ&$+OceoD(2 z%6F7)zc1u%EE8y<1I`Shf~P3f>qydj8wtp??%GV*4hYQ|@_*{)5lXwnHwi$(eO~#o zG`^&qL$u$!+57IBo~ut+M8Z9@!f_eoL9~s#JznRV$Sh0dVunI?`l22+wK)g7``}N* z0O5JuV6GuHpF%X&KImRMR2wGSe@M$^m5N^GPhg1-qDFlijh4D!?C?4EoKl2yC}sW* z8YG9JPSQb?@|mO0XXAS1=XsoD8wfvZWJBKwLjljJJ{A8808kn76*;L~#pM{JhzrBc znF-Sw@c@>$XrGwPpX3OxHJg0P=aaQhbiTPCb%zyUi=|hs_TvM}8b0-nKIqAVr%*T8 ztH3Mk7{@$0H`Y68_F(6>;Pk&|^}w`LVSN^VT~i+iJ4Qz)dXJmKl5j{fDRy#>Yx_I` zRe4c!w8z}~Sf?faH&)^ZB{ZaYLm)>W8TcKcyk*t1arAZYG%l=P6FeG=vxqSwk6oa@ z5e1B4RNyHYsGn}$qX9!1OZ-rd1UVUv_!{x~I?O@3;pyR+Mk%y^$#YKpuo>+A2!s*B z!yTL!eHxI9Jm1yMUw>0{n=(M~unA7+j^&Lq;O$Tj_6A)MV_JH~u{ljI(3Fv?r;Wf} zFAz3oK;En@(6(c4ptc*+dSi5&l#yR?p17-&;U-+0>VHXe9ba2$$@i} z$w=$)ao9Lh~>Bu!k8$t&j!RiZ9HK4+zHjPhulFaR>HZR ziYi*fzDWs|5Y$=Y?$O+0Ia$n((}+Z;+U~ZK<(0@tPqt&4Iw6<_d<`I4M~E+q*XajbmF_0&&Yg=_H%OrM1(?8Qnsc|C0rFW=y15YoXmq zanP}3S?hf=jnOZ{^q@vXIuc+M67dDCw(M~=4+--@;JvG*#J(q=zjm0D03U zgPxj0mTeP3mkb}#i*~-=IjY7)Yb}NTN^wY2_B(Al!34GPYLorJuK|UZvqeDRbggMy zUmb+!(lm`yFO|Ij=^=JZ3QR2brhlW|>+g0EqB< zTz&abfU{MXBxs5lcB&mGHUg?mgOdmdy*ZiA+&$EIe%8Y7(iW~I|6_EyM}=A>$B@!AYBrEZ2rTAT;IpnvB6x$GKJEal zIgCcCgL{0p;9qq8Vph9kr**Hgdy}C`RkGS6ZK2y-hdcAjz8ij*Wr#Q-Ans9nE`Fm2 z#tOMYFzJ5RRlMn^aDwZTbqC)|p5JqP?xQtE016ulKg=4h{rYd-8SIL%DtbBy+k<1Y z7x=2pZ&5fwdevYkc9rKXOG)qH7-X|xT zQwT3L_w_5vf-&@VnvHZ!LhmcO;ndHLeQH>QX)+l5h4%5f$=`g0PWbkn62!Qk-Z2Fn z$teGio_sl|D~X(Ni)ez3BtbS@johz)yxCD&)s8k|e~SQ8XTMc|?&N3kLai`im*Ho? z9f)+<*J?`8>U>}b^OeZ^5B}Dw*#gi$PU9LAJA~<11Rd0bIU!o<%S+)6>Up}70)~Ki z5)|GnBfXZy+QMh0Z7%A@TA-oOT5Uzgfhp>yWi%Mh)g- zwN(SPM{GUJWv~N|ifJ?!9@z0r6RAi0y{?7!ee<0gb0_FfzFs}P0{StI&reJUW#?tfoE~%Zay8M&MU~=awFSMkpcDPqyA5u> zsrSj=34m>Aps#+>2eR3dx>Qg)e%IDFr*e4w(mI4VfbxXq2+w38*mO~z%9R(0kPZb4 z@<>F+9qd-aM`a1|lX+pkBydN+Dy1J&i_DMhztu*#Aj4DdLihXwY`a~<6pUZqAu$iz zBb4Lnd(fL;gkDOZEImqFT&R)9luHWoC8BjNjL<_`wQ&mLSJ?o4$>QKO=3R15oJZ=5 zAM{Z-(yb8TAgZv+syF}X4#*(MO4}l*$ts-qt?UW6hiY*%!zgWaW>M?M(7{F z{BE>0m$~yvme9Y!D>IWVE_JX%f(y@^I)N`o#)WKp{f&f>D>q}yUxSR+vbu3 zkFT=**A(nC50V6Z)%#`S8?3u^GtIHMhQgm+4Q}g6O2LgUkvkMtaX_{hF!$^3d?gH%9 zt8XZp^FiV(%`)|9_k=L|Z8mammz04Xe=~!Q{>!ce`R%#SwH42^;3n&H%gFJO09Ci# zG)_pmk{@^=MhR$}g23MGR**h>JWVwv10^UXlnoB_sun*tWL<^cE-4f^WDA6Rr{Z{< zSbYET++p7eQ@v;{d3HvynEunl5zh$e_tTqJkL7?-3DRPxPs!)ZIzxux5~FctjmnEE zkS4@gM@@A{TpHE$4zD$OF6uyt`HSTmn-q_^IVAh>Cyv@;^b(ZW2`qqWY3L+OPUEkp zE3}*$g-aPa%v|G@CM7{6oE*s$U~yNFLIOq6q2jtDXYh(3*FCC!6~-2tBZCmih^Ugd zC~VpLZdvCMSmVZK>jZOPRL0!#wh2D93z4~-?Zd#-4{NheHc9iD>R=c?o>`C&eloj{ z%CNWGdcADw2ALN+0>C9xjj#y3AF{6K;x>kkHfn3wZ$M6Gi2hIUAMT_aqAgRwTYQZ{ z{4V;dH%~mfl%G^*xrD+k0JhL^s7iyl=AsyexZ>3+Gt?@xaE$tVgG98f((0K`YkQj# z(olQZKI{+^&{CtfvrDl~5j*lV(npwO7 zt1tA*CR6v9QS_I+G)U`?Wl6x6Du;pnzHEqJe2>VUe8lI+NVL%(34hxmp*yH29f zfY;m{#hV$(0*jVGs104qb(73Fd#8h@2Kc(z!@m=5*_vw z=EuRI%gIdbpULpKm*M=(fSGYp1o`9Qr`Z%gAVpP52T9I?CzdcHW;4&yFV~UEYuzj> z%hGfyb6#$sNI|91`RI~Vf$Hm@P+8M{YnVdX;8&qB9!_HM@_QKd>qzMKAT$Vt{%^yi zwy{ifK?)G@H6(&N=C)ZM|Q-tXWAegQ+n+L|2I?3mcLI zU9T$v5_~&ZX~^St{IEk!8#fw!t&^)sFo%oX)ukYcPnw)6(dcJMJ4LIl9_C#m+LB@o zg+a^y0Wc9*4Ol^`m=)$V&q2vPY_lJSPb;Gu=szTXZ~00N#ifC`>f?rUFb@$#A(Q?} zjmd1-qBkcn_#*^w4Q-RjL>ZKtnj@M-*h)NO2cgMKL_IfDJC& zC4OkH2-{B3L)fz@;$TAY+qKyhh^mGeIK~W~(^1~wX@CGjfr{teLGt|gxH732s5LeJ z>e!5@+P)gYF_i~9oA&xnEfQ5of5@7H6~u>SDddybOksE9H##1sRy#2JZm!r<-b*)) zp=aJq(0#MY6~MD71~`ncD*|&0Jb}K*HTCb4Yy$XReiyWc2!+c_0=y}R5oVYi$#z(<7F?&^InzF54B-B zOP_wneYE?WCuq8^aWv&5c~t`GOj*Kb8xf;07)}(J>=B6(?|&BYETqT+J#Gq=8)quL zlV~%QxFKkegHQLp+*b4`KcEAZ&W1Bl7`L{0WlCm!>0DzQ$r8F3Md%$vG_B=~mKw~k$}Zlp^uUm|9NbH#FG{I$ScWQDMHmYyz9&!8?y z7Yzu(LFmWgSx25X0IMcPH&7dJ`XP{FIl-6AW-NXR%_9`>UBsnXvQbYh-o#BFo+}PB zlAmSF_d^la60M+^Q&c2SFwlqzZB<7MV@U*3I~l`X@160c>VQeI>W|}_LAp#?LxkZp zvIFX4NKdy)*Ez(gFC}bd(@;hD2B!^4qxpEKIqj)l0$wxNcO>OJDKT4z6NHwA6s=#> zaV~liW0T7BZCrkiOvp`C61nbz6WGe)kj)B5;V=2BLu@L~DYLh9l$TJFk|}0e?tE1c z!t|U3$7)pw$XLes8}yH!vLs7)oXdKyaX8cU`wZhrn}oOS3F^4U8h`qq|bBr$16hz0D-Jj z_9nx-FcX+slIq&rGWKcBt+f)WwDc$7dp)F4wHJ*q6=t)XRVe(C6pUboNd7lR!86jP zvy<^oMMvDEN24f!On(kJ{!$3k9-Mst@kg&7*uxu?Su&OejK(iGW-PaVq`;OV&?Md~ zPQOSOzDL6Vkq3YZ7L);`b_#=F$_KiU=CIbwaJdiRvFaghVWOAB+Pb9hE>F1$+u-Cj`0$&-kDsjgmMEhV{;G zI*(gREJEgQ4U>$%cq9)$(5?R*PtrV21xHJ+O(i(WWZCBdK}o}h`&f@ax-9@a z839@6*62tz9|WETPNRrs1t^UEgKa7;G;qs`Vg$luNdpR^f+{fv`pu&GA!w2O=4e>9 z_X*GUE;x<lhanRAGvdScezCwcf=6kUk9ivx?Bb1bGmfdiIK(crC{HzA zg3wTIagJ_vI3G^{L!QOun6`O8?&^M^iwCKGkPs?+@m> z%~oZlP#x|1M7sXcap;Rkk)4(3Btc}VrS_NgHi-*K(>7AChHLMgoX3hjapYWFx-;R_ zc?||ELMlVi`9pPrzbYy6&2((^=rx0g<6|Zwp_WI8kBkU^dqc1h?C1!CvoF4>-DyWO zTf}3qMoFr${m-Pa%X#Va)-PdCFAZUq$z_X%Xhlm>rPdkqHk%chaRk99n9w?d$MVD~ zfe2G)UOVqVmTS&#KIIb3Qf;oBEP%qO;S^mScOo5*VrE5x_aAamaqM$|&=4}g6NZFd z{Ps~4a1e`GQc-KP(%?sjsbGsw7vw|bfl$*hyJ*=LDnSozk1foMTx-WvhOk5i+zWPJGpEuMq@mXmi+&Euf2#!3lHUz#eVq_>%$HUbtWJo4! z<)1sIhJg3tw5Wc%ItKQP6&vT2(Mg|&0!j-;xMmHJuUGVSEFJhkrDXOX;|+ixE-T@c zOF+41WDcT{UTify>Z zhC~n4!!mOIN-`Zi4k(S~T{%OFXL5IieF}kxJHQ9F< zJT^8*r@a1dEGGR5-O>{n^*DbKmLs>(762Gi+kURG8GtC8zhWs&s5Miz2l5BvrVo~Y zu9gu;+|%0`??mk&9ci3|0-AyDnR~Dn8TA+7Pif5$5GbfOyZBcj$WcJ>_ROkOPI%8b z7`RbX)q8-WP%Sm87V6=zWtz>UF0DPy@6`-zqVnk$aHXs4d1804s5rOoO~}nhGrj!p z#n(kHtg36OlQeMxgi^C--(Z8n(t6ScEim-C%_6RE9fVu)zs-^>!X#jzZRX-$XrA4@V~4@Y$p*qY zq=s^{opT<*@b))pae zVzn~=cC4&a!f;04xS2fA6HCBS5C&#@17Ha~40@beXS^#f%k-k+1Zs~2-Xd|uJ#nch zBuN!G?oTImg=A)LIKR?SVR9MIR$ghlxpK_59l46XFIPk?i$gKm;>L(-h}JMo;GJ!i z_V0++3#g%f-EP<9e!Cb9l_rthC?<)IEOv3{T!llPzqRHLvUaa?vtooNL<1 zjwA7=MYYscI4#U6>lL?@Uji~V7+QG?m%GvbDKEnLFVM)zNT=2{E(*SSmr-*#(Q+`a z$3A$ouY>;={NN!CMX*=;xn)z7^j`a+ZMNimxc;_F8e5q9lGsgx zt$^xN1TrGdME5=*ibiiJ)z)^^925qtzyCSJWDta#RHOmK+RPU@guN(zu| z@$Y@Q2WVpMMon$mqJV>aALf~`76CN{b$9l;(R8Z3Eehw!S{;ISK@e4K=8N83JlE!S zL!iS;SXv))Q(rc{z>qXUgCF=l*x1g4d&0QP0HB;OXHwhf7>fUAX+LFia_4C+H6O^z zQfwLh46O3#c|W2roF?#PdF@L4njwJFGf+@c_PYZk&2j;w>lE0ulV>-fegUHIP1SAF z{FwR|h5!1r^%7Dpb_ptc2ko3~4&i;cX)V})S%+0KCZZ*>b|Op==}#59YLsSb;W7}5 z;EyiVr|19NVY2Yz)d+9iaXyzAiTR?n>rYi_sB!KC5;i*h-e zy||KNhq~CfgDw9t$d~(^$ikDr0hHhzT({wE^B71M{3OXzS+dsPm+@NDnh=X(xJ%5Q z_zv}Re61t_SkvgR6&u=&S5G4$h2^%w9pAA<0W!PpicPKsp{vXXS;zC~-}6?JS#Cm<14alz}y3 z$9Q&esE>5dBZDLq${XC(0PeOgv8-er>Xis-O)vLPR$0~#057?x<3Q`3asroHHn78<6`YGH-&N9%g+W(q++5>B{yTZ=sStLBrhkFy zMckbuurYfPf%TXeqWkaP6$yim6xf4M_33%+{M#x-+fevQV*eGG3xR>X$|UDp-8+rRJfQ`u}Cde&+1P(L<1qJSC88&d6y{Im<1hxTPj)>NsB@W zFpPA2v#1xdVo5DE&{%c{>`Qe5Yu^tXSJu+Wz2~F}KN)hN4uz)U@*vX*@=D`hE*G5o zrJGTp<3uoC?R$O`$Q!6l3u}VkJ=`#d5@&>8NwC)gNbkvy4*R7{fiDnuW|jG&g$V2+Zs zel%_70MJDbbg(!HsS^`P%3jTCK;`dXy!XZ~2T;N%%EP)F@EVE?Zc(w*vjc2`TIYjs zL}ZkpTzH%Z9i}1a#MAOrk{f2Nn37~DVJ5-vUDbM~Q?(@A&+hY#Aft5^7EBAuYC47y>FXdNXf zqoNgc1tDZLDOVWbGua7C-ZRkxh@xBD;tvaZ^AI&=S&1C;4sqndUd@gByOYd9!N5i} z42OzedycaQ7=a~I!=@9KC@gfJBBZ1fZ^E9-i)V%9rkEqdyUIPyEt{H2JwcnLzXZ5g z{segD?G&yPQYsB4eI}DWaI?}#9p2Q!Q$fqc{M2VZPDyc85~1p#kRJ2s*9mRMrtNJN zy^23ot(i)-eeIikY3pCnnx6)%ICvTrn%Wu_E)2F%%m$OGZXf0o!i~+7(|2ucxZQ=~ zxg$fFE&Rw-T?GSSv#(n6uyFN9d3%Et={{2pHO4U%SXO2x0E z`qf}l5gd~`zl^NyB~$;zZ#CBW2f@-i_~d|P{`yL^!&7n`3)lb&@UeY$vzC?GDxQxG z^6b<3i|K7&oe)AG$ZuIacbrRNZtLA}e*c=G^>3Hv`{fHh^IuFKnycKS;-osIFltBV zWaz^`mhj74Lzl6_f7T&S z>|?2iuXYB%l=%1W5)n8SzlXSJqIasAy~66YJn@|Xc>SL|Zj8P|>8p#jf>TPA@LlCQ z`#}ik+1J4jAA?~0x1n8#HqQj6HSaLDz4s3Mp1nXF-k9C?3sQCPDw z!_Gpp!G}yY7~=o0vU69znHn|;IG(561fM+L<08D6))F+Do^6rYsm0a9MR6uC=%tpe z-ds|bD0!4<(t+Z+zpJwft!UI6i`sul(Aglfd)0AZ(}L(yoG@Hicfeu>G+J{n3EXd# zm%6kpNdEbSss*Y}5&hR7P|couL}@g~=?2^H6;}(j@Bcf?j6>cJRQ=&qrr+`Me!|8{ z={*D$O@8StIQfxSBgu2Fx)2Hzm0zxvA|x1i;UW}RJg`urFb`<5x3IW0{NnK0h`*** z|7BYE(Pux4ffvkMe9*6_E1^lny1TZ7>*T);BMP1;7ISgk&c-jB2TNSW6IG9p`-#yM zj?Gt&@3sM9`|0JE@roLE^T?SAA4&s{n#cP|6N*!sNI}ES@q7#H_zwO?E(ZE#O(+CC z{VCh-SU@j7?*6%Sm*m!`bFGdd$>f|K{Zw%sT`H`$t zZz#ql+u-`>UtXIytJzcc8xfLeSQt?NZ>(8P8-VZvKRw$vXM^@-vRbflI?B5arXZqF zc))1gxv&S&1kxQ7nL~>ZQY>zJd7N-96yn`n{Y%Asv&abh8h?4tv>7l8hz&tVnq`0a za*>$-vahPMl)%4*0{{RgyaA{N096QokPcuq|8}Su+E-Pqpec{=22bRAye4D^TpN}v z7d$DP0?PP>=(o-b4kO?%Mmry1O1<@mW38Va}cS~p18Sxi<31@?Zv`bT(F8%9OCR2DNi-;w30 zh}F%a9yb8EPAcRRD?j;Ue$D60fve-W8fUC_{I>HL5U#*9^6o{}Y72^qE9N_TSWUl@ z7(Nm4){3P~-+jPzIhtk#lAiQ{J%ZmgLgs~N4`p-(TsyU~#^y%T?Dk1;scs8rHKcZJ zF?eb-PDGnJd$V+NAyf@r|2TyH{BRuew)nMhvs}?=1GpM95opr)mQzAPc_U1Ot}y z8ZcGycSHd{xy%W*d(ggYr|?>IS)mZIGPF%SR5gbA!Tj0J`HW}tl#GX8RDC^M>?DPI z7+^oucGbxKO$80pj0V}&nT%r-M@AJdE~~rl9=wEUNXvY5MK{k=!QR?=PV{Xb7n4W} znn~*jGe=^45}dXuz%~c$Ue1v9f*D9)YE#z%@6&H+?NLtU>!RXfhL>s;HT*-~)Z0QZ zOF0+Arp;W#<-*#>{(8JV&)V$=Z2(Eu?SqO8!HDDL$=Q4;W)pX-N^^>X&?~76Cdh*2 z&oA7G}dRgL{+i4i4BVs2ld# zRbixrp;fmarAa7TDD>rOvce?M1XFgkH1?vS;+g}J5W5`GkGy3Oub((gyY5uthw#ba zoTe$$>P!kxGx&BApP1|^06*l}XCoQkm=F@n>mDci5l0uYIGk*m$5hCuUfX>E=OJnp zBt2Tmugu2wQw`e&I(Ie()+?~0ontkVIxXpF(i<2HI$$T%xo3KL@3^6o$zjLM76vBL zmf)p2VLUQt4Pb{+TY?(*o~;oN7Fj$0I;>on#W?;Yp#aIf_QyO6Wq_}yOFB_z+OB_! ziEW)W4E5eN&&(*eY#G1gb95N`LaSR*dfiV4+0kzyzfqUTG=)Uqc&`tn;!x`NgxnTrBFxg$}o-*G1&QHra2-d8^yp3lYE@5!tzc!aamin@gj{mw;kt*FYTI|8bJQ zl^RcwLqtEc^2JC0w;vYUXCr)fzU0kvYe?b@fo-?`)n%fv{hKUBC~{aDb!ya$8=^6H z81-)Q-ZZM0KIU+^-@SBV1=JW_ekxb#1b&t0#-ZJF+M$YKm&E4i9s#a8WBZFo$kb2t zXm&0uixBkVeWBxZ?T;bTHD*ThZ3;~@0+WK1W}WK<(IK?JXxoCJ-3R$=SGST~q($t- zAoG7!u#7L(h)u_D?TEL$0KlK4j8!HIkc>_Z+XvD@LaSYCNTe}Uy{x=1vbTUMrN?{SMP?uLlJz@wvJeFFX2`1 z&lmTntjyUF)#;FLh~6WSNn7B_d4)5{;`A^F8{y`AtAJS6!RRxKnIEKhW+~>yC$Z=o z7&!?`RkQ+DpI-Cf_WGa3=4T}_@gWZx8@`pFz4%0cnJ(j|jAv;I(`rbqN@Wba;jII9 zop)x^eDDt*yDd;s4_M87T@xJinW~v*qZ4ASkcw7x43UVWiqUf9f1V8P5fT3!pFA~2 zfOM$w{IEOgX8dnYkR#ziIRiA0G8~~4gC&h6#v$$xCBN1ufS~8Kh2KLdH(S|ef}f{= zcneEs8NGa<*g^?5*R$2TK_G6L!_HR*Jlx5K2rVVKH%^kNym1rD`65f2w^&wfJUJNWeaPcD~j`} ziV5UQ!GAhfIFoY)h7ixQ&tOaL_9x8lAyQhf35kA+#5jpzp&no#Jt;A`L5qJ=m0=pbQ(G6P?Aby}u)LALN*qSpfDcDT;R`XUk z&(f727R$dSYqj>TAOcph#xSJeIo?x|Xo0LB&5eJ^0*D}>SRavKjft%Q^#+#t;IMD3 z0{jB9AbL$d;NsknJOrw$-4s7*_y&D~%s(?s68VQNx2hRlxhh-Ww6?Yi*Gy8mvuf}n zI@aRhwY@IBZ0hF{u`hQE%o8)mS5ds&ZlERbUoyph>t31Dpz0rPQ~KF3D6~#J>S&O_ zlwoWm2d#~p1tPJR{USf(X{0HB;sUEu)XeH^tug)J)9KiPm%Ut@#Pv9CtKDGl2x3 zfP-25$#uK#lVG#tAoayh4MNkqhQLehT%Xb$xjp6rVMv|>evtemVnZrNu*^Ntq9GJ! zJ$Gc{JY1AwBmZ|DCD0OR-O9zK3$^5gR;%X69Y~1Ompmp`KbvU9JdztAGQ%+Lv;eG{ zISO1#`ROW<;u~I7X`~PM;7zT@ke-s;W_sA5guxqh#Zdtr|0=CpK3#lIsAx_!!}hR^ zXdZg#4g5xh7g=Nqx*5K2wf+mAT)Ub{(#?6U3o8Kg1SLGPu|@P_eW1fpSM~MpC6dxs zIIiBxY)Jt5!EE5+upa@<*6&uRAS==1*{AI6+g@RZr9Vlw1g5{)mmb?7I;J8sh19Wo zL7cn#CS7#XQ)tw1LJBxN>5-Y0q126K~eEr0X5#}NA^_9OFi^J_`9gM`6(NET18Ab+c+F+vo zu+L0`{95*!ub=XTf;aj!RiLEY__5QxBxKG`h(veJ(OrOxT|=M1XSU?jX$)M#+Y8Ce zF=-!&2x<0+XI7_q1sGR#Z_US)v@5X;c&Co)*VJ`HHakHJD7j8rYYY^@Ls@)}yJ>C1 z=e6^s0})>oOb6fw;dS&uZ3llHuq`s8etXDAhJ@8PMlx+_F*0s-4#I*_Erb^`3f_}h z?8-s9S`!y-{m=I6QOR+_bO+gjHN{SU3XaJ#!`$~0>GW~#bt`YF(~gQn9LI$+oPl(G z`brI}KJOR)Qw1OOk*P?x?!PMzAeu0`cw-N zGU4RxNnn~RoC7f6T)@TY%KXogRk|kLfNT9h&WK^$z zkhG?P6%J(p0-L`CSq(ioACZNOxNN%{=Xm|a+^XL@LT8N2?r;+&NfQS91Z2`Khr`Zu zAb8LBmnHT=vQH>BIJ87mL~Kdg)mpO1kjx~ic8AYRFU!4eU66a#FhUsmprmyL1-_YE z6ya7~Mu*OyNP(h>Yn4}i9wwK9d(W5rf7D79e5o`omJ$ACo$DFY6Flo0MjiQmvcO^q zs9jmzZSU3D8^WiFLjz#tdT0x)d^Oms%gwO9s7@%$I3?UQeyahU<{7Yj)b|T(YvZM} zkN)s3sfwI|scE!k?3z6?+W<8+p>>*`TIRRWJB(S8O`^YG5))*{s-uHw>k65PuGL0G zpO)sFo)R?RpFTEF{KmHt}KvFuoAl0 z@i_Xvs*NC{`Obg5<4I05MqYR(yV|D^ctQTW=Wy881pN(rSq-;66z0BCVL#$!6b~zU ze*D8cbWC!5v^f-Uxk!sWs>59B&stoePml2<49H;hibfr*M82mz7d3r-ykgz4I$%fW zaGk8^9>0Xtb|PPIU<()lrO4B$Ed-5d%`7DxhOVW6$S<0fqbygv3QIOxU5p9|3^{~9 znRRNAGvhG$XHtpzYwqNsS8K475Dtisn`rzWhmm&7|Lb~) z_tOsC)#_>zCLXoKj$u=OZ5}HjP!~&WjzUp&j7ybxo^*+6oW-ThnbJ_0mn^9{xDi09XvQb$l-jkvJ=1Yr@3hLuKO#0cX^>mlJMNO|V(u)n?(Y<7Y9SP5RFzp&zN zai8bcsayyvgLP&!`d4-^{1Jvi{5%4f@o>l1>e7vT{3{DtpXeIHtlB6dpT*88>IeM- zlIV$JclQb`mrjm4CYUHB&TI4{aLq~7%}oG5GBs{n!-#OhT7ELX?~ z^iiZp8~eYT3WNT;dX*9Q-n)rYo3q>QQ$TLb5U*rkv8W=x;4H^jvU?~DXXg5G%iE8y z&-jFnS<6N*Sj`?iWwrMk?_lJ09@OMCW}4hr@8(RmxhP*FvA!p$IUE&1kwui3CR!1a zi`uY=XVp0mpZ4<}2*5kXqg-5t*vC(foShtz5zuZd&Q3Mj+jjWv&k0t3_4)z?TXy1T%D{dVGjPX*w8pn-7Kc8&1E z*g}Yspb|*iSTrY8R!od`Z?%D#WU4KMK9KWH?#AzcmTbJKwZNhsP-Ph*fluP;3xg&5 zr_fL%%ngYzXP#jJ-mIbzcA3{^K{`;)X#!(PyaOfM<`+t_OBl#N08l4hnW6ptB~Ecs zT+tU7=w>PzSX1UoqiSx>=DNIcn2**gmB(`+#f$Q z+MgSHx#zCJn-8WFDPkT+K5qh8BoMYGjkk*a4F<&&IJGI|7V<|78$cqHPQC5y!^sYc z3x`U1Kg(=Lx4jI>0>l(pHc_Fy_k6ur(tE3xMzI?JrT=#nEVxW7Drkq}EX#AHW2^r2 zW!4jJuRk(>Ns6u~XHQ067E#%Zsk2Ovr(%vj zoqKto%{ZG{rTd0TC~{@u3#cS2HeVsSE1=(;uv6uMImnCKfH)fd^flOP9;v~rD3bql zbtF$;q@QSRu}AXKQApc^>-7%{lA@5wifj*>2wv3F6ZNn#r~&x(#olj~CqK{QhEez0{QC<73PU zlsk8F#T9O|*4*lG>C+#?8s8ATSf3{LH-tfk$ty;+#ON7_9#=`dpbJ-&;Ba|2Ju}O* zH)v77W2uf)g)rIn6_n$#Vc+ZZbsF6C{`@1sxaoGnz*vjOjbkobIPqGnDa!9=$xH`N zDjmE3m$jYgEZb@UXHMg4D%1zeuq|k{hi>x$&2MG2{{d7zS$pmV)!l@n-jeDvw^2Lp z!Zr~EZ-N;wk<=G@aL4dQa$~MRU#*9LkDGJvd|)l0%;E*t2^Y*|y=gY#h@Bk^x9lSK zrvvJ^_5{=(dOD_0X_2PkDQ%A}i;h9bS(0Zp_qIT{I?paVM^=34x(95U-PcDw29zqf5DCILKYlJh-ZF2Oc zbRDsm`n4vMuVxF!ri8x%nGh|`Y~@^4Zn(RJfMEP#`Z_62C}Oq5)eq#$`U7w)(I8bU}Q0P$F5a>j8Y3R%tQB=q61?G6?U4alI zDBtphEYL!Wfhi`fjLxCBfQ`q2X#H5>#uKLI|1)dS;;ey_NZ#cHo+?5H(zoy8T`vV9 z0@f#DM+-08?{Qma5X=xvqU^wsHSBx?N@{%k-CTpcU$0|J0gvs+ibxoN0Xt+fAorTeFfeEh^VUtJaItz{@|#>e2$P~Y zzaSe54*Ia@NhM?LBZoG?7^6NW@b$`X9W4IiO5@;fY&AYpuX!~&bQ~s_nY)FRZIEEE z3Xw)tk{*`$ENro79&>QehX9e&PvBDaDB3ZO9hAQK{^apK25+`qz&5c7L0e2w4(zZ~ z0?_aC66UzdWP6)ZXSZ7><*enJu9Y(lM zJ%YHKF~R)!T5vsd4?(T{vDCd^=^a^{*Be@e~b;(&ES5R;08O}!oqt{6s-JLQZMNQeTsnkbSkwsVc~v8&lfX1 z369I8=@CWf4d&mXmayCWaO^xDwHIZYK^&-Nh=`BgPhpE|{#QdXmGoZ~?8-t;PRUNG ze%`{KlygDnt?k-kdcu5UX>PfMGPuV8qVbh9U224Z>yt2bXQeHYn%c)1=fN~V~^pgm8OOVL5W=tWES5vw+7cfi*sA6f!LinDH^-!6Fi zlqyk5mjEk3)W6f8nx`?oOk2;Abt4b1p0~92KzwlCJ6tdQbT%)1r}2Y z@tyCAiY~}U|M5M1cGUdU;ZBCAh=XBW7z?6TT2`lf@miWV4-UT*#P^?s$ybG0m{xTU zt`;2_QN*AhK-+&8#EMx;@la83L507}gx`nUuh;~I`LOHLAAi@Q>$bbCr%WM$v&bEZ z7v>5fR#Q>5j>uZHn>z<&MIctN{}aua1SgZEVo8kM9OBFKFdd5`+HC{VE9F2Pew^}3 z*!$cfm%pyS&`NXZCw-iq;VQ-r;SpV02Q2x|@w3$oU|x>Hz-A!H(U_be=fgvf2@<<; zZ{4C?&tvHu`v@3k_h)?V0*nz;p!9u-PEbJzcsu*{Lf098iMM^IkP3wV`lr_6Y0Gic z0kPnT)48Y+(4U%%h}0t?u||_W4#|OQkZ$8)49IM zb8c`&2V!Ek4LH!Sfb@rw_6c{}Zg6VDgm4gE6p5ZJ1mS>q%2ymSn(KcRAl3AByjB+8 zwl31>J}#vM!h%L8ics3@vW%4`ru(*DV-*dQ`sfr%ky#5smA(7{S<`{wNu1D*AFO1R z130~etr~TitUN^@Y>1JOfLLp{mK;`aApO|)wnJEkmqyz5=ev(t`!aXfLSDpAyIS+2 z%S68A!gM&()qQ$Vxx7H|3t~yGwfcVfV##PUmJl@nrTi4Nhpf5=!D<_96Er*E9;OGiY8eMa05yu|yCg!-d2dcZTwmh_u^rP|#j zL~{T>HTSRkomdWdpyBk9Bm5t&D{9Xn_ZvN)mXs1piT-lgnUxjmakkmaLHu&Va0!pl zFM()+9Xt*PaRfcDn{?KMHp92Fn&?XBnJDaB5qqhtpxPo2$)VBu8cB*RL@ijc zxEzr4plmYI%*VZn+cAZX0GV9vZbKic6OL>yo9xS2`KfP@0wN_nU$v4d-f6VCAbM7)OuTjq*%OI0 z`jtkCXOoypr8oU1j7es-F1%ubPWBItdwXQKUnVpSAdei5lGj-Rj5-H<)$N_9R&|d* z4aYiCTN;57&P5rxK{ZM%CG}!3&pA4UY}0+JODquM;-stGL3=XAhHtr1B=8A4cL4|E zHdd6?Wj6P~+spKVwvm|m$GBd$wp~gbq-q$^7OdpWE5qm#zP+q2B50l9@1la;$>3pQ zo{YrIz!UpM2_MR?pobZ`J@@R~S)|pJ(y7DGgAXhes~5NcvB8C>w4Gh=Xix|BKB;IgdOb?Ix_ObIB5A0MhP z|HaC)QH9%o5x*nE%bn~>QZ7gX0y_gkKdh#G>IPE9W&Y8aL_>TL=`J$x^T=T!mb^oL zMn|bh$ynhbmTP-?)gu&GH&~L7{0K2YL5bX`zVho6{K*h+3DjJ_Ov2wz(=plG#}5I0GR;1t2QXQiae3x9Z4b7uLQr&?=p?(kF(1ysZ&@Q8zk<_+3gn*r z&BFw)fP*3vJf#u;j(dCyA1J-0E@3J;hTOrC8I$$o)3JNFw#|{{Bl5XEx2oh@ZD7^W z4$R3~P<>}u-fj3$d$>E!ZsTFJbF02-PnLkzknkO-8~&GNNbI0|pkyDH<-IPqcgF@F z)C&c(DE9VY3~=mJWwQpBM0c3-gEZ5-TlyY4$8GC^6Mol-txl3k=v1zQ$0tos?f5I9 z2y<>4NO;Zw&s>){kkuJZz^$K2JxmIa;J+pzs4RYB>UK@6Obwx zR;v=}X<&)0=h-7=(Pki$?{Ha=C#POC^n7mWP1_5$j}3VkXn?~O{rV&5K5!N4i9E+rZELhGU>OlRy`FC`2>%Hb*V8NJ1c=)y zKoX@BiKMl2=#8lL$L17AlQ#q98Csd>8w-@n32+wOLNQ=z2H?CwX2r5zL}doKCbAZ=d zdjo8$)n92zUDe04-ihM@fEF5UN&V9w#;Yb%qlOk*^XD`^$&}|xL=4JIk_5fz;m7NW zG-zVAoSu$j(DQOltD54U$Lm>Ry?b=rgX9?n1hcA;{Wni88d`TwoqHv%H1SmD+}%-2 zK9fx@ToM%mWPO4&t(raBgH@uhe)v2?fk)Nq_)za?RK1Yk0uQAxc71F{S>WKR?Rsjp zGX;rNzp1L9JC%L^swFYz`UMlXBl?VWA#?#ux=98tE^fD*(gJF0WO)-L1x?QI_b zR~^?U>!d#g@-(c)RTRBfIoh(;kZ4*=Omx4q(j<`g$AXT|eo``CcIUNBe}DAwdzZp{IGdu`(0H#ZkPLp#N`QecYgx^J%92+*Q2HGvpQFZq z&!4P9+fc|jiZAbR^NRm<>eRSn?T*W|cngd9!_k~f6esf%$>g-e(C2%a2m$rPW@GWF z{RP6KFV&C$h_5gHGIBQXDDM34szQlN!;|0D+BQkZKqf@*41OXYC?+@ zMg72$ug>dZsVkLSMK4Vgt4~C2HUb zVwt|ugfS*g$+3J})Tl!tnU86lA3LND2QPk(C*^v82h8tvTa|`7n;zDLU>HG1Dq)6f zGdDDnXEP;TRxe!vlIoT0pWo9nJrYH~Uh$UF^|~^|?_fnL*-683kAs+3=@XsQE9fgO zGe7_Uf*&E^7y*DmKmHW}!bbEcH}aV5#PbtWnkNLVd2t2(V2G1m$&i2ydyH7$DO2XU zS5cQOC;?GtF3%Bx42nWk<zMrR+O|ZA}>%g*H z`O-Jy671VD%`}R=85u5#jk6q)tH~Q|&>b2~QLDI+40>@{<_)JhBslziL`@!va^f_e zSYI-oB0vlb4U_YXQ7!x%FdRmjXUZtKhItNQ-=Ik(yLurkL5wf9@mXt%6@35NW-0k|kws^v zJT3F6^W&zmxMKs2nVcTzH(KlU6LaY14{{T??4#P{s^FE;}(SqxCLD(&nDT9FOzhM0L zA(*dD$R}D@`H4J~QV>G2izTSR`{fRo%y#y2d+@iT`|Om84iCXYCRO&7# zv^PDItepeZX+XVFd6J^M=YqzWQQa8`OpA?OEmOB(f%Gri(TM6RgI1#N^H7mBv$3RO zFoANi#ZP(txY9=+`C9~C!zcNDyGM=dfb6S~>D{lg0WWiDk&1 zcmQO31s;z~lNX2Zu3G&6(Y&-9FxfvDG|%#Rc`S5EL4DESo4_`24^YcXxAXN9(nL;R zib%PcW$zo3ox<n9| z+=Q{zf=_jEI(sV{31XNE;@tqVR6aj1YNo5B8f1~hrQznbI>H}gyJm<4nJmluhS>HR z+qr@}sMK55vXtz_RzDOQ7ia9+K%PDXaH{{81KPYM5Gn<7zo3!w+FrhANmaS(sV0wW zKl25-@c*OM8;xlX?^p)u)!drxn(p4}tKo~asF29a`KXlpji)3}aCgCkaU4oUu6+h2 zB^|THV{Ca%TPJ9_c>}b{b!+Ns_dED212j0I=*pD8)<-(X_yU;-wP+HXEaE=EA+=8k zLIVDjKPG93qfi>?X1jgtHL!Vdqx%d8=-%5*u&2dxQqo3i*^#RC~cQVVa{7yOpHK=dWx}2QX=Ak>giz< z$(M(7Z@F%f-)NACCLS8?HQFaqu2WF`@N>B?6 zDY&5&b>&Z$^n;@IkZF*HRiP{H>dAp&t>3xVcaVV*#C zC=&LXb2kD8rKuNQ8IuYy-r2e_ydjyxsu|aMgiL%E{xl@BcX3L1J#)#uPOaLT1#%6( zjx1{yr>t*0jm3ZQAx(jLVq6RBmZh~LdjjEm@(7xgI6p15v4wr5$EH%)2;o2+r;J~T zmFP|WUyk-oEfdF`*L0U&{-k*DDdMTY@kiJn53my{J*=^VormbCamrkb`P_u(YVu(W zpBa{AaZNdxyb>+@uQsgkeoV|Ti+b%Q&Dh995O%#u&I^|m9B<}!2%B^lNnKhv{HYkL z7H3&*fR--gYwYK{0HY%F;?Z_3E>Fy>AyCbq(ZBGq0-bF8nnu2g3}DeUF`ajj7|{{A z9KtG+-r>Sxy^CGzNkC+pZQwD@Hx?rYF)VEbgL{ zBldEi+@(dF;%26y4vHm1G*v+F31+36(-};BQ2oQ}V9sK#U9ZvKAG>NkXF#2TDlK)L zdzq&p)^H=IdPg(?5YHAU3puLChM=g=*DUa-hpFM8Zu2Eb0Hvep_#0y*JiZ3cqK;8s z+=dp;^53|GGYB}`fkCJrk|QLr+tCnkLZeg}BrMy7mh$WG0>6Z`Dz0Wcoxp#XLBA!gf{&DRiyh`nQe6WfMEj|Ft!{wO8S6D z$vYS~pYx*;F(5&$Y*%og4P&?;(5`GJH1=2}DH|(f9cllfW5H77h7jp+NKs@Yas-@A}G=CkH*M`V(PjgYpjfgy5gS&Y1}TD5$N zL~EjpyPJ`8{%vdXOdA@)XjI5XzTk_My(tg92lP~&)vA$mcQ*JxVIcoDj&phVoMlkV z-m)?$>0JjuSv#SI+kS9G;${_<8Mo1`0myZlmvcSZaf>H-Skmr)Ht{H7UD?3Yk2Enr85W8aDl>hUfS*L29WY%ctc3|Rml<_IHms}_H=?FI zucAn*L?P9Op5AN1<8BK?$@f-5Wi~)|*S}dTYjQ^?`t@AS%8!I2OK$ zVN@3p4=^MUA2=S5Fd3IkQ6~KKh4hA_PCg_a>%q_P2MzxB+$#0$lj3f%0Q=Y z3uR%o{M(zxxU0ZUstZsF$letNCC*x0sLjgPhZXL^r#KLN37-Pjn*(+Qi8_kJn1mO* zGLJHmd!E02{;X>OaVr0HMK_$x=e{kWsfwsPwu@K6i^s8JH~8BB3?{>0c9(T%0_x!U zA5skaY?@{Dm5s&*axJz+{wA>=9A{?tcq zwri273)rPC;N<~Mx+gx{e%uOorrl7HS84hJZR=zrpwAS2`zHzT6xON&<}G}|>(^j#*-YvkbEj`-kdV4NoSL*@dA~+zX<)i1g~hGmf9OdBBsCOOiw*#qmqy9jvetNhl@`hPJvV=X{72uZ7UN1}v2acmC&eUWCf)D%+a$_5YNc z{+pA3HSz9O3mI7_J~yp@+&Vl8K|<4*V;80Du|)d8vZ{{N93fAZ7AOd5{+O>Y>~iI} z+c+yuhzyWItvqU5QO z(tI2Y_IhsLt!eD)Jx(BJr$IvQ|ryTK( z^*)dn&*;1M8V~AJS_Ml<-eyCVUzF?OPCnf@Bkw#(X<)0UJP54FAyR3U^O6S=fv9(( z$)Lu4WmhhOtiQUAmT0|z2ezZc7ZT5o@OxBj@02Us(wF!RU6I0YRQDxTwqy6gWwxne zJ(4$B{yoCp2F4+SV$Fgfk)6F)#o5sKATWR3ri4{6?mq{rpoZc=9W;{;{+7_=0Bm-8 zTIVsS2bGc>j%?07_}G~7I2WJ9zU)^?o4da{p`)~hOe0RA!9(;}Td3;S0$%7MvSu1p z%r)~%z1C}jMdItj>IskKjUa`9VCdN4H;-XXly7K!e49*qASG?siVN6{nc)BeH*nmU$C-90;0i-LqHP7or?kh%)J?@h=; z?uRpb^z$mvx?9+*R=>MeuMojg&9v&FmjISNrO9)$mz6O!s0-6xvZdF!HO?D2Tn*(6 z)9{$XOMy35x*cwU5$BG`2lCK^Dz7!;!}2TJ2RDnfs2wmnQ)BLgS9*aXwkmNKdO@p; zFVG9@*E2}HrX|1I`N|1RQI8AS9nEC|g68obiX^NRR{sXmM^B(Ok_VzM=vxO;!g(XA zTXCpFM$g@@T{w=y-mtCJ;wABAj1Jb;XUtH%ZM%Ed$xWp~TnMYM;%$|H%pj>iH08CW zl`wc;9rWCqmAD;ilASc+MBa9bGztjIyAUA)=2_I!*koS4FPkDnheTKDFanAOeu8X? zJ-8;3x--qMe{#&raqpbT0}?V32Bp*K%gV@Xv{>(~eFyU$le6`9Ub0kHk^NvP$I~Oi zQkTZJ@`CO*N&2R8t6PBQZR^*EQG#jUmjNTCmgm_xe@f%ao|ixdmO!U-K&MdzjzI1C zeZ`VDh&@tk7*tI$y~Q{8EMOpl>KM0JiMkxOWHkmWcV6x($s1biD44@b9M4W8+7zCTlXf&Arxtk1q#iaGP@ z?&cpsgn>XH6j{-FIfGKpTE`W~m!BBJVn`_`dmfq%F<|3*4sZ`)ZyJ{+uTS4lk$N8< zf?4J?iZp}T+lcZTD`qCrCmHip;t>Qfe5CN5ci`COqKQo@IHyck=n4h^NQ<}h&VMp2 z9q5zA{(akc@>9vN|9pbDyyBNwjPoy^7at|`zy3I*%^_Y}E>#?5nvqVH%ub5yp1o4N zi{a_!{bDx}a#+k5){LBedFvmwAoDt%3zkp!9MJqbu=%^t@N|9XqU3GSbrldnfJZO3 zy)cbq**C6)`eq9dH$r8u>@0f5KgJ$2A7+_ab={g1NB%=)y+l3^{N}BmS>la#P8OA! zT5VA+vr%+5k}$KF@E_3d`C4+{>MQtZOM&^r`Tzs77)BG6j~Fz_cH6f0Uh9U&_Twx{ zW#-{I<(jXpX*Fe669iI9cjV_FHp!d^R->Uc_-&t{!gf$)kb*Qcd$!4s|8$}1RZrHs za>snBEYU6o)v~4gd1F^#lpmKk^JJ{j8D2+xx3YtCA`haXy7$af;3D~7xw{96i({LV zNy~$w?C3h&25jfH9=u>gA$a2`$l;(sqR~;rSFPyTEJ}VGeeU2ax%A+#zzl^^^9?g; z%f)n0^R>BO^o-_hi*C^xUAlak9hveTCK_Z`v$2Ma)hUQ}+RhBUCn6ppG{zdMH@Iba z?Uf?*&@nJhlBOogr21{SzM!`RbBNCXzaF8~)ZA#8I7vsf?FmWrcD3$cCye(}o5;Q zow>&a!i1vGIQU4M0Ns}<0ZkT6N$UwC;c)`NOU}r8PhSm9zmtWX0AC&v9=V}V|MS}d z~E{$GX*RkIzK}B387rz zsAj}lHSE!2g?hhFx(^tffo0S(E1*19EeK!Z!1HrKCY=Y)4=@!A{)?()Jz4~qGg4?x z1!(_mvw0VTn&E}VIMq~!@smJpT;O^_7+fi7p~co4EEWqcAaXjL{5NIcGs8x z`@0rJ(hbSW3FOU1Kd1y?T5nE8_Y`K|00QMz3t4_NXFuy`nl4cS^-qT>En8eu3bO7Z zWe2=(J|V4H2iv`auQTmi>Q3$7uf;BAU93|X-?6_)$Dav`xb=3XMIE97aF$Y^7hmbnJ z@jR9u76+y{inOvN#E|y!n6Ygc0x}TQ@IwB=FIRM9LV)0FQ6>Qvp%eS|$vz|nP5g;c z9Qtl**er-@*UP>^RUYHBhn|h+>&2_Ta~R=VVNd^qMJ|HW@YTI7GnwyBa}{A#J9mo~ zO`1dU2`2pTiK9c!sJ4b>Gdnne`H(XU>gq8Xe_}>emJLD;mnCY3aS8P;_P+{aQWFzH z%*&6Ed8&W~mtv0Hhp?2=$d?9W3MDx^hPcmfKsR5$U(JIEi$`@Q*|H|Ahs@iO#uZ8( zCjWNMR>0#!d5Y&MHm|j-U2u#BvXS?O`Ar-Yf)gCoUV^2U^?j

    *08|NepD->`t$W`Lq5x{MSN4D4C9b54E%Du6 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

    O%%_TP1H_rMh85{uk3&cT71V|L2+iu-`gqlW!k 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%0D`ibl@S_uCtfgK$L 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

    n zxqfY^UrRgSvZ~sk*J`iw7+du8KGpoU2>8~m_$>!I47^{v8}fmWx{KXnVf;?D$9ij- zb5wyfl#Ds@XqiCwOemx6zbzq9ON02jy5Y!Tys@=1xA(a z{g_OTIe*^Bl0ITxNzeUFNYDaJ+LipeE?KlmkpE&}ois~Km;?$6kZ?kvo3ELEF{iI( zO~31zu`dlSwX4PRKS@o-g9+CK1pFQyFM)}r=k>Vg=qWA}+O^w@&x9?kf(lpPNG!tB z_-rEZb($dlb}&K=mN@n!V$7+liD67kaUeb2FMDs1LgeKy?isUXZvsT5?R6Oi$A5z_p3(j2we&$U!{nxd&~(hDWB zjJowLIzhQkvQHYDXZKgJjP z6&D_0qGAprR-yceLN^%ss%)3!-kD7hy0x*bf5$m#qBG{#y5VO#HE`@u zRS3>|EmRo>H(&S4b!Ist!e&)?d`(mUw~K^k`puEXG`Hfvu;U3+^ns+NAF_WVnGJN8 zy(*fG6(4X<1;=e=kshPLR0ukf6b0JrsGs9ewr|Fmr-*MTl1 zgnrLqRTZHD)|N1-7)!nF6Cx*Yci6F?Y=CQjN8Mi9k`c5fM{Wzf1dkF%%h7Dr6ayM~R|l)p01aTr87i`FGz zFm9Ol1%kf3!aJO`-@fjZTj=C+RJ*mm@O~sq(mmhmM9RBK#TPfaUx>E8meu$4cIEP` z&ey5@Z*cwsMM61SQA$U}1pfbG%qNyTx%-t}a~4Q!1isCuchr{XguOC>0QHN%uZV;3H8RbbvHuHs%$vIHaX6VlL#(@77K@hIvVu+xq0+n z*q9?%E9m$%VC)gHZK5EZ4J{$NwebA2ChXJBZCcvRWE!h(v0U&OE$o_sg>K%71)j1B zKYD+AV@yJz1~CT%!}{uvap#Z+7~Od{WHoL=RJvIn8$PxJ5SJ$9S_V;^3=2yPRw2Uh zM?42Y7-9308IhS%v(v2-%?w67&q3!h^a6Y+u$bSDg_=%QxV(pMF?p+UW&wX9>O_wWj+ zVQaPRXO}o+F$3~SkGg!R<`a@gR4ZQ(0k#2w-ro(isB)nq6%olm!{SF*5k9-X%E62N z9WJhjDnFJ4_y`skr)Q9@lmE|__T=(YON`Vn&I}O7ve;9pIV+o~!|Yy#VHCrwxA`<( zp*N&dXDT;lcqZYq2$M-8==ESM?I>qY&N(Fv7-oigEsy|rAk$-G?2XwV zm(i;N^Gzke!_X1n?MYv!g{lpEceQ-`lF;6?YC1go8=4}=L%bKghG`g|WwjM}b3IQ% z9~|G*B5pKDJ`OI(WpwzTT1YA_@O#M}2 zB8ai@E8&UXO8!%X;3p*K-tyH0smBb%@joM30`u#~igIj_JDm^Pb{UKJ9pN_7nvC3;Ubmro? zMOY*lPMnu_0|Xl%HW3KTRKDTDB+oyxl7CpjEW@VL+3LU5N<(6zg;NE$Zw;C8=hsrE z+#7#IH4P-9gc`Thc@K01z8eGJ`bOnTbvDGWXWfL?V9@Ly269^8unBqIn?5AG!oa(p zkdX#bS%Is{@5Fs@U7Hu#hZe-Fo=St{8KI`gVGoda(2c9az&6kIIY!=64dRd&HAIc* zzle(|_R2Q(r|j5WOpqX>8;^frBs;$e-YYEh{>l4;RlmCNgI{RlZF!qjIAYCY>}V1~ z*^6L!T@(lV1{#Ot9TuC3vKR~7bwhF$f68rS+hZy5KPRP%dH*jq(1%t0Pr}#J@FJ1x zqm@F!#6Q=I+zq~XlKF6%8p z;4v3fDgPV$ab=%%MGhj}`PGo@-)L2lMP5Ev)?)+q6!b|lpG)R+Bgrxt3!GmMGP+*a zLVPkr|X8GpFB>MNXeBG5IOhvl|joDGmd*gRcka&%m)96IWLV_fDM zA4$k9MG(Bp$T8Y)X01z+PIoFkguBZl&kq!`(YM8!kZmk!3xD%FI7=xA2nz2yvAr8B zE&vaanMPRx!O}{vwX84*QFa>K{O4Y|St6M}&xos$4SA-}mYXq=ppq++@G$*uFj1RC z%NO^IYe}28`C})9@AuqgobHq;3njZJkU2V^$KRPqM949ehY6iO6@%wr7N9d0GWUJD zD&lu%gNB-WJap0PmGRKQs(rWfF0|nl_~(&XePCf0z*eb9rsd_E_9G$13NDB8dR#p< z_&+)ijS9i^n*Hy$@N5ft3B*Ym4`}J_R`W=@DOLams%w6<^!9wd3kpbeCdaG%Wgzig zK)+fFnS1aib>N7B;N@ZJ&>EqCJ5a@TvDd<_wz1mk}XR?}qcYH(IwQ922_| zoT==NXv^S_{9F;y%DM835D}c~eU(X)?w~hO!~qe;b(E>bI|d_NOH`h{UQ)?L}g=43TZ;kJjg{CTAQpulsvtv ze9ZNB3?4^??U5bW;_ivt#5y-c@!4!RBQfk3S5*+x2{9AZJdX7iK+BpwhyJ9b=L9QF zdK3xZl)6eMQPvZ^seK~`^}y5U-uTZix*QlJ$vrokvGFuoX9G7_ zma&(ExO%+>u#*of{5iS7l+;u#da6HLjTJ~ayyBm=80UJI*(JH8v2hOh3VDxw4) zSm_A{bHpli5Q*RXXFK&|;cDmZ==U444?W?J%A(WkW+FFcz(MCmLr~7`%1>CPwP1Nx%c%1W0eKya ztoHA)Z+O?7mWe*pNJjah?JyZ6J4V%1nHoZ7x=;Fl>z4s8m0WsrfCwGEhwY5~=U|i% zOucG{z@hT$VWl()m-w!Qh#y!&v5d2dsx?9?@ZA*lj`CgsPVDE{!6Tc$n5BL^w2m8yPy-fb5SW<+M@E@Zu2q zoY0yzIF|@>zyMp_wGN0xnYq&>ZtCYC4-jJ4v$UWR_G4d~gQ~ZIU8Hj*`=RdBr&S>* znlwiIxqQ{6hRdFf6p4*hnw)YmXYzpw=xqwd1zWjOFEdqTqgzuaj^tQMc z)u4}2&)7^<8#kUuswDG~vQU$D0d}V>RHGPVdwf*Xe&qP#M}rokd}R(x-UgipGO)lxX}7 zgxOPvj7BPt3=<)!eLvSa{l<2lX6fp3#^*wpVgB9t+Ya=cP6)rWsxS{SOmxoheN zCCmLO^?n8%TE|Yf0+6od+5+f~#NLdY16tUX*L)#SOKM)Ggw#ZCILYEN0L8i(z$9`n z3M4_X0EHEp92DXE`(6oGYAon1i@sSJ9`{8_HMG{cW4R??i77PLMB|Q z8QJIAgfwALDlMdnenkGQZ=`gIPa4jy%2ny-%hAnXJs*#zV?hfOi4=BQ3fKJa0O75$ zFaJ#FyIA)7n){&_RcbP!9$7H*jbGZFLLDJ_{uDlj3)5C*j2?N`Qg)8SpsMg-NLNit zEm;&5Zg|wsFR+MH=h@HC2Qp-I2Y=ZDF?JAM7A=N{r@KzrZtXVeF9vc&Vti-A+K>a* zE6tm0{loY(-yyV(t-hAqP$lM_)5X@x{d|VljkxwsFy*+(rH5ifw19W@0y=+!eI9M^ zXZYXV6}|Sz@xF!Wfxo^_-E8v`IXY59(!K}WNuzD&@GH%F=T%d@D>$%Y3`VE?$ly8k z#`%@kxH3_`pV?|Oi+x6|9N6Z)nGvb zbE9FytW=?YghoK!Kr&_ylH&>IRn0m*q4^`LYU(I?j$`zD`}h*UKk!OJC6WaBm?*5=V2v@r+&CT3l?Fn z>06ViVa(Aae8prR9; z-$H}r7RpQFVLkyXIr;t%5srx1Id0HBW2_#5awz6*<*m z&O1{a9C+@o0Uql~FG9haT|Wu^ek3GPJba#bA4z*851|y9PotTFXs{mNh^_im4*H*1 zh#z2Wj>E0O_)3tU$2aRq?OhOpNy`Km+K!gcddDRZH|R@@JiKOT+E3D5zF7k!m`h(6E{)IA`nCFT#!wdwsI)gfQ#)CB4>A^u%gk9&y!#9VD z*+-@2t123P#rGV%$3P1w#avN*XQq(o_Z+NlfOpcJibB79x3@fF4D_dL@7@*ZyB1-hIYY>mL36{NS`2GQv$>!CN}4@kckL{ zmHIZ*RjzO?xZg6jz`<84LK;oFI33CsY)V}>kg^9+LGUen=< z!arEB#AJk^gk3XjtZXK~7(nJ|ZzGgwtc2l*Yfi$ddb5SBPeizFHwnRnnY z($NZ3SbB9oHE7lPe8ggdw$VEqzi$-cfxXP z1F4oYke;X`bs|ZO`?fcEAw43O28Rw4gQ+H*&~^w|^Y$iBI;FZpO}b)6R*o9QCl2)y zZLPmaI)3xVxYG2KY29(o(kwvbGP0E~RJ4Fl46pHH0tbtKH&UKUarMFI`rV~D;m2gs z#Jx+;1<1MO9iV~SLg{o?xrXK_n&QfG=2VW4ggC8U{V6VXHa(^rBarNcAJwF`VCXj) z(q}`w+zfDMGTiiH-WSEgR22rT#ZaAL@e88WgUamzy?y>F?q94eEgDbm0w!w;SHl>tmPrv`nZ%J&2uvSVXRM z$smRYKBnJ;L<75VH9wXO`j30TkM2!sL;64IXzM z5I;BRS^8}k3wFxF*1X=gFAzU8__MNj0?T00R*LkB5sCDE53Jb{Ym^pd4elk+Evo%d z;`(Qp(mhyA9UA0jPw)vdtydJ8@#P|tWX+WDJJ-d$`TEu1GunNs%G?{Vd2i9azadsC zq_|C9l8wIW+2qDQKRnd_Vo~e6bbz+(QZwHR%Wr)k=O*X<>uih~Zc8JPj|mX_I(K$m zC=eL=9yvFG4vjt^T`k}1WaO!Or5}ni;rxXJQ;Jte7m1KN_~q$!SXZ>AgS2n(X&~vnWxVNh_z9vri{ObMiDp6 zj4qWkgU70lisdeZ#*nbGx`0)sM1zq}uN`twI8QEWLL8m4R~=6c^VB!FTcm7` z52qh=pUyC!eLH~IGC!ji6#HVEf{1Q_`Z6$^?JIWkhicTh0m+TVXBJ|l)CP{bUb|YnI4t0lM%g?DG63@b(E;C8ik-~TAvy(R z7$4MvMY=u_TttTzh+ga%bb#3p$hIeUyf!R1(Cf#{DsC$1VWLJ_r9ETZ<|=zoRxOJq zN|f%IC3lLu!uz?mkieeM?)z#t%?lyPKepGYZGx(RD*XMnMLD0+paIKc@6Dsd(2R}% zo1->+!rqp|awNt@k@>D8(*&r|o%HyW=diyKgEE4mjl$}zMDHmEdYK`?m>W!xpTzb|K7df!qo@{n# zQ_V`|Lw0;JQOp~T!;d_4k^)LzYgjd*3wnw8BI3pdQ~+Fcj}C&GULzR`*tHCuIf+ks zLt%rfn(Crsq}Q~Vv`jjf<<-K~=BnfYO+9W@L(X*|X)as1AJ2mlLU9aXPUb(R_YP89 zv(9OA!|{_{oni{wdRJyg~umH8}OsO%R5F&Lh_^ZjL%QGupzKLP>iY$9nISIgwu9` zxUoosH~(DGc|G9rq>&81y7VC(7l%PNiqorb;v_zC{DP2$@4>kiH9cadGPaep7KHbE zR4c}1#%xQMb!$unZ|VTBcCFu(3wYeHDrvLut;;I~vHBEm`&G?O9S|TEh0V`MKO4Zj z`?AvQkQJ=K)3kN{MKozlxts~E$LtLY(r-G9oYCK5jY5~va#l4kmSxpc_aRjqR?H60 zaB5Ev%r}zJb^|ltvTS7R@@-o$rVDLKuIHYhMb2cTpDw-h&v|$)^Z~f9!Tmzg!+^hnsTg6<^@r|@W2R9v!O8HWR1857ZKH`DNSpab_Uo)-FZ%Ptzgg_5YL(e|f*iuS3{mA#tbH zrgQ%u0t8GgfRq)j139dEDfnTMn*5SVB0oMxO98<!RiP5u)4H zcKn_PbK(RylTR_Nq!~V8aX#b^g~_Iit1FcIS{;npz)XlyPcu&8eCg7D>vYBJu^Fz? z&w~P_F=FiSjUar9zRIEC_i1|C+`b>#hLa_K%bd?RRU(f0S{0bgnxr5q( zIO`>i3F-TU172g@R?`}5g`sb55d2fWIt*tnAl;MU!R?&^5kz3KPuu^uFG4km&4pLl|{fZ!m4z|cAWhmLkZvxZoQ zu;`anZs0NYxQIIg>#y7)2tjP@#`2xdpsaMy!z&rcc zCV`Z28kHPxfwzNVjQLDZ7alwUYG4j}oPn68iDJGI)pb6qOnOF#QJ6PPN4jI{L0?a6 zLMB<3k4ifQ2=f>4_Ea-4(S{t&TBj}aRbzp3`<2J|#5n&EUdI zTPfBQ|KPRP!SyB2DAjlQ0gw9xf>L63(g#{_?J;C=quE5F$({9q14Is2w-B<*E${%5 zvALJJ?OVHB{ahqS71|&?B)Q3D#{Um%ea+5w!2A-wKQJ*jo2&C~2?V1_MbnZippykG z3(4*=o31+;v)8sW3Mk2RR7`O$!sCmdHK}2%Hll@%F`(bR_8V^x+7dz@6t@Ik<7np| z0fwT6mJVaLtPjVa>tYLJE%(5zDZcerZt|xab<5MR>f54$rxD6NXM#`tE{mG#0@()E z^i-f*bw9oIVZph+ zt5oUPE7T=vta9D-cErO~eHF&h#}6PX-VFc>rDE-$O~%SB*oCu3J%zD!NF#$4I9F!O zzlz&h=;rY;4?SRKuAwM82ZPScBLb@Z5mO;w*fHFAj+1TA|IMd&Cfv};KWF~0dXM+E zH1EdCtUeBps4%4!HpJ|(41Q9xQNi!J^99FaBPJ(N$m47ysl5E15XC}jA4Ger^DQ=T z^a~_j`sisFg8xAhhVg|4;oC9iS6>_l$(Z^xdkovIOegQ2+!fP1!{JCPEviRtD>#cE z_o(w}f_oaR62T8qQ4f_lWG1j79Kg$9C;YPEp|1~#|8F8HWSZ5FWn2V~6a9G3MeDZ2 zSzEl?67{X`!6Bd&Gyk*^G6tBJiUJ$0$UMs<4nvz0O0K_hE>=C&b~f4PLx*l-^7Tw= zlb$IOKDklvull=HMBpYw%nM;}6I_M3^FN38)B>V)2{AO8Akz1U$U`K3z&v$njhOLO z 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

    pGRKwueLfKq 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%0A}iez;>})sH9B1z+d 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 + + + + + \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/auth/cas2/cas2postlogin.xhtml b/oxAuth/Server/src/main/webapp/auth/cas2/cas2postlogin.xhtml new file mode 100644 index 00000000..b00cc99f --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/cas2/cas2postlogin.xhtml @@ -0,0 +1,90 @@ + + + + + + + + + + #{msgs['cas2login.pageTitle']} + + + + + +

    + + + + \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/auth/cert/cert-invalid.xhtml b/oxAuth/Server/src/main/webapp/auth/cert/cert-invalid.xhtml new file mode 100644 index 00000000..95734e0d --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/cert/cert-invalid.xhtml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/auth/cert/cert-login.xhtml b/oxAuth/Server/src/main/webapp/auth/cert/cert-login.xhtml new file mode 100644 index 00000000..641f32a2 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/cert/cert-login.xhtml @@ -0,0 +1,13 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/auth/cert/cert-not-selected.xhtml b/oxAuth/Server/src/main/webapp/auth/cert/cert-not-selected.xhtml new file mode 100644 index 00000000..655f1154 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/cert/cert-not-selected.xhtml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/auth/cert/login.xhtml b/oxAuth/Server/src/main/webapp/auth/cert/login.xhtml new file mode 100644 index 00000000..4e122606 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/cert/login.xhtml @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + +
    +
    + +
    +
    \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/auth/compromised/complogin.xhtml b/oxAuth/Server/src/main/webapp/auth/compromised/complogin.xhtml new file mode 100644 index 00000000..4736f226 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/compromised/complogin.xhtml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + +
    +
    + + + +
    +
    + +
    +
    +
    +
    +
    +

    | + + | + + + | + +

    +
    +
    +
    +
    +
    +
    +
    diff --git a/oxAuth/Server/src/main/webapp/auth/compromised/newpassword.xhtml b/oxAuth/Server/src/main/webapp/auth/compromised/newpassword.xhtml new file mode 100644 index 00000000..637e4a62 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/compromised/newpassword.xhtml @@ -0,0 +1,117 @@ + + + + + + + + + + + oxAuth Wikid - Login + + + + + + +
    +
    + + + + + + +
    +
    + +
    +
    diff --git a/oxAuth/Server/src/main/webapp/auth/deduce/loginD.xhtml b/oxAuth/Server/src/main/webapp/auth/deduce/loginD.xhtml new file mode 100644 index 00000000..bf54abcf --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/deduce/loginD.xhtml @@ -0,0 +1,144 @@ + + + + + + + #{msgs['pwdless.pageTitle']} + + +
    +

    #{msgs['casa.login.panel_title']}

    + + +
    + #{msgs['pwdless.choose']} +
      +
    + + +
    + + + +
    + + + + +
    +
    + + + +
    + +
    +
    + + + +
    + +
    diff --git a/oxAuth/Server/src/main/webapp/auth/duo/duologin.xhtml b/oxAuth/Server/src/main/webapp/auth/duo/duologin.xhtml new file mode 100644 index 00000000..8d797df1 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/duo/duologin.xhtml @@ -0,0 +1,61 @@ + + + + + + + + + + + + #{msgs['duologin.title']} + +
    + + + + + + + + + \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/auth/duo/js/Duo-Web-v2.min.js b/oxAuth/Server/src/main/webapp/auth/duo/js/Duo-Web-v2.min.js new file mode 100644 index 00000000..c087b80a --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/duo/js/Duo-Web-v2.min.js @@ -0,0 +1 @@ +window.Duo=function(e,t){var i=/^(?:AUTH|ENROLL)+\|[A-Za-z0-9\+\/=]+\|[A-Za-z0-9\+\/=]+$/;var n=/^ERR\|[\w\s\.\(\)]+$/;var o="duo_iframe",a="",r="sig_response",s,f,u,d,m;function c(e,t){throw new Error("Duo Web SDK error: "+e+(t?"\n"+"See "+t+" for more information":""))}function h(e){return e.replace(/([a-z])([A-Z])/,"$1-$2").toLowerCase()}function g(e,t){if("dataset"in e){return e.dataset[t]}else{return e.getAttribute("data-"+h(t))}}function p(e,i,n,o){if("addEventListener"in t){e.addEventListener(i,o,false)}else{e.attachEvent(n,o)}}function l(e,i,n,o){if("removeEventListener"in t){e.removeEventListener(i,o,false)}else{e.detachEvent(n,o)}}function w(t){p(e,"DOMContentLoaded","onreadystatechange",t)}function v(t){l(e,"DOMContentLoaded","onreadystatechange",t)}function E(e){p(t,"message","onmessage",e)}function b(e){l(t,"message","onmessage",e)}function _(e){if(!e){return}if(e.indexOf("ERR|")===0){c(e.split("|")[1])}if(e.indexOf(":")===-1||e.split(":").length!==2){c("Duo was given a bad token. This might indicate a configuration "+"problem with one of Duo's client libraries.","https://www.duosecurity.com/docs/duoweb#first-steps")}var t=e.split(":");f=e;u=t[0];d=t[1];return{sigRequest:e,duoSig:t[0],appSig:t[1]}}function y(){m=e.getElementById(o);if(!m){throw new Error("This page does not contain an iframe for Duo to use."+'Add an element like '+"to this page. "+"See https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe "+"for more information.")}q();v(y)}function D(e){return Boolean(e.origin==="https://"+s&&typeof e.data==="string"&&(e.data.match(i)||e.data.match(n)))}function A(t){if(t){if(t.host){s=t.host}if(t.sig_request){_(t.sig_request)}if(t.post_action){a=t.post_action}if(t.post_argument){r=t.post_argument}if(t.iframe){if("tagName"in t.iframe){m=t.iframe}else if(typeof t.iframe==="string"){o=t.iframe}}}if(m){q()}else{m=e.getElementById(o);if(m){q()}else{w(y)}}v(A)}function L(e){if(D(e)){R(e.data);b(L)}}function q(){if(!s){s=g(m,"host");if(!s){c("No API hostname is given for Duo to use. Be sure to pass "+"a `host` parameter to Duo.init, or through the `data-host` "+"attribute on the iframe element.","https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe")}}if(!u||!d){_(g(m,"sigRequest"));if(!u||!d){c("No valid signed request is given. Be sure to give the "+"`sig_request` parameter to Duo.init, or use the "+"`data-sig-request` attribute on the iframe element.","https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe")}}if(a===""){a=g(m,"postAction")||a}if(r==="sig_response"){r=g(m,"postArgument")||r}m.src=["https://",s,"/frame/web/v1/auth?tx=",u,"&parent=",e.location.href,"&v=2.1"].join("");E(L)}function R(t){var i=e.createElement("input");i.type="hidden";i.name=r;i.value=t+":"+d;var n=e.getElementById("duo_form");if(!n){n=e.createElement("form");m.parentElement.insertBefore(n,m.nextSibling)}n.method="POST";n.action=a;n.appendChild(i);n.submit()}w(A);return{init:A,_parseSigRequest:_,_isDuoMessage:D}}(document,window); \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/auth/email_auth/entertoken.xhtml b/oxAuth/Server/src/main/webapp/auth/email_auth/entertoken.xhtml new file mode 100644 index 00000000..ffba4cd3 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/email_auth/entertoken.xhtml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +

    Authentication Token

    +

    Enter the token received in your email

    + ver_code + + + + + + +
    +
    +
    +
    + + +
    + +
    \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/auth/fido2/js/base64js.js b/oxAuth/Server/src/main/webapp/auth/fido2/js/base64js.js new file mode 100644 index 00000000..8b055fb5 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/fido2/js/base64js.js @@ -0,0 +1 @@ +(function(r){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=r()}else if(typeof define==="function"&&define.amd){define([],r)}else{var e;if(typeof window!=="undefined"){e=window}else if(typeof global!=="undefined"){e=global}else if(typeof self!=="undefined"){e=self}else{e=this}e.base64js=r()}})(function(){var r,e,n;return function(){function r(e,n,t){function o(i,a){if(!n[i]){if(!e[i]){var u=typeof require=="function"&&require;if(!a&&u)return u(i,!0);if(f)return f(i,!0);var d=new Error("Cannot find module '"+i+"'");throw d.code="MODULE_NOT_FOUND",d}var c=n[i]={exports:{}};e[i][0].call(c.exports,function(r){var n=e[i][1][r];return o(n?n:r)},c,c.exports,r,e,n,t)}return n[i].exports}var f=typeof require=="function"&&require;for(var i=0;i0){throw new Error("Invalid string. Length must be a multiple of 4")}return r[e-2]==="="?2:r[e-1]==="="?1:0}function c(r){return r.length*3/4-d(r)}function v(r){var e,n,t,i,a;var u=r.length;i=d(r);a=new f(u*3/4-i);n=i>0?u-4:u;var c=0;for(e=0;e>16&255;a[c++]=t>>8&255;a[c++]=t&255}if(i===2){t=o[r.charCodeAt(e)]<<2|o[r.charCodeAt(e+1)]>>4;a[c++]=t&255}else if(i===1){t=o[r.charCodeAt(e)]<<10|o[r.charCodeAt(e+1)]<<4|o[r.charCodeAt(e+2)]>>2;a[c++]=t>>8&255;a[c++]=t&255}return a}function l(r){return t[r>>18&63]+t[r>>12&63]+t[r>>6&63]+t[r&63]}function h(r,e,n){var t;var o=[];for(var f=e;fd?d:u+a))}if(o===1){e=r[n-1];f+=t[e>>2];f+=t[e<<4&63];f+="=="}else if(o===2){e=(r[n-2]<<8)+r[n-1];f+=t[e>>10];f+=t[e>>4&63];f+=t[e<<2&63];f+="="}i.push(f);return i.join("")}},{}]},{},[])("/")}); diff --git a/oxAuth/Server/src/main/webapp/auth/fido2/js/base64url.js b/oxAuth/Server/src/main/webapp/auth/fido2/js/base64url.js new file mode 100644 index 00000000..42054ead --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/fido2/js/base64url.js @@ -0,0 +1,64 @@ +// Copyright (c) 2018, Yubico AB +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +(function(root, factory) { + if (typeof define === 'function' && define.amd) { + define(['base64js'], factory); + } else if (typeof module === 'object' && module.exports) { + module.exports = factory(require('base64js')); + } else { + root.base64url = factory(root.base64js); + } +})(this, function(base64js) { + + function ensureUint8Array(arg) { + if (arg instanceof ArrayBuffer) { + return new Uint8Array(arg); + } else { + return arg; + } + } + + function base64UrlToMime(code) { + return code.replace(/-/g, '+').replace(/_/g, '/') + '===='.substring(0, (4 - (code.length % 4)) % 4); + } + + function mimeBase64ToUrl(code) { + return code.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); + } + + function fromByteArray(bytes) { + return mimeBase64ToUrl(base64js.fromByteArray(ensureUint8Array(bytes))); + } + + function toByteArray(code) { + return base64js.toByteArray(base64UrlToMime(code)); + } + + return { + fromByteArray: fromByteArray, + toByteArray: toByteArray, + }; + +}); diff --git a/oxAuth/Server/src/main/webapp/auth/fido2/js/webauthn.js b/oxAuth/Server/src/main/webapp/auth/fido2/js/webauthn.js new file mode 100644 index 00000000..fb0983ce --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/fido2/js/webauthn.js @@ -0,0 +1,169 @@ +// Copyright (c) 2018, Yubico AB +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +(function(root, factory) { + if (typeof define === 'function' && define.amd) { + define(['base64url'], factory); + } else if (typeof module === 'object' && module.exports) { + module.exports = factory(require('base64url')); + } else { + root.webauthn = factory(root.base64url); + } +})(this, function(base64url) { + + function extend(obj, more) { + return Object.assign({}, obj, more); + } + + /** + * Create a WebAuthn credential. + * + * @param request: object - A PublicKeyCredentialCreationOptions object, except + * where binary values are base64url encoded strings instead of byte arrays + * + * @return a PublicKeyCredentialCreationOptions suitable for passing as the + * `publicKey` parameter to `navigator.credentials.create()` + */ + function decodePublicKeyCredentialCreationOptions(request) { + const excludeCredentials = request.excludeCredentials.map(credential => extend( + credential, { + id: base64url.toByteArray(credential.id), + })); + + const publicKeyCredentialCreationOptions = extend( + request, { + attestation: 'direct', + user: extend( + request.user, { + id: base64url.toByteArray(request.user.id), + }), + challenge: base64url.toByteArray(request.challenge), + excludeCredentials, + }); + + return publicKeyCredentialCreationOptions; + } + + /** + * Create a WebAuthn credential. + * + * @param request: object - A PublicKeyCredentialCreationOptions object, except + * where binary values are base64url encoded strings instead of byte arrays + * + * @return the Promise returned by `navigator.credentials.create` + */ + function createCredential(request) { + return navigator.credentials.create({ + publicKey: decodePublicKeyCredentialCreationOptions(request), + }); + } + + /** + * Perform a WebAuthn assertion. + * + * @param request: object - A PublicKeyCredentialRequestOptions object, + * except where binary values are base64url encoded strings instead of byte + * arrays + * + * @return a PublicKeyCredentialRequestOptions suitable for passing as the + * `publicKey` parameter to `navigator.credentials.get()` + */ + function decodePublicKeyCredentialRequestOptions(request) { + const allowCredentials = request.allowCredentials && request.allowCredentials.map(credential => extend( + credential, { + id: base64url.toByteArray(credential.id), + })); + + const publicKeyCredentialRequestOptions = extend( + request, { + allowCredentials, + challenge: base64url.toByteArray(request.challenge), + }); + + return publicKeyCredentialRequestOptions; + } + + /** + * Perform a WebAuthn assertion. + * + * @param request: object - A PublicKeyCredentialRequestOptions object, + * except where binary values are base64url encoded strings instead of byte + * arrays + * + * @return the Promise returned by `navigator.credentials.get` + */ + function getAssertion(request) { + console.log('Get assertion', request); + return navigator.credentials.get({ + publicKey: decodePublicKeyCredentialRequestOptions(request), + }); + } + + + /** Turn a PublicKeyCredential object into a plain object with base64url encoded binary values */ + function responseToObject(response) { + + let clientExtensionResults = {}; + + try { + clientExtensionResults = response.getClientExtensionResults(); + } catch (e) { + console.error('getClientExtensionResults failed', e); + } + + if (response.response.attestationObject) { + return { + type: response.type, + id: response.id, + response: { + attestationObject: base64url.fromByteArray(response.response.attestationObject), + clientDataJSON: base64url.fromByteArray(response.response.clientDataJSON), + }, + clientExtensionResults, + }; + } else { + return { + type: response.type, + id: response.id, + rawId: base64url.fromByteArray(response.rawId), + response: { + authenticatorData: base64url.fromByteArray(response.response.authenticatorData), + clientDataJSON: base64url.fromByteArray(response.response.clientDataJSON), + signature: base64url.fromByteArray(response.response.signature), + userHandle: response.response.userHandle && base64url.fromByteArray(response.response.userHandle), + }, + clientExtensionResults, + }; + } + } + + return { + decodePublicKeyCredentialCreationOptions, + decodePublicKeyCredentialRequestOptions, + createCredential, + getAssertion, + responseToObject, + }; + +}); diff --git a/oxAuth/Server/src/main/webapp/auth/fido2/login.xhtml b/oxAuth/Server/src/main/webapp/auth/fido2/login.xhtml new file mode 100644 index 00000000..74b3efd5 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/fido2/login.xhtml @@ -0,0 +1,195 @@ + + + + + + + + + + + + oxAuth - Fido2 Login + + + + + + + + + + + + +
    +
    +

    #{msgs['fido2.verification.stepverification']}

    +

    #{msgs['fido2.verification.usedevice']}

    + step_ver +

    #{msgs['fido2.verification.insertkey']}

    +

    #{msgs['fido2.verification.useit']}

    + +

    +
    +
    +
    +
    + +

    + + + +

    +
    + + +

    + + + +

    +
    + +
    +
    +
    +
    diff --git a/oxAuth/Server/src/main/webapp/auth/fido2/platform.xhtml b/oxAuth/Server/src/main/webapp/auth/fido2/platform.xhtml new file mode 100644 index 00000000..71e96762 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/fido2/platform.xhtml @@ -0,0 +1,214 @@ + + + + + + + + + + + + oxAuth - Fido2 Login + +
    + +
    + + + + + + +
    +
    +

    #{msgs['fido2.verification.stepverification']}

    +

    #{msgs['fido2.touch.verification.usedevice']}

    + step_ver +

    #{msgs['fido2.touch.verification.insertkey']}

    +

    #{msgs['fido2.touch.verification.useit']}

    + + + +

    +
    +
    +
    +
    + +

    + + + +

    +
    + +

    + + + +

    +
    + +
    +
    + + +
    +
    + diff --git a/oxAuth/Server/src/main/webapp/auth/fido2/secKeys.xhtml b/oxAuth/Server/src/main/webapp/auth/fido2/secKeys.xhtml new file mode 100644 index 00000000..4c6922ef --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/fido2/secKeys.xhtml @@ -0,0 +1,209 @@ + + + + + + + + + + + + oxAuth - Fido2 Login + +
    + + +
    + + + + + + + +
    +
    +

    #{msgs['fido2.verification.stepverification']}

    +

    #{msgs['fido2.verification.usedevice']}

    + step_ver +

    #{msgs['fido2.verification.insertkey']}

    +

    #{msgs['fido2.verification.useit']}

    + +

    +
    +
    +
    +
    + +

    + + + +

    +
    + + +

    + + + +

    +
    + +
    +
    +
    +
    diff --git a/oxAuth/Server/src/main/webapp/auth/fido2/step1.xhtml b/oxAuth/Server/src/main/webapp/auth/fido2/step1.xhtml new file mode 100644 index 00000000..fb0be7ca --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/fido2/step1.xhtml @@ -0,0 +1,198 @@ + + + + + + + + + + + + + +
    + + + +
    + +
    +
    diff --git a/oxAuth/Server/src/main/webapp/auth/forgot_password/entertoken.xhtml b/oxAuth/Server/src/main/webapp/auth/forgot_password/entertoken.xhtml new file mode 100644 index 00000000..7843c0bc --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/forgot_password/entertoken.xhtml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +

    Password Token

    +

    Enter the token received in your email

    + ver_code + + + + + + +
    +
    +
    +
    + +
    + +
    \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/auth/forgot_password/forgot.xhtml b/oxAuth/Server/src/main/webapp/auth/forgot_password/forgot.xhtml new file mode 100644 index 00000000..3c078538 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/forgot_password/forgot.xhtml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +

    Password Reset

    + + +

    Enter your e-mail:

    + + + + +

    +

    + +

    + + + + +
    +
    +
    + + +
    + +
    + +
    \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/auth/forgot_password/newpassword.xhtml b/oxAuth/Server/src/main/webapp/auth/forgot_password/newpassword.xhtml new file mode 100644 index 00000000..39f7444a --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/forgot_password/newpassword.xhtml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +

    New Password

    +

    Enter your new password:

    + + + + + + + + +
    +
    +
    +
    + +
    + +
    \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/auth/gplus/gpluslogin.xhtml b/oxAuth/Server/src/main/webapp/auth/gplus/gpluslogin.xhtml new file mode 100644 index 00000000..1e28f742 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/gplus/gpluslogin.xhtml @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + + + #{msgs['gpluslogin.title']} + + + + + + +
    +
    + + + + + + + + + +
    +
    + + + +
    +
    diff --git a/oxAuth/Server/src/main/webapp/auth/gplus/gpluspostlogin.xhtml b/oxAuth/Server/src/main/webapp/auth/gplus/gpluspostlogin.xhtml new file mode 100644 index 00000000..d1e44ed9 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/gplus/gpluspostlogin.xhtml @@ -0,0 +1,90 @@ + + + + + + + + + + #{msgs['gpluslogin.pageTitle']} + + + + + + + + + + \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/auth/idfirst/alter_login.xhtml b/oxAuth/Server/src/main/webapp/auth/idfirst/alter_login.xhtml new file mode 100644 index 00000000..ce3cede6 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/idfirst/alter_login.xhtml @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + +
    +
    + +
    +
    +
    +
    +
    +

    + + | + + + + + | + + + + + + | + + + + +

    +
    +
    +
    +
    +
    + + +
    +
    \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/auth/idfirst/idfirst_login.xhtml b/oxAuth/Server/src/main/webapp/auth/idfirst/idfirst_login.xhtml new file mode 100644 index 00000000..573ddb4d --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/idfirst/idfirst_login.xhtml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + +
    + + + + +
    + +
    +
    \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/auth/inwebo/iwauthenticate.xhtml b/oxAuth/Server/src/main/webapp/auth/inwebo/iwauthenticate.xhtml new file mode 100644 index 00000000..fb33dbd9 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/inwebo/iwauthenticate.xhtml @@ -0,0 +1,78 @@ + + + + + + + + + + + #{msgs['inwebo.pageTitle']} + + +
    + + + +
    +
    +
    \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/auth/inwebo/iwlogin.xhtml b/oxAuth/Server/src/main/webapp/auth/inwebo/iwlogin.xhtml new file mode 100644 index 00000000..2bc45baf --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/inwebo/iwlogin.xhtml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + +
    + +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +

    #{msgs['otp.scanQRCode']}

    +
    +
    +
    +
    +
    +

    + Unable to scan the QR code? +

    + +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    diff --git a/oxAuth/Server/src/main/webapp/auth/otp/otplogin.xhtml b/oxAuth/Server/src/main/webapp/auth/otp/otplogin.xhtml new file mode 100644 index 00000000..c64639b0 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/otp/otplogin.xhtml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + +
    +
    +

    #{msgs['otp.verification']}

    +

    #{msgs['otp.usedevice']}

    + ver_code +

    #{msgs['otp.entercode']}

    +

    #{msgs['otp.getcode']}

    + +

    + +
    +
    +
    +
    + + +
    +
    diff --git a/oxAuth/Server/src/main/webapp/auth/otp_sms/otp_sms.xhtml b/oxAuth/Server/src/main/webapp/auth/otp_sms/otp_sms.xhtml new file mode 100644 index 00000000..b09b9366 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/otp_sms/otp_sms.xhtml @@ -0,0 +1,152 @@ + + + + + + + + + + + #{msgs['otp_sms.pageTitle']} + + + + + + + + + +
    +
    +

    #{msgs['otp_sms.verification']}

    +

    #{msgs['otp_sms.usedevice']}

    + phone-ver +

    #{msgs['otp_sms.verificationcode']}

    +

    + + +

    + + + +
    +
    + +

    + #{msgs['otp_sms.termsPrivacy']} +

    +
    +
    +
    + +
    +
    \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/auth/oxpush/oxauthenticate.xhtml b/oxAuth/Server/src/main/webapp/auth/oxpush/oxauthenticate.xhtml new file mode 100644 index 00000000..05669db6 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/oxpush/oxauthenticate.xhtml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + #{msgs['oxpush.pageTitle']} + + +
    + + + +
    +
    +
    \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/auth/oxpush/oxlogin.xhtml b/oxAuth/Server/src/main/webapp/auth/oxpush/oxlogin.xhtml new file mode 100644 index 00000000..95e44c9d --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/oxpush/oxlogin.xhtml @@ -0,0 +1,122 @@ + + + + + + + + + + #{msgs['oxpush.title']} + + + + + + +
    +
    + + + + + + +
    +
    +
    +
    \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/auth/oxpush/oxpair.xhtml b/oxAuth/Server/src/main/webapp/auth/oxpush/oxpair.xhtml new file mode 100644 index 00000000..611004cf --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/oxpush/oxpair.xhtml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + #{msgs['oxpush.pageTitle']} + + +
    + + + +
    +
    +
    \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/auth/passport/img/apple.png b/oxAuth/Server/src/main/webapp/auth/passport/img/apple.png new file mode 100644 index 0000000000000000000000000000000000000000..d852c10925f8f59f5bb1579c832554099b2d68a4 GIT binary patch literal 8171 zcmV004&%004{+008|`004nN004b?008NW002DY000@xb3BE2000Uv zX+uL$Nkc;*P;zf(X>4Tx07%E3mUmQC*A|D*y?1({%`nm#dXp|Nfb=dP9RyJrW(F9_ z0K*JTY>22pL=h1IMUbF?0i&TvtcYSED5zi$NDxqBFp8+CWJcCXe0h2A<>mLsz2Dkr z?{oLrd!Mx~03=TzE-wX^0w9?u;0Jm*(^rK@(6Rjh26%u0rT{Qm>8ZX!?!iDLFE@L0LWj&=4?(nOT_siPRbOditRHZrp6?S8Agej zFG^6va$=5K|`EW#NwP&*~x4%_lS6VhL9s-#7D#h8C*`Lh;NHnGf9}t z74chfY%+(L4giWIwhK6{coCb3n8XhbbP@4#0C1$ZFF5847I3lz;zPNlq-OKEaq$AW zE=!MYYHiJ+dvY?9I0Av8Ka-Wn(gPeepdb@piwLhwjRWWeSr7baCBSDM=|p zK0Q5^$>Pur|2)M1IPkCYSQ^NQ`z*p zYmq4Rp8z$=2uR(a0_5jDfT9oq5_wSE_22vEgAWDbn-``!u{igi1^xT3aEbVl&W-yV z=Mor9X9@Wki)-R*3DAH5Bmou30~MeFbb%o-16IHmI084Y0{DSo5DwM?7KjJQfDbZ3 zF4znTKoQsl_JT@K1L{E|XaOfc2RIEbfXm=IxC!on2Vew@gXdrdyaDqN1YsdEM1kZX zRY(gmfXpBUWDmJPK2RVO4n;$85DyYUxzHA<2r7jtp<1XB`W89`U4X7a1JFHa6qn9`(3jA6(BtSg7z~Dn z(ZN_@JTc*z1k5^2G3EfK6>}alfEmNgVzF3xtO3>z>xX4x1=s@Ye(W*qIqV>I9QzhW z#Hr%UaPGJW91oX=E5|kA&f*4f6S#T26kZE&gZIO;@!9wid_BGke*-^`pC?EYbO?5Y zU_t_6GogaeLbybDNO(mg64i;;!~i0fxQSRnJWjkq93{RZ$&mC(E~H43khGI@gmj*C zkMxR6CTo)&$q{4$c_+D%e3AT^{8oY@VI<)t!Is!4Q6EtGo7CCWGzL)D>rQ4^>|)NiQ$)EQYB*=4e!vRSfKvS(yRXb4T4 z=0!`QmC#PmhG_4XC@*nZ!dbFoNz0PKC3A9$a*lEwxk9;CxjS<2<>~Tn@`>`hkG4N#KjNU~z;vi{c;cwx$aZXSoN&@}N^m;n^upQ1neW`@Jm+HLvfkyqE8^^jVTFG14;RpP@{Py@g^4IZC^Zz~o6W||E74S6BG%z=? zH;57x71R{;CfGT+B=|vyZiq0XJ5(|>GPE&tF3dHoG;Cy*@v8N!u7@jxbHh6$uo0mV z4H2`e-B#~iJsxQhSr9q2MrTddnyYIS)+Vhz6D1kNj5-;Ojt+}%ivGa#W7aWeW4vOj zV`f+`tbMHKY)5t(dx~SnDdkMW+QpW}PR7~A?TMR;cZe^KpXR!7E4eQdJQHdX<`Vr9 zk0dT6g(bBnMJ7e%MIVY;#n-+v{i@=tg`KfG`%5fK4(`J2;_VvR?Xdf3 zsdQ;h>DV6MJ?&-mvcj_0d!zPVEnik%vyZS(xNoGwr=oMe=Kfv#KUBt7-l=k~YOPkP z-cdbwfPG-_pyR=o8s(azn)ipehwj#T)V9}Y*Oec}9L_lWv_7=H_iM)2jSUJ7MGYU1 z@Q#ce4LsV@Xw}%*q|{W>3^xm#r;bG)yZMdlH=QkpEw!z*)}rI!xbXP1Z==5*I^lhy z`y}IJ%XeDeRku;v3frOf?DmPgz@Xmo#D^7KH*><&kZ}k0<(`u)y&d8oAIZHU3 ze|F(q&bit1spqFJ#9bKcj_Q7Jan;4!Jpn!am%J}sx$J)VVy{#0xhr;8PG7aTdg>bE zTE}(E>+O9OeQiHj{Lt2K+24M{>PF{H>ziEz%LmR5It*U8<$CM#ZLizc@2tEtFcdO$ zcQ|r*xkvZnNio#z9&IX9*nWZ zp8u5o(}(f=r{t&Q6RH!9lV+2rr`)G*K3n~4{CVp0`RRh6rGKt|q5I;yUmSnwn^`q8 z{*wQ4;n(6<@~@7(UiP|s)_?Z#o8&k1bA@l^-yVI(c-Q+r?ES=i<_GMDijR69yFPh; zdbp6hu<#rAg!B711SuW>000SaNLh0L01FcU01FcV0GgZ_0000NbVXQnQ*UN;cVTj6 z08nXoWo&I>bZ>GXGcGV3oVkSn000McNliru-3tv23mIuk#PR?D6!1wzK~#8Nt=vb* zb!XMa@nelWQBhO`QBa9-rZ^B(6ciM}HVC4`iAF3F2T4TiCHCIC(Zmva@4ffl6+sbE zXNu$VeZM>W4=bDVd{`JC7fBMt?FMjch z{ZD@KlRdq4cI%)2{O6b3hhP2bSIhRB-~49(v!DI!5DR?i_yC(d|LjTUXX8hTDOdXT z&g^VzQHBpm+yI zBowE(a^-JduKcrg@qpf*tutq%<*{gC8vW_rL%BL)@+gBrgerJsVU! znVzhP>1!=sp5o~8p@Z!C&$#x*Kux=NfBoxU_y7L)zxTiU-S5_3V>N_eQf@jq*u{VE zd*3_wAjdk16nZ}5b`mg>f@_4Xf;h5Ez-I5N+gCU{WUr~>t;LjAEdOF>j`Hxy?+<_Y z!~Q@2`Op61i!a_k@PQB9pMLu3``vE0f6|klv}~QHG5%eB=OYFX%WkcX?|%2YYfa|& zo$q{S*>(=5tO#ofIHhv{Hfu-`Kv&~1uD8F@jc&BR;SF!NKmYvm*VG`QdgaorHNAYKTI9FRpX5(Y(smU> zV!GfCh%{@G@3c)~tTjjG4{Ys|#aKI&e(FdQ_WR%eet)Ms-D!V~Yg}VZ$&GJ(<0Wr$ zlbh_XbDit#U-q(>EkE({bh}u$S2KI-Z1C-GfBQhn<%p@Eiea#(hsh{A*_vOZMiQhW zAwRj<+2qXDS=Nd}?jQg7$NiVT{N*(vCIt~^oN>kh_Sd@Bwf66M&wG}?2@zXfXQ{KZ z+489ezV_9MByc;eiIKQwVKygmdf>+v(px*I*bYDD*FL}2&eD)6`OIfNvz{R)q$vpz z!~U1Q{N-?tJY8`OllIn{RvRlK~zV@}to-sL<*q(#^#5|M_87aU=bLq-C3z_ls$fHx- zpa1;l{bL{d*!|V7e)ZLSlj5fjL=e4+GA(k5h4RR+{`$yA^L)sNU`uD8ZC65&kTMb? zB%6cSTTkSJm}2Rg52ei~~pmS6F7Af|Qt2+9|{vz&YG zxvOc-HznI?ft%j+ru%>X^PekDLtRYGdXA{WIc^E(=QI8Juz%$%Us*9=EeHD|hEoMv zGbp}hB=ln9b35)%ngTA%UF@QTFoDk@Rwulg}qV`N`GvB*Fu@X^E6K zzVVG~x|}ITHInjb8mzne&ivAszO;1SjgaSDallTW|ACm2k`V(AP(XWun&79|h?0oj zK63J@NH%M7Yx4lU;DQU*3mXFFErg)!U;p}x6dwzGaCDwn&2#}lec9xtv!AyJn>~E- zi(fqK*&|TiT?K21fs>-Q%>bRf1Ni1sVPG#lazG65r!OC!y^kUfdeDQ`XQ1tmXwMu! znm_#E53e8bvw68S0rJq*pr&h-&e@S6?(?7j{PL&EKT;Mkl;E(MhYql=h-~()i&t1V z@{jCDnl_+Ugekb!z3#P$X<|%|DLLnybJmAMxy06_y!PT-Yf=qONj8!_eYUMW66tr8 zHZg8bO2}CQ>8U4ZJ0b`N9{z zunLyHGkUg_w=2$WZ3=*nR4+u1ocR??XPS03C~<+D>?@pXA6fRr@Nq>o_@_VpX}w=a z{(%pCVE>6vd}8@|M{&z&#J&q5pB!Sm#UjH8NYAVY;49XrXufZK>s!}b?}Zm$xTL-F z*z=J)GVL`VA$0sBf^Q^#?sK18a@|TZAg8W4k`!ni?2)5r4^EA|{q1kxKls59-rwwI zH(Q^GUH#xq_7=Ce#r|Ord)WTXZ+`RPI~ZrxL)dy9*=$DTgKKt3vuJyW=6b76bIa`zLWW}d*s3~~*)1SVc z4GMD`_z41@6Mo#b_ua{9r=7O^tRMNvN3J4FndaqlF~IbE{No>AFMNE(c*n7J7TG#< zR?j9E!bLDa8Vv{n7XYk3{pn9H$sTb#MvO&9Kmr>=*t?>FHRCq%nWb;k6s5RHMwkr* zBx>NSv(8%NzW@F2-@p6a@7_QA+0WkJ^PcxyHb7=XoP?RzPq3tT&n`sksp-)dX<|qh z2<-K0y`C{OO({4+1W8DbQRKO89{>2qABwJs3YysJ=e~vzLc^79FAK6yJL0 z-Gc3k>_MI;wJBD!i>^0BAm>w``qWyxZRwF>f4G%2jxlH*DVzlK66SzNh=5~-9cwgQ zM5ND$lrI9p%r*DhymOBjQa)?sm_!eBKPgR&zD$W8n-+udv5$RhziXV30NqY>-FrUx zJ>@A+IZ6stSg1fvAYtxnjNIz8ZLzgYe{cr0=TmL+lgLs3*=L`A4J$k5|p*c*gp4uO~nPiHMJW^rP#u7_#g?^q~*!cahP9sfUp9GBK}u)vGRd3qgg2 zS#0~MJ83`lb6*IxnHQ$7gZmDEI68jDbJ(aQ7 zh|&3FmgEYW(-J8aQN!B;+h;!sGX^55eSY*aE^PhEam!oYa=rO#Uc(yKo5uSb=eB`J zf)9T1gO?H0lo%zW#>q2hzO!^IRYa3>jEMFpVK#6KY^R!>8Q-~qP2K(gK%*Mho4zzn z*VIIewYP76ix{_%4PrP0qP(|AULj4)EQUUL46a&Nxj0DQej7)JkQu+VZ*@>D-dFB? z=R2=2wjxDeO-$sNqSkDD)9onTK7t!9Nw60&9atlQeqw8{U~3I-CtyoZIB8OXm}BRq zQ}-)g@ru=lCP@vuee|a{(mq^z>7~nugk3BOMh>Ir%afn{`WD`>3O^Z+RblKng-uJG( z!MNm-OD>xjq;wnEd%?TQUGB0D>AYeX==jyj<@p|IhKhv~pO1g~%`H5)0e{hUpWgj?Y#%*VPpN9SX9E{Y&4}^F?!{GK`q4BJF1qNVrHd@G ziHfNFcgWz}v~V;repTUcw-LD|Xck%n+kM2e-p19`)NR3blFwGxd45Bnhj+Z=9g7T8 zK)U#v*1VpP0d(-Tx4muonyMYPkb*6eqpurofBW07AJ>h0#f1u+>l%ew=oW1Gl-K$s zbWS))-`S?d!&grl)@%e!nH^E~kw~|rj6o!s8b6`>6-K0-s)1v+3XYKW`Tk$?ZuQ;% z>VC{)9&@-|On`MHAfVfajUMRP*TlGWCUgB{@(VEsDBPi^Y5*#D>vwEYpojVu}^-17!IdxBq5hCMvM~BnE0E z-}GB#>-^T`pZffZX=*%I5TGv&M^d+!HL@awe9K$jvfp*DA;?-lx0K{P?JKAvt_WKq z+Rws|`E9{>+x(o2F!Q7nqka!m&HBavTqDFEA|b`^#_c^$kix#>h!9NnRJQgtr^F7c zI0shXM8!Xa(W@|l>PkHidpPwo9Zlf=c zj7f^y30Z7zDK_Nn{D%@LjTQ&G#oYfAvpqP}aCVr)Z0+s0U^~|6d_ZTLNLQZpGjBfi z;V-rP!3UN5)1-R)NMJ#)bNb+3Efl1SPi0Xfjar_sXR!yo?e6Q{=E=_k@}?G-$AQ(q(9 z0yQ)3(}$dO~3|d zTOG)$(e2^ksx~t7w!W&lwvb2t{&G}Ldei5tU;XL@WZ3K9e%(rqlmKf%jTrEG+S8tP zIbya4AH!_!%EtVEV0$j89lW3EPxA)B`jxMI@Bp#l|5&R?YaFV^1CvRjiw^_t)-p~fm>_+=?kYwXj)=!>Pi2`c@ncNdaK(O zm{0a_D%~;r6`}LQbuRcKuF256eoRYbq^H|8I>6DsTL_a8gIO#LH=^vThsX(MGuX}CPqL6`q0_o z6N)%SOu8_2<;j1G{Epd9!mO<`{4-?F77e@j4FOU>uT4smvu-7eCc_%6IbkqXWAmBM zeC9e(A@hpY2+&OuCg{U#Doo$<O#BY@O&Bp}B-nu%d| z@qyoAV%#uh%!kLLLajdH#xJY{}!fAh%98#H9@g#U2=Y)keaMf`+1wCpZj8Fb3udCZ{|-YmyZqQr4#E9 zp1pUy-~}&Ob({KXCn1s$!G~|t!nP(RBD!rNg%oJbhZH=MBCgL;no(R|SocK}9BUx> z<=%bod*AgHhd*h*$356>Ac9|>r6j^)Cezp;R{!@YE-YDtkqBQp||Gi zfhe)#LhyafECAW5f#MUIe=hA)d zbDzrk9yRj4u84i?^xtllry@zq<`<&751f>{~3no#<9F6&L0c;CO}>4jFg(T zcE&b8y?qeRXFX#$XiUQP8jc*&0qh+tn4YhF{;s;1$35v@UN@xHB}Pcy;+a2T1T^SWGXW>@_d8e@H5KIrz6u51rj!>`tLIg&f7u#WX32 zWbAZFNyL$K4iHZw2NWy^J&2R5rWIsQ`h%Ycv6rWFE1F(C#n)`UWLI83GMza2t=UK@ zw|d$2wDTXuM~cxUK|>0GigHL-rv@ZaO<1#m9OcU2 zI-g{9$pOwQhnVW%H}foFA|T1JqOeF-7&}5F1j%ZuBC}88z~Ox4?woY9!2IP(FIP?0 z#5PXj)l)4bY;}CedP*-3-7W%1!4Bd$rzIQE3pQc%N_KGhrcO?l>eSQDH!2k60|7&aiFE9U8RR6fR z|4U2%(9r+c+5cNx|H;Y!jEw(rasSlR|E{k8kdXgqX#Yb)|0pQ`pP&CeKL0j0{}dGe zcX$6EAOB%t|Fg6IsHp#khyR+I|4mDKpa1{`$Vo&&RA_?!khCJ z`4izEE&i2=*j#+HmQ0pg^v0`PT7{7F%w)1rnUE9ivHiPh5kgX5Z*x?ekQnHj6;I6~ z1=={B=xq*T;J(|7ei2=)41P>Du(L~9>f--o!!$mvVV%aB%hgDZ3uarJ#DZD ztU=DgcS|5fWN7<;E-c3I)v?Ta*>K z%Lzf3JG3MYzi!D>*w~Oebc8*jGyz$&0T%2WRv`D_>|#d9u<Pf!w9R4aQ5j_p&LEjh%JbCyIFCmCAzE#I9nmESe3fp4lCOXY8?dH{Vw z-D_tQy^r5ip`>B()FcSw8dJ5=0XG!-stC&?vyoybBe}YJGVv=#SZ4oPQURDa4gV|t z?dg1rgKygxu7XGZ&8%{Q+4ziiCXV}-jN6t4{<)fjAXg@JJ$BLs|^K7xAQl!us zrAP(}c1G<|)5F~{SybC!m^5;I&zyiaj`S)|t_zjGqzo-!>gCux%&WiK!)Q~u2%xxI zMGr*->xDKW659f7tMn|MgXm`gX!tjFf&!2-}A%2KA%K3ryH3|TMnT!g@)|8`P#*a{Y9_8(Ue z7TqCG(-xS2D%e;))q)To%dj51X>pZO^T6s#AHt^aLUNCSUF0Oxk~Qn0&`iP+`u2>g zKooi2b`ycp$)K7N0@i(cxDm81T?+Pp8i1nKlJ;l>wKP|_AMABa5i4WCc2jUyQ;9fN4H)WSOv}tS0mvrV;@DuP8cg2c>S&61Cd61xG zfSLNiH!9RL$N!9)raI|il#u!x8PE1%1m#Y1>PDJd6uggj5b@X~JI#{z-}=UyUMdPw zwl~GT^Amg3s9i^LUKYm3nSJ+IMQ^B*g)Bu$aerZzS@jXc&g3S1tf}<`aL*f5NBJ&0 z3s(XlL+YCOs{POG;kI*;{hFnq*GAf`)ueIPotx1SepLK<tiqQmp$9Z zb{mgt#B+TV#u8x$d|<7^Q3mPM3Dt~U9=q$YVhujF(J`z8aW%1I`K>%vIobx4$XEQy=Q3?hTJS<<|z?(EC0?*rope Y0Ux+h-h`XKmH+?%07*qoM6N<$f;N0qmH+?% literal 0 HcmV?d00001 diff --git a/oxAuth/Server/src/main/webapp/auth/passport/img/facebook.png b/oxAuth/Server/src/main/webapp/auth/passport/img/facebook.png new file mode 100644 index 0000000000000000000000000000000000000000..6f2d7a0f1eb0511012e8114ee9805eb67db29f7d GIT binary patch literal 4822 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

    NnlU$0zi`cjMZF>+M!v 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 literal 0 HcmV?d00001 diff --git a/oxAuth/Server/src/main/webapp/auth/passport/img/google.png b/oxAuth/Server/src/main/webapp/auth/passport/img/google.png new file mode 100644 index 0000000000000000000000000000000000000000..f92017b3bf7fe2e92e12bf5bb0bc2727a5377a7c GIT binary patch literal 105019 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%0AZ}-GYb$jpA*3`1mxFq_^EkDf458_ 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

    sJF-Bm|iKbAFdy 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 LQ1kf-s~i6Vya!of literal 0 HcmV?d00001 diff --git a/oxAuth/Server/src/main/webapp/auth/passport/img/openidconnect.png b/oxAuth/Server/src/main/webapp/auth/passport/img/openidconnect.png new file mode 100644 index 0000000000000000000000000000000000000000..ef62a545def330af0a1dbf713d75cf653861ff9e GIT binary patch literal 1670 zcmaJ?TT~K=7DW@%5QS9K$tDz?p-`K$a+*vN0VQwwNadoLj|g-sbkZeRmS_>7HTfWv z;7X`t-V`*{@W#T^lE%Z^Oo&SJ+o@1}vwA}V6dH1EP+U4yF z>q)|hN>CW_V}mSs5R__n`IBE^QTP=y++S@;yXiVV9LSBMGY>8I-z222IJ}=P+uUD9 z-gpp4=T&d+A?(^hUrLdNaxX`}obMT*TCHJMXOBCXPF)+ubK`Ej#pYEZpZ4XP==^^` zOi10nH5HeaD_UC(AP~qv6}5k5_b+fbJSILq>s&($Au}_xS|&Ti&Ua&fG4CB1fQcoN z!B@3$BNwSu)_AfX9uKOiuFiV9$$TLjUo4w2`qw~DPmiLb!?1C!fWtB0heAE}&2xT6 zq)@(VG&?qFr(Ide=jP>+zzmc?3qzAcQ&UrlhiKx=?ElI*1qFQ-v)dphtXo3n_a&q8 zsj0%YwzioGIcqBEIf?GF)51@2xV^o7gziaf_fnpZjb+!@*Z+I}4|d;45t=_TEmb2U zgfnN(I6992tF5XRp7;p^>2z1ISUi6W8x|I(8X9V!+|wd&Zq{owD<~x=%g!x*Ld4Q& zebNy7uC_K%t{L>x%KU4y1rt-#)qtyZTxR6P%mDI{K)~U0xLiDw$qXWq2(`4)N#Y-S zot;b6>d<{?bkX8;Kfmo!WR~H_(Sgqxc@3}puD;vgW*M=fc<``6ctZ*gI109O7ObTm z)N?mq_i6R!z?z*LtWpY;8v9eqm50ip;NSop4(M_0eo%M1V7gyewT2H;)g?^NyzS+y z)asM2v_B&?;Ux}t8JCEmp(pR(zn?fac09>u-P$=X^F{dl(Gy<(vUE9w3veOmMcaY>fQ4DX*rr@&vEqAyVOFC5tDx1+Yl@0)$-HxTMKqFDt95+MOZ#1+P&CyGED&9}0bcOg7gw`qXJUe6kdgjCE^PuqXQ#c$BBj3%~F@|ce?!pXtbsyXo zQCeJlTSzhuwd~4WSh(`EqJqk^dhJ)8!U6rxI%sEu7kB%=eD}`LV{m(fvAF94!le+3 zcVdPNjg9qn8MhMMV#oo|?D8g6hV_sn{5+5IB)3sBR&X)9v@kDrizO_`&CVLY@ zl9G<-s$B}NUtby%Tlywmcr!h{h&=xTdr9;d4Q}4`P-3lADqGGVI)9ZoA_BcclNQ!7 zLZI=5C{S~0A%tP^ANt(bd{tGZ$dLoGs^h2CAv5fN2}3@R#%gi$+Tl;^IT+4VRXAO zw}5=&4o-`mY`RXPIrEVZD7c>jur(gw=hQna7L(g~uPH`kcY~L)wfbv>kze3es@FS- z*I(_@qeo(}J1F~?4q1A*AbQp8w)_w4@)Bdm_`;Vt8`SQqT21am-0BFyX4PWq>DHsO zLidkPD2x9sx(ds?oD;LRBBGH}p*UWA?RM@PRQYe5{zh9WscOsq>1n9hq#fF-1A}ip z3A^KU&BeS~D!8z`;iN%dfgi3+cn+nLnj?*kqWFCNJ0=chZ`8A#DyZ@mCa-k_e{neK zesIr&P*2PSP1=Hd`gpO*c+uJf{ft>oZeAkJh$ih6Q1w)hkIYYocHVAd(OG9v&7st)IP&1zzPC^&gkkOQ2_q>cR@}A z_cF8c_W&?>qqH?Z<$Z!nAP~2gE>hhjfMM+&i;u|&e@Bb-I6@a<4#7l%?2|Mlz!ACE zxgK$k){QZ_uMAE&)|S-}y@NiMy(Bt}UlVp-26Jy0{(h){z- zHTG){5~2*9r+OykH~s`}^iik17alD%Z%k4DOv|4%k|eo}Lq_JFYA`ZD`^EaT5qz02 z5Tg(y6w(ZVK_fw6E-*Vg1_X}S071YaAPg9Ty#oVbW^REP)gfvy^_dwc02KCcH7Fxg z2dba|W~|DEtnbyWAh-^QkrT{>aN!D9%i{OnwMYbk zFG5(I`@}#P1;jeJ7RCTULeKVO>4F$NohJv})ES}CF!2n=yI^JH+1v8#aI#tlrv_3D~*(ywJo85JUb)ULbt4FSf6pyMENO zs>)}L#$RK%PckvdxVN?CiHV0R4EB#9tDjOZOmJ<}WT?)$t)8x)9PO|jZUJ+Xb6QKk z&UzM#X%~DD_h4ci;I|7c2yhtZ5|9(Kbh5@}PS@>lei zcosPN?JrF1{&;;Lz{~8Bzdm``AK!ZMI^Z96G}9?>U$d8TWlDZGYdEi8zXZaNzuTH6 z7XDDGSsGa8{t$0xF98IA`Pk@kLa>jo|B%bePd+v5#@B!tM+{Tj+xw&KnIecEdP@@C z!Bi&VA9(<@b|Fy+Pq9mvSS?=4{cbpU_E^-+%n%6kf9hM58xd&^&OEpFWBJ>

    ko8 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 pUwJrpZPUE_r1tkO_||nBXO + + + + + + + + + + + + + + + + +

    + + + + + diff --git a/oxAuth/Server/src/main/webapp/auth/passport/passportpostlogin.xhtml b/oxAuth/Server/src/main/webapp/auth/passport/passportpostlogin.xhtml new file mode 100755 index 00000000..9d861848 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/passport/passportpostlogin.xhtml @@ -0,0 +1,66 @@ + + + + + + + + + #{msgs['passport.oxAuthPassportLogin']} + + +
    + +
    + + +
    +
    diff --git a/oxAuth/Server/src/main/webapp/auth/passport/sample-redirector.xhtml b/oxAuth/Server/src/main/webapp/auth/passport/sample-redirector.xhtml new file mode 100644 index 00000000..81dc392d --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/passport/sample-redirector.xhtml @@ -0,0 +1,22 @@ + + + + + + + + + If you are seeing this message, no relayState cookie was found. No final redirect could be made. + + + diff --git a/oxAuth/Server/src/main/webapp/auth/phonefactor/pflogin.xhtml b/oxAuth/Server/src/main/webapp/auth/phonefactor/pflogin.xhtml new file mode 100644 index 00000000..4099fc80 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/phonefactor/pflogin.xhtml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + #{msgs['phonefactor.pageTitle']} + + +
    + + + +
    +
    +
    \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/auth/pwd/newpassword.xhtml b/oxAuth/Server/src/main/webapp/auth/pwd/newpassword.xhtml new file mode 100644 index 00000000..5fca4df0 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/pwd/newpassword.xhtml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + +
    +
    +
    + +
    + + + + +
    +
    +

    +
    + +
    +
    + + +
    + +
    +
    +
    +
    +
    +
    diff --git a/oxAuth/Server/src/main/webapp/auth/recaptcha/login.xhtml b/oxAuth/Server/src/main/webapp/auth/recaptcha/login.xhtml new file mode 100644 index 00000000..71bf3554 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/recaptcha/login.xhtml @@ -0,0 +1,187 @@ + + + + + + + + + + + + + +
    + + + +
    + + + + +
    + oxAuth - Super-Gluu Login + + +
    + +
    + + + + + +
    +
    + + + + + +
    +
    +
    + + +
    +
    +

    #{msgs['supergluu.enroll.enrollyourdevice']}

    +

    +

    #{msgs['supergluu.enroll.scanqrcode']}

    +
    +
    +
    +
    +

    +
    +
    +
    +
    +

    #{msgs['supergluu.enroll.downloadsupergluu']}

    +
    + + + + + + + +
    +

    #{msgs['supergluu.enroll.supergluuisfreesecure']}

    + +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    + + +

    + A Super Gluu push notification has been sent to your
    + registered device. Approve the push to login. +

    +
    +
    +

    Didn't receive the push notification?

    + Scan + QR code instead If you no + longer control the device you registered,
    contact + your Gluu server admin to enroll a new device +
    +
    +
    +
    + +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/oxAuth/Server/src/main/webapp/auth/thumbsignin/README.md b/oxAuth/Server/src/main/webapp/auth/thumbsignin/README.md new file mode 100644 index 00000000..2e72309e --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/thumbsignin/README.md @@ -0,0 +1,3 @@ +# ThumbSignIn UI Files + +This folder contains the UI files to handle ThumbSignIn autentication and registration flow. diff --git a/oxAuth/Server/src/main/webapp/auth/thumbsignin/expired.xhtml b/oxAuth/Server/src/main/webapp/auth/thumbsignin/expired.xhtml new file mode 100644 index 00000000..a09ab0b7 --- /dev/null +++ b/oxAuth/Server/src/main/webapp/auth/thumbsignin/expired.xhtml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + +
    +
    +
    +

    Oops ! Session Expired.

    +
    Thank you for using ThumbSignIn Idp.
    + + + +
    ");l.appendTo("body").addClass(t.className).css({top:s.top-a,left:s.left-r,height:i.innerHeight(),width:i.innerWidth(),position:n?"fixed":"absolute"}).animate(o,t.duration,t.easing,function(){l.remove(),"function"==typeof e&&e()})}}),V.fx.step.clip=function(t){t.clipInit||(t.start=V(t.elem).cssClip(),"string"==typeof t.end&&(t.end=G(t.end,t.elem)),t.clipInit=!0),V(t.elem).cssClip({top:t.pos*(t.end.top-t.start.top)+t.start.top,right:t.pos*(t.end.right-t.start.right)+t.start.right,bottom:t.pos*(t.end.bottom-t.start.bottom)+t.start.bottom,left:t.pos*(t.end.left-t.start.left)+t.start.left})},Y={},V.each(["Quad","Cubic","Quart","Quint","Expo"],function(e,t){Y[t]=function(t){return Math.pow(t,e+2)}}),V.extend(Y,{Sine:function(t){return 1-Math.cos(t*Math.PI/2)},Circ:function(t){return 1-Math.sqrt(1-t*t)},Elastic:function(t){return 0===t||1===t?t:-Math.pow(2,8*(t-1))*Math.sin((80*(t-1)-7.5)*Math.PI/15)},Back:function(t){return t*t*(3*t-2)},Bounce:function(t){for(var e,i=4;t<((e=Math.pow(2,--i))-1)/11;);return 1/Math.pow(4,3-i)-7.5625*Math.pow((3*e-2)/22-t,2)}}),V.each(Y,function(t,e){V.easing["easeIn"+t]=e,V.easing["easeOut"+t]=function(t){return 1-e(1-t)},V.easing["easeInOut"+t]=function(t){return t<.5?e(2*t)/2:1-e(-2*t+2)/2}});y=V.effects,V.effects.define("blind","hide",function(t,e){var i={up:["bottom","top"],vertical:["bottom","top"],down:["top","bottom"],left:["right","left"],horizontal:["right","left"],right:["left","right"]},s=V(this),n=t.direction||"up",o=s.cssClip(),a={clip:V.extend({},o)},r=V.effects.createPlaceholder(s);a.clip[i[n][0]]=a.clip[i[n][1]],"show"===t.mode&&(s.cssClip(a.clip),r&&r.css(V.effects.clipToBox(a)),a.clip=o),r&&r.animate(V.effects.clipToBox(a),t.duration,t.easing),s.animate(a,{queue:!1,duration:t.duration,easing:t.easing,complete:e})}),V.effects.define("bounce",function(t,e){var i,s,n=V(this),o=t.mode,a="hide"===o,r="show"===o,l=t.direction||"up",h=t.distance,c=t.times||5,o=2*c+(r||a?1:0),u=t.duration/o,d=t.easing,p="up"===l||"down"===l?"top":"left",f="up"===l||"left"===l,g=0,t=n.queue().length;for(V.effects.createPlaceholder(n),l=n.css(p),h=h||n["top"==p?"outerHeight":"outerWidth"]()/3,r&&((s={opacity:1})[p]=l,n.css("opacity",0).css(p,f?2*-h:2*h).animate(s,u,d)),a&&(h/=Math.pow(2,c-1)),(s={})[p]=l;g").css({position:"absolute",visibility:"visible",left:-s*p,top:-i*f}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:p,height:f,left:n+(u?a*p:0),top:o+(u?r*f:0),opacity:u?0:1}).animate({left:n+(u?0:a*p),top:o+(u?0:r*f),opacity:u?1:0},t.duration||500,t.easing,m)}),V.effects.define("fade","toggle",function(t,e){var i="show"===t.mode;V(this).css("opacity",i?0:1).animate({opacity:i?1:0},{queue:!1,duration:t.duration,easing:t.easing,complete:e})}),V.effects.define("fold","hide",function(e,t){var i=V(this),s=e.mode,n="show"===s,o="hide"===s,a=e.size||15,r=/([0-9]+)%/.exec(a),l=!!e.horizFirst?["right","bottom"]:["bottom","right"],h=e.duration/2,c=V.effects.createPlaceholder(i),u=i.cssClip(),d={clip:V.extend({},u)},p={clip:V.extend({},u)},f=[u[l[0]],u[l[1]]],s=i.queue().length;r&&(a=parseInt(r[1],10)/100*f[o?0:1]),d.clip[l[0]]=a,p.clip[l[0]]=a,p.clip[l[1]]=0,n&&(i.cssClip(p.clip),c&&c.css(V.effects.clipToBox(p)),p.clip=u),i.queue(function(t){c&&c.animate(V.effects.clipToBox(d),h,e.easing).animate(V.effects.clipToBox(p),h,e.easing),t()}).animate(d,h,e.easing).animate(p,h,e.easing).queue(t),V.effects.unshift(i,s,4)}),V.effects.define("highlight","show",function(t,e){var i=V(this),s={backgroundColor:i.css("backgroundColor")};"hide"===t.mode&&(s.opacity=0),V.effects.saveStyle(i),i.css({backgroundImage:"none",backgroundColor:t.color||"#ffff99"}).animate(s,{queue:!1,duration:t.duration,easing:t.easing,complete:e})}),V.effects.define("size",function(s,e){var n,i=V(this),t=["fontSize"],o=["borderTopWidth","borderBottomWidth","paddingTop","paddingBottom"],a=["borderLeftWidth","borderRightWidth","paddingLeft","paddingRight"],r=s.mode,l="effect"!==r,h=s.scale||"both",c=s.origin||["middle","center"],u=i.css("position"),d=i.position(),p=V.effects.scaledDimensions(i),f=s.from||p,g=s.to||V.effects.scaledDimensions(i,0);V.effects.createPlaceholder(i),"show"===r&&(r=f,f=g,g=r),n={from:{y:f.height/p.height,x:f.width/p.width},to:{y:g.height/p.height,x:g.width/p.width}},"box"!==h&&"both"!==h||(n.from.y!==n.to.y&&(f=V.effects.setTransition(i,o,n.from.y,f),g=V.effects.setTransition(i,o,n.to.y,g)),n.from.x!==n.to.x&&(f=V.effects.setTransition(i,a,n.from.x,f),g=V.effects.setTransition(i,a,n.to.x,g))),"content"!==h&&"both"!==h||n.from.y!==n.to.y&&(f=V.effects.setTransition(i,t,n.from.y,f),g=V.effects.setTransition(i,t,n.to.y,g)),c&&(c=V.effects.getBaseline(c,p),f.top=(p.outerHeight-f.outerHeight)*c.y+d.top,f.left=(p.outerWidth-f.outerWidth)*c.x+d.left,g.top=(p.outerHeight-g.outerHeight)*c.y+d.top,g.left=(p.outerWidth-g.outerWidth)*c.x+d.left),delete f.outerHeight,delete f.outerWidth,i.css(f),"content"!==h&&"both"!==h||(o=o.concat(["marginTop","marginBottom"]).concat(t),a=a.concat(["marginLeft","marginRight"]),i.find("*[width]").each(function(){var t=V(this),e=V.effects.scaledDimensions(t),i={height:e.height*n.from.y,width:e.width*n.from.x,outerHeight:e.outerHeight*n.from.y,outerWidth:e.outerWidth*n.from.x},e={height:e.height*n.to.y,width:e.width*n.to.x,outerHeight:e.height*n.to.y,outerWidth:e.width*n.to.x};n.from.y!==n.to.y&&(i=V.effects.setTransition(t,o,n.from.y,i),e=V.effects.setTransition(t,o,n.to.y,e)),n.from.x!==n.to.x&&(i=V.effects.setTransition(t,a,n.from.x,i),e=V.effects.setTransition(t,a,n.to.x,e)),l&&V.effects.saveStyle(t),t.css(i),t.animate(e,s.duration,s.easing,function(){l&&V.effects.restoreStyle(t)})})),i.animate(g,{queue:!1,duration:s.duration,easing:s.easing,complete:function(){var t=i.offset();0===g.opacity&&i.css("opacity",f.opacity),l||(i.css("position","static"===u?"relative":u).offset(t),V.effects.saveStyle(i)),e()}})}),V.effects.define("scale",function(t,e){var i=V(this),s=t.mode,s=parseInt(t.percent,10)||(0===parseInt(t.percent,10)||"effect"!==s?0:100),s=V.extend(!0,{from:V.effects.scaledDimensions(i),to:V.effects.scaledDimensions(i,s,t.direction||"both"),origin:t.origin||["middle","center"]},t);t.fade&&(s.from.opacity=1,s.to.opacity=0),V.effects.effect.size.call(this,s,e)}),V.effects.define("puff","hide",function(t,e){t=V.extend(!0,{},t,{fade:!0,percent:parseInt(t.percent,10)||150});V.effects.effect.scale.call(this,t,e)}),V.effects.define("pulsate","show",function(t,e){var i=V(this),s=t.mode,n="show"===s,o=2*(t.times||5)+(n||"hide"===s?1:0),a=t.duration/o,r=0,l=1,s=i.queue().length;for(!n&&i.is(":visible")||(i.css("opacity",0).show(),r=1);l li > :first-child").add(t.find("> :not(li)").even())},heightStyle:"auto",icons:{activeHeader:"ui-icon-triangle-1-s",header:"ui-icon-triangle-1-e"},activate:null,beforeActivate:null},hideProps:{borderTopWidth:"hide",borderBottomWidth:"hide",paddingTop:"hide",paddingBottom:"hide",height:"hide"},showProps:{borderTopWidth:"show",borderBottomWidth:"show",paddingTop:"show",paddingBottom:"show",height:"show"},_create:function(){var t=this.options;this.prevShow=this.prevHide=V(),this._addClass("ui-accordion","ui-widget ui-helper-reset"),this.element.attr("role","tablist"),t.collapsible||!1!==t.active&&null!=t.active||(t.active=0),this._processPanels(),t.active<0&&(t.active+=this.headers.length),this._refresh()},_getCreateEventData:function(){return{header:this.active,panel:this.active.length?this.active.next():V()}},_createIcons:function(){var t,e=this.options.icons;e&&(t=V(""),this._addClass(t,"ui-accordion-header-icon","ui-icon "+e.header),t.prependTo(this.headers),t=this.active.children(".ui-accordion-header-icon"),this._removeClass(t,e.header)._addClass(t,null,e.activeHeader)._addClass(this.headers,"ui-accordion-icons"))},_destroyIcons:function(){this._removeClass(this.headers,"ui-accordion-icons"),this.headers.children(".ui-accordion-header-icon").remove()},_destroy:function(){var t;this.element.removeAttr("role"),this.headers.removeAttr("role aria-expanded aria-selected aria-controls tabIndex").removeUniqueId(),this._destroyIcons(),t=this.headers.next().css("display","").removeAttr("role aria-hidden aria-labelledby").removeUniqueId(),"content"!==this.options.heightStyle&&t.css("height","")},_setOption:function(t,e){"active"!==t?("event"===t&&(this.options.event&&this._off(this.headers,this.options.event),this._setupEvents(e)),this._super(t,e),"collapsible"!==t||e||!1!==this.options.active||this._activate(0),"icons"===t&&(this._destroyIcons(),e&&this._createIcons())):this._activate(e)},_setOptionDisabled:function(t){this._super(t),this.element.attr("aria-disabled",t),this._toggleClass(null,"ui-state-disabled",!!t),this._toggleClass(this.headers.add(this.headers.next()),null,"ui-state-disabled",!!t)},_keydown:function(t){if(!t.altKey&&!t.ctrlKey){var e=V.ui.keyCode,i=this.headers.length,s=this.headers.index(t.target),n=!1;switch(t.keyCode){case e.RIGHT:case e.DOWN:n=this.headers[(s+1)%i];break;case e.LEFT:case e.UP:n=this.headers[(s-1+i)%i];break;case e.SPACE:case e.ENTER:this._eventHandler(t);break;case e.HOME:n=this.headers[0];break;case e.END:n=this.headers[i-1]}n&&(V(t.target).attr("tabIndex",-1),V(n).attr("tabIndex",0),V(n).trigger("focus"),t.preventDefault())}},_panelKeyDown:function(t){t.keyCode===V.ui.keyCode.UP&&t.ctrlKey&&V(t.currentTarget).prev().trigger("focus")},refresh:function(){var t=this.options;this._processPanels(),!1===t.active&&!0===t.collapsible||!this.headers.length?(t.active=!1,this.active=V()):!1===t.active?this._activate(0):this.active.length&&!V.contains(this.element[0],this.active[0])?this.headers.length===this.headers.find(".ui-state-disabled").length?(t.active=!1,this.active=V()):this._activate(Math.max(0,t.active-1)):t.active=this.headers.index(this.active),this._destroyIcons(),this._refresh()},_processPanels:function(){var t=this.headers,e=this.panels;"function"==typeof this.options.header?this.headers=this.options.header(this.element):this.headers=this.element.find(this.options.header),this._addClass(this.headers,"ui-accordion-header ui-accordion-header-collapsed","ui-state-default"),this.panels=this.headers.next().filter(":not(.ui-accordion-content-active)").hide(),this._addClass(this.panels,"ui-accordion-content","ui-helper-reset ui-widget-content"),e&&(this._off(t.not(this.headers)),this._off(e.not(this.panels)))},_refresh:function(){var i,t=this.options,e=t.heightStyle,s=this.element.parent();this.active=this._findActive(t.active),this._addClass(this.active,"ui-accordion-header-active","ui-state-active")._removeClass(this.active,"ui-accordion-header-collapsed"),this._addClass(this.active.next(),"ui-accordion-content-active"),this.active.next().show(),this.headers.attr("role","tab").each(function(){var t=V(this),e=t.uniqueId().attr("id"),i=t.next(),s=i.uniqueId().attr("id");t.attr("aria-controls",s),i.attr("aria-labelledby",e)}).next().attr("role","tabpanel"),this.headers.not(this.active).attr({"aria-selected":"false","aria-expanded":"false",tabIndex:-1}).next().attr({"aria-hidden":"true"}).hide(),this.active.length?this.active.attr({"aria-selected":"true","aria-expanded":"true",tabIndex:0}).next().attr({"aria-hidden":"false"}):this.headers.eq(0).attr("tabIndex",0),this._createIcons(),this._setupEvents(t.event),"fill"===e?(i=s.height(),this.element.siblings(":visible").each(function(){var t=V(this),e=t.css("position");"absolute"!==e&&"fixed"!==e&&(i-=t.outerHeight(!0))}),this.headers.each(function(){i-=V(this).outerHeight(!0)}),this.headers.next().each(function(){V(this).height(Math.max(0,i-V(this).innerHeight()+V(this).height()))}).css("overflow","auto")):"auto"===e&&(i=0,this.headers.next().each(function(){var t=V(this).is(":visible");t||V(this).show(),i=Math.max(i,V(this).css("height","").height()),t||V(this).hide()}).height(i))},_activate:function(t){t=this._findActive(t)[0];t!==this.active[0]&&(t=t||this.active[0],this._eventHandler({target:t,currentTarget:t,preventDefault:V.noop}))},_findActive:function(t){return"number"==typeof t?this.headers.eq(t):V()},_setupEvents:function(t){var i={keydown:"_keydown"};t&&V.each(t.split(" "),function(t,e){i[e]="_eventHandler"}),this._off(this.headers.add(this.headers.next())),this._on(this.headers,i),this._on(this.headers.next(),{keydown:"_panelKeyDown"}),this._hoverable(this.headers),this._focusable(this.headers)},_eventHandler:function(t){var e=this.options,i=this.active,s=V(t.currentTarget),n=s[0]===i[0],o=n&&e.collapsible,a=o?V():s.next(),r=i.next(),a={oldHeader:i,oldPanel:r,newHeader:o?V():s,newPanel:a};t.preventDefault(),n&&!e.collapsible||!1===this._trigger("beforeActivate",t,a)||(e.active=!o&&this.headers.index(s),this.active=n?V():s,this._toggle(a),this._removeClass(i,"ui-accordion-header-active","ui-state-active"),e.icons&&(i=i.children(".ui-accordion-header-icon"),this._removeClass(i,null,e.icons.activeHeader)._addClass(i,null,e.icons.header)),n||(this._removeClass(s,"ui-accordion-header-collapsed")._addClass(s,"ui-accordion-header-active","ui-state-active"),e.icons&&(n=s.children(".ui-accordion-header-icon"),this._removeClass(n,null,e.icons.header)._addClass(n,null,e.icons.activeHeader)),this._addClass(s.next(),"ui-accordion-content-active")))},_toggle:function(t){var e=t.newPanel,i=this.prevShow.length?this.prevShow:t.oldPanel;this.prevShow.add(this.prevHide).stop(!0,!0),this.prevShow=e,this.prevHide=i,this.options.animate?this._animate(e,i,t):(i.hide(),e.show(),this._toggleComplete(t)),i.attr({"aria-hidden":"true"}),i.prev().attr({"aria-selected":"false","aria-expanded":"false"}),e.length&&i.length?i.prev().attr({tabIndex:-1,"aria-expanded":"false"}):e.length&&this.headers.filter(function(){return 0===parseInt(V(this).attr("tabIndex"),10)}).attr("tabIndex",-1),e.attr("aria-hidden","false").prev().attr({"aria-selected":"true","aria-expanded":"true",tabIndex:0})},_animate:function(t,i,e){var s,n,o,a=this,r=0,l=t.css("box-sizing"),h=t.length&&(!i.length||t.index()",delay:300,options:{icons:{submenu:"ui-icon-caret-1-e"},items:"> *",menus:"ul",position:{my:"left top",at:"right top"},role:"menu",blur:null,focus:null,select:null},_create:function(){this.activeMenu=this.element,this.mouseHandled=!1,this.lastMousePosition={x:null,y:null},this.element.uniqueId().attr({role:this.options.role,tabIndex:0}),this._addClass("ui-menu","ui-widget ui-widget-content"),this._on({"mousedown .ui-menu-item":function(t){t.preventDefault(),this._activateItem(t)},"click .ui-menu-item":function(t){var e=V(t.target),i=V(V.ui.safeActiveElement(this.document[0]));!this.mouseHandled&&e.not(".ui-state-disabled").length&&(this.select(t),t.isPropagationStopped()||(this.mouseHandled=!0),e.has(".ui-menu").length?this.expand(t):!this.element.is(":focus")&&i.closest(".ui-menu").length&&(this.element.trigger("focus",[!0]),this.active&&1===this.active.parents(".ui-menu").length&&clearTimeout(this.timer)))},"mouseenter .ui-menu-item":"_activateItem","mousemove .ui-menu-item":"_activateItem",mouseleave:"collapseAll","mouseleave .ui-menu":"collapseAll",focus:function(t,e){var i=this.active||this._menuItems().first();e||this.focus(t,i)},blur:function(t){this._delay(function(){V.contains(this.element[0],V.ui.safeActiveElement(this.document[0]))||this.collapseAll(t)})},keydown:"_keydown"}),this.refresh(),this._on(this.document,{click:function(t){this._closeOnDocumentClick(t)&&this.collapseAll(t,!0),this.mouseHandled=!1}})},_activateItem:function(t){var e,i;this.previousFilter||t.clientX===this.lastMousePosition.x&&t.clientY===this.lastMousePosition.y||(this.lastMousePosition={x:t.clientX,y:t.clientY},e=V(t.target).closest(".ui-menu-item"),i=V(t.currentTarget),e[0]===i[0]&&(i.is(".ui-state-active")||(this._removeClass(i.siblings().children(".ui-state-active"),null,"ui-state-active"),this.focus(t,i))))},_destroy:function(){var t=this.element.find(".ui-menu-item").removeAttr("role aria-disabled").children(".ui-menu-item-wrapper").removeUniqueId().removeAttr("tabIndex role aria-haspopup");this.element.removeAttr("aria-activedescendant").find(".ui-menu").addBack().removeAttr("role aria-labelledby aria-expanded aria-hidden aria-disabled tabIndex").removeUniqueId().show(),t.children().each(function(){var t=V(this);t.data("ui-menu-submenu-caret")&&t.remove()})},_keydown:function(t){var e,i,s,n=!0;switch(t.keyCode){case V.ui.keyCode.PAGE_UP:this.previousPage(t);break;case V.ui.keyCode.PAGE_DOWN:this.nextPage(t);break;case V.ui.keyCode.HOME:this._move("first","first",t);break;case V.ui.keyCode.END:this._move("last","last",t);break;case V.ui.keyCode.UP:this.previous(t);break;case V.ui.keyCode.DOWN:this.next(t);break;case V.ui.keyCode.LEFT:this.collapse(t);break;case V.ui.keyCode.RIGHT:this.active&&!this.active.is(".ui-state-disabled")&&this.expand(t);break;case V.ui.keyCode.ENTER:case V.ui.keyCode.SPACE:this._activate(t);break;case V.ui.keyCode.ESCAPE:this.collapse(t);break;default:e=this.previousFilter||"",s=n=!1,i=96<=t.keyCode&&t.keyCode<=105?(t.keyCode-96).toString():String.fromCharCode(t.keyCode),clearTimeout(this.filterTimer),i===e?s=!0:i=e+i,e=this._filterMenuItems(i),(e=s&&-1!==e.index(this.active.next())?this.active.nextAll(".ui-menu-item"):e).length||(i=String.fromCharCode(t.keyCode),e=this._filterMenuItems(i)),e.length?(this.focus(t,e),this.previousFilter=i,this.filterTimer=this._delay(function(){delete this.previousFilter},1e3)):delete this.previousFilter}n&&t.preventDefault()},_activate:function(t){this.active&&!this.active.is(".ui-state-disabled")&&(this.active.children("[aria-haspopup='true']").length?this.expand(t):this.select(t))},refresh:function(){var t,e,s=this,n=this.options.icons.submenu,i=this.element.find(this.options.menus);this._toggleClass("ui-menu-icons",null,!!this.element.find(".ui-icon").length),e=i.filter(":not(.ui-menu)").hide().attr({role:this.options.role,"aria-hidden":"true","aria-expanded":"false"}).each(function(){var t=V(this),e=t.prev(),i=V("").data("ui-menu-submenu-caret",!0);s._addClass(i,"ui-menu-icon","ui-icon "+n),e.attr("aria-haspopup","true").prepend(i),t.attr("aria-labelledby",e.attr("id"))}),this._addClass(e,"ui-menu","ui-widget ui-widget-content ui-front"),(t=i.add(this.element).find(this.options.items)).not(".ui-menu-item").each(function(){var t=V(this);s._isDivider(t)&&s._addClass(t,"ui-menu-divider","ui-widget-content")}),i=(e=t.not(".ui-menu-item, .ui-menu-divider")).children().not(".ui-menu").uniqueId().attr({tabIndex:-1,role:this._itemRole()}),this._addClass(e,"ui-menu-item")._addClass(i,"ui-menu-item-wrapper"),t.filter(".ui-state-disabled").attr("aria-disabled","true"),this.active&&!V.contains(this.element[0],this.active[0])&&this.blur()},_itemRole:function(){return{menu:"menuitem",listbox:"option"}[this.options.role]},_setOption:function(t,e){var i;"icons"===t&&(i=this.element.find(".ui-menu-icon"),this._removeClass(i,null,this.options.icons.submenu)._addClass(i,null,e.submenu)),this._super(t,e)},_setOptionDisabled:function(t){this._super(t),this.element.attr("aria-disabled",String(t)),this._toggleClass(null,"ui-state-disabled",!!t)},focus:function(t,e){var i;this.blur(t,t&&"focus"===t.type),this._scrollIntoView(e),this.active=e.first(),i=this.active.children(".ui-menu-item-wrapper"),this._addClass(i,null,"ui-state-active"),this.options.role&&this.element.attr("aria-activedescendant",i.attr("id")),i=this.active.parent().closest(".ui-menu-item").children(".ui-menu-item-wrapper"),this._addClass(i,null,"ui-state-active"),t&&"keydown"===t.type?this._close():this.timer=this._delay(function(){this._close()},this.delay),(i=e.children(".ui-menu")).length&&t&&/^mouse/.test(t.type)&&this._startOpening(i),this.activeMenu=e.parent(),this._trigger("focus",t,{item:e})},_scrollIntoView:function(t){var e,i,s;this._hasScroll()&&(i=parseFloat(V.css(this.activeMenu[0],"borderTopWidth"))||0,s=parseFloat(V.css(this.activeMenu[0],"paddingTop"))||0,e=t.offset().top-this.activeMenu.offset().top-i-s,i=this.activeMenu.scrollTop(),s=this.activeMenu.height(),t=t.outerHeight(),e<0?this.activeMenu.scrollTop(i+e):s",options:{appendTo:null,autoFocus:!1,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null,change:null,close:null,focus:null,open:null,response:null,search:null,select:null},requestIndex:0,pending:0,liveRegionTimer:null,_create:function(){var i,s,n,t=this.element[0].nodeName.toLowerCase(),e="textarea"===t,t="input"===t;this.isMultiLine=e||!t&&this._isContentEditable(this.element),this.valueMethod=this.element[e||t?"val":"text"],this.isNewMenu=!0,this._addClass("ui-autocomplete-input"),this.element.attr("autocomplete","off"),this._on(this.element,{keydown:function(t){if(this.element.prop("readOnly"))s=n=i=!0;else{s=n=i=!1;var e=V.ui.keyCode;switch(t.keyCode){case e.PAGE_UP:i=!0,this._move("previousPage",t);break;case e.PAGE_DOWN:i=!0,this._move("nextPage",t);break;case e.UP:i=!0,this._keyEvent("previous",t);break;case e.DOWN:i=!0,this._keyEvent("next",t);break;case e.ENTER:this.menu.active&&(i=!0,t.preventDefault(),this.menu.select(t));break;case e.TAB:this.menu.active&&this.menu.select(t);break;case e.ESCAPE:this.menu.element.is(":visible")&&(this.isMultiLine||this._value(this.term),this.close(t),t.preventDefault());break;default:s=!0,this._searchTimeout(t)}}},keypress:function(t){if(i)return i=!1,void(this.isMultiLine&&!this.menu.element.is(":visible")||t.preventDefault());if(!s){var e=V.ui.keyCode;switch(t.keyCode){case e.PAGE_UP:this._move("previousPage",t);break;case e.PAGE_DOWN:this._move("nextPage",t);break;case e.UP:this._keyEvent("previous",t);break;case e.DOWN:this._keyEvent("next",t)}}},input:function(t){if(n)return n=!1,void t.preventDefault();this._searchTimeout(t)},focus:function(){this.selectedItem=null,this.previous=this._value()},blur:function(t){clearTimeout(this.searching),this.close(t),this._change(t)}}),this._initSource(),this.menu=V("

    gNN$epR7| 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

t}9Wkt>5D7+qQ`Oy%&BLjE~&B9Za&qesvezEDSS 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 literal 0 HcmV?d00001 diff --git a/oxAuth/Server/integrations/uaf/img/oob_qr_code.png b/oxAuth/Server/integrations/uaf/img/oob_qr_code.png new file mode 100644 index 0000000000000000000000000000000000000000..ce0716ef00c1329964586a6fd4d4e84a5efa75a8 GIT binary patch literal 77419 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: Open RP URL +Browser->RP: Protected resource +RP->Gluu Server: Start AuthZ & AuthN +Gluu Server->UAF Script: Start User/Password + UAF +UAF Script->UAF Script: 1) Verify user/password +UAF Script->Nok Nok Server: START_OOB +Nok Nok Server->UAF Script: QR code for mobile application +UAF Script->Browser: 2) Render UAF login page with QR code + +Person->UAF Mobile: Start UAF application +UAF Mobile->Browser: Scan QR code +UAF Mobile->Nok Nok Server: INIT_OBB +Person->UAF Mobile: Biometric authentication +UAF Mobile->Nok Nok Server: FINISH_OBB + +Browser->Nok Nok Server: STATUS_OBB with delay 5 seconds +Browser->UAF Script: 3) Send response +UAF Script->Nok Nok Server: STATUS_OBB +UAF Script->Nok Nok Server: CANCEL_OBB if UAF authentication failed + +RP->Gluu Server: Request authorization +RP->Gluu Server: Request tokens +RP->Gluu Server: Request user_info diff --git a/oxAuth/Server/integrations/whispeak/README.md b/oxAuth/Server/integrations/whispeak/README.md new file mode 100644 index 00000000..e49f7dbc --- /dev/null +++ b/oxAuth/Server/integrations/whispeak/README.md @@ -0,0 +1,73 @@ +# Whispeak +## Overview +[Whispeak](https://whispeak.io) Whispeak is a speaker recognition software as a service platform that makes it easy to add a biometric factor to authentication processes. Don’t choose between security and fluidity. + +This script allows to enroll and authenticate using voice biometrics as single factor or second factor. +It integrates with Passport Gluu installation in order to secure the enrollment and to provide a fallback for authentication if not possible to use voice. + +This integration is optional if enrollment securization is not needed nor fallback. + +## Prerequisites + +- An account in Whispeak for your company so to have a configured API with a valid API key. https://whispeak.io/ +- Some custom attributes added to user profiles in Gluu. +- Two custom libraries to be added to your Gluu installation. +- Whispeak custom script and related resources included in Gluu packaging [Whispeak interception script](https://github.com/GluuFederation/oxAuth/blob/master/Server/integrations/whispeak/whispeak_open_v1.py) + +## Properties +Whispeak script has this mandatory properties +| Property | Description | Example | +|-----------------------|-------------------------------|---------------| +|API_BASE_URL |URL of the Whispeak Web Service|`https://YOUR-CUSTOMER-NAME.whispeak.io/v1`| +|API_KEY |API key |`YOUR-API-KEY`| +|API_APP_PATH |API PATH |`/YOUR-API-PATH`| + +For passport integrations +| Property | Description | Example | +|-----------------------|-------------------------------|---------------| +|KEY_STORE_FILE |Address of key store fille for passport|`Usually: /etc/certs/passport-rp.jks`| +|KEY_STORE_PASSWORD |Key store password |`YOUR KEYSTORE PASSWORD`| + +Other additional properties +| Property | Description | Example | +|-----------------------|-------------------------------|---------------| +|SECOND_FACTOR |Ask for password as first step |`True|False`| +|LOG_LEVEL |Level of logging|`DEBUG|INFO|WARNING|ERROR`| +|MAX_NUMBER_OF_ERRORS_FALLBACK |Number of errors to show fallback method |`Default 0`| +|MAX_NUMBER_OF_ERRORS_VERIFY |Number of errors on verify to delete enrolled signature |`Default 3`| + +## Whispeak Documentation + +If you want to have more information about the API calls used in this script please visit https://doc.whispeak.io/v1 +If you want to contact us just use the contact form over https://whispeak.io/ + +## Configure attributes + +As indicated in Gluu doc: https://gluu.org/docs/gluu-server/admin-guide/attribute/ + +You need to add these three text fields: +- whispeakSignatureId +- whispeakRevocationPwd +- whispeakRevocationUiLink + +## Add librearies +- You need to add these two jars: primefaces-8.0.jar and httpmime-4.5.13.jar +- Specified in Gluu doc at: https://gluu.org/docs/gluu-server/operation/custom-design/ + +## Configure oxTrust + +Follow the steps below to configure the Whispeak module in the oxTrust Admin GUI. + +1. Navigate to `Configuration` > `Person Authentication Scripts`. +1. Scroll down to the Whispeak authentication script + +1. Configure the properties, all of which are mandatory, according to your API + +1. Enable the script by ticking the check box +![enable](../img/admin-guide/enable.png) + +Now Whispeak's biometric authentication is available as an authentication mechanism for your Gluu Server. This means that, using OpenID Connect `acr_values`, applications can now request Whispeak biometric authentication for users. + +!!! Note + To make sure Whispeak has been enabled successfully, you can check your Gluu Server's OpenID Connect configuration by navigating to the following URL: `https:///.well-known/openid-configuration`. Find `"acr_values_supported":` and you should see `"whispeak"`. + diff --git a/oxAuth/Server/integrations/whispeak/whispeak_open_v1.py b/oxAuth/Server/integrations/whispeak/whispeak_open_v1.py new file mode 100644 index 00000000..db713b4f --- /dev/null +++ b/oxAuth/Server/integrations/whispeak/whispeak_open_v1.py @@ -0,0 +1,1326 @@ +# oxAuth is available under the MIT License (2008). +# See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2016, Gluu +# +# Author: Whispeak _ 2021 +# +from base64 import b64decode + +import json +import sys +import time +import logging +import re + +from java.io import BufferedReader, InputStreamReader +from java.lang import String +from java.net import URI +from java.util import Arrays, Collections +from javax.faces.application import FacesMessage +from javax.faces.context import FacesContext +from org.apache.commons.io import IOUtils +from org.apache.http import HttpStatus +from org.apache.http.client.methods import HttpDelete, HttpGet, HttpPost +from org.apache.http.client.utils import URIBuilder +from org.apache.http.entity import ContentType +from org.apache.http.entity.mime import MultipartEntityBuilder +from org.apache.http.impl.client import HttpClientBuilder +from org.gluu.config.oxtrust import LdapOxPassportConfiguration +from org.gluu.jsf2.message import FacesMessages +from org.gluu.jsf2.service import FacesService +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.oxauth.model.common import User +from org.gluu.oxauth.model.jwt import Jwt +from org.gluu.oxauth.security import Identity +from org.gluu.oxauth.service import AuthenticationService, UserService +from org.gluu.service import CacheService +from org.gluu.oxauth.service.common import EncryptionService +from org.gluu.oxauth.service.net import HttpService +from org.gluu.oxauth.util import ServerUtil +from org.gluu.persist import PersistenceEntryManager +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.util import StringHelper + + +SCRIPT_VERSION = 'whispeak_open_v1.py' + + +class PersonAuthentication(PersonAuthenticationType): + + def __init__(self, current_time_millis): + self.current_time_millis = current_time_millis + + ################################################################################ + # Gluu auxiliary configuration and status functions + ################################################################################ + + ################################################################################ + # Initialization functions + + def init(self, custom_script, configuration_attributes): + self.logger = logging.getLogger(__name__) + log_format = '%(levelname)s - [%(filename)s:%(lineno)s - %(funcName)20s()] - %(message)s' + logging.basicConfig(format=log_format) + self.logger.setLevel(logging.INFO) + + self.logger.info("Going to Setting log level") + log_level = configuration_attributes.get("LOG_LEVEL") + if log_level is not None: + log_level_value = log_level.getValue2() + self.logger.info("Setting log level %s", log_level_value) + self.logger.setLevel(logging.getLevelName(log_level_value)) + if log_level_value == "DEBUG": + def trace(frame, event, arg): + if event == "call": + filename = frame.f_code.co_filename + if filename == SCRIPT_VERSION: + function_name = frame.f_code.co_name + if function_name != "log": + print("Function {}", function_name) + return trace + sys.settrace(trace) + else: + sys.settrace(None) + + success = self._process_key_store_properties(configuration_attributes) + + if success: + self.provider_key = "provider" + self.passport_dn = self._get_passport_config_dn() + self.passport_enabled = True + else: + self.logger.debug("Passport Not initialized") + self.passport_enabled = False + self.logger.debug( + "Initialization ok, passport enabled status %s", success) + self._set_configuration_attributes(configuration_attributes) + + self.storage_working_parameters = True + self.variable_debugging = False + return True + + def _put(self, key, variable): + if self.storage_working_parameters or (key in self._getWorkingParameters()): + if self.variable_debugging: + self.logger.debug( + "Put variable \"%s\" in working parameters with key \"%s\"", variable, key) + CdiUtil.bean(Identity).setWorkingParameter(key, variable) + else: + unique_key = CdiUtil.bean( + Identity).getSessionId().getId() + "-" + key + if self.variable_debugging: + self.logger.debug( + "Put variable \"%s\" in cache with key \"%s\"", variable, unique_key) + CdiUtil.bean(CacheService).put(unique_key, variable) + + def _get(self, key): + if self.storage_working_parameters or (key in self._getWorkingParameters()): + variable = CdiUtil.bean(Identity).getWorkingParameter(key) + if self.variable_debugging: + self.logger.debug( + "Get variable \"%s\" from working parameters with key \"%s\"", variable, key) + return variable + else: + unique_key = CdiUtil.bean( + Identity).getSessionId().getId() + "-" + key + variable = CdiUtil.bean(CacheService).get(unique_key) + if self.variable_debugging: + self.logger.debug( + "Get variable \"%s\" from cache with key \"%s\"", variable, unique_key) + return variable + + def _get_passport_config_dn(self): + file = open('/etc/gluu/conf/gluu.properties', 'r') + for line in file: + prop = line.split("=") + if prop[0] == "oxpassport_ConfigurationEntryDN": + prop.pop(0) + break + + file.close() + return "=".join(prop).strip() + + def _process_key_store_properties(self, configuration_attributes): + file = configuration_attributes.get("KEY_STORE_FILE") + password = configuration_attributes.get("KEY_STORE_PASSWORD") + + if file is not None and password is not None: + file = file.getValue2() + password = password.getValue2() + + if StringHelper.isNotEmpty(file) and StringHelper.isNotEmpty(password): + self.key_store_file = file + self.key_store_password = password + return True + + self.logger.debug( + "Properties key_store_file or key_store_password not found or empty") + return False + + def _return_page(self, page, step): + page = page.replace('//', '/') + return page + + def _set_configuration_attributes(self, configuration_attributes): + if configuration_attributes.containsKey("API_BASE_URL"): + url = configuration_attributes.get("API_BASE_URL").getValue2() + self.api_base_url = configuration_attributes.get( + "API_BASE_URL").getValue2() + if configuration_attributes.containsKey("API_APP_PATH"): + path = configuration_attributes.get("API_APP_PATH").getValue2() + self.api_app_path = path + if configuration_attributes.containsKey("API_KEY"): + api_key = configuration_attributes.get("API_KEY").getValue2() + self.api_key = api_key + + if configuration_attributes.containsKey("FALLBACK_RETURN_HOST"): + fallback_return_host = configuration_attributes.get( + "FALLBACK_RETURN_HOST").getValue2() + self.fallback_return_host = fallback_return_host + else: + self.fallback_return_host = "auth.whispeak.io" + + if configuration_attributes.containsKey("MAX_NUMBER_OF_ERRORS_VERIFY"): + number_of_errors_verify = configuration_attributes.get( + "MAX_NUMBER_OF_ERRORS_VERIFY").getValue2() + self.max_number_of_errors_verify = int(number_of_errors_verify) + else: + self.max_number_of_errors_verify = 3 + + if configuration_attributes.containsKey("MAX_NUMBER_OF_ERRORS_FALLBACK"): + number_of_errors_fallback = configuration_attributes.get( + "MAX_NUMBER_OF_ERRORS_FALLBACK").getValue2() + self.max_number_of_errors_fallback = int(number_of_errors_fallback) + else: + self.max_number_of_errors_fallback = 0 + + if configuration_attributes.containsKey("CHECK_ONLY_USERNAME"): + check_domain = configuration_attributes.get( + "CHECK_ONLY_USERNAME").getValue2() + self.check_only_username = check_domain + else: + self.check_only_username = False + + if configuration_attributes.containsKey("SECOND_FACTOR"): + second_factor = configuration_attributes.get( + "SECOND_FACTOR").getValue2() + self.logger.debug("Second factor is %s", second_factor) + self.second_factor = second_factor + else: + self.second_factor = False + + if not url or not path or not api_key: + if not url: + self.logger.error("Mandatory Property: API_BASE_URL") + if not path: + self.logger.error("Mandatory Property: API_APP_PATH") + if not api_key: + self.logger.error("Mandatory Property: API_KEY") + self._set_message_error( + FacesMessage.SEVERITY_ERROR, "login.authConfigurationError") + return False + self.endpoint = "{url}/apps{path}".format(url=url, path=path) + + def _initialize_clean_config(self, configuration_attributes): + + self._reinitialize_storage() + + self._put("show_password", self.second_factor) + self._put("show_oidc_panel", False) + self._put("show_return_client_panel", False) + self._put("external_providers", False) + + return True + + def _reinitialize_storage(self): + self.logger.debug("Clean Working Parameters") + for key in self._getWorkingParameters(): + self._put(key, None) + for key in self._getStorage(): + self._put(key, None) + + def _print_storage(self): + all_storage = self._getWorkingParameters() + self._getStorage() + self.logger.debug(all_storage) + for key in all_storage: + self._get(key) + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def getAlternativeAuthenticationMethod(self, usage_type, configuration_attributes): + if configuration_attributes.containsKey("ALTERNATIVE_ACR_VALUE"): + ALTERNATIVE_ACR_VALUE = configuration_attributes.get( + "ALTERNATIVE_ACR_VALUE").getValue2() + self.logger.debug("Alternative acr value %s", + ALTERNATIVE_ACR_VALUE) + + return ALTERNATIVE_ACR_VALUE + return False + + def getExtraParametersForStep(self, configuration_attributes, step): + # Used in xhtml pages + parameters = self._getWorkingParameters() + # Used internally in script + if self.storage_working_parameters: + parameters = Arrays.asList( + self._getWorkingParameters() + self._getStorage()) + else: + parameters = Arrays.asList(self._getWorkingParameters()) + return parameters + + def _getStorage(self): + storage = ["username", + "selected_provider", + "passport_user_profile", + "whispeak_signature_id", + "user_password", + "user_profile_oidc", + "retry_error", + "no_identification_step", + "error_number", + "error_number_verify", + "count_authentication_steps", + "token", + "next_step", + "username_from_parameters"] + return storage + + def _getWorkingParameters(self): + parameters = ["flow", + "enroll_challenge", + "external_providers", + "asr_text", + "revocation_ui_link", + "revocation_pwd", + "show_password"] + return parameters + + def getCountAuthenticationSteps(self, configuration_attributes): + steps = int(self._get( + "count_authentication_steps") or 7) + self.logger.debug( + "Total authentication count steps before jump %s", steps) + steps = self._set_jump_steps( + steps, True) + self.logger.debug( + "Total authentication count steps after jump %s", steps) + + return steps + + ################################################################################ + # Fallback on platform down + + def isValidAuthenticationMethod(self, usage_type, configuration_attributes): + if self._get("flow"): + if not self._isWhispeakAlive(): + return False + + return True + + def _isWhispeakAlive(self): + self.logger.debug("Whispeak ENDPOINT %s", self.endpoint) + + url = URI(self.api_base_url + "/health") + + get_connection = HttpGet(url) + + try: + + http_service_response = HttpClientBuilder.create().build().execute(get_connection) + if http_service_response.getStatusLine().getStatusCode() != HttpStatus.SC_OK: + http_service_response.close() + raise Exception() + http_service_response.close() + except Exception as e: + self.logger.error("Contact Whispeak Server FAILED %s", str(e)) + self._set_message_error( + FacesMessage.SEVERITY_ERROR, "login.authTimeout") + return False + finally: + get_connection.releaseConnection() + self.logger.debug("Whispeak Heartbeat Alive") + return True + + ################################################################################ + # End lifecycle + + def destroy(self, configuration_attributes): + self.logger.info("Destroy SUCCESS") + return True + + def get_logout_external_url(self, configuration_attributes, request_parameters): + return None + + def logout(self, configuration_attributes, request_parameters): + return True + + ################################################################################ + # Gluu step management functions + ################################################################################ + + def _set_user_name_from_parameters(self): + username = self._get_user_name_from_request() + + if username is None: + username = self._get( + "username_from_parameters") + if username is None: + return False + else: + self._put("username_from_parameters", username) + + self._set_user_and_flow(username) + + if not self.second_factor and username: + self.logger.debug("No identification step will be shown") + self._put("no_identification_step", True) + + return True + + def _get_user_name_from_request(self): + extra_parameters = ServerUtil.getFirstValue(FacesContext.getCurrentInstance( + ).getExternalContext().getRequest().getParameterMap(), "extraParameters") + + if extra_parameters: + username = json.loads(extra_parameters)["username"] + self.logger.debug( + "Username from extra_parameters is %s", username) + else: + username = ServerUtil.getFirstValue(FacesContext.getCurrentInstance( + ).getExternalContext().getRequest().getParameterMap(), "username") + self.logger.debug("Username from direct parameter is %s", username) + + return username + + ################################################################################ + # Called before each step to retrieve xhtml page + + def getPageForStep(self, configuration_attributes, step): + if CdiUtil.bean(Identity).getSessionId(): + # Session is initialized + flow = self._get("flow") + enroll_challenge = self._get( + "enroll_challenge") + step = self._set_jump_steps(step) + else: + username = self._get_user_name_from_request() + self.logger.debug("username is \"%s\"", username) + if username and not self.second_factor: + step = step + 1 + flow = "enroll" + user = CdiUtil.bean(UserService).getUserByAttribute( + 'mail', username) + if user: + if user.getAttribute('whispeakSignatureId'): + flow = "auth" + + if step == 1: + page = self._return_page( + "/whispeak_open_identification.xhtml", step) + + if step == 2: + if flow == "enroll": + page = self._return_page( + "/whispeak_open_ask_enroll.xhtml", step) + else: + page = self._return_page( + "/whispeak_open_authentication_submit.xhtml", step) + + if step == 3: + if flow == "enroll" and enroll_challenge: + if enroll_challenge == "Reject": + page = self._return_page( + "/whispeak_open_passport_fallback.xhtml", step) + else: + page = self._return_page( + "/whispeak_open_passport.xhtml", step) + else: + page = self._return_page( + "/whispeak_open_passport_loading.xhtml", step) + + if step == 4: + page = self._return_page( + "/whispeak_open_passport_loading.xhtml", step) + + if step == 5: + page = self._return_page( + "/whispeak_open_enrollment_submit.xhtml", step) + + if step == 6: + page = self._return_page( + "/whispeak_open_authentication_submit.xhtml", step) + + if step == 7: + page = self._return_page( + "/whispeak_open_revocation_data_show.xhtml", step) + + self.logger.debug("Page %s for Step %s", page, step) + + return page + + ################################################################################ + # Called to know which step goes next + + def getNextStep(self, configuration_attributes, request_parameters, step): + + next_step = self._get("next_step") + + if next_step: + self._put("next_step", "") + else: + next_step = -1 + + self.logger.debug("Next step %s current step %s", next_step, step) + + return next_step + + ################################################################################ + # Called before each step to execute logic + + def prepareForStep(self, configuration_attributes, request_parameters, step): + self.logger.debug( + "Get session id prepare for step \"%s\" data \"%s\"", CdiUtil.bean(Identity).getSessionId().getId(), CdiUtil.bean(Identity).getSessionId().getSessionAttributes()) + + if step == 1: + username_from_parameters = self._get( + "username_from_parameters") + self._initialize_clean_config(configuration_attributes) + if username_from_parameters is not None: + self._put("username_from_parameters", username_from_parameters) + self._set_user_name_from_parameters() + + step = self._set_jump_steps(step) + + try: + flow = self._get("flow") + self.logger.debug("Flow %s Step %s", flow, step) + + if step > 4 or (step == 2 and flow == "auth"): + self._set_access_token_and_text( + self._get_access_token_and_text("enroll") + if flow == "enroll" and step == 5 + else self._get_access_token_and_text("auth") + ) + + if (step > 2 or (step == 2 and flow == "auth")) and self.passport_enabled: + self.logger.debug("Preparing passport") + self._prepare_passport() + except Exception as e: + self.logger.error( + "Exception in prepareForStep function, returning to step 1 %s", str(e)) + self._put("next_step", 1) + self._set_message_error( + FacesMessage.SEVERITY_INFO, "login.send_restart", False) + + return True + + ################################################################################ + # Gluu authentication functions + ################################################################################ + + ################################################################################ + # Core gluu authentication + + def authenticate(self, configuration_attributes, request_parameters, step): + + self.logger.debug( + "Get session id \"%s\" data \"%s\"", CdiUtil.bean(Identity).getSessionId().getId(), CdiUtil.bean(Identity).getSessionId().getSessionAttributes()) + if self.variable_debugging: + self._print_storage() + self.logger.debug("Step %s", step) + + if not self.passport_enabled: + if not self._redirect_to_client_fallback(configuration_attributes, request_parameters): + return False + + if not self._check_script_exec_in_order(request_parameters, step): + return False + + step = self._set_jump_steps(step) + step_ok = False + + try: + self._put("retry_error", False) + + enroll_challenge = request_parameters.get( + "loginForm:enroll_challenge") + self._put("enroll_challenge", + enroll_challenge[0] if enroll_challenge is not None else None) + + step_ok = self.__class__.__dict__["_PersonAuthentication__step{step}".format(step=step)]( + self, + request_parameters, + step) + + count_authentication_steps = self._get( + "count_authentication_steps") + + self.logger.info("Step %s result is %s", step, step_ok) + + if step_ok: + if step + 1 > count_authentication_steps: + user = self._get("username") + CdiUtil.bean(AuthenticationService).authenticate(user) + self.logger.info( + "Flow is finished login user %s in gluu service, in step %s", user, step) + else: + self.logger.warning("Step %s failed", step) + if self._get("retry_error"): + current_error_number = self._get( + "error_number") or 0 + current_error_number = current_error_number + 1 + self._put("error_number", current_error_number) + number_of_errors_fallback = self.max_number_of_errors_fallback + self.logger.debug("Current nb of errors %s Fallback at nb of errors %s", self._get( + "error_number"), number_of_errors_fallback) + if current_error_number >= number_of_errors_fallback: + if not self.passport_enabled: + self._put("show_return_client_panel", True) + else: + self._put("show_oidc_panel", True) + else: + self.logger.warning( + "Non retryable error returning to step 1, retry_error is %s", self._get("retry_error")) + self._put("next_step", 1) + self._set_message_error( + FacesMessage.SEVERITY_INFO, "login.send_restart", False) + + except Exception as e: + self.logger.error( + "Exception in authentication function, returning to step 1 %s", str(e)) + self._put("next_step", 1) + self._set_message_error( + FacesMessage.SEVERITY_INFO, "login.send_restart", False) + + if self.variable_debugging: + self._print_storage() + return step_ok + + def _redirect_to_client_fallback(self, configuration_attributes, request_parameters): + client_url = FacesContext.getCurrentInstance().getExternalContext( + ).getRequestCookieMap().get("rp_origin_id").getValue() + + host = self.fallback_return_host + if bool(client_url) and not host in client_url: + fallback_return_url = re.search( + r"(https://[a-zA-Z\-\.]+)/.*", client_url).group(1) + else: + if configuration_attributes.containsKey("FALLBACK_REDIRECT_URL"): + fallback_return_url = configuration_attributes.get( + "FALLBACK_REDIRECT_URL").getValue2() + else: + fallback_return_url = host + self.logger.debug("fallback_return_url %s", fallback_return_url) + self._put("client_url", fallback_return_url) + + # Get enroll challenge response + enroll_challenge_parameter = request_parameters.get( + "loginForm:enroll_challenge") + enroll_challenge = enroll_challenge_parameter[0] if enroll_challenge_parameter is not None else None + + rejected_enroll = enroll_challenge == "Reject" + + redirect_to_client = bool(ServerUtil.getFirstValue( + request_parameters, "loginForm:redirect-to-client")) or rejected_enroll + + if bool(redirect_to_client): + client_url = self._get( + "client_url") + if bool(client_url): + self.logger.debug("Redirecting to %s", client_url) + CdiUtil.bean(FacesService).redirectToExternalURL(client_url) + else: + self._set_message_error( + FacesMessage.SEVERITY_INFO, "login.restart", False) + self._put("next_step", 1) + self.logger.error("Not possible to get client URL, restarting") + return False + return True + + def _check_script_exec_in_order(self, request_parameters, step): + origin_page_param = ServerUtil.getFirstValue( + request_parameters, "loginForm:origin-page") + + if origin_page_param: + expected_page = self.getPageForStep(None, step) + + origin_page = self._return_page(origin_page_param, step) + self.logger.debug("origin_page %s", origin_page) + self.logger.debug("expected_page %s", expected_page) + + if origin_page != expected_page: + self._set_message_error( + FacesMessage.SEVERITY_INFO, "login.restart", False) + self._put("next_step", 1) + self.logger.error( + "origin_page and expected_page differ restart on step 1") + return False + return True + + def _set_jump_steps(self, step, back=False): + if self._get("no_identification_step"): + step = self._jump(step, back, 1) + self.logger.debug( + "Jumping over first step, so updated to %s", step) + if (not self.passport_enabled) and 2 < step < 8: + step = self._jump(step, back, 2) + self.logger.debug( + "Jumping over passport steps, so updated to %s", step) + return step + + def _jump(self, step, back, amount): + if back: + return step - amount + return step + amount + + ################################################################################ + # First step: user identification with email + + def __step1(self, request_parameters, step): + + credentials = CdiUtil.bean(Identity).getCredentials() + + username = credentials.getUsername() + if not username: + return False + + user = self._set_user_and_flow(username) + + # ONLY FOR DEMO PURPOSES + # as we are already authenticating user here before voice to some extent, possibly insecure + if self.second_factor: + user_password = ServerUtil.getFirstValue( + request_parameters, "loginForm:password") or credentials.getPassword() + if StringHelper.isNotEmptyString(username) and StringHelper.isNotEmptyString(user_password): + if not user or not user.getAttribute('userPassword'): + self._put("user_password", user_password) + return True + authenticated = CdiUtil.bean(AuthenticationService).authenticate( + username, user_password) + if not authenticated: + self.logger.info( + "Password mismatch for user %s", username) + self._set_message_error( + FacesMessage.SEVERITY_ERROR, "whispeak.login.2fa.passwordMismatch") + return authenticated + return False + + return True + + def _set_user_and_flow(self, username): + user = CdiUtil.bean(UserService).getUserByAttribute('mail', username) + whispeak_signature_id = '' + + self._put("username", username) + + if user: + whispeak_signature_id = user.getAttribute('whispeakSignatureId') + + if not user or not whispeak_signature_id: + self.logger.info( + "User %s does not exist or is not enrolled, will be created", username) + self._put("flow", "enroll") + self._put("count_authentication_steps", 7) + self.logger.debug("Updated total steps to: %s", + self._get("count_authentication_steps")) + else: + self.logger.info( + "User %s is already enrolled, proceed for authentication", username) + self._put("flow", "auth") + self._put("count_authentication_steps", 2) + self.logger.debug("Updated total steps to: %s", + self._get("count_authentication_steps")) + return user + + ################################################################################ + # Second step: enrollment challenge question, auth or passport + + def __step2(self, request_parameters, step): + flow = self._get("flow") + + if flow == "auth": + if self._check_and_activate_alternative_provider_selected(request_parameters): + self._put("count_authentication_steps", 3) + self.logger.debug("Updated total steps to: %s", + self._get("count_authentication_steps")) + redirect_result = self._redirect_oidc() + return redirect_result + + login_voice = self._get_login_voice_and_set_text( + request_parameters) + if not login_voice: + self.logger.warning( + "Authentication flow, voice is NOT present, retryable error") + self._put("retry_error", True) + return False + else: + self.logger.debug( + "Authentication flow, voice is present in request with size in bytes %s", (len(login_voice))) + + username = self._get("username") + user = CdiUtil.bean(UserService).getUserByAttribute( + 'mail', username) + + whispeak_signature_id = user.getAttribute('whispeakSignatureId') + logged_in = self._whispeak_voice( + "auth", login_voice, whispeak_signature_id) + if logged_in: + user_password = self._get( + "user_password") + if user_password: + user.setAttribute('userPassword', user_password) + CdiUtil.bean(UserService).updateUser(user) + self.logger.info( + "User %s is authenticated via voice in Whispeak", username) + return logged_in + + else: + enroll_challenge = self._get( + "enroll_challenge") + if enroll_challenge == "Reject": + self.logger.debug("User does not want to enroll, fallback") + if self.passport_enabled: + self._put("count_authentication_steps", 4) + else: + self._put("count_authentication_steps", 3) + self.logger.debug("Updated total steps to: %s", + self._get("count_authentication_steps")) + return True + + ################################################################################ + # Third step: passport redirect + + def __step3(self, request_parameters, step): + + flow = self._get("flow") + + if flow == "enroll": + self._check_and_activate_alternative_provider_selected( + request_parameters) + redirect_result = self._redirect_oidc() + return redirect_result + + jwt_param = ServerUtil.getFirstValue(request_parameters, "user") + return self._is_oidc_authenticated(jwt_param) + + ################################################################################ + # Fourth step: passport return, token processing + + def __step4(self, request_parameters, step): + + jwt_param = ServerUtil.getFirstValue(request_parameters, "user") + user_profile_oidc = self._is_oidc_authenticated(jwt_param) + self.logger.debug( + "user_profile_oidc when retrieved \"%s\"", user_profile_oidc) + if user_profile_oidc: + self.logger.debug("Saving user profile") + self._put('user_profile_oidc', user_profile_oidc) + return True + return False + + ################################################################################ + # Fifth step: enroll with passport fallback + + def __step5(self, request_parameters, step): + + redirect_result = self._adjust_fallback_steps_passport_and_redirect( + request_parameters) + if redirect_result: + self.logger.info("redirects") + return redirect_result + + self.logger.info("Processing voice enroll") + + whispeak_signature_id = self._whispeak_voice( + "enroll", self._get_login_voice_and_set_text(request_parameters)) + + if whispeak_signature_id: + self._put("whispeak_signature_id", whispeak_signature_id) + + authentication_result = whispeak_signature_id + + return authentication_result + + ################################################################################ + # Sixth step: verify enroll with passport fallback + + def __step6(self, request_parameters, step): + + redirect_result = self._adjust_fallback_steps_passport_and_redirect( + request_parameters) + if redirect_result: + self.logger.info("redirects") + return redirect_result + + self.logger.info("Processing voice enroll verification") + + whispeak_signature_id = self._get( + "whispeak_signature_id") + username = self._get("username") + logged_in = self._whispeak_voice("auth", self._get_login_voice_and_set_text( + request_parameters), whispeak_signature_id) + if logged_in: + user = CdiUtil.bean(UserService).getUserByAttribute( + 'mail', username) + user_profile_oidc = self._get( + 'user_profile_oidc') + if not user: + user = self._create_user( + username, whispeak_signature_id, user_profile_oidc) + else: + self._update_user(user, user_profile_oidc) + user.setAttribute('whispeakSignatureId', whispeak_signature_id) + user.setAttribute('whispeakRevocationUiLink', + self._get("revocation_ui_link")) + user.setAttribute('whispeakRevocationPwd', + self._get("revocation_pwd")) + user_password = self._get("user_password") + if user_password: + user.setAttribute('userPassword', user_password) + CdiUtil.bean(UserService).updateUser(user) + else: + current_error_number_verify = self._get( + "error_number_verify") + 1 + self._put("error_number_verify", current_error_number_verify) + max_number_of_errors_verify = self.max_number_of_errors_verify + self.logger.debug("Current nb of errors %s Verify at nb of errors %s", + current_error_number_verify, max_number_of_errors_verify) + if current_error_number_verify >= max_number_of_errors_verify: + self.logger.debug("Proceeding to delete signature") + self._delete_signature(whispeak_signature_id) + self._put("retry_error", False) + self._set_message_error( + FacesMessage.SEVERITY_ERROR, "whispeak.login.signatureDoesNotExist") + + return logged_in + + ################################################################################ + # Seventh step: show revocation info and confirm + def __step7(self, request_parameters, step): + + return True + + ################################################################################ + # Whispeak Functions + ################################################################################ + + def _adjust_fallback_steps_passport_and_redirect(self, request_parameters): + if self._check_and_activate_alternative_provider_selected(request_parameters): + self._put("count_authentication_steps", 4) + self._put("next_step", 4) + redirect_result = self._redirect_oidc() + return redirect_result + return False + + def _get_login_voice_and_set_text(self, request_parameters): + + login_voice_base64 = ServerUtil.getFirstValue( + request_parameters, "loginForm:voiceBase64") + + if login_voice_base64: + login_voice = self._base64_to_file(login_voice_base64) + asr_text = ServerUtil.getFirstValue( + request_parameters, "loginForm:asr-text-retry") + self.logger.debug( + "Retrieved from form asr_text of length %s", len(asr_text)) + if asr_text: + self._put("asr_text", asr_text) + + if not login_voice: + self.logger.warning( + "Authentication flow, voice is NOT present in request so return false and keep step") + self._put("retry_error", True) + return False + else: + self.logger.debug("Size bytes of file %s", len(login_voice)) + + return login_voice + + def _base64_to_file(self, login_voice_base64): + if not login_voice_base64: + return None + + voice_bytes = b64decode(login_voice_base64.encode('ascii')) + + return voice_bytes + + def _get_access_token_and_text(self, for_method): + self.logger.info("Get Token for: %s", for_method) + + whispeak_service_url = "{endpoint}/{route}".format( + endpoint=self.endpoint, route=for_method) + + url = URI(whispeak_service_url) + + self.logger.debug("URL Token %s", url) + get_connection = HttpGet(url) + bearer = "Bearer {key}".format(key=self.api_key) + self.logger.debug("Bearer Token: %s", bearer) + get_connection.setHeader("Authorization", bearer) + + try: + http_get_response = HttpClientBuilder.create().build().execute(get_connection) + http_response_entity = http_get_response.getEntity() + http_response_content = http_response_entity.getContent() + if http_get_response.getStatusLine().getStatusCode() != HttpStatus.SC_OK: + self.logger.error("Whispeak Obtain Access Token - SERVER resp NOT OK code %s", + http_get_response.getStatusLine().getStatusCode()) + http_get_response.close() + return None + + data = json.loads(IOUtils.toString(http_response_content, "UTF-8")) + http_get_response.close() + self.logger.debug("Access Token and Text: %s", data) + return data + + except Exception as e: + self.logger.error( + "Whispeak Obtain Access Token Exception %s", str(e)) + return None + finally: + get_connection.releaseConnection() + + def _delete_signature(self, whispeak_signature_id): + + whispeak_service_url = "{endpoint}/signatures/{whispeak_signature_id}".format( + endpoint=self.endpoint, whispeak_signature_id=whispeak_signature_id) + + url = URI(whispeak_service_url) + self.logger.debug("URL Delete %s", url) + get_connection = HttpDelete(url) + bearer = "Bearer {key}".format(key=self.api_key) + self.logger.debug("Bearer Delete, bearer %s", bearer) + get_connection.setHeader("Authorization", bearer) + + try: + http_delete_response = HttpClientBuilder.create().build().execute(get_connection) + if http_delete_response.getStatusLine().getStatusCode() != HttpStatus.SC_OK: + self.logger.error("Whispeak Delete signature - SERVER resp NOT OK code %s", + http_delete_response.getStatusLine().getStatusCode()) + return False + self.logger.info("Whispeak Delete signature code %s", + http_delete_response.getStatusLine().getStatusCode()) + except Exception as e: + self.logger.error( + "Whispeak Delete Signature Exception %s", str(e)) + return False + finally: + if http_delete_response: + http_delete_response.close() + get_connection.releaseConnection() + + return True + + def _set_access_token_and_text(self, data): + if not data or not data['text']: + self.logger.error("Not possible to get tokens") + self._set_message_error( + FacesMessage.SEVERITY_ERROR, "login.authConfigurationError") + return False + self._put("token", data['token']) + self._put("asr_text", data['text']) + return True + + def _whispeak_voice(self, operation, login_voice, whispeak_signature_id=None): + + whispeak_service_url = "{endpoint}/{operation}".format( + endpoint=self.endpoint, operation=operation) + + builder = URIBuilder(whispeak_service_url) + url = builder.build() + self.logger.debug("URL %s", url) + http_service_request = HttpPost(url) + + try: + token = self._get("token") + self.logger.debug("JWT temp token %s", token) + http_service_request.setHeader("Authorization", "Bearer " + token) + multipart_builder = MultipartEntityBuilder.create() + multipart_builder.addBinaryBody( + "file", login_voice, ContentType.APPLICATION_OCTET_STREAM, "gluu" + ".wav") + if operation == "auth": + self.logger.info("Whispeak Signature %s", + whispeak_signature_id) + multipart_builder.addTextBody( + "id", whispeak_signature_id, ContentType.TEXT_PLAIN) + multipart = multipart_builder.build() + http_service_request.setEntity(multipart) + time_sent = time.time() + http_service_response = HttpClientBuilder.create().build().execute( + http_service_request) + self.logger.info( + "Whispeak API, received response in %ss", (time.time() - time_sent)) + + response_body = self._response_content_entity( + http_service_response) + status_code = http_service_response.getStatusLine().getStatusCode() + if status_code == HttpStatus.SC_OK: + self.logger.info("Operation %s SUCCEED with code %s", + operation, http_service_response.getStatusLine()) + return True + if status_code == HttpStatus.SC_CREATED: + self._put("revocation_ui_link", + response_body['revocation']['revocation_ui_link']) + self._put( + "revocation_pwd", response_body['revocation']['signature_secret_password']) + return response_body["id"] + if status_code == 404: + self._remove_signature_from_user(whispeak_signature_id) + return False + self._put("retry_error", True) + self._set_message_error( + FacesMessage.SEVERITY_ERROR, self._whispeak_error_message(status_code)) + self.logger.warning( + "Operation FAILED with code%s", http_service_response.getStatusLine()) + return False + except Exception as e: + if http_service_response: + self.logger.info("Operation FAILED with code %s", + http_service_response.getStatusLine()) + self.logger.error("Whispeak Auth Exception %s", str(e)) + return False + finally: + if http_service_response: + http_service_response.close() + http_service_request.releaseConnection() + + def _remove_signature_from_user(self, whispeak_signature_id): + user = CdiUtil.bean(UserService).getUserByAttribute( + 'whispeakSignatureId', whispeak_signature_id) + user.setAttribute('whispeakSignatureId', '') + user.setAttribute('whispeakRevocationUiLink', '') + user.setAttribute('whispeakRevocationPwd', '') + CdiUtil.bean(UserService).updateUser(user) + self.logger.info( + "Removed non existent user signature from Gluu %s to force enrollment again (probably speaker secret was removed)", whispeak_signature_id) + self._put("next_step", "1") + self._set_message_error( + FacesMessage.SEVERITY_ERROR, "whispeak.login.signatureDoesNotExist") + + def _whispeak_error_message(self, code): + error_messages = { + 400: "whispeak.apiError.badRequest", + 401: "whispeak.apiError.unauthorized", + 403: "whispeak.apiError.invalidCredential", + 404: "whispeak.apiError.signatureNotFound", + 415: "whispeak.apiError.unsupportedAudioFile", + 419: "whispeak.apiError.voiceMismatch", + 420: "whispeak.apiError.audioConstraintsFailed", + 430: "whispeak.apiError.invalidEnrollSignature" + } + return error_messages[code] + + ################################################################################ + # Passport functions + ################################################################################ + + def _prepare_passport(self): + if not self._get("external_providers"): + self.registered_providers = self._parse_provider_configs() + self._put("external_providers", json.dumps( + self.registered_providers)) + + def _get_passport_redirect_url(self, provider): + + self.logger.debug("Prepare passport for Provider %s", provider) + + # provider is assumed to exist in self.registered_providers + url = None + + token_endpoint = "https://%s/passport/token" % CdiUtil.bean( + FacesContext).getExternalContext().getRequest().getServerName() + + self.logger.debug( + "Obtaining token from passport at %s", token_endpoint) + resultResponse = CdiUtil.bean(HttpService).executeGet( + HttpClientBuilder.create().build(), token_endpoint, Collections.singletonMap("Accept", "text/json")) + http_response = resultResponse.getHttpResponse() + message_bytes = CdiUtil.bean( + HttpService).getResponseContent(http_response) + + response = CdiUtil.bean( + HttpService).convertEntityToString(message_bytes) + self.logger.debug("Response Code %s", + http_response.getStatusLine().getStatusCode()) + if not http_response.getStatusLine().getStatusCode() == HttpStatus.SC_OK: + self._set_message_error( + FacesMessage.SEVERITY_ERROR, "passport.unavailable") + token_obj = json.loads(response) + if 'token_' in token_obj: + url = "/passport/auth/%s/%s" % (provider, token_obj["token_"]) + return url + + def _check_and_activate_alternative_provider_selected(self, request_parameters): + provider = ServerUtil.getFirstValue( + request_parameters, "loginForm:provider") + if not provider: + return False + if not hasattr(self, 'registered_providers'): + self.registered_providers = self.parseProviderConfigs() + is_configured_provider = len( + [prvd for prvd in self.registered_providers if next(iter(prvd)) == provider]) > 0 + + if is_configured_provider: + # it's a recognized external IDP + self._put("selected_provider", provider) + return True + + return False + + def _redirect_oidc(self): + provider = self._get("selected_provider") + if not provider: + return False + url = self._get_passport_redirect_url(provider) + if not url: + return False + self.logger.info("Redirecting user to OIDC url: %s", url) + self._put("selected_provider", None) + CdiUtil.bean(FacesService).redirectToExternalURL(url) + return True + + def _is_oidc_authenticated(self, jwt_param): + + # Parse JWT and validate + self.logger.info("Checking user OIDC token") + jwt = Jwt.parse(jwt_param) + (user_profile) = self._get_user_profile(jwt) + if user_profile is None: + return False + + auth_step1_username = self._get("username") + + if not self._validate_username_oidc(auth_step1_username, user_profile): + self.logger.error( + "FAIL not possible to verify username returns false") + self._set_message_error( + FacesMessage.SEVERITY_ERROR, "login.authOidcMismatch") + return False + return user_profile + + def _validate_username_oidc(self, auth_step1_username, user_profile): + + oidc_username = user_profile["mail"][0] + + if self.check_only_username: + auth_step1_username = auth_step1_username.split("@")[0] + oidc_username = oidc_username.split("@")[0] + + self.logger.debug( + "Validate username matches with third-party username") + + if oidc_username != auth_step1_username: + self.logger.warning( + "Usernames from OIDC does not match username from auth step 1") + return False + return True + + def _get_user_profile(self, jwt): + jwt_claims = jwt.getClaims() + user_profile_json = None + user_profile_json = CdiUtil.bean(EncryptionService).decrypt( + jwt_claims.getClaimAsString("data")) + user_profile = json.loads(user_profile_json) + return user_profile + + def _parse_provider_configs(self): + registered_providers = [] + registered_providers = self._parse_all_providers() + to_remove = [] + for provider in registered_providers: + provider = list(provider.values())[0] + if provider["type"] == "saml": + to_remove.append(provider) + else: + provider["saml"] = False + for provider in to_remove: + registered_providers.pop(provider) + self.logger.debug("Configured providers: %s", registered_providers) + + return registered_providers + + def _parse_all_providers(self): + registered_providers = [] + config = LdapOxPassportConfiguration() + config = CdiUtil.bean(PersistenceEntryManager).find( + config.getClass(), self.passport_dn).getPassportConfiguration() + config = config.getProviders() if config is not None else config + if config is not None and len(config) > 0: + for prvdetails in config: + if prvdetails.isEnabled(): + registered_providers.append({prvdetails.getId(): + { + "id": prvdetails.getId(), + "emailLinkingSafe": prvdetails.isEmailLinkingSafe(), + "requestForEmail": prvdetails.isRequestForEmail(), + "logo_img": prvdetails.getLogoImg(), + "displayName": prvdetails.getDisplayName(), + "type": prvdetails.getType(), + } + }) + return registered_providers + + ################################################################################ + # Generic Auxiliary functions + ################################################################################ + + def _create_user(self, user_email, signature_id=None, profile=None, user_password=None): + new_user = User() + + username = user_email.split("@")[0] + new_user.setAttribute("uid", user_email, True) + new_user.setAttribute("givenName", username, True) + new_user.setAttribute("displayName", username, True) + new_user.setAttribute("sn", "-", True) + new_user.setAttribute("mail", user_email, True) + new_user.setAttribute("gluuStatus", "active", True) + new_user.setAttribute("whispeakSignatureId", signature_id) + new_user.setAttribute("password", user_password) + + if profile: + self._fill_user(new_user, profile) + + new_user = CdiUtil.bean(UserService).addUser(new_user, True) + + return new_user + + def _update_user(self, user, profile): + self._fill_user(user, profile) + CdiUtil.bean(UserService).updateUser(user) + + def _fill_user(self, user, profile): + if profile: + for attr in profile: + if attr != self.provider_key: + values = profile[attr] + user.setAttribute(attr, values) + if attr == "mail": + ox_trust_mails = [] + for mail in values: + ox_trust_mails.append( + '{"value":"{mail}","primary":false}') + user.setAttribute("oxTrustEmail", ox_trust_mails) + + def _set_message_error(self, severity, msg, clear=True): + if clear: + self._new_messages() + error_message = String.format("#{msgs['%s']}", msg) + self.logger.debug("Error message to be returned %s", error_message) + CdiUtil.bean(FacesMessages).add(severity, error_message) + + def _new_messages(self): + CdiUtil.bean(FacesMessages).clear() + + def _response_content_entity(self, http_service_response): + input_stream = http_service_response.getEntity().getContent() + reader = BufferedReader(InputStreamReader(input_stream, "UTF-8"), 8) + string_buffer = "" + line = reader.readLine() + while line is not None: + string_buffer = string_buffer + line.encode('utf-8') + "\n" + line = reader.readLine() + return json.loads(string_buffer) diff --git a/oxAuth/Server/integrations/wwpass/INSTALLATION.md b/oxAuth/Server/integrations/wwpass/INSTALLATION.md new file mode 100644 index 00000000..59e1f770 --- /dev/null +++ b/oxAuth/Server/integrations/wwpass/INSTALLATION.md @@ -0,0 +1,244 @@ +# Setting Up WWPass Authentication in Gluu Server + +## Introduction + +[WWPass](https://wwpass.com/) replaces the traditional username and password +login with a more advanced multi-factor authentication solution. WWPass employs +strong cryptography and robust combination of authentication factors to deliver +a secure and user-friendly authentication experience. WWPass authentication +starts with a smartphone app or a hardware token as the first authentication +factor. Then additional authentication factors such as PIN or biometrics can be +added to verify the user identity further. + +[Gluu Server](https://gluu.org/docs/gluu-server/4.1/) is a container +distribution of free open source software (FOSS) for identity and access +management (IAM). Gluu Server combines SAML 2.0, LDAP, OpenID Connect, +and other authentication and authorization protocol implementations to create +a platform for user authentication, identity information, and policy decisions. + +Combining WWPass strong multi-factor authentication with the versatility of +Gluu Server helps to build secure IAM solutions that can be used for single +sign-on (SSO), customer identity and access management (CIAM), and identity +federation. + +## Prerequisites + +This tutorial assumes that you have the following: + +- Gluu Server 4.1.1 installed on Ubuntu Server 18.04 or 16.04; +- An administrative account on this Ubuntu Server; +- An application certificate and private key for WWPass authentication; +- WWPass Key app or hardware token; + +### Obtain Application Certificate and Private Key From WWPass + +To obtain an application certificate and private key go to +[wwpass.com](https://wwpass.com), click **Sign Up** to create a developer +account or **Log In** if you already have an account. Then follow the website +instructions to register your application domain and issue the application +certificate. + +## Prepare WWPass Integration Files + +Copy this directory to your Gluu server and make it current +```console +cd wwpass +``` + +Files in `pages` directory should be deployed to +`/opt/gluu-server/opt/gluu/jetty/oxauth/custom/pages/` + +```console +sudo cp -rL pages/* /opt/gluu-server/opt/gluu/jetty/oxauth/custom/pages/ +``` + +Files in `static` directory should be deployed to +`/opt/gluu-server/opt/gluu/jetty/oxauth/custom/static/` + +```console +sudo cp -rL pages/* /opt/gluu-server/opt/gluu/jetty/oxauth/custom/static/ +``` + +Copy `wwpass.py` to `/opt/gluu-server/opt/gluu/python/libs/` + +```console +sudo cp wwpass.py /opt/gluu-server/opt/gluu/python/libs/ +``` + +Copy `ticket.json` to `/opt/gluu-server/opt/wwpass_gluu/cgi` + +``` console +sudo mkdir -p /opt/gluu-server/opt/wwpass_gluu/cgi +sudo cp ticket.json /opt/gluu-server/opt/wwpass_gluu/cgi +``` + +Make sure `ticket.json` is executable + +```console +sudo chmod 755 /opt/gluu-server/opt/wwpass_gluu/cgi/ticket.json +``` + +Copy WWPass application certificate and private key to +`/opt/gluu-server/opt/wwpass_gluu/` +(assuming the certificate and key files are in your home directory) + +```console +sudo cp ~/gluu_client.crt /opt/gluu-server/opt/wwpass_gluu/gluu_client.crt +sudo cp ~/gluu_client.key /opt/gluu-server/opt/wwpass_gluu/gluu_client.key +``` + +Replace `~/gluu_client.crt` and `~/gluu_client.key` with the names and location +of your certificate and key files. + +Copy WWPass CA certificate `wwpass.ca.crt` to `/opt/gluu-server/opt/wwpass_gluu/` + +```console +sudo cp wwpass.ca.crt /opt/gluu-server/opt/wwpass_gluu +``` + +Log in to the Gluu Server container + +```console +sudo /sbin/gluu-serverd login +``` + +Change ownership of files and directories just copied + +```console +chown -R jetty:jetty /opt/jetty +chown root:gluu /opt/gluu/python/libs/wwpass.py +``` + +## Configure Apache + +Use your favorite console text editor to change Apache configuration + +```console +[vi|nano|joe|...] /opt/gluu-server/etc/apache2/sites-available/https_gluu.conf +``` + +Scroll down the file until you find the last `...` tag and +insert the following snippet below it: + +```apache + + require all granted + + +ScriptAlias "/wwpass/" "/opt/wwpass_gluu/cgi/" + + + SetHandler cgi-script + Options +ExecCGI + Order deny,allow + Allow from all + +``` + +Save the file and exit the editor. + +Enable mod_cgi and restart apache: + +```console +a2enmod cgi +systemctl restart apache2 +``` + +Check that `ticket.json` is working. +Go to `https:///wwpass/ticket.json`. +If your setup is correct, you will see output like this: + +```json +{"result": true, "data": "SPNAME:07629a1963c5e4f4f339ecb852b7a0bf10a90c62@p-sp-05-50:16033", "ttl": 600, "encoding": "plain"} +``` + +## Configure Gluu Server + +Log in as administrator and go to "Configuration -> Manage Custom scripts". + +In `Person Authentication` tab, click `Add custom script configuration` at the bottom of the page + +Create custom script `wwpass` with Location Type set to `Database`. + +Add the following to `Custom Property` fields by clicking on `Add new property` button: + +- wwpass_crt_file: /opt/wwpass_gluu/gluu_client.crt +- wwpass_key_file: /opt/wwpass_gluu/gluu_client.key +- registration_url: URL of the registration web application, if you have one. Do not add this property otherwise +- recovery_url: URL for account recovery, if you have one. Do not add this property otherwise. + +To require PIN or biometrics during login, add: + +- use_pin: True + +To enable binding WWPass keys with email, add: + +- allow_email_bind: True + +Note, your Gluu server should be able to send emails, see +`Configuration -> Organization configuration -> SMTP Server Configuration`. + +To enable binding WWPass keys with existing usernames and passwords, add: + +- allow_password_bind: True + +To enable binding WWPass keys with another WWPass key, add: + +- allow_passkey_bind: True + +Replace the content of `Script` textbox with the content of `wwpassauth.py` from `gluu-master` + +Set `Enabled` checkbox to enable the custom script: + +Click the `Update` button. + +It's also recommended to increase the unauthenticated session lifetime to give +users more time to bind their WWPass keys. + +Go to `Configuration -> JSON configuration -> OxAuth Configuration`, find +`sessionIdUnauthenticatedUnusedLifetime` setting and set it to `600` or more. + +Click `Update` to save settings. + +### Set up Authentication Method + +Before switching to WWPass authentication, make sure you have opened +an administrator session in a different browser (not in a different window, +but a completely different browser). Do not close that browser window and +reload it from time to time to make sure your session does not expire. + +Keep the backup session opened until you are sure that WWPass authentication +works properly or you might lock yourself out of Gluu. If that happens, refer to: +[Gluu FAQ](https://gluu.org/docs/gluu-server/operation/faq/#revert-an-authentication-method) + +In the Gluu Admin interface, navigate to `Configuration -> Manage Authentication -> Default Authentication Method`. +Set both options to "wwpass". + +Click `Update` to save settings. + +### Test Your Setup + +Open "https:///" in a different browser (not the one you used to +configure Gluu). + +Try to sign in to your Gluu server with WWPass and bind your key using either +email or username and password. + +If something does not work as expected, return to your main browser and revert +`Configuration -> Manage Authentication -> Default Authentication Method` back +to `auth_ldap_server` while you troubleshoot the problem. + +## Troubleshooting + +Relevant Gluu log files are: + +- `/opt/gluu-server/opt/gluu/jetty/oxauth/logs/oxauth.log` +- `/opt/gluu-server/opt/gluu/jetty/oxauth/logs/oxauth_script.log` + +Check the files above for any errors. Errors in `wwpass` interception script +are also displayed in Gluu web interface at 'Configuration -> Manage Custom +scripts'. If there are any errors in the script, its name will be in red and +the script editor will display the red button named "Show Error". + +Feel free to contact WWPass at support@wwpass.com if you have troubles +integrating WWPass in your Gluu Server. diff --git a/oxAuth/Server/integrations/wwpass/README.md b/oxAuth/Server/integrations/wwpass/README.md new file mode 100644 index 00000000..62b46b60 --- /dev/null +++ b/oxAuth/Server/integrations/wwpass/README.md @@ -0,0 +1,19 @@ +# WWPass integration for Gluu + +[WWPass](https://wwpass.com/) replaces the traditional username and password +login with a more advanced multi-factor authentication solution. WWPass employs +strong cryptography and robust combination of authentication factors to deliver +a secure and user-friendly authentication experience. WWPass authentication +starts with a smartphone app or a hardware token as the first authentication +factor. Then additional authentication factors such as PIN or biometrics can be +added to verify the user identity further. + +## Installation + +To install WWPass support in Gluu server refer to [INSTALLATION.md](INSTALLATION.md). + +To install additional components refer to main wwpass-gluu repository at https://github.com/wwpass/gluu + +## Contacts + +Feel free to contact WWPass at support@wwpass.com if you have trouble integrating WWPass in your Gluu setup \ No newline at end of file diff --git a/oxAuth/Server/integrations/wwpass/pages/auth/wwpass/checkemail.xhtml b/oxAuth/Server/integrations/wwpass/pages/auth/wwpass/checkemail.xhtml new file mode 100644 index 00000000..59f10a80 --- /dev/null +++ b/oxAuth/Server/integrations/wwpass/pages/auth/wwpass/checkemail.xhtml @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + +

2$v43epbLvGpRx=R)`Kj2me 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 literal 0 HcmV?d00001 diff --git a/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp/0.0.1-SNAPSHOT/oath-otp-0.0.1-SNAPSHOT-sources.jar b/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp/0.0.1-SNAPSHOT/oath-otp-0.0.1-SNAPSHOT-sources.jar new file mode 100644 index 0000000000000000000000000000000000000000..03c2aabd55df7b7266105ed3f0e04ff7454a48e2 GIT binary patch literal 13536 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 fzYP7UgFnNJG7$FB&> + 4.0.0 + + com.lochbridge.oath + oath-parent + 0.0.1-SNAPSHOT + + oath-otp + OATH OTP + A module for generating and validating OTPs. + + + + com.google.guava + guava + + + \ No newline at end of file diff --git a/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp/maven-metadata-local.xml b/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp/maven-metadata-local.xml new file mode 100644 index 00000000..e2477503 --- /dev/null +++ b/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-otp/maven-metadata-local.xml @@ -0,0 +1,11 @@ + + + com.lochbridge.oath + oath-otp + + + 0.0.1-SNAPSHOT + + 20160926101332 + + diff --git a/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-parent/0.0.1-SNAPSHOT/_remote.repositories b/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-parent/0.0.1-SNAPSHOT/_remote.repositories new file mode 100644 index 00000000..d0af97df --- /dev/null +++ b/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-parent/0.0.1-SNAPSHOT/_remote.repositories @@ -0,0 +1,3 @@ +#NOTE: This is an Aether internal implementation file, its format can be changed without prior notice. +#Mon Sep 26 13:13:30 MSK 2016 +oath-parent-0.0.1-SNAPSHOT.pom>= diff --git a/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-parent/0.0.1-SNAPSHOT/maven-metadata-local.xml b/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-parent/0.0.1-SNAPSHOT/maven-metadata-local.xml new file mode 100644 index 00000000..8ebf18a3 --- /dev/null +++ b/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-parent/0.0.1-SNAPSHOT/maven-metadata-local.xml @@ -0,0 +1,19 @@ + + + com.lochbridge.oath + oath-parent + 0.0.1-SNAPSHOT + + + true + + 20160926101330 + + + pom + 0.0.1-SNAPSHOT + 20160926101330 + + + + diff --git a/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-parent/0.0.1-SNAPSHOT/oath-parent-0.0.1-SNAPSHOT.pom b/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-parent/0.0.1-SNAPSHOT/oath-parent-0.0.1-SNAPSHOT.pom new file mode 100644 index 00000000..ee4048d1 --- /dev/null +++ b/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-parent/0.0.1-SNAPSHOT/oath-parent-0.0.1-SNAPSHOT.pom @@ -0,0 +1,182 @@ + + 4.0.0 + com.lochbridge.oath + oath-parent + 0.0.1-SNAPSHOT + pom + OATH Project + https://github.com/johnnymongiat/oath + + OATH provides components for building one-time password authentication systems. + + + + UTF-8 + UTF-8 + 18.0 + + + + + Johnny Mongiat + johnnymongiat@gmail.com + America/Montreal + + committer + + + + + + scm:git:https://github.com/johnnymongiat/oath.git + scm:git:https://github.com/johnnymongiat/oath.git + https://github.com/johnnymongiat/oath.git + + + + github + https://github.com/johnnymongiat/oath/issues + + + + Travis CI + https://travis-ci.org/johnnymongiat/oath + + + + + The MIT License (MIT) + http://opensource.org/licenses/MIT + manual + + + + + + junit + junit + 4.11 + test + + + org.mockito + mockito-core + 1.9.5 + test + + + + + + + com.google.guava + guava + ${guava.version} + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.7 + 1.7 + + + + org.apache.maven.plugins + maven-source-plugin + 2.3 + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + ${javadoc.doclint.none} + + + + attach-javadocs + + jar + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.5 + + + + true + + + + + + org.codehaus.mojo + cobertura-maven-plugin + 2.6 + + + xml + html + + true + + + + org.eluder.coveralls + coveralls-maven-plugin + 3.0.1 + + + + + + + + org.apache.maven.plugins + maven-project-info-reports-plugin + 2.7 + + true + true + + + + + + + oath-otp + oath-otp-keyprovisioning + + + \ No newline at end of file diff --git a/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-parent/maven-metadata-local.xml b/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-parent/maven-metadata-local.xml new file mode 100644 index 00000000..e496352c --- /dev/null +++ b/oxAuth/Server/integrations/otp/repository/com/lochbridge/oath/oath-parent/maven-metadata-local.xml @@ -0,0 +1,11 @@ + + + com.lochbridge.oath + oath-parent + + + 0.0.1-SNAPSHOT + + 20160926101330 + + diff --git a/oxAuth/Server/integrations/otp/sample/otp_configuration.json b/oxAuth/Server/integrations/otp/sample/otp_configuration.json new file mode 100644 index 00000000..43b27d1c --- /dev/null +++ b/oxAuth/Server/integrations/otp/sample/otp_configuration.json @@ -0,0 +1,13 @@ +{ + "htop":{ + "keyLength":20, + "digits":6, + "lookAheadWindow":1 + }, + "totp":{ + "keyLength":20, + "digits":6, + "timeStep":30, + "hmacShaAlgorithm": "sha1" + } +} diff --git a/oxAuth/Server/integrations/otp/sequence_diagram.txt b/oxAuth/Server/integrations/otp/sequence_diagram.txt new file mode 100644 index 00000000..f03514bb --- /dev/null +++ b/oxAuth/Server/integrations/otp/sequence_diagram.txt @@ -0,0 +1,31 @@ +Title OTP enrollment/authentication workflow + +Person->Browser: Open RP URL +Browser->RP: Protected resource +RP->Gluu Server: Start AuthZ & AuthN +OTP Script->OTP Script: Verify user/password + +alt: User enrollment + OTP Script->OTP Script: Check if person not issued OTP key already + OTP Script->Browser: Render otpauth QR code with OTP key + Person->OTP comp. auth.: Scan QR code + OTP comp. auth.->Person: New one time password + Person->Browser: Enter one time password + Browser->OTP Script: + OTP Script->OTP Script: Validate one time password + OTP Script->OTP Script: Strore OTP key in user entry + OTP Script->Gluu Server: User pass enrollment +else User authentication + OTP Script->OTP Script: Check if person issued OTP key already + OTP comp. auth.->Person: New one time password + Person->Browser: Enter one time password + Browser->OTP Script: + OTP Script->OTP Script: Validate one time password + OTP Script->Gluu Server: User pass enrollment +end + +Gluu Server->Browser: Return code +Browser->RP: Return code + +RP->Gluu Server: Request tokens +RP->Gluu Server: Request user_info diff --git a/oxAuth/Server/integrations/passport/PassportExternalAuthenticator.py b/oxAuth/Server/integrations/passport/PassportExternalAuthenticator.py new file mode 100644 index 00000000..31b16e3b --- /dev/null +++ b/oxAuth/Server/integrations/passport/PassportExternalAuthenticator.py @@ -0,0 +1,631 @@ +# oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. +# Copyright (c) 2019, Gluu +# +# Author: Jose Gonzalez +# Author: Yuriy Movchan +# +from org.gluu.jsf2.service import FacesService +from org.gluu.jsf2.message import FacesMessages + +from org.gluu.oxauth.model.common import User, WebKeyStorage +from org.gluu.oxauth.model.configuration import AppConfiguration +from org.gluu.oxauth.model.crypto import CryptoProviderFactory +from org.gluu.oxauth.model.jwt import Jwt, JwtClaimName +from org.gluu.oxauth.model.util import Base64Util +from org.gluu.oxauth.service import AppInitializer, AuthenticationService +from org.gluu.oxauth.service.common import UserService, EncryptionService +from org.gluu.oxauth.service.net import HttpService +from org.gluu.oxauth.security import Identity +from org.gluu.oxauth.util import ServerUtil +from org.gluu.config.oxtrust import LdapOxPassportConfiguration +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.persist import PersistenceEntryManager +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.util import StringHelper +from java.util import ArrayList, Arrays, Collections + +from javax.faces.application import FacesMessage +from javax.faces.context import FacesContext + +import json +import sys +import datetime + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Passport. init called" + + self.extensionModule = self.loadExternalModule(configurationAttributes.get("extension_module")) + extensionResult = self.extensionInit(configurationAttributes) + if extensionResult != None: + return extensionResult + + print "Passport. init. Behaviour is social" + success = self.processKeyStoreProperties(configurationAttributes) + + if success: + self.providerKey = "provider" + self.customAuthzParameter = self.getCustomAuthzParameter(configurationAttributes.get("authz_req_param_provider")) + self.passportDN = self.getPassportConfigDN() + print "Passport. init. Initialization success" + else: + print "Passport. init. Initialization failed" + return success + + + def destroy(self, configurationAttributes): + print "Passport. destroy called" + return True + + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, requestParameters): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + + def authenticate(self, configurationAttributes, requestParameters, step): + + extensionResult = self.extensionAuthenticate(configurationAttributes, requestParameters, step) + if extensionResult != None: + return extensionResult + + print "Passport. authenticate for step %s called" % str(step) + identity = CdiUtil.bean(Identity) + + # Loading self.registeredProviders in case passport destroyed + if not hasattr(self,'registeredProviders'): + print "Passport. Fetching registered providers." + self.parseProviderConfigs() + + if step == 1: + # Get JWT token + jwt_param = ServerUtil.getFirstValue(requestParameters, "user") + + if jwt_param != None: + print "Passport. authenticate for step 1. JWT user profile token found" + + # Parse JWT and validate + jwt = Jwt.parse(jwt_param) + if not self.validSignature(jwt): + return False + + if self.jwtHasExpired(jwt): + return False + + (user_profile, jsonp) = self.getUserProfile(jwt) + if user_profile == None: + return False + + sessionAttributes = identity.getSessionId().getSessionAttributes() + self.skipProfileUpdate = StringHelper.equalsIgnoreCase(sessionAttributes.get("skipPassportProfileUpdate"), "true") + + return self.attemptAuthentication(identity, user_profile, jsonp) + + #See passportlogin.xhtml + provider = ServerUtil.getFirstValue(requestParameters, "loginForm:provider") + if StringHelper.isEmpty(provider): + + #it's username + passw auth + print "Passport. authenticate for step 1. Basic authentication detected" + logged_in = False + + credentials = identity.getCredentials() + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + if StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password): + authenticationService = CdiUtil.bean(AuthenticationService) + logged_in = authenticationService.authenticate(user_name, user_password) + + print "Passport. authenticate for step 1. Basic authentication returned: %s" % logged_in + return logged_in + + elif provider in self.registeredProviders: + #it's a recognized external IDP + identity.setWorkingParameter("selectedProvider", provider) + print "Passport. authenticate for step 1. Retrying step 1" + #see prepareForStep (step = 1) + return True + + if step == 2: + mail = ServerUtil.getFirstValue(requestParameters, "loginForm:email") + jsonp = identity.getWorkingParameter("passport_user_profile") + + if mail == None: + self.setMessageError(FacesMessage.SEVERITY_ERROR, "Email was missing in user profile") + elif jsonp != None: + # Completion of profile takes place + user_profile = json.loads(jsonp) + user_profile["mail"] = [ mail ] + + return self.attemptAuthentication(identity, user_profile, jsonp) + + print "Passport. authenticate for step 2. Failed: expected mail value in HTTP request and json profile in session" + return False + + + def prepareForStep(self, configurationAttributes, requestParameters, step): + + extensionResult = self.extensionPrepareForStep(configurationAttributes, requestParameters, step) + if extensionResult != None: + return extensionResult + + print "Passport. prepareForStep called %s" % str(step) + identity = CdiUtil.bean(Identity) + + if step == 1: + #re-read the strategies config (for instance to know which strategies have enabled the email account linking) + self.parseProviderConfigs() + identity.setWorkingParameter("externalProviders", json.dumps(self.registeredProviders)) + + providerParam = self.customAuthzParameter + url = None + + sessionAttributes = identity.getSessionId().getSessionAttributes() + self.skipProfileUpdate = StringHelper.equalsIgnoreCase(sessionAttributes.get("skipPassportProfileUpdate"), "true") + + #this param could have been set previously in authenticate step if current step is being retried + provider = identity.getWorkingParameter("selectedProvider") + if provider != None: + url = self.getPassportRedirectUrl(provider) + identity.setWorkingParameter("selectedProvider", None) + + elif providerParam != None: + paramValue = sessionAttributes.get(providerParam) + + if paramValue != None: + print "Passport. prepareForStep. Found value in custom param of authorization request: %s" % paramValue + provider = self.getProviderFromJson(paramValue) + + if provider == None: + print "Passport. prepareForStep. A provider value could not be extracted from custom authorization request parameter" + elif not provider in self.registeredProviders: + print "Passport. prepareForStep. Provider '%s' not part of known configured IDPs/OPs" % provider + else: + url = self.getPassportRedirectUrl(provider) + + if url == None: + print "Passport. prepareForStep. A page to manually select an identity provider will be shown" + else: + facesService = CdiUtil.bean(FacesService) + facesService.redirectToExternalURL(url) + + return True + + + def getExtraParametersForStep(self, configurationAttributes, step): + print "Passport. getExtraParametersForStep called" + if step == 1: + return Arrays.asList("selectedProvider", "externalProviders") + elif step == 2: + return Arrays.asList("passport_user_profile") + return None + + + def getCountAuthenticationSteps(self, configurationAttributes): + print "Passport. getCountAuthenticationSteps called" + identity = CdiUtil.bean(Identity) + if identity.getWorkingParameter("passport_user_profile") != None: + return 2 + return 1 + + + def getPageForStep(self, configurationAttributes, step): + print "Passport. getPageForStep called" + + extensionResult = self.extensionGetPageForStep(configurationAttributes, step) + if extensionResult != None: + return extensionResult + + if step == 1: + return "/auth/passport/passportlogin.xhtml" + return "/auth/passport/passportpostlogin.xhtml" + + + def getNextStep(self, configurationAttributes, requestParameters, step): + + if step == 1: + identity = CdiUtil.bean(Identity) + provider = identity.getWorkingParameter("selectedProvider") + if provider != None: + return 1 + + return -1 + + def getLogoutExternalUrl(self, configurationAttributes, requestParameters): + print "Get external logout URL call" + return None + + def logout(self, configurationAttributes, requestParameters): + return True + +# Extension module related functions + + def extensionInit(self, configurationAttributes): + + if self.extensionModule == None: + return None + return self.extensionModule.init(configurationAttributes) + + + def extensionAuthenticate(self, configurationAttributes, requestParameters, step): + + if self.extensionModule == None: + return None + return self.extensionModule.authenticate(configurationAttributes, requestParameters, step) + + + def extensionPrepareForStep(self, configurationAttributes, requestParameters, step): + + if self.extensionModule == None: + return None + return self.extensionModule.prepareForStep(configurationAttributes, requestParameters, step) + + + def extensionGetPageForStep(self, configurationAttributes, step): + + if self.extensionModule == None: + return None + return self.extensionModule.getPageForStep(configurationAttributes, step) + +# Initalization routines + + def loadExternalModule(self, simpleCustProperty): + + if simpleCustProperty != None: + print "Passport. loadExternalModule. Loading passport extension module..." + moduleName = simpleCustProperty.getValue2() + try: + module = __import__(moduleName) + return module + except: + print "Passport. loadExternalModule. Failed to load module %s" % moduleName + print "Exception: ", sys.exc_info()[1] + print "Passport. loadExternalModule. Flow will be driven entirely by routines of main passport script" + return None + + + def processKeyStoreProperties(self, attrs): + file = attrs.get("key_store_file") + password = attrs.get("key_store_password") + + if file != None and password != None: + file = file.getValue2() + password = password.getValue2() + + if StringHelper.isNotEmpty(file) and StringHelper.isNotEmpty(password): + self.keyStoreFile = file + self.keyStorePassword = password + return True + + print "Passport. readKeyStoreProperties. Properties key_store_file or key_store_password not found or empty" + return False + + + def getCustomAuthzParameter(self, simpleCustProperty): + + customAuthzParameter = None + if simpleCustProperty != None: + prop = simpleCustProperty.getValue2() + if StringHelper.isNotEmpty(prop): + customAuthzParameter = prop + + if customAuthzParameter == None: + print "Passport. getCustomAuthzParameter. No custom param for OIDC authz request in script properties" + print "Passport. getCustomAuthzParameter. Passport flow cannot be initiated by doing an OpenID connect authorization request" + else: + print "Passport. getCustomAuthzParameter. Custom param for OIDC authz request in script properties: %s" % customAuthzParameter + + return customAuthzParameter + +# Configuration parsing + + def getPassportConfigDN(self): + + f = open('/etc/gluu/conf/gluu.properties', 'r') + for line in f: + prop = line.split("=") + if prop[0] == "oxpassport_ConfigurationEntryDN": + prop.pop(0) + break + + f.close() + return "=".join(prop).strip() + + + def parseAllProviders(self): + + registeredProviders = {} + print "Passport. parseAllProviders. Adding providers" + entryManager = CdiUtil.bean(PersistenceEntryManager) + + config = LdapOxPassportConfiguration() + config = entryManager.find(config.getClass(), self.passportDN).getPassportConfiguration() + config = config.getProviders() if config != None else config + + if config != None and len(config) > 0: + for prvdetails in config: + if prvdetails.isEnabled(): + registeredProviders[prvdetails.getId()] = { + "emailLinkingSafe": prvdetails.isEmailLinkingSafe(), + "requestForEmail" : prvdetails.isRequestForEmail(), + "logo_img": prvdetails.getLogoImg(), + "displayName": prvdetails.getDisplayName(), + "type": prvdetails.getType() + } + + return registeredProviders + + + def parseProviderConfigs(self): + + registeredProviders = {} + try: + registeredProviders = self.parseAllProviders() + toRemove = [] + + for provider in registeredProviders: + if registeredProviders[provider]["type"] == "saml": + toRemove.append(provider) + else: + registeredProviders[provider]["saml"] = False + + for provider in toRemove: + registeredProviders.pop(provider) + + if len(registeredProviders.keys()) > 0: + print "Passport. parseProviderConfigs. Configured providers:", registeredProviders + else: + print "Passport. parseProviderConfigs. No providers registered yet" + except: + print "Passport. parseProviderConfigs. An error occurred while building the list of supported authentication providers", sys.exc_info()[1] + + self.registeredProviders = registeredProviders + +# Auxiliary routines + + def getProviderFromJson(self, providerJson): + + provider = None + try: + obj = json.loads(Base64Util.base64urldecodeToString(providerJson)) + provider = obj[self.providerKey] + except: + print "Passport. getProviderFromJson. Could not parse provided Json string. Returning None" + + return provider + + + def getPassportRedirectUrl(self, provider): + + # provider is assumed to exist in self.registeredProviders + url = None + try: + facesContext = CdiUtil.bean(FacesContext) + tokenEndpoint = "https://%s/passport/token" % facesContext.getExternalContext().getRequest().getServerName() + + httpService = CdiUtil.bean(HttpService) + httpclient = httpService.getHttpsClient() + + print "Passport. getPassportRedirectUrl. Obtaining token from passport at %s" % tokenEndpoint + resultResponse = httpService.executeGet(httpclient, tokenEndpoint, Collections.singletonMap("Accept", "text/json")) + httpResponse = resultResponse.getHttpResponse() + bytes = httpService.getResponseContent(httpResponse) + + response = httpService.convertEntityToString(bytes) + print "Passport. getPassportRedirectUrl. Response was %s" % httpResponse.getStatusLine().getStatusCode() + + tokenObj = json.loads(response) + url = "/passport/auth/%s/%s" % (provider, tokenObj["token_"]) + except: + print "Passport. getPassportRedirectUrl. Error building redirect URL: ", sys.exc_info()[1] + + return url + + + def validSignature(self, jwt): + + print "Passport. validSignature. Checking JWT token signature" + valid = False + + try: + appConfiguration = AppConfiguration() + appConfiguration.setWebKeysStorage(WebKeyStorage.KEYSTORE) + appConfiguration.setKeyStoreFile(self.keyStoreFile) + appConfiguration.setKeyStoreSecret(self.keyStorePassword) + appConfiguration.setKeyRegenerationEnabled(False) + + cryptoProvider = CryptoProviderFactory.getCryptoProvider(appConfiguration) + valid = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), jwt.getHeader().getKeyId(), + None, None, jwt.getHeader().getSignatureAlgorithm()) + except: + print "Exception: ", sys.exc_info()[1] + + print "Passport. validSignature. Validation result was %s" % valid + return valid + + + def jwtHasExpired(self, jwt): + # Check if jwt has expired + jwt_claims = jwt.getClaims() + try: + exp_date_timestamp = float(jwt_claims.getClaimAsString(JwtClaimName.EXPIRATION_TIME)) + exp_date = datetime.datetime.fromtimestamp(exp_date_timestamp) + hasExpired = exp_date < datetime.datetime.now() + except: + print "Exception: The JWT does not have '%s' attribute" % JwtClaimName.EXPIRATION_TIME + return False + + return hasExpired + + + def getUserProfile(self, jwt): + jwt_claims = jwt.getClaims() + user_profile_json = None + + try: + user_profile_json = CdiUtil.bean(EncryptionService).decrypt(jwt_claims.getClaimAsString("data")) + user_profile = json.loads(user_profile_json) + except: + print "Passport. getUserProfile. Problem obtaining user profile json representation" + + return (user_profile, user_profile_json) + + + def attemptAuthentication(self, identity, user_profile, user_profile_json): + + uidKey = "uid" + if not self.checkRequiredAttributes(user_profile, [uidKey, self.providerKey]): + return False + + provider = user_profile[self.providerKey] + if not provider in self.registeredProviders: + print "Passport. attemptAuthentication. Identity Provider %s not recognized" % provider + return False + + uid = user_profile[uidKey][0] + externalUid = "passport-%s:%s" % (provider, uid) + + userService = CdiUtil.bean(UserService) + userByUid = userService.getUserByAttribute("oxExternalUid", externalUid, True) + + email = None + if "mail" in user_profile: + email = user_profile["mail"] + if len(email) == 0: + email = None + else: + email = email[0] + user_profile["mail"] = [ email ] + + if email == None and self.registeredProviders[provider]["requestForEmail"]: + print "Passport. attemptAuthentication. Email was not received" + + if userByUid != None: + # This avoids asking for the email over every login attempt + email = userByUid.getAttribute("mail") + if email != None: + print "Passport. attemptAuthentication. Filling missing email value with %s" % email + user_profile["mail"] = [ email ] + + if email == None: + # Store user profile in session and abort this routine + identity.setWorkingParameter("passport_user_profile", user_profile_json) + return True + + userByMail = None if email == None else userService.getUserByAttribute("mail", email) + + # Determine if we should add entry, update existing, or deny access + doUpdate = False + doAdd = False + if userByUid != None: + print "User with externalUid '%s' already exists" % externalUid + if userByMail == None: + doUpdate = True + else: + if userByMail.getUserId() == userByUid.getUserId(): + doUpdate = True + else: + print "Users with externalUid '%s' and mail '%s' are different. Access will be denied. Impersonation attempt?" % (externalUid, email) + self.setMessageError(FacesMessage.SEVERITY_ERROR, "Email value corresponds to an already existing provisioned account") + else: + if userByMail == None: + doAdd = True + elif self.registeredProviders[provider]["emailLinkingSafe"]: + + tmpList = userByMail.getAttributeValues("oxExternalUid") + tmpList = ArrayList() if tmpList == None else ArrayList(tmpList) + tmpList.add(externalUid) + userByMail.setAttribute("oxExternalUid", tmpList, True) + + userByUid = userByMail + print "External user supplying mail %s will be linked to existing account '%s'" % (email, userByMail.getUserId()) + doUpdate = True + else: + print "An attempt to supply an email of an existing user was made. Turn on 'emailLinkingSafe' if you want to enable linking" + self.setMessageError(FacesMessage.SEVERITY_ERROR, "Email value corresponds to an already existing account. If you already have a username and password use those instead of an external authentication site to get access.") + + username = None + try: + if doUpdate: + username = userByUid.getUserId() + print "Passport. attemptAuthentication. Updating user %s" % username + self.updateUser(userByUid, user_profile, userService) + elif doAdd: + print "Passport. attemptAuthentication. Creating user %s" % externalUid + newUser = self.addUser(externalUid, user_profile, userService) + username = newUser.getUserId() + except: + print "Exception: ", sys.exc_info()[1] + print "Passport. attemptAuthentication. Authentication failed" + return False + + if username == None: + print "Passport. attemptAuthentication. Authentication attempt was rejected" + return False + else: + logged_in = CdiUtil.bean(AuthenticationService).authenticate(username) + print "Passport. attemptAuthentication. Authentication for %s returned %s" % (username, logged_in) + return logged_in + + + def setMessageError(self, severity, msg): + facesMessages = CdiUtil.bean(FacesMessages) + facesMessages.setKeepMessages() + facesMessages.clear() + facesMessages.add(severity, msg) + + + def checkRequiredAttributes(self, profile, attrs): + + for attr in attrs: + if (not attr in profile) or len(profile[attr]) == 0: + print "Passport. checkRequiredAttributes. Attribute '%s' is missing in profile" % attr + return False + return True + + + def addUser(self, externalUid, profile, userService): + + newUser = User() + #Fill user attrs + newUser.setAttribute("oxExternalUid", externalUid, True) + self.fillUser(newUser, profile) + newUser = userService.addUser(newUser, True) + return newUser + + + def updateUser(self, foundUser, profile, userService): + + # when this is false, there might still some updates taking place (e.g. not related to profile attrs released by external provider) + if (not self.skipProfileUpdate): + self.fillUser(foundUser, profile) + userService.updateUser(foundUser) + + + def fillUser(self, foundUser, profile): + + for attr in profile: + # "provider" is disregarded if part of mapping + if attr != self.providerKey: + values = profile[attr] + print "%s = %s" % (attr, values) + foundUser.setAttribute(attr, values) + + if attr == "mail": + oxtrustMails = [] + for mail in values: + oxtrustMails.append('{"value":"%s","primary":false}' % mail) + foundUser.setAttribute("oxTrustEmail", oxtrustMails) diff --git a/oxAuth/Server/integrations/passport/README.md b/oxAuth/Server/integrations/passport/README.md new file mode 100644 index 00000000..f6057707 --- /dev/null +++ b/oxAuth/Server/integrations/passport/README.md @@ -0,0 +1,10 @@ +Passport is a person authentication module for oxAuth that enables [Google+ Authentication](https://www.google.com), [Twitter Authentication](https://www.twitter.com), [Facebook Authentication](https://www.facebook.com) etc. for user authentication. + +The module has a few properties: + + 1) generic_remote_attributes_list - It's mandatory property. Comma separated list of attribute names. Specify list of User claims(attributes) which script should use to map to local attributes. The count of attributes in this property should be equal to count attributes in generic_local_attributes_list property. +Example: `username, email, name, name, givenName, familyName, provider` + + 2) generic_local_attributes_list - It's mandatory property. Comma separated list of attribute names. Specify list of local attributes mapped from passport userInfo response. The count of attributes in this property should be equal to count attributes in generic_remote_attributes_list property. Local attributes list should contains next mandatory attributes: uid, mail, givenName, sn, cn. +Example: `uid, mail, cn, displayName, givenName, sn, provider` + diff --git a/oxAuth/Server/integrations/passport/sample/passport_script_entry.ldif b/oxAuth/Server/integrations/passport/sample/passport_script_entry.ldif new file mode 100644 index 00000000..7142616e --- /dev/null +++ b/oxAuth/Server/integrations/passport/sample/passport_script_entry.ldif @@ -0,0 +1,16 @@ +dn: inum=@!1111!2FDB.CF02,ou=scripts,o=gluu +objectClass: oxCustomScript +objectClass: top +description: Passport authentication module +displayName: passport +gluuStatus: false +inum: @!1111!2FDB.CF02 +oxConfigurationProperty: {"value1":"generic_remote_attributes_list","value2":"username, email, name, name, givenName, familyName, provider","description":""} +oxConfigurationProperty: {"value1":"generic_local_attributes_list","value2":"uid, mail, cn, displayName, givenName, sn, provider","description":""} +oxLevel: 60 +oxModuleProperty: {"value1":"usage_type","value2":"interactive","description":""} +oxModuleProperty: {"value1":"location_type","value2":"ldap","description":""} +oxRevision: 1 +oxScript:: +oxScriptType: person_authentication +programmingLanguage: python diff --git a/oxAuth/Server/integrations/passwordless/PasswordlessAuthentication.py b/oxAuth/Server/integrations/passwordless/PasswordlessAuthentication.py new file mode 100644 index 00000000..7bbfdf2e --- /dev/null +++ b/oxAuth/Server/integrations/passwordless/PasswordlessAuthentication.py @@ -0,0 +1,445 @@ +# Author: Jose Gonzalez +from java.lang import System +from java.net import URLDecoder, URLEncoder +from java.util import Arrays, ArrayList, Collections, HashMap + +from javax.faces.application import FacesMessage +from javax.servlet.http import Cookie +from javax.faces.context import FacesContext + +from org.oxauth.persistence.model.configuration import GluuConfiguration + +from org.gluu.oxauth.security import Identity +from org.gluu.oxauth.util import ServerUtil +from org.gluu.oxauth.service import AuthenticationService, UserService +from org.gluu.oxauth.service.custom import CustomScriptService +from org.gluu.model.custom.script import CustomScriptType +from org.gluu.model.custom.script.type.auth import PersonAuthenticationType +from org.gluu.model import SimpleCustomProperty +from org.gluu.persist import PersistenceEntryManager +from org.gluu.service.cdi.util import CdiUtil +from org.gluu.util import StringHelper + +from org.gluu.jsf2.message import FacesMessages + +try: + import json +except ImportError: + import simplejson as json +import sys + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + self.ACR_SG = "super_gluu" + self.PREV_LOGIN_SETTING = "prevLoginsCookieSettings" + + self.modulePrefix = "pwdless-external_" + + def init(self, customScript, configurationAttributes): + print "Passwordless. init called" + self.authenticators = {} + self.uid_attr = self.getLocalPrimaryKey() + + self.prevLoginsSettings = self.computePrevLoginsSettings(configurationAttributes.get(self.PREV_LOGIN_SETTING)) + + custScriptService = CdiUtil.bean(CustomScriptService) + self.scriptsList = custScriptService.findCustomScripts(Collections.singletonList(CustomScriptType.PERSON_AUTHENTICATION), "oxConfigurationProperty", "displayName", "oxEnabled") + dynamicMethods = self.computeMethods(configurationAttributes.get("snd_step_methods"), self.scriptsList) + + if len(dynamicMethods) > 0: + print "Passwordless. init. Loading scripts for dynamic modules: %s" % dynamicMethods + + for acr in dynamicMethods: + moduleName = self.modulePrefix + acr + try: + external = __import__(moduleName, globals(), locals(), ["PersonAuthentication"], -1) + module = external.PersonAuthentication(self.currentTimeMillis) + + print "Passwordless. init. Got dynamic module for acr %s" % acr + configAttrs = self.getConfigurationAttributes(acr, self.scriptsList) + + if acr == self.ACR_SG: + application_id = configurationAttributes.get("supergluu_app_id").getValue2() + configAttrs.put("application_id", SimpleCustomProperty("application_id", application_id)) + + if module.init(None, configAttrs): + module.configAttrs = configAttrs + self.authenticators[acr] = module + else: + print "Passwordless. init. Call to init in module '%s' returned False" % moduleName + except: + print "Passwordless. init. Failed to load module %s" % moduleName + print "Exception: ", sys.exc_info()[1] + else: + print "Passwordless. init. Not enough custom scripts enabled. Check config property 'snd_step_methods'" + return False + + print "Passwordless. init. Initialized successfully" + return True + + def destroy(self, configurationAttributes): + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, configurationAttributes): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + print "Passwordless. authenticate for step %d" % step + + userService = CdiUtil.bean(UserService) + authenticationService = CdiUtil.bean(AuthenticationService) + identity = CdiUtil.bean(Identity) + + if step == 1: + user_name = identity.getCredentials().getUsername() + if StringHelper.isNotEmptyString(user_name): + + foundUser = userService.getUserByAttribute(self.uid_attr, user_name) + + if foundUser == None: + print "Passwordless. Unknown username '%s'" % user_name + elif authenticationService.authenticate(user_name): + availMethods = self.getAvailMethodsUser(foundUser) + + if availMethods.size() > 0: + acr = availMethods.get(0) + print "Passwordless. Method to try in 2nd step will be: %s" % acr + + module = self.authenticators[acr] + logged_in = module.authenticate(module.configAttrs, requestParameters, step) + + if logged_in: + identity.setWorkingParameter("ACR", acr) + print "Passwordless. Authentication passed for step %d" % step + return True + + else: + self.setError("Cannot proceed. You don't have suitable credentials for passwordless login") + else: + self.setError("Wrong username or password") + else: + user = authenticationService.getAuthenticatedUser() + if user == None: + print "Passwordless. authenticate for step 2. Cannot retrieve logged user" + return False + + #see alternative.xhtml + alter = ServerUtil.getFirstValue(requestParameters, "alternativeMethod") + if alter != None: + #bypass the rest of this step if an alternative method was provided. Current step will be retried (see getNextStep) + self.simulateFirstStep(requestParameters, alter) + return True + + session_attributes = identity.getSessionId().getSessionAttributes() + acr = session_attributes.get("ACR") + #this working parameter is used in alternative.xhtml + identity.setWorkingParameter("methods", self.getAvailMethodsUser(user, acr)) + + success = False + if acr in self.authenticators: + module = self.authenticators[acr] + success = module.authenticate(module.configAttrs, requestParameters, step) + + if success: + print "Passwordless. authenticate. 2FA authentication was successful" + if self.prevLoginsSettings != None: + self.persistCookie(user) + else: + print "Passwordless. authenticate. 2FA authentication failed" + + return success + + return False + + + def prepareForStep(self, configurationAttributes, requestParameters, step): + print "Passwordless. prepareForStep %d" % step + + identity = CdiUtil.bean(Identity) + session_attributes = identity.getSessionId().getSessionAttributes() + + if step == 1: + try: + loginHint = session_attributes.get("login_hint") + print "Passwordless. prepareForStep. Login hint is %s" % loginHint + isLoginHint = loginHint != None + + if self.prevLoginsSettings == None: + if isLoginHint: + identity.setWorkingParameter("loginHint", loginHint) + else: + users = self.getCookieValue() + + if isLoginHint: + + idx = self.findUid(loginHint, users) + if idx >= 0: + u = users.pop(idx) + users.insert(0, u) + else: + identity.setWorkingParameter("loginHint", loginHint) + + if len(users) > 0: + identity.setWorkingParameter("users", json.dumps(users, separators=(',',':'))) + + # In login.xhtml both loginHint and users are used to properly display the login form + except: + print "Passwordless. prepareForStep. Error!", sys.exc_info()[1] + + return True + + else: + user = CdiUtil.bean(AuthenticationService).getAuthenticatedUser() + + if user == None: + print "Passwordless. prepareForStep. Cannot retrieve logged user" + return False + + acr = session_attributes.get("ACR") + print "Passwordless. prepareForStep. ACR = %s" % acr + + identity.setWorkingParameter("methods", ArrayList(self.getAvailMethodsUser(user, acr))) + + if acr in self.authenticators: + module = self.authenticators[acr] + return module.prepareForStep(module.configAttrs, requestParameters, step) + else: + return False + + def getExtraParametersForStep(self, configurationAttributes, step): + + print "Passwordless. getExtraParametersForStep %d" % step + list = ArrayList() + + if step > 1: + acr = CdiUtil.bean(Identity).getWorkingParameter("ACR") + + if acr in self.authenticators: + module = self.authenticators[acr] + params = module.getExtraParametersForStep(module.configAttrs, step) + if params != None: + list.addAll(params) + + list.addAll(Arrays.asList("ACR", "methods")) + print "extras are %s" % list + return list + + def getCountAuthenticationSteps(self, configurationAttributes): + return 2 + + def getPageForStep(self, configurationAttributes, step): + if step > 1: + acr = CdiUtil.bean(Identity).getWorkingParameter("ACR") + if acr in self.authenticators: + module = self.authenticators[acr] + page = module.getPageForStep(module.configAttrs, step) + + print "Passwordless. getPageForStep %d is %s" % (step, page) + return page + + return "/passwordless/login.xhtml" + + def getNextStep(self, configurationAttributes, requestParameters, step): + print "Passwordless. getNextStep called %d" % step + if step > 1: + acr = ServerUtil.getFirstValue(requestParameters, "alternativeMethod") + if acr != None: + print "Passwordless. getNextStep. Use alternative method %s" % acr + CdiUtil.bean(Identity).setWorkingParameter("ACR", acr) + #retry step with different acr + return 2 + + return -1 + + def logout(self, configurationAttributes, requestParameters): + return True + +# Miscelaneous + + def getLocalPrimaryKey(self): + entryManager = CdiUtil.bean(PersistenceEntryManager) + config = GluuConfiguration() + config = entryManager.find(config.getClass(), "ou=configuration,o=gluu") + #Pick (one) attribute where user id is stored (e.g. uid/mail) + uid_attr = config.getOxIDPAuthentication().get(0).getConfig().getPrimaryKey() + print "Passwordless. init. uid attribute is '%s'" % uid_attr + return uid_attr + + + def setError(self, msg): + facesMessages = CdiUtil.bean(FacesMessages) + facesMessages.setKeepMessages() + facesMessages.clear() + facesMessages.add(FacesMessage.SEVERITY_ERROR, msg) + + + def computeMethods(self, sndStepMethods, scriptsList): + snd_step_methods = [] if sndStepMethods == None else StringHelper.split(sndStepMethods.getValue2(), ",") + methods = [] + + for m in snd_step_methods: + for customScript in scriptsList: + if customScript.getName() == m and customScript.isEnabled(): + methods.append(m) + + print "Passwordless. computeMethods. %s" % methods + return methods + + + def getConfigurationAttributes(self, acr, scriptsList): + + configMap = HashMap() + for customScript in scriptsList: + if customScript.getName() == acr: + for prop in customScript.getConfigurationProperties(): + configMap.put(prop.getValue1(), SimpleCustomProperty(prop.getValue1(), prop.getValue2())) + + print "Passwordless. getConfigurationAttributes. %d configuration properties were found for %s" % (configMap.size(), acr) + return configMap + + + def getAvailMethodsUser(self, user, skip=None): + methods = ArrayList() + + for method in self.authenticators: + try: + module = self.authenticators[method] + if module.hasEnrollments(module.configAttrs, user) and (skip == None or skip != method): + methods.add(method) + except: + print "Passwordless. getAvailMethodsUser. hasEnrollments call could not be issued for %s module" % method + print "Exception: ", sys.exc_info()[1] + + print "Passwordless. getAvailMethodsUser %s" % methods.toString() + return methods + + + def simulateFirstStep(self, requestParameters, acr): + #To simulate 1st step, there is no need to call: + # getPageforstep (no need as user/pwd won't be shown again) + # isValidAuthenticationMethod (by restriction, it returns True) + # prepareForStep (by restriction, it returns True) + # getExtraParametersForStep (by restriction, it returns None) + print "Passwordless. simulateFirstStep. Calling authenticate (step 1) for %s module" % acr + if acr in self.authenticators: + module = self.authenticators[acr] + auth = module.authenticate(module.configAttrs, requestParameters, 1) + print "Passwordless. simulateFirstStep. returned value was %s" % auth + + def computePrevLoginsSettings(self, customProperty): + settings = None + if customProperty == None: + print "Passwordless. Previous logins feature is not configured. Set config property '%s' if desired" % self.PREV_LOGIN_SETTING + else: + try: + settings = json.loads(customProperty.getValue2()) + if settings['enabled']: + print "Passwordless. PrevLoginsSettings are %s" % settings + else: + settings = None + print "Passwordless. Previous logins feature is disabled" + except: + print "Passwordless. Unparsable config property '%s'" % self.PREV_LOGIN_SETTING + + return settings + + def getCookieValue(self): + ulist = [] + coo = None + httpRequest = ServerUtil.getRequestOrNull() + + if httpRequest != None: + for cookie in httpRequest.getCookies(): + if cookie.getName() == self.prevLoginsSettings['cookieName']: + coo = cookie + + if coo == None: + print "Passwordless. getCookie. No cookie found" + else: + print "Passwordless. getCookie. Found cookie" + forgetMs = self.prevLoginsSettings['forgetEntriesAfterMinutes'] * 60 * 1000 + + try: + now = System.currentTimeMillis() + value = URLDecoder.decode(coo.getValue(), "utf-8") + # value is an array of objects with properties: uid, displayName, lastLogon + value = json.loads(value) + + for v in value: + if now - v['lastLogon'] < forgetMs: + ulist.append(v) + # print "==========", ulist + except: + print "Passwordless. getCookie. Unparsable value, dropping cookie..." + + return ulist + + + def findUid(self, uid, users): + + i = 0 + idx = -1 + for user in users: + if user['uid'] == uid: + idx = i + break + i+=1 + return idx + + + def persistCookie(self, user): + try: + now = System.currentTimeMillis() + uid = user.getUserId() + dname = user.getAttribute("displayName") + + users = self.getCookieValue() + idx = self.findUid(uid, users) + + if idx >= 0: + u = users.pop(idx) + else: + u = { 'uid': uid, 'displayName': '' if dname == None else dname } + u['lastLogon'] = now + + # The most recent goes first :) + users.insert(0, u) + + excess = len(users) - self.prevLoginsSettings['maxListSize'] + if excess > 0: + print "Passwordless. persistCookie. Shortening list..." + users = users[:self.prevLoginsSettings['maxListSize']] + + value = json.dumps(users, separators=(',',':')) + value = URLEncoder.encode(value, "utf-8") + coo = Cookie(self.prevLoginsSettings['cookieName'], value) + coo.setSecure(True) + coo.setHttpOnly(True) + # One week + coo.setMaxAge(7 * 24 * 60 * 60) + + response = self.getHttpResponse() + if response != None: + print "Passwordless. persistCookie. Adding cookie to response" + response.addCookie(coo) + except: + print "Passwordless. persistCookie. Exception: ", sys.exc_info()[1] + + + def getHttpResponse(self): + try: + return FacesContext.getCurrentInstance().getExternalContext().getResponse() + except: + print "Passwordless. Error accessing HTTP response object: ", sys.exc_info()[1] + return None + \ No newline at end of file diff --git a/oxAuth/Server/integrations/passwordless/README.md b/oxAuth/Server/integrations/passwordless/README.md new file mode 100644 index 00000000..6c0fdbe6 --- /dev/null +++ b/oxAuth/Server/integrations/passwordless/README.md @@ -0,0 +1,3 @@ +# Passwordless authentication script + +Check https://gluu.org/docs/gluu-server/4.3/authn-guide/passwordless/ diff --git a/oxAuth/Server/integrations/passwordless/bundle.zip b/oxAuth/Server/integrations/passwordless/bundle.zip new file mode 100644 index 0000000000000000000000000000000000000000..161dac39cecf46284e5d58d8cee84671307edf12 GIT binary patch literal 428691 zcmbTcV~i$1*Z$eIZQHhO+t%D|+qUiQX`9owZQhM(+wPuyHrf5>*>|(q&7Lo(KK!n$ zPAZjCsZ^=RgF|3|gZw8%g@)Ds_xyhb#D4~kPHxO9|BK)i{=X8i{|8|WaCiSNOyYkS zA^$gKXX)nV{a>*C|A4Xn@35nnsk@uae{19>{x6jh{y%EW-Cf-r?f*-v`#+|m_X0!N@^9BbBL@*2$MEU=Z zot2}58>6YGrK_X8B_k&jD;pCllPLheWN+)hWak=}Yn$jMjxoOXiIIuTChpnY2JFhx zfg)|942z0tt;sH)WnqU(JZ|~?7q}|87UlK(Vn7eSM?Xsr(pq$vF zV}0)`V%@=`yBHw06L!#27=hida{98+LBw=()7R_&l5PF7(xqG9e^2LV#ME80{SW79 zrZ3U!1m4m4Va&zf{qxExK&a03b9z0WUHyDKJemsd^LKk4L{k^^ z>g*OW)?9A}JkM@Uen&_O-rSFOAQLy-c=~H|8(p;*3cnwrk3R}Idq_BRZwCCl*SzAr zog829-q?9Q9;I&2y0mNquaW3$1AJQ-?>&V39!Dq99eZELYLxSS9;OR(fc<~I42BE5 z#m4_VEh`)Qo?gES`=1YM@A;nAX+Kx3ZMO(7ZjSV9?)nG1KEJ(*gqq90J+G&#SN8LF zbE6h)`+v<+_kXz@UMO(r;%@8!zgzcqm%i?vrEf={pV!@l{Jv~YMvu9l1pV^-zhw9B zZ)W1>2~`P!e7ahRN`W5hKUWk&{k`vKT|kGI?i%J>rUJj_$D{j}mdN_VztqfM?|*e$ z?q4pa9gVs1YT!2}(!NubZ$?Kvg+A>%cI^9r-)_DSXKt=7pI@Jro-QMzI9K7IhxccH zzp3X5C!XF$Pt;ug4LvwR1Azf8pGURB^Gw9IkPR8e+FM_LpfmoFCVJ-6SbcSGA72^= z{P8-t5gAF$RQ}mt{XU;JHrUH^GlZFE{8u#KI=!sy`P6Q$y?6VX_%9j-KFsUQ3(N51 z(Y9m%#nV|J?#;#Xbq-nk7HAOI-TmkDytlCL{mLgW_UDS5uXN<(NLb3}eil3PvwL?I zoc13|bKu_J^xbP0*Z1puq#i@Yirn3Fr~b`=_PuW(AD^x*M~N8ij_FZjED0_x>>BC$ z>-n4Vvh76XxeR43?Embx#=wI{`dc;e|O)* zv&V#u9LGDdYym4)8d zqJ;{Q0OnqOJfH8;YzccmZzCaZprBR__ny!3S#{nBpaE5Vz+U)h4$!+&wr7%}Pdd_P# zq?6*a6|oOdLx*@#D4|J(n=b-C(<=2pT9L-aGFZb;Fl_5U#*^r#*}7iGM-9}?Rn?WK ztF|^pcF+XbQInp?F>0j{fysRlCCGF(gsKDMg~k@aW`%C*u`eoSGSP_iS7-mt>nlUx{{^yz_jHyES3KjN-;1E9 z7^p{vuP&aWI=@WwqTyejv3fPfQByiBFO^Wzfh?Kj;w5AauaP_Y0cckQ)3X-wd(uMt zY#viOZ7MI8aO6HERhGP5XTg~#I}EpJ*TY(XB%Y3o(zQ;#f?K%&z6^PS8pnlL2+R3I>oS$^Aca~Ehd#ZZ5J^W)H_X5X&2 z=|kEqzp*zXnuaDhEGq6dadD$ji(k4ht5#y7C8eKck}l)99nWz0gbR%!Q*?4`2q6r@n{- zzh^TjgO$hSU^P%uH%lnLL0l$_smd_4KWk$#LWjQWAswl|vGtgei5OL_P!oH;$jvp? zB0czu~*|N34Xr%;I>%utc83k1`&wXbVhBpB#vzt{*g1zjuIJxVY;zdjgb zsmpD(YqyAbwpXoV%o#0){Cr&;<38Wm+en96lsiUCIaf+rX=#GmW<-_)W&GYF}AHo1(9gO{KV?D<1|_Q`NWa41>&Rt#WR^u*KK4{=^_w zE*jH!q1q|FZ<(*!E!UO}0nd?rS+-E+E{^`2tH)!!1hJ@?qzEQwGg9tb9f}ucbx!2M zVdEC#O#